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-бекенд і мапування підсвіток скопіювали ту саму матрицю, усі вони виправлялися разом, а код сплощення тепер уживає ту саму конвенцію за годинниковою стрілкою, що й рендерер
Як 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
Чому анотації ламаються, коли ви повертаєте лише /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 — ця сторінка
Чого сплощення не покриває?
Сплощення — це геометричне переписування власних об'єктів однієї сторінки, і кілька ситуацій випадають з нього тихо, а не голосно
- Сторінки, чия ефективна ротація вже 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