Технічна стаття

Декларативна верстка PDF у Delphi з тегованим виводом

HotPDF може будувати розбитий на сторінки документ із декларативного дерева, а не з координат. Ви складаєте THPDFDOMDocument із секцій, стеків, тексту, списків і таблиць, передаєте його THPDFDOMRenderer, і рендерер вимірює, розбиває на сторінки, малює обрамлення сторінки та, за запитом, формує структурне дерево PDF/UA, яке робить результат доступним. Код верстки жодного разу не обчислює координату y

Кожен, хто підтримував генератор звітів, керований координатами, знає, чому це важливо. Перша версія працює. Потім адреса клієнта розростається до трьох рядків, у таблиці з'являються нові рядки, локалізований заголовок переноситься, і кожна наступна позиція y виявляється неправильною. Виправлення накопичуються у вигляді ручних перевірок розриву сторінки, розкиданих по бізнес-логіці, а вимога тегованого PDF, що надходить через два роки, не може бути допасована до коду, який поняття не має, що таке абзац

Чим володіє дерево і чому володіння строге

DOM забезпечує одиничне володіння на кожному рівні: документ володіє своїми секціями, секція володіє своїм тілом, верхньою та нижньою частинами, а стеки, контейнери й таблиці володіють своїми дочірніми елементами. Повторне використання відбувається через Clone або через зареєстровану фабрику, ніколи — прикріпленням того самого об'єкта до двох батьків. Це правило не формальність. Компонент, що з'являється в дереві двічі, вимірювався б двічі з різними обмеженнями та звільнявся б двічі при знищенні

Практичний наслідок для коду, що викликає, полягає в тому, що допоміжні функції повертають нові екземпляри. Реєстрація фабрики через RegisterComponent і виклик CreateComponent дають вам іменований рецепт, що щоразу створює свіжий компонент, — саме так у дерево потрапляють повторювані елементи обрамлення на кшталт блока підпису чи юридичного колонтитула

uses
  HPDFDoc, HPDFLayoutDOM;

var
  Doc: THPDFDOMDocument;
  Section: THPDFDOMSection;
  Table: THPDFDOMTable;
  Row: THPDFDOMTableRow;
  I: Integer;
begin
  Doc := THPDFDOMDocument.Create;
  Doc.GenerateStructure := True;        // формувати структурне дерево PDF/UA
  Doc.Language := 'en-US';

  Section := Doc.AddSection;
  Section.PageWidth := 595;           // A4 у пунктах
  Section.PageHeight := 842;
  Section.MarginLeft := 56;
  Section.MarginTop := 56;
  Section.MarginRight := 56;
  Section.MarginBottom := 56;
  Section.Style.FontName := 'Helvetica';
  Section.Style.FontSize := 10;

  Section.Body.AddHeading('Annual maintenance report', 1);
  Section.Body.AddText('Every asset inspected during the reporting ' +
    'period is listed below, grouped by site.');
  Section.Body.AddSpacer(12);

  Table := THPDFDOMTable.Create('assets');
  Table.AddColumn(3);                 // вагові коефіцієнти, не абсолютні ширини
  Table.AddColumn(1);
  Table.AddColumn(1);
  Table.RepeatHeaders := True;
  Row := Table.AddRow(18, True);      // рядок заголовка
  Row[0].Text := 'Asset';
  Row[1].Text := 'Last service';
  Row[2].Text := 'Status';
  for I := 0 to High(Assets) do
  begin
    Row := Table.AddRow(16);
    Row[0].Text := Assets[I].Name;
    Row[1].Text := Assets[I].ServiceDate;
    Row[2].Text := Assets[I].Status;
  end;
  Section.Body.Add(Table);
end;

Як розбиття на сторінки уникає квадратичної вартості?

Наївний спосіб розбити дерево на сторінки — клонувати все, що не влізло, і перенести на наступну сторінку. У таблиці з десятьма тисячами рядків це клонує решту рядків на кожній сторінці й перетворює лінійний документ на квадратичний

Натомість HotPDF розбиває вузько. Рендерер верхнього рівня обходить дочірні елементи тіла за індексом і ніколи не клонує цілу секцію чи тіло. Лише вкладені стеки й контейнери, що справді перетинають межу сторінки, отримують клонування свого зачепленого піддерева, а два важкі листові типи несуть курсор, а не копію: продовження тексту зберігає діапазон вихідних символів, який ще залишається, а продовження таблиці зберігає зріз рядків, які ще не розміщено. Довгі документи лишаються лінійними, а довгі абзаци коштують однаково, розбиваються вони раз чи п'ять разів

Вимірювання лишається чесним щодо побічних ефектів. THPDFLayoutElement.Measure зобов'язаний бути вільним від побічних ефектів малювання, а фактичне розміщення завжди проходить через THotPDF.PlaceLayoutElement — ту саму центральну процедуру, що повторно вимірює розміщений фрагмент, налаштовує володіння переповненням і фіксує діагностику. Рендерер DOM вирішує лише політику нової сторінки, обрамлення сторінки, інтервали та тривалість життя продовжень

Правила заголовків таблиці, що запобігають нескінченному документу

Повторення заголовків таблиці на сторінках звучить просто, але приховує два режими збою. HotPDF вимагає, щоб рядки заголовка з'являлися лише в першій послідовній серії рядків і щоб перший розподіл вміщав усі рядки заголовка плюс принаймні один рядок тіла. Без другого правила заголовок, вищий за простір, що лишився, дав би сторінку, що містить лише заголовок, за нею — ще одну ідентичну сторінку, і так нескінченно

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

Існує також захисна стеля на глибину продовжень, бо власний компонент вільний реалізувати Split так, що завжди повертатиме еквівалентний хвіст. Рендерер перевіряє обмеження після відокремлення хвоста й перед початком наступної сторінки, а поточна ітерація звільняє хвіст у власному блоці finally, тож компонент стороннього розробника з неправильною поведінкою провалюється з діагностованою помилкою, а не заповнює диск

Один логічний елемент, багато фрагментів сторінок

Автоматичне теговання — це те місце, де модель розбиття на сторінки та модель структури мають узгодитися. Абзац, розбитий на дві сторінки, — це один логічний абзац, тож він має залишатися одним структурним елементом. Але ідентифікатори позначеного вмісту прив'язані до сторінки, тож кожен видимий фрагмент потребує власного MCID на тій сторінці, де він з'являється

HotPDF розв'язує це, зберігаючи єдиний структурний елемент і додаючи посилання на позначений вміст у його масив /K для кожного фрагмента, де пара /Pg і /MCID ідентифікує сторінку та ідентифікатор. Слот ParentTree для цього MCID вказує назад на той самий елемент. Це точнісінько те, чого очікує ISO 14289, і саме тому клони продовження відрізняються від звичайних клонів: звичайний Clone означає новий логічний вміст і отримує нову семантичну ідентичність, тоді як внутрішній клон продовження успадковує ідентичність компонента, який він продовжує

Повторне використання елементів шукається через індекс семантичних ідентичностей, відсортований за вказівником компонента й опрацьовуваний бінарним порівнянням, що утримує пошук логарифмічним на великих деревах. Індекс містить лише посилання без володіння; тривалість життя самих структурних об'єктів лишається за графом об'єктів PDF

Правила структури, які рендерер застосовує заздалегідь

Коли увімкнено GenerateStructure, кілька правил PDF/UA перевіряються під час рендерингу дерева, а не після того, як файл уже існує. Заголовки починаються з рівня 1 і не можуть пропускати рівні. LI може з'являтися лише всередині L, а Lbl і LBody — лише всередині LI. TR належить таблиці, а TH і TD — рядку. Ілюстрація без альтернативного тексту відхиляється в режимі PDF/UA

Раннє відхилення тут — свідомий вибір. Валідатор, що повідомляє про відсутній альтернативний текст після запису документа, каже вам, що пакет із десяти тисяч виписок потребує перегенерування; рендерер, що відмовляється прийняти компонент, каже вам, який саме компонент, поки дані, що його породили, ще в області видимості. Перевірка відповідності все одно належить у конвеєрі як окремий крок, і механіка цього описана у статті валідація PDF/A, PDF/X і PDF/UA

var
  Pdf: THotPDF;
  Renderer: THPDFDOMRenderer;
  Stats: THPDFDOMRenderStatistics;
begin
  Pdf := THotPDF.Create(nil);
  Renderer := THPDFDOMRenderer.Create;
  try
    Pdf.FileName := 'maintenance-report.pdf';
    Pdf.BeginDoc;
    Stats := Renderer.Render(Doc, Pdf);
    Pdf.EndDoc;

    Writeln(Format('%d page(s), %d placement(s), %d split(s)',
      [Stats.PageCount, Stats.PlacementCount, Stats.SplitCount]));
    Writeln(Format('structure elements=%d marked content=%d artifacts=%d',
      [Stats.StructureElementCount, Stats.MarkedContentCount,
       Stats.ArtifactCount]));
    Writeln(Format('deepest continuation chain: %d',
      [Stats.MaximumContinuationDepth]));
  finally
    Renderer.Free;
    Doc.Free;
    Pdf.Free;
  end;
end;

Запис статистики корисніший, ніж здається на перший погляд. Різке зростання SplitCount після зміни шаблону зазвичай означає, що компонент почав вимірюватися вищим за свій контейнер. Повзуче зростання MaximumContinuationDepth — раннє попередження про компонент, чий Split робить замало прогресу на сторінку. А порівняння ArtifactCount із кількістю сторінок продовження підтверджує, що повторювані заголовки справді були позначені як артефакти

Де DOM вписується поряд із прямим API

DOM не замінює пряме малювання; він розташовується поверх тих самих об'єктів сторінки. Усе, що розміщує рендерер, можна перемежовувати з прямими викликами на THotPDF, що важливо, коли звіту потрібен один елемент, розміщений вручну, наприклад зображення підпису в точному місці. Закриття сторінки лишається під керуванням AddPage і EndDoc, тож режим негайного скидання не тримає завершені сторінки в пам'яті, а резидентна пам'ять лишається під керуванням поточних продовжень, шрифтових ресурсів і звичайного графа об'єктів документа

Обирайте DOM, коли вміст керований даними, а верстка — правилами, і лишайте пряме малювання для фіксованої графіки. Якщо ваш поточний біль стосується саме розбиття таблиць на сторінки, вужчий підхід у статті генерування таблиць у PDF варто прочитати спершу, а поведінка на рівні тексту, як-от вирівнювання, описана у статті вирівнювання тексту

Декларативна верстка, автоматичне теговання та API прямого малювання постачаються в одному компоненті для Delphi та C++Builder; повний перелік можливостей на сторінці компонента HotPDF PDF для Delphi