Прямокутник, намальований навколо абзацу під час рецензування, не мусить ставати позначкою всередині самого PDF. THPDFViewerModel у HotPDF відкриває AddHighlightRegion — метод, що зберігає кожне підсвічування як запис у пам'яті, а не як зміну завантаженого документа, тож рецензент може розмітити десятки сторінок, а файл на диску залишиться байт-у-байт таким самим, яким був. Наблизьте до 6400%, поверніть сторінку на 90 градусів, перемкніться з «За шириною» на «За сторінкою» — і той самий прямокутник усе одно опиниться на тому самому абзаці, бо обчислення координат проходять через фактичну геометрію рендера в момент, коли позначку намалювали
Інструментарій рецензування, побудований навколо переглядача PDF, постійно натрапляє на цю проблему. Екран правок, прохід контролю якості по згенерованих рахунках-фактурах, внутрішній робочий процес затвердження — усім їм потрібно дозволити комусь привернути увагу до області сторінки, не перетворюючи кожну чернеткову позначку на постійну зміну файлу, і не вдаючись до повноцінної підсистеми анотацій лише для того, щоб показати кольоровий прямокутник, поки хтось іще вирішує, чи позначка взагалі доречна. HotPDF відповідає на це виділеним шаром підсвічування, що повністю розташований на боці Model поділу, описаного в статті про побудову власного переглядача PDF з архітектурою MVC у Delphi, і саме тому той самий список підсвічувань можна прогнати через модульний тест без жодного дескриптора вікна в полі зору
Що насправді зберігає AddHighlightRegion у HotPDF?
AddHighlightRegion зберігає рівно три речі на кожну позначку: індекс сторінки з відліком від нуля, THPDFRectangle у координатах простору користувача PDF та TColor, усе упаковане як запис THPDFViewerHighlight усередині THPDFViewerModel. Виклик Viewer.HighlightRegion(PageIndex, PageRect, clYellow), або еквівалентний Model.AddHighlightRegion, додає один із цих записів до приватного масиву й повертає його індекс, і цей індекс — єдиний дескриптор, який отримує викликач: окремого об'єкта немає, інтерфейсу з підрахунком посилань немає, звільняти нічого не треба. Усі інші можливості цієї статті — малювання позначки, перепроєктування після зміни масштабу, видалення — побудовані поверх цього одного невеликого запису
Кожен прямокутник нормалізується й обрізається перед тим, як бути прийнятим. AddHighlightRegion міняє місцями ліву й праву межі, якщо рецензент тягне справа наліво, міняє місцями верх і низ для перетягування вгору, а потім обрізає результат за MediaBox сторінки, отриманим через GetLoadedPageBox. Прямокутник, що в результаті має нульову ширину, нульову висоту або опиняється повністю поза сторінкою, відхиляється одразу: метод повертає -1, і нічого до списку не додається. Це значення повернення не декоративне: пакет підсвічувань, відновлений із зовнішнього файлу рецензування чи із застарілих координат після заміни сторінки, може непомітно втратити записи, якщо викликач це не перевіряє
Як підсвічування залишається вирівняним після масштабування чи обертання?
Підсвічування залишається вирівняним, бо HotPDF зберігає його в просторі сторінки PDF і перепроєктовує в екранний простір при кожному перемальовуванні, замість того щоб зберігати екранний прямокутник, який застарів би тієї ж миті, коли зміниться рівень масштабу. THPDFViewerModel.PagePointToView та обернений до нього ViewPointToPage виконують це проєктування у два етапи: спочатку власний запис сторінки /Rotate, потім незалежне ViewRotation переглядача, яке ніколи не записується назад у PDF і впливає лише на те, що відображає переглядач. Скасування перетворення при відпусканні миші виконує ті самі два етапи у зворотному порядку, і саме це дозволяє підсвічуванню, намальованому при великому масштабі на сторінці, повернутій на 270 градусів, опинитися рівно в потрібному місці після того, як рецензент скидає перегляд назад до «За сторінкою»
DPI, що використовується для цього проєктування, важить не менше за обертання. Переглядач HotPDF фіксує точний DPI растрового зображення, що зараз на екрані, у FRenderedDPI одразу після кожного рендера, і ImageMouseUp передає це саме значення в ViewPointToPage, тож координата миші завжди перетворюється з використанням роздільної здатності, з якою її справді намальовано, а не роздільної здатності, перерахованої з поточної властивості масштабу. CreatePageSnapshot та споріднені методи обмежують DPI діапазоном від 12 до 2400, але інтерактивний шлях рендера такої стелі не має: стандартна драбина масштабів сягає 6400%, що при базовому значенні 96 DPI дає далеко за 2400 DPI, тож повторне використання обмеження в стилі знімка для відображення координат зсунуло б кожне підсвічування на кілька пікселів на верхівці діапазону масштабу. Два менші параметри за замовчуванням доповнюють взаємодію: перетягування коротше двох пікселів по будь-якій осі трактується як клацання й не створює підсвічування, а підсвічування не може розпочатися, поки хоча б одна сторінка справді не відрендерилася, оскільки FRenderedDPI починається з нуля
Підключення інтерактивного підсвічування до екрана рецензування
Увімкнення інтерактивного підсвічування — це робота із трьома властивостями самого елемента керування THPDFViewer: встановіть InteractionMode у vimHighlight замість типового vimBrowse, оберіть HighlightColor, що за замовчуванням дорівнює clYellow, і обробіть OnMarqueeSelect, щоб дізнатися, що саме щойно намалював рецензент. Усе інше — захоплення миші, малювання пунктирного прямокутника виділення під час перетягування рецензентом, перетворення точки відпускання назад у простір сторінки, виклик AddHighlightRegion, — відбувається всередині елемента керування ще до того, як спрацює ця подія
type
TReviewForm = class(TForm)
Viewer: THPDFViewer;
ReviewLog: TMemo;
procedure FormCreate(Sender: TObject);
private
procedure ViewerMarqueeSelect(Sender: TObject; Shift: TShiftState;
PageIndex: Integer; const PageRect: THPDFRectangle;
HighlightIndex: Integer);
end;
// PdfDoc is a THotPDF already loaded elsewhere on the form
procedure TReviewForm.FormCreate(Sender: TObject);
begin
Viewer.PDFDocument := PdfDoc;
Viewer.InteractionMode := vimHighlight;
Viewer.HighlightColor := clLime;
Viewer.OnMarqueeSelect := ViewerMarqueeSelect;
end;
procedure TReviewForm.ViewerMarqueeSelect(Sender: TObject; Shift: TShiftState;
PageIndex: Integer; const PageRect: THPDFRectangle; HighlightIndex: Integer);
begin
ReviewLog.Lines.Add(Format('page %d, mark #%d at (%.1f, %.1f)-(%.1f, %.1f)',
[PageIndex + 1, HighlightIndex, PageRect.Left, PageRect.Bottom,
PageRect.Right, PageRect.Top]));
end;
OnMarqueeSelect спрацьовує лише для перетягування, що справді створило підсвічування: клацання, надто мале, щоб рахуватися перетягуванням, одразу очищає накладення виділення, а перетягування, що повністю опиняється поза сторінкою, доходить до AddHighlightRegion, але відхиляється там так само, як відхилявся б програмний виклик, тож подія в обох випадках залишається німою. Одна деталь реалізації, яку варто знати, якщо підсвічування раптом перестає реагувати біля країв елемента керування: захоплення миші належить самому THPDFViewer, нащадку TScrollBox, а не внутрішньому TImage, що показує растрове зображення сторінки, і саме це дозволяє рецензенту перетягнути за межу відрендереної сторінки й усе одно отримати чисте відпускання
Додавання, видалення й повторне читання підсвічувань із коду
Підсвічування взагалі не мусять з'являтися лише через перетягування миші. Viewer.HighlightRegion(PageIndex, PageRect, Color), що спрямовує в той самий Model.AddHighlightRegion, який внутрішньо викликає інтерактивне перетягування, є публічним саме для того, щоб екран рецензування міг відновити підсвічування з даних, які вже має: коментарів, завантажених із бази даних, результатів текстового пошуку чи позначок, відновлених із попереднього сеансу. Оскільки координати — це прості числа простору користувача PDF, ніщо в цьому шляху не залежить від того, чи сторінку вже відрендерили, на відміну від інтерактивного перетягування, якому потрібно, щоб FRenderedDPI уже мав реальне значення
var
I: Integer;
Item: TPriorComment; // your own record: PageIndex + PageRect
NewIndex: Integer;
begin
for I := 0 to PriorComments.Count - 1 do
begin
Item := TPriorComment(PriorComments[I]);
NewIndex := Viewer.HighlightRegion(Item.PageIndex, Item.PageRect, clAqua);
if NewIndex < 0 then
LogWarning('comment %d fell outside the page and was dropped', [I]);
end;
end;
Видалення одного підсвічування — це те місце, де проявляється зберігання на основі масиву. RemoveHighlightRegion видаляє один запис і зсуває кожен наступний запис на одну позицію вниз, щоб закрити прогалину, а це означає, що будь-який індекс, захоплений раніше — з події OnMarqueeSelect чи з попереднього перелічення, — перестає бути надійним, щойно щось попереду нього в списку буде видалено. OnHighlightChange спрацьовує при кожному додаванні, видаленні та виклику ClearHighlightRegions, але не несе жодної інформації про те, що саме змінилося, тож безпечний шаблон — трактувати це як сигнал перебудувати будь-який список, що показує панель рецензування, з HighlightCount і TryGetHighlightRegion, а не латати закешований індекс на місці
procedure TReviewForm.ViewerHighlightChange(Sender: TObject);
var
I: Integer;
Mark: THPDFViewerHighlight;
begin
MarkList.Items.Clear;
for I := 0 to Viewer.Model.HighlightCount - 1 do
if Viewer.Model.TryGetHighlightRegion(I, Mark) then
MarkList.Items.AddObject(Format('page %d', [Mark.PageIndex + 1]),
TObject(I));
end;
Коли позначка натомість має стати справжньою анотацією Highlight?
Область підсвічування має стати справжньою анотацією тієї миті, коли їй потрібно пережити межі одного-єдиного екземпляра THPDFViewer. HotPDF також відкриває AddHighlightAnnotation для нової сторінки та AddLoadedHighlightAnnotation для вже завантаженого документа, і, попри майже ідентичну назву, це зовсім інший механізм: обидва записують справжню анотацію розмітки тексту за ISO 32000-1 §12.5.6.10, PDF /Subtype /Highlight, у масив /Annots сторінки, з /QuadPoints, що позначають точний ряд гліфів, і будь-який переглядач PDF, що відповідає специфікації, відрендерить це після збереження файлу, а не лише власний переглядач HotPDF. Та сама межа механізмів вирішує, чи позначка пройде туди й назад через XFDF: анотація, створена через AddLoadedHighlightAnnotation, — це звичайний об'єкт PDF, який ExportLoadedAnnotationsToXFDF підхоплює і передає в Acrobat чи інший інструмент рецензування як розмітку ISO 19444-1, описану в статті про імпорт і експорт анотацій PDF як XFDF у Delphi, тоді як область, додана через AddHighlightRegion, невидима для цього експорту, бо її взагалі ніколи не записували в граф об'єктів: вона існує лише доти, доки існує THPDFViewerModel, що її створив. Повна родина типів розмітки та геометричних анотацій, доступних на сторінці, і те, як прямокутник розміщує кожен із них, описана в статті про анотації PDF у Delphi з HotPDF, а практичне правило просте: тримайте позначку одноразовою, поки документ ще обговорюється, і фіксуйте її як анотацію, щойно рішення стане остаточним
Де шар підсвічування зупиняється
Шар підсвічування, зі свого боку, не намагається виглядати як напівпрозорий маркер: RefreshDocument малює кожну область як двопіксельний контурний прямокутник власного кольору поверх кешованого растрового зображення сторінки, так само як малює збіги пошуку, а не змішує кольорову заливку з текстом під нею, тож класичний вигляд «жовтого змиву» доводиться домальовувати в коді застосунку або відкласти до власного потоку зовнішнього вигляду просунутої анотації. Одна можливість, яку варто повторно використати, щойно область уже існує, — CreateCurrentPageRegionSnapshot, який бере той самий THPDFRectangle, що вже несе підсвічування, і рендерить саме цю область у растрове зображення, корисно для прикріплення невеликого зображення попереднього перегляду до коментаря рецензування без експорту всієї сторінки. Збірка рецензування не мусить наперед обирати між двома механізмами: за замовчуванням робіть кожну нову позначку одноразовою областю THPDFViewerHighlight, поки гілка коментарів залишається відкритою, і викликайте AddLoadedHighlightAnnotation лише тоді, коли рецензент її вирішує, що тримає завантажений PDF незайманим під час туди-сюди обговорень, які створюють найбільше плинних змін. Описаний тут елемент керування переглядачем — частина стандартного компонента HotPDF для Delphi та C++Builder, поряд з рештою API анотацій і форм, згаданих вище