Аннотации

Добавьте поддержку аннотаций в просмотрщик

Аннотации в Doconut работают в двух направлениях: пользователи рисуют их в виджете браузера, а сервер сохраняет их по страницам, либо ваш код создает их и загружает в открывающуюся сессию. В любом случае они отображаются на страницах и могут быть внедрены в экспорт PDF или PNG.

Поддержка аннотаций ограничена возможностью лицензии Annotation (предоставляется автоматически при активной временной лицензии).

Включить пользовательский интерфейс аннотаций

Аннотация — это модуль Viewer, а не отдельная панель инструментов. Страница должна включать ресурсы Viewer, панель инструментов Viewer, монтирование Viewer и инициализированный objViewer; затем лента Annotation монтируется и привязывается к тому же экземпляру.

Включайте файлы аннотаций вместе с файлами просмотрщика. Они ограничены лицензией, поэтому их теги появляются только при предоставлении возможности. В 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   // встроенная лента аннотаций
});
ViewBag.IsAnnotationEnabled = viewer.IsAnnotationEnabled;

Сохраняйте полную композицию Viewer в разметке:

html
<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
@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):

csharp
// 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).

Экспорт с встраиванием аннотаций

csharp
// 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 изображения, который невозможно разрешить вообще, приводит к исчезновению аннотации без ошибки — проверьте отрисованные пиксели или сетевой журнал, а не код статуса.

Рабочий процесс сохранения

  1. Откройте документ и получите его токен.
  2. Загрузите ранее сохранённый XML или закодированные данные в этот токен.
  3. Позвольте виджету читать и редактировать аннотации сессии.
  4. Получите XML с помощью GetAnnotationXML(token), когда ваше приложение решит сохранить данные.
  5. Экспортируйте PDF или PNG, когда требуется плоский результат.
  6. Закройте сессию документа.

Не используйте токен просмотрщика в качестве постоянного идентификатора аннотации — он исчезает вместе с сессией, а переразбор пула приложений завершает каждую сессию на отдельном сервере. Сохраняйте данные аннотаций, привязывая их к собственным идентификаторам документа и версии.

Примечания по безопасности и рендерингу

  • Запросы аннотаций используют ту же безопасность сессии и токена, что и запросы страниц.
  • Проверяйте и контролируйте любой пользовательский URL изображения.
  • Большие свободные нагрузки и экспорты высокого разрешения увеличивают потребление памяти; тестируйте реальные документы и значения масштабирования.

Устранение неполадок

СимптомПроверка
Лента аннотаций отсутствуетВозможность Annotation и четыре флага CSS/скриптов аннотации
Сохранение сообщает об ошибкеИстечение срока действия токена или сессии, и BasePath виджета
ctlDoc_Created (или другая функция события) никогда не запускаетсяИмя функции: id просмотрщика + _ + событие, как глобальная функция
C#‑аннотации не отображаютсяНумерация страниц начинается с единицы, и данные были загружены в активный токен
Аннотация изображения отображается на экране, но не в экспортеХост изображения имеет частный или loopback‑адрес — см. выше
Повторно открытый документ не содержит аннотацийСохраните XML или данные вне сессии просмотрщика, затем загрузите их в новый токен

Была ли эта страница полезной?