注释

为查看器添加注释支持

在 Doconut 中,注释有两个方向:用户在浏览器小部件中绘制,服务器按页面保存;或者你的代码构建注释并加载到打开的会话中。无论哪种方式,它们都会在页面上渲染,并且可以烧录到 PDF 或 PNG 导出文件中。

注释支持受 Annotation 许可证功能的限制(在有效的临时许可证下自动授予)。

启用注释 UI

注释是 Viewer 模块,而不是独立的工具栏。页面必须包含 Viewer 资源、Viewer 工具栏、Viewer 挂载点以及已初始化的 objViewer;随后会挂载并附加注释功能区(Ribbon)到同一个实例。

将注释文件与 Viewer 文件一起发布。它们受许可证控制,只有在授予相应功能时才会出现对应的标签。在 MVC 5 中,在控制器中构建这些标签(视图中没有 @inject):

csharp
var viewer = Doconut.DoconutHost.Services.GetRequiredService<Viewer>();
ViewBag.ViewerCss = viewer.ReferenceCss(new CssConfig
{
    IncludeViewerCss     = true,
    IncludeAnnotationCss = true    // jquery-ui.min.css + annotationBar.css
});
ViewBag.ViewerScripts = viewer.ReferenceScripts(new ScriptConfig
{
    IncludeJQuery            = true,
    IncludeViewerScripts     = true,
    IncludeAnnotationScripts = true,  // jquery-ui, raphael.js, annotation.js
    IncludeAnnotationBar     = true   // the embedded annotation ribbon
});
ViewBag.IsAnnotationEnabled = viewer.IsAnnotationEnabled;

在标记中保留完整的 Viewer 组合:

html
<nav id="toolbar" aria-label="Document viewer controls">
    <!-- Viewer controls, including the button that opens Annotation -->
</nav>
<div id="annBarMount"></div>
<div id="divDocViewer"><div id="div_ctlDoc"></div></div>

注释文件会在 annBarMount 中生成功能区(Ribbon),你不需要复制其按钮或对话框。先初始化 docViewer,然后仅在服务器确认已授权注释功能时才创建功能区:

html
@Html.Raw(ViewBag.ViewerScripts)
<script>
    var annBar = null;
    var currentToken = '';

    var objViewer = $('#div_ctlDoc').docViewer({
        BasePath: '/',
        ResPath:  'doconut-res/images',
        onAnnLoaded:    function () { if (annBar) annBar.handleAnnLoaded(); },
        onAnnSaved:     function () { if (annBar) annBar.handleAnnSaved(); },
        onAnnSaveError: function () { if (annBar) annBar.handleAnnSaveError(); },
        onAnnClosed:    function () { if (annBar) annBar.handleAnnClosed(); },
        onError:        function (message) { console.error('Viewer error:', message); }
    });

    @if ((bool)ViewBag.IsAnnotationEnabled)
    {
        <text>
    annBar = $('#annBarMount').doconutAnnotationBar({
        docId: 'ctlDoc',
        getRequestParams: function () { return { token: currentToken }; },
        onLayout: function () { requestAnimationFrame(function () { objViewer.Refit(); }); }
    });
    annBar.attach(objViewer);
        </text>
    }
</script>

如果没有此功能,ReferenceScripts 会省略功能区的脚本,因此服务器端的 if 可以防止未授权的 doconutAnnotationBar(...) 调用导致页面出错。在 Web Forms 中,同样的保护写法是 <% if (IsAnnotationEnabled) { %> … <% } %>。

功能区的保存操作会将数据 POST 到 DocImage.axd(AnnSave),并按页面存入文档会话。加载(AnnLoad)会在带有注释的页面渲染时自动触发。

你可以使用 annBar.open() 与 annBar.close() 在自定义工具栏中打开或关闭功能区。其公开 API 如下:

方法用途
attach(objViewer)将功能区连接到已初始化的查看器;仅需调用一次
open() / close()进入或退出注释编辑模式
reset()将功能区恢复为关闭且非编辑状态
isOpen() / annotating()读取功能区状态 / 查看器的注释编辑状态
reopenEditable()重新加载当前页面的注释为可编辑对象
updateActionState()在主机变化后刷新保存和删除控件
headerSlot()主机拥有的可选头部插槽

onStatus、onToast、onLayout、onEditStart 与 onEditEnd 为可选的主机回调。endpoints 对象还可以提供 exportPdf、exportPng、imageUpload 与 imageList;未配置端点的控件将保持隐藏。

两种回调方式,双向绑定

注释代码使用了两种回调风格,当以另一种方式绑定时,都会悄然失效:

  • onAnnLoaded、onAnnSaved、onAnnSaveError、onAnnClosed 是 docViewer({...}) 的 选项,类似于每个查看器的回调。
  • Created、Changed、Deleted 和 Properties 是 通过名称查找的全局函数:事件名前缀为查看器的 id。对于 #div_ctlDoc 中的查看器,其 id 为 ctlDoc,因此函数名为 ctlDoc_Created、ctlDoc_Changed、ctlDoc_Deleted 和 ctlDoc_Properties。拼写错误或改名的函数根本不会被调用。

SaveAnnotations() 并不返回是否保存成功:结果通过 onAnnSaved 或 onAnnSaveError 传回。读取其返回值的代码从未清除“未保存更改”标志。

在 C# 中构建注释

获取绑定到打开会话的管理器,添加注释并加载它们(使用 using Doconut.Annotations; 引入类型,使用 using System.Drawing; 引入 Rectangle 和 Color):

csharp
// POST /Annotations/LoadSample?token=...
[HttpPost]
public ActionResult LoadSample(string token)
{
    var viewer = Doconut.DoconutHost.Services.GetRequiredService<Viewer>();

    // Bound to the open session's page dimensions
    var manager   = viewer.GetAnnotationManager(token);
    var pageCount = viewer.GetPageCount(token);

    // One stamp per page
    for (int page = 1; page <= pageCount; page++)
    {
        manager.Add(new StampAnnotation(page, new Rectangle(30, 20, 240, 90),
            $"PAGE {page}", 28, 4, Color.Maroon)
        {
            Opacity = 60,
            Rotate  = -8
        });
    }

    manager.Add(new NoteAnnotation(1, new Rectangle(420, 150, 220, 120),
        "Loaded from C# code.", Color.FromArgb(255, 255, 255, 170), 14));

    // Load into the session - the viewer fetches them, and the renderer burns them into
    // image/PDF exports.
    viewer.LoadAnnotationData(token, manager);
    return new HttpStatusCodeResult(200);
}

注释类型

所有类型位于 Doconut.Annotations,继承自 BaseAnnotation(页码加上边界 Rectangle):

类型说明
StampAnnotation带有字体大小、边框、颜色的文字印章;支持 Opacity、Rotate
NoteAnnotation带有文本、背景颜色、字体大小、TitleColor 的便利贴
RectangleAnnotation边框和填充颜色,Title/ShowTitle
CircleAnnotation边框和填充,ShowBorder
EllipseAnnotation边框和填充,ShowBorder
TriangleAnnotation边框颜色,BackColor,ShowBorder
LineAnnotation具有宽度和颜色的直线
ArrowAnnotation带箭头的线;可设置 Direction(ArrowDirection,罗盘方向,默认 E)
FreehandAnnotation从编码的 FreehandData 点生成的自由笔画
ImageAnnotation来自 URL 的图像。当注释被添加时,相对 URL 会相对于当前请求的主机解析——请参见下面的导出说明

AnnotationManager API

成员目的
Add(BaseAnnotation)将注释加入队列
GetAnnotations() / GetAnnotations(int page)检查管理器中持有的注释
ClearAnnotations() / ClearAnnotations(int page)删除全部 / 按页删除
GetAnnotationData() / GetAnnotationData(int page)编码的注释数据——小部件使用的封装
GetAnnotationXml()XML 形式

Viewer 将加载和读取操作镜像到会话上:LoadAnnotationData(token, manager) 或 LoadAnnotationData(token, encodedData)、LoadAnnotationXML(token, xml),以及 GetAnnotationXML(token)。

导出并嵌入注释

csharp
// PDF of all pages with annotations rendered onto them
public async Task<ActionResult> ExportPdf(string token)
{
    var viewer = Doconut.DoconutHost.Services.GetRequiredService<Viewer>();
    byte[] pdf = await viewer.ExportAnnotationsToPdfAsync(token, zoom: 100, ct: Response.ClientDisconnectedToken);
    return File(pdf, "application/pdf", "export.pdf");
}

// Or a ZIP of per-page PNGs
public async Task<ActionResult> ExportPngZip(string token)
{
    var viewer = Doconut.DoconutHost.Services.GetRequiredService<Viewer>();
    byte[] zip = await viewer.ExportAnnotationsToPngZipAsync(token, zoom: 100, ct: Response.ClientDisconnectedToken);
    return File(zip, "application/zip", "annotations-png.zip");
}

导出使用与屏幕渲染相同的燃烧方式,并遵循相同的许可和自定义水印决策。

在内网主机上导出图像注释显示 “Image blocked”

查看器和导出程序从不同的位置获取 ImageAnnotation 的图像:

场景谁下载图像在内网或开发主机上
浏览器中的查看器用户的浏览器图像显示
ExportAnnotationsToPdfAsync / …PngZipAsync服务器一个红色的 "Image blocked" 占位符

当服务器获取图像以嵌入时,它会拒绝指向私有网络的地址:环回地址(localhost、127.0.0.1)、0.0.0.0、链路本地地址(169.254.x.x)、私有网段 10.x、172.16–31.x、192.168.x、运营商级 NAT(100.64.0.0/10),以及 IPv6 链路本地和唯一本地地址。这一机制可以防止服务器被利用通过精心构造的图像 URL 访问内部系统,且没有可关闭的开关。

相对图像 URL 会相对于当前请求的主机进行解析。因此在 localhost、内网服务器或任何通过私有地址访问的站点上,图像会在查看器中显示,而导出文件中则会出现占位符。这是防护机制在起作用,而不是渲染失败。如果导出必须包含图像,请将图像托管在具有公共地址的主机上并使用绝对 URL。完全无法解析的图像 URL 会导致该注释消失且不报错——请检查渲染后的像素或网络日志,而不是状态码。

持久化工作流

  1. 打开文档并获取其令牌。
  2. 将先前存储的 XML 或编码数据加载到该令牌中。
  3. 让小部件读取并编辑会话注释。
  4. 当您的应用决定持久化时,使用 GetAnnotationXML(token) 获取 XML。
  5. 当需要平面交付物时导出 PDF 或 PNG。
  6. 关闭文档会话。

不要将查看器令牌用作永久的注释标识符——它会随会话结束而失效,且应用程序池的回收会在单个服务器上终止所有会话。请将注释数据存储在您自己的文档和版本标识符下。

安全性和渲染说明

  • 注释请求使用与页面请求相同的会话和令牌安全机制。
  • 验证并控制任何用户提供的图像 URL。
  • 大型手写负载和高分辨率导出会增加内存使用;请使用真实文档和缩放值进行测试。

故障排除

症状检查
注释功能区缺失Annotation 功能以及四个注释 CSS/脚本 标志
保存时报告错误令牌或会话过期,以及小部件的 BasePath
ctlDoc_Created(或其他事件函数)从不运行函数名应为:viewer id + _ + event,作为全局函数
C# 注释未出现页码从 1 开始,并且数据已加载到活动令牌中
图像注释在屏幕上显示但在导出中未出现图像的主机使用私有或环回地址——见上文
重新打开的文档没有注释在查看器会话之外持久化 XML 或数据,然后加载到新的令牌中

此页面对您有帮助吗?