注释
为查看器添加注释支持
在 Doconut 中,注释有两个方向:用户在浏览器小部件中绘制,服务器按页面保存;或者你的代码构建注释并加载到打开的会话中。无论哪种方式,它们都会在页面上渲染,并且可以烧录到 PDF 或 PNG 导出文件中。
注释支持受 Annotation 许可证功能的限制(在有效的临时许可证下自动授予)。
启用注释 UI
注释是 Viewer 模块,而不是独立的工具栏。页面必须包含 Viewer 资源、Viewer 工具栏、Viewer 挂载点以及已初始化的 objViewer;随后会挂载并附加注释功能区(Ribbon)到同一个实例。
将注释文件与 Viewer 文件一起发布。它们受许可证控制,只有在授予相应功能时才会出现对应的标签。在 MVC 5 中,在控制器中构建这些标签(视图中没有 @inject):
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 组合:
<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.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):
// 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)。
导出并嵌入注释
// 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 会导致该注释消失且不报错——请检查渲染后的像素或网络日志,而不是状态码。
持久化工作流
- 打开文档并获取其令牌。
- 将先前存储的 XML 或编码数据加载到该令牌中。
- 让小部件读取并编辑会话注释。
- 当您的应用决定持久化时,使用
GetAnnotationXML(token)获取 XML。 - 当需要平面交付物时导出 PDF 或 PNG。
- 关闭文档会话。
不要将查看器令牌用作永久的注释标识符——它会随会话结束而失效,且应用程序池的回收会在单个服务器上终止所有会话。请将注释数据存储在您自己的文档和版本标识符下。
安全性和渲染说明
- 注释请求使用与页面请求相同的会话和令牌安全机制。
- 验证并控制任何用户提供的图像 URL。
- 大型手写负载和高分辨率导出会增加内存使用;请使用真实文档和缩放值进行测试。
故障排除
| 症状 | 检查 |
|---|---|
| 注释功能区缺失 | Annotation 功能以及四个注释 CSS/脚本 标志 |
| 保存时报告错误 | 令牌或会话过期,以及小部件的 BasePath |
ctlDoc_Created(或其他事件函数)从不运行 | 函数名应为:viewer id + _ + event,作为全局函数 |
| C# 注释未出现 | 页码从 1 开始,并且数据已加载到活动令牌中 |
| 图像注释在屏幕上显示但在导出中未出现 | 图像的主机使用私有或环回地址——见上文 |
| 重新打开的文档没有注释 | 在查看器会话之外持久化 XML 或数据,然后加载到新的令牌中 |
此页面对您有帮助吗?