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

Сплощення ротації сторінки PDF без ламання page boxes

HotPDF сплощує ротацію сторінок PDF через THotPDF.FlattenLoadedPageRotation: метод обгортає вміст кожної повернутої сторінки за годинниковою стрілкою трансформацією cm, переписує кожен page box, який сторінка справді має, повертає геометрію анотацій, матриці appearance, явні призначення і теговану структурну геометрію на той самий кут, а потім ставить /Rotate у 0. Сторінка виглядає ідентично в переглядачі, але її система координат тепер стоїть прямо. Це стає важливим щойно нижчий за течією інструмент, print RIP чи ваш власний код штампування ігнорує /Rotate і кладе речі в сировий user space

Типовий тригер — сканер чи мобільний застосунок захоплення, який пише ландшафтні сторінки як портретні медіа з /Rotate 90. Кожен переглядач показує їх правильно, тож ніхто не помічає, поки хтось не поставить номер сторінки в «правому нижньому» і той не приземлиться боком уздовж лівого краю, або крок імпозиції, що читає лише /MediaBox, не розкладе портретний слот для ландшафтної сторінки. Сплощення звучить як матрична робота на один рядок. На практиці воно торкається п'яти page boxes, трьох сортів анотаційної геометрії, цілей посилань документа і дерева структури, і в кожного з цього в ISO 32000-1 своє правило

У бік повертає /Rotate сторінку PDF?

/Rotate повертає сторінку за годинниковою стрілкою для показу і друку, кратностями 90 градусів (ISO 32000-1 §7.7.3.3, Table 30). На 90 градусах лівий край медіа стає верхом, а верхній край — правим боком, тож у пристрійному просторі з y вниз відображення — це X = (y - Bottom) * Scale і Y = (x - Left) * Scale. На 270 градусах правий край стає верхом. /Rotate — також одна з лише чотирьох успадковуваних атрибутів сторінки, разом із /Resources, /MediaBox і /CropBox (§7.7.3.4), тож словник сторінки без власного /Rotate все одно може бути повернутий предком /Pages. THotPDF.GetLoadedPageRotation проходить ланцюг /Parent і нормалізує результат у 0–359 — це те значення, яке вам треба, а не сирий ключ на сторінці

Напрямок легко зіпсувати так, що це виживає в тестах, і ранні збірки HotPDF зробили саме це. Стара матриця сторінка-в-пристрій міняла місцями y-компоненти для 90 і 270, що дає відображення через діагональ замість ротації: орієнтація матриці перекидається відносно неротованого випадку. Обидва кути все ще «виглядають повернутими», бітмап має обмінені ширину і висоту, а зворотний шлях від сторінки до вигляду й назад повертає стартову точку, тож перевірки розмірів і round-trip тести всі проходять. Єдина надійна перевірка — де опиниться кутова мітка, звірена піксель-за-пікселем із референсним рендерером. Оскільки модель переглядача, SIMD render-бекенд і мапування підсвіток скопіювали ту саму матрицю, усі вони виправлялися разом, а код сплощення тепер уживає ту саму конвенцію за годинниковою стрілкою, що й рендерер

Як HotPDF сплощує ротацію сторінки в Delphi: портретна сторінка, збережена з /Rotate 90, показується за годинниковою стрілкою як ландшафтний вигляд 792 на 612, пристрійне відображення X = (y - Bottom) * Scale, Y = (x - Left) * Scale пересуває кожен кут, а обмін y-компонент матриці дає відображення, яке ловить лише порівняння кутових міток
Переглядачі повертають сторінку за годинниковою стрілкою для показу, тоді як байти лишаються портретними — GetLoadedPageRotation спершу проходить ланцюг /Parent, бо /Rotate — один із чотирьох успадковуваних атрибутів сторінки

Як FlattenLoadedPageRotation переписує сторінку

FlattenLoadedPageRotation(PageRange, Info) обробляє кожну сторінку в PageRange, чия ефективна ротація — 90, 180 чи 270, і повертає кількість сплощених сторінок. Порожній PageRange означає всі сторінки; інакше рядок уживає звичайний одно-базований синтаксис '1-3,7', а номер сторінки поза діапазоном підіймає виняток, а не пропускається. Початкові потоки вмісту ніколи не перекодовуються. Метод дописує новий потік із q 0 -1 1 0 -Bottom Width+Left cm (для 90 градусів) попереду /Contents сторінки, додає в кінець потік із Q і наостанок пише явний /Rotate 0 у словник сторінки, щоб успадковане значення на вузлі /Pages не могло повернути сторінку вдруге

var
  Pdf: THotPDF;
  Info: THPDFRotationFlattenInfo;
  Flattened: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('scanned-batch.pdf') > 0 then
    begin
      // '' = усі сторінки; сторінки на 0 градусів скануються, але лишаються спокій
      Flattened := Pdf.FlattenLoadedPageRotation('', Info);
      Writeln(Format('Scanned %d, flattened %d pages', [Info.ScannedPageCount, Info.FlattenedPageCount]));
      Writeln(Format('Turned %d annotations, %d destinations, %d tagged geometry entries',
        [Info.TransformedAnnotationCount, Info.TransformedDestinationCount,
         Info.TransformedStructureGeometryCount]));
      if Flattened > 0 then
        Pdf.SaveLoadedDocument('scanned-batch-upright.pdf');
    end;
  finally
    Pdf.Free;
  end;
end;

Запис THPDFRotationFlattenInfo варто логувати, а не викидати. ScannedPageCount — розмір діапазону, FlattenedPageCount дорівнює значенню, що повертається, а три лічильники Transformed... кажуть, чи мав документ посилання, закладки чи теговану геометрію, націлені на повернуті сторінки. Пакет, де кожен файл звітує нуль призначень, — норм; тегований PDF/UA-файл, що звітує нуль структурної геометрії, коли ви очікували bounding boxes ілюстрацій, — сигнал оглянути його руками

Які page boxes переписує сплощення і в якому порядку?

Сплощення переписує лише бокси, які сторінка вже має, і читає кожен бокс, перш ніж писати будь-який. Порядок важливий через ланцюг усталених значень: GetLoadedPageBox(PageIndex, pbCropBox, ...) повертає /MediaBox, коли сторінка не має /CropBox, а /BleedBox, /TrimBox і /ArtBox усталено до CropBox (§14.11.2). Раніша версія читала, трансформувала і писала один бокс за раз. Вона переписувала MediaBox першим, потім читала «CropBox», отримувала назад уже повернутий MediaBox, повернула його вдруге і писала CropBox, якого сторінка ніколи не мала, — що обрізало ландшафтну сторінку до квадрата. Правила успадкування діляться так само: MediaBox і CropBox шукаються вздовж ланцюга /Parent, тоді як Bleed, Trim і ArtBox зараховуються, лише якщо сидять на самому словнику сторінки, тож блудний /TrimBox на вузлі /Pages вважається відсутнім і ніколи не копіюється на сторінку

procedure DumpPageGeometry(Pdf: THotPDF; PageIndex: Integer);
var
  L, B, R, T: Single;
begin
  Writeln('Effective /Rotate: ', Pdf.GetLoadedPageRotation(PageIndex));
  if Pdf.GetLoadedPageBox(PageIndex, pbMediaBox, L, B, R, T) then
    Writeln(Format('MediaBox [%g %g %g %g]', [L, B, R, T]));
  // True навіть без ключа /TrimBox: значення відкатується до CropBox, потім MediaBox
  if Pdf.GetLoadedPageBox(PageIndex, pbTrimBox, L, B, R, T) then
    Writeln(Format('TrimBox  [%g %g %g %g]', [L, B, R, T]));
  // Пресет Letter; GetLoadedPageVisibleBox не торкається виходів при невдачі
  L := 0; B := 0; R := 612; T := 792;
  Pdf.GetLoadedPageVisibleBox(PageIndex, L, B, R, T);
  Writeln(Format('Visible  [%g %g %g %g]', [L, B, R, T]));
end;

Прогоніть той хелпер до і після сплощення, і числа пояснять самі себе. Для сторінки на 90 градусів із MediaBox [0 0 612 792] сплощений MediaBox стає [0 0 792 612]; кожен переписаний бокс відображається тим самим поворотом за годинниковою стрілкою, відносно початку початкового MediaBox, тож новий MediaBox завжди стартує в початку, а решта боксів тримає свою позицію всередині нього. GetLoadedPageVisibleBox повертає те, що показують переглядачі і друкують принтери, — CropBox, обрізаний до MediaBox і нормалізований так, що Left менший за Right, — і рендерер, SVG-експорт, переглядач і друкований шлях HotPDF усе вживають той самий бокс. Коли вам треба розмір сторінки, який бачить людина, викликайте GetLoadedPageVisibleBox, а не читайте /MediaBox

Чому HotPDF читає кожен page box перед записом будь-якого під час FlattenLoadedPageRotation: BleedBox, TrimBox і ArtBox усталено до CropBox, який сам відкатується до MediaBox, тож повернення боксів по одному змушувало CropBox прочитати вже переписаний MediaBox, а другий поворот писав бокс, якого сторінка ніколи не мала, обрізаючи ландшафтну сторінку до квадрата
Ланцюг усталених значень означає, що вивід одного бокса — це ввід іншого: прочитайте все спершу, трансформуйте відносно початку початкового MediaBox, потім пишіть

Чому анотації ламаються, коли ви повертаєте лише /Rect?

Анотації ламаються, бо потік appearance не малюється прямо в /Rect. За §12.5.5 переглядач спершу трансформує /BBox форми його /Matrix, а потім масштабує і переносить bounding box того результату в /Rect. Поверніть лише /Rect — і штамп 200 × 40 буде втиснутий у слот 40 × 200, нечитабельний і на боку. FlattenLoadedPageRotation тому право-домножує поворот сторінки за годинниковою стрілкою на кожну appearance /Matrix (для 90 градусів — [0 -1 1 0 0 0] у конвенції рядкових векторів), по appearances /N, /R і /D та кожному стані всередині них. Один потік appearance може бути спільним для кількох анотацій чи станів, тож кожен потік повертається рівно один раз за виклик. Єдиний випадок без чистої відповіді — потік, спільний між сторінками з різними ротаціями; він слідує за першою сторінкою, яка до нього дійшла

Ще два правила тримають поля форм і стікері на місцях. Запис /MK /R віджета (§12.5.6.19) — кут проти годинникової стрілки, тож кут сторінки за годинниковою стрілкою віднімається з нього за модулем 360; пропустіть це — і наступна регенерація appearance намалює текст поля не в тому напрямку. Анотації з прапорцем NoRotate (бітова позиція 5, значення 16, §12.5.3) лишаються прямими на повернутій сторінці і обертаються навколо верхнього лівого кута свого /Rect, тож сплощення зберігає їхню ширину, висоту і пряме appearance і лише пересуває той кут туди, куди його ставить поворот. Понад анотації метод також повертає /QuadPoints, /Vertices, /L і /InkList, переписує явні призначення, що називають сторінку (точки /XYZ, прямокутники /FitR, а /FitH / /FitV міняються місцями на 90 і 270 градусах, §12.3.2.2), і трансформує теговану геометрію, як-от записи /BBox атрибутів для елементів структури, чия /Pg — ця сторінка

Чому анотації ламаються, коли сторінку HotPDF сплощують самим поворотом /Rect: штамп 200 на 40 масштабується в слот 40 на 200 і стає нечитабельним, тож FlattenLoadedPageRotation право-домножує поворот за годинниковою стрілкою на кожну appearance /Matrix по /N, /R і /D, коригує протигодинникову /MK /R і обертає анотації NoRotate навколо їхнього верхнього лівого кута
Переглядач втискає трансформований BBox appearance у /Rect, тож сам потік мусить повернутися — один прохід на спільне appearance, рівно один раз за виклик

Чого сплощення не покриває?

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

  • Сторінки, чия ефективна ротація вже 0, чи в яких MediaBox відсутній або має нульову ширину чи висоту, пропускаються без помилки; звірте значення, що повернулося, з кількістю сторінок, які ви очікували змінити
  • Form XObjects, на які посилаються ресурси сторінки, тримають власний /BBox у form space, бо зовнішній cm уже повертає їх; скан дерева структури слідує лише за /K і /A, тож він ніколи не заходить у ресурси чи анотації сторінки вдруге
  • Призначення знаходяться скануванням кожного непрямого об'єкта один раз на сплощену сторінку, тож великий документ із сотнями повернутих сторінок платить за той прохід на кожній
  • Сторінковий рендерер HotPDF не малює анотації, тож візуальна перевірка повернутих штампів потребує спершу FlattenLoadedAnnotations
// Впікаємо appearances у вміст, щоб рендерер міг їх показати,
// потім рендеримо сторінку 1 до і після зняття її /Rotate
Pdf.FlattenLoadedAnnotations('1');
Before := Pdf.RenderLoadedPageToBitmap(0, 96);
try
  Pdf.FlattenLoadedPageRotation('1', Info);
  After := Pdf.RenderLoadedPageToBitmap(0, 96);
  try
    Assert((Before.Width = After.Width) and (Before.Height = After.Height));
    // Порівнюйте пікселі кутової мітки тут, а не лише розміри
  finally
    After.Free;
  end;
finally
  Before.Free;
end;

Для глибшого тла анотаційний бік цієї історії продовжується в синтезуванні appearances анотацій перед сплощенням, рендерер за порівнянням до-і-після покритий у рендерингу завантаженої сторінки PDF у бітмап, а редакція та N-up зшивання на завантажених PDF показують ту саму техніку дописування в потік вмісту, на яку спираються префікс і суфікс ротації. HotPDF, включно з FlattenLoadedPageRotation і читачами page boxes, доступний для Delphi і C++Builder на сторінці HotPDF Delphi PDF component