Техническа статия

Flatten въртенето на PDF страница в Delphi без чупене

HotPDF изравнява въртенето на PDF страници с THotPDF.FlattenLoadedPageRotation: методът опакова съдържанието на всяка обърната страница в transform cm по часовниковата стрелка, пренаписва всяка page кутия, която страницата действително има, обръща геометрията на анотациите, appearance матриците, изричните дестинации и геометрията на tagged структурата със същия ъгъл, а после задава /Rotate на 0. Страницата изглежда идентична във viewer, но координатната ѝ система вече е изправена. Това има значение в момента, в който инструмент надолу по веригата, print RIP или вашият код за щампиране игнорира /Rotate и поставя неща в суров user space

Типичният спусък е скенер или мобилно capture приложение, което записва пейзажни страници като портретни media с /Rotate 90. Всеки viewer ги показва правилно, така че никой не забелязва, докато някой не щампне номер на страница в „долния десен ъгъл" и той не излезе настрани по левия ръб, или стъпка за imposition, четяща само /MediaBox, не подреди портретен слот за пейзажна страница. Изравняването звучи като матрична работа на един ред. На практика пипа пет page кутии, три вида анотационна геометрия, линк целите на документа и структурното дърво, а всяко от тях има собствено правило в ISO 32000-1

В коя посока /Rotate обръща PDF страница?

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

Посоката лесно се сбърква по начин, който оцелява при тестване, и по-стари HotPDF build-ове точно това направиха. Старата page-to-device матрица разменяше y компонентите за 90 и 270, което дава отражение по диагонала вместо въртене: ориентацията на матрицата се обръща спрямо необърнатия случай. И двата ъгла пак „изглеждат обърнати", bitmap-ът има разменени ширина и височина, а round trip от страница към изглед и обратно връща началната точка, така че проверките за размер и round-trip тестовете минават всички. Единствена надеждна проверка е където свършва ъглов маркер, сравнен пиксел по пиксел с reference renderer. Понеже viewer моделът, SIMD render backend-ът и highlight съпоставянето бяха копирали същата матрица, всички бяха поправени заедно, а кодът за изравняване вече ползва същата конвенция по часовниковата стрелка като renderer-а

Как HotPDF изравнява въртенето на страница в Delphi: портретна страница, съхранена с /Rotate 90, се показва по часовниковата стрелка като пейзажен изглед 792 на 612, device съпоставянето X = (y - Bottom) * Scale, Y = (x - Left) * Scale мести всеки ъгъл, а размяната на y компонентите на матрицата дава отражение, което само сравнение с ъглов маркер хваща
Viewer-ите обръщат страницата по часовниковата стрелка за показване, докато байтовете си остават портретни — GetLoadedPageRotation първо минава /Parent веригата, защото /Rotate е един от четирите наследяеми page атрибута

Как FlattenLoadedPageRotation пренаписва страница

FlattenLoadedPageRotation(PageRange, Info) обработва всяка страница в PageRange, чиято ефективна ротация е 90, 180 или 270, и връща броя изравнени страници. Празен PageRange значи всички страници; иначе низът ползва обичайния едно-базиран синтаксис '1-3,7', а номер на страница извън обхвата вдига изключение, вместо да бъде прескачан. Оригиналните content stream-и никога не се преенкодират. Методът залепя отпред нов stream, съдържащ q 0 -1 1 0 -Bottom Width+Left cm (за 90 градуса), към /Contents на страницата, залепя отзад stream с Q и най-после записва изричен /Rotate 0 в page речника, така че наследена стойност върху /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;

Record-ът THPDFRotationFlattenInfo си заслужава да се логва, а не да се изхвърля. ScannedPageCount е размерът на диапазона, FlattenedPageCount е равен на връщаната стойност, а трите брояча Transformed... ви казват дали документът е имал линкове, bookmarks или tagged геометрия, сочещи към обърнатите страници. Партида, в която всеки файл докладва нула дестинации, е ок; tagged PDF/UA файл, докладвал нула structure геометрия, когато сте очаквали figure bounding boxes, е сигнал да го огледате на ръка

Кои page кутии пренаписва изравняването и в какъв ред?

Изравняването пренаписва само кутиите, които страницата вече има, и чете всяка кутия, преди да е записало някоя. Редът има значение заради веригата по подразбиране: GetLoadedPageBox(PageIndex, pbCropBox, ...) връща /MediaBox, когато страницата няма /CropBox, а /BleedBox, /TrimBox и /ArtBox се подразбират от CropBox (§14.11.2). По-ранна версия четеше, трансформираше и записваше по една кутия наведнъж. Тя пренаписа MediaBox първо, после прочете „CropBox", получи обратно вече обърнатия MediaBox, обърна го втори път и записа CropBox, който страницата никога не е имала — което отреже пейзажна страница до квадрат. Правилата за наследяване се разделят по същия начин: MediaBox и CropBox се търсят по /Parent веригата, докато Bleed, Trim и ArtBox броят само ако седят върху самия page речник, така че изгубен /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]));
  // Вярно дори без ключ /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;

Пуснете този helper преди и след изравняването и числата сами си обясняват. За страница на 90 градуса с MediaBox [0 0 612 792] изравненият MediaBox става [0 0 792 612]; всяка пренаписана кутия се мапва през същото обръщане по часовниковата стрелка, спрямо оригиналното начало на MediaBox, така че новият MediaBox винаги започва от началото, а другите кутии пазят позицията си вътре в него. GetLoadedPageVisibleBox връща това, което viewer-ите показват и принтерите печатат — CropBox, отрязан до MediaBox и нормализиран така, че Left е по-малък от Right — а renderer-ът, SVG експортът, viewer-ът и print пътят на HotPDF ползват всички тази съща кутия. Когато ви трябва размерът на страницата, който човек вижда, викайте GetLoadedPageVisibleBox, а не четете /MediaBox

Защо HotPDF чете всяка page кутия, преди да е записало някоя, по време на FlattenLoadedPageRotation: BleedBox, TrimBox и ArtBox се подразбират от CropBox, който самият се връща към MediaBox, така че обръщането на кутиите една по една накара CropBox да прочете вече пренаписания MediaBox, а второ обръщане записа кутия, която страницата никога не е имала, отрязвайки пейзажна страница до квадрат
Веригата по подразбиране значи, че изходът на една кутия е входът на друга — прочетете всичко първо, трансформирайте спрямо оригиналното начало на MediaBox, чак после записвайте

Защо анотациите се чупят, ако обърнете само /Rect?

Анотациите се чупят, защото appearance stream не се чертае направо в /Rect. Под §12.5.5 viewer-ът първо трансформира /BBox на формата чрез неговия /Matrix, после мащабира и премества bounding box-а на този резултат в /Rect. Обърнете само /Rect и 200 × 40 щампа се изцедва в 40 × 200 слот, нечетема и настрани. Затова FlattenLoadedPageRotation умножава отдясно обръщането на страницата по часовниковата стрелка върху всеки appearance /Matrix (за 90 градуса [0 -1 1 0 0 0] в конвенцията row-vector), през appearance-ите /N, /R и /D и всяко състояние вътре в тях. Един appearance stream може да се споделя от няколко анотации или състояния, така че всеки stream се обръща точно веднъж на извикване. Единственият случай без чист отговор е stream, споделен през страници с различни ротации; той следва първата страница, която го стигне

Още две правила държат form полетата и стикерните бележки на място. Записът /MK /R на widget (§12.5.6.19) е ъгъл обратно на часовниковата стрелка, затова от него се изважда часовниковият ъгъл на страницата, по модул 360; пропуснете го и следващото преизграждане на appearance ще нарисува текста на полето в грешната посока. Анотации с флага NoRotate (битова позиция 5, стойност 16, §12.5.3) остават изправени на обърната страница и се въртят около горния ляв ъгъл на /Rect им, така че изравняването пази ширината, височината и изправения им вид и мести само този ъгъл там, където обръщането го слага. Освен анотациите методът обръща също /QuadPoints, /Vertices, /L и /InkList, пренаписва изрични дестинации, които назовават страницата (/XYZ точки, /FitR правоъгълници, а /FitH / /FitV разменени при 90 и 270 градуса, §12.3.2.2), и трансформира tagged геометрия като записи /BBox на атрибути за структурни елементи, чийто /Pg е страницата

Защо анотациите се чупят, когато HotPDF страница се изравни само с обръщане на /Rect: 200 на 40 щампа се мащабира в 40 на 200 слот и става нечетема, затова FlattenLoadedPageRotation умножава отдясно часовниковото обръщане върху всеки appearance /Matrix през /N, /R и /D, нагажда обратно-часовниковия /MK /R и върти NoRotate анотациите около техния горен ляв ъгъл
Viewer-ът нагажда трансформирания BBox на appearance-а в /Rect, така че самият stream трябва да се обърне — един проход на споделен appearance, точно веднъж на извикване

Какво изравняването не покрива?

Изравняването е геометрично пренаписване на собствените обекти на една страница, а няколко ситуации излизат извън него тихо, а не на викове

  • Страници с ефективна ротация вече 0, или с липсващ MediaBox или MediaBox с нулева ширина или височина, се прескачат без грешка; сравнявайте връщаната стойност с броя страници, които сте очаквали да се променят
  • Form XObjects, сочени от page ресурсите, пазят собствените си /BBox в form space, защото външният cm вече ги обръща; сканирането на структурното дърво следва само /K и /A, така че никога не влиза втори път в ресурсите или анотациите на страницата
  • Дестинациите се откриват със сканиране на всеки indirect обект по веднъж на изравнена страница, така че голям документ със стотици обърнати страници плаща за това минаване на всяка
  • Page renderer-ът на HotPDF не чертае анотации, така че визуална проверка на обърнати щампи първо се нуждае от FlattenLoadedAnnotations
// Изпиче appearance-ите в съдържанието, за да може renderer-ът да ги покаже,
// после рендерирай страница 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;

За по-дълбок фон разказът продължава с анотационната страна в синтезирането на annotation appearance-и преди изравняването им, renderer-ът зад сравнението преди-и-след е покрит в рендерирането на заредена PDF страница към bitmap, а redaction и N-up долепяне върху заредени PDF-и показва същата техника за долепяне към content stream, на която разчитат префиксът и суфиксът за ротация. HotPDF, включително FlattenLoadedPageRotation и четците на page кутии, е наличен за Delphi и C++Builder на страницата на HotPDF Delphi PDF компонента