Аннотации
Добавьте поддержку аннотаций в просмотрщик
Аннотации в Doconut работают в двух направлениях: пользователи рисуют их в виджете браузера, а сервер сохраняет их по страницам, либо ваш код создает их и загружает в открывающуюся сессию. В любом случае они отображаются на страницах и могут быть внедрены в экспорт PDF или PNG.
Поддержка аннотаций ограничена возможностью лицензии Annotation (предоставляется автоматически при активной временной лицензии).
Включить пользовательский интерфейс аннотаций
Аннотация — это модуль Viewer, а не отдельная панель инструментов. Страница должна включать ресурсы Viewer, панель инструментов Viewer, монтирование Viewer и инициализированный objViewer; затем лента Annotation монтируется и привязывается к тому же экземпляру.
Включайте файлы аннотаций вместе с файлами просмотрщика. Они ограничены лицензией, поэтому их теги появляются только при предоставлении возможности. В 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 // встроенная лента аннотаций
});
ViewBag.IsAnnotationEnabled = viewer.IsAnnotationEnabled;Сохраняйте полную композицию Viewer в разметке:
<nav id="toolbar" aria-label="Document viewer controls">
<!-- Элементы управления просмотрщиком, включая кнопку, открывающую Annotation -->
</nav>
<div id="annBarMount"></div>
<div id="divDocViewer"><div id="div_ctlDoc"></div></div>Файлы Annotation генерируют ленту внутри annBarMount; вам не нужно копировать её кнопки или диалоговые окна. Сначала инициализируйте docViewer, затем создавайте ленту только когда сервер подтверждает, что Annotation лицензирована:
@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) { %> … <% } %>.
Сохранение из ленты отправляет данные в 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являются глобальными функциями, найденными по имени: к событию добавляется префикс идентификатора просмотрщика. Для просмотрщика в#div_ctlDocидентификатор —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>();
// Привязан к размерам страниц открытой сессии
var manager = viewer.GetAnnotationManager(token);
var pageCount = viewer.GetPageCount(token);
// Один штамп на страницу
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));
// Загрузить в сессию — просмотрщик получает их, а рендерер встраивает их в
// image/PDF экспорты.
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 разрешается относительно хоста текущего запроса при добавлении аннотации — см. примечание об экспорте ниже |
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 всех страниц с отрисованными аннотациями
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");
}
// Или ZIP с PNG‑файлами по каждой странице
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");
}Экспорт использует тот же механизм «сжигания», что и отображение на экране, а также ту же лицензию и решение о пользовательском водяном знаке.
Аннотации изображений на интранет‑хосте экспортируются как "Изображение заблокировано"
The viewer and the export fetch an ImageAnnotation's image from different places:
| Источник | Кто загружает изображение | На интранет‑или девелопмент‑хосте |
|---|---|---|
| Просмотрщик в браузере | Браузер пользователя | Изображение отображается |
ExportAnnotationsToPdfAsync / …PngZipAsync | Сервер | Красный "Изображение заблокировано" заполнитель |
Когда сервер получает изображение для встраивания, он отклоняет адреса, которые указывают обратно во внутренние сети: loopback (localhost, 127.0.0.1), 0.0.0.0, link‑local (169.254.x.x), частные диапазоны 10.x, 172.16–31.x и 192.168.x, carrier‑grade NAT (100.64.0.0/10), а также IPv6 link‑local и unique‑local адреса. Это защищает сервер от использования в качестве средства доступа к внутренним системам через специально сформированный URL изображения, и отключить эту защиту невозможно.
Относительный URL изображения разрешается относительно хоста текущего запроса, поэтому на localhost, на интранет‑сервере или на любом сайте, доступном через частный адрес, изображение отображается в просмотрщике, а экспорт содержит вместо него заполнитель. Это работа защиты, а не ошибка рендеринга. Если экспорт должен содержать изображение, разместите его на хосте с публичным адресом и используйте абсолютный URL. URL изображения, который невозможно разрешить вообще, приводит к исчезновению аннотации без ошибки — проверьте отрисованные пиксели или сетевой журнал, а не код статуса.
Рабочий процесс сохранения
- Откройте документ и получите его токен.
- Загрузите ранее сохранённый XML или закодированные данные в этот токен.
- Позвольте виджету читать и редактировать аннотации сессии.
- Получите XML с помощью
GetAnnotationXML(token), когда ваше приложение решит сохранить данные. - Экспортируйте PDF или PNG, когда требуется плоский результат.
- Закройте сессию документа.
Не используйте токен просмотрщика в качестве постоянного идентификатора аннотации — он исчезает вместе с сессией, а переразбор пула приложений завершает каждую сессию на отдельном сервере. Сохраняйте данные аннотаций, привязывая их к собственным идентификаторам документа и версии.
Примечания по безопасности и рендерингу
- Запросы аннотаций используют ту же безопасность сессии и токена, что и запросы страниц.
- Проверяйте и контролируйте любой пользовательский URL изображения.
- Большие свободные нагрузки и экспорты высокого разрешения увеличивают потребление памяти; тестируйте реальные документы и значения масштабирования.
Устранение неполадок
| Симптом | Проверка |
|---|---|
| Лента аннотаций отсутствует | Возможность Annotation и четыре флага CSS/скриптов аннотации |
| Сохранение сообщает об ошибке | Истечение срока действия токена или сессии, и BasePath виджета |
ctlDoc_Created (или другая функция события) никогда не запускается | Имя функции: id просмотрщика + _ + событие, как глобальная функция |
| C#‑аннотации не отображаются | Нумерация страниц начинается с единицы, и данные были загружены в активный токен |
| Аннотация изображения отображается на экране, но не в экспорте | Хост изображения имеет частный или loopback‑адрес — см. выше |
| Повторно открытый документ не содержит аннотаций | Сохраните XML или данные вне сессии просмотрщика, затем загрузите их в новый токен |
Была ли эта страница полезной?