Artykuł techniczny

Spłaszczanie obrotu strony PDF bez psucia ramek w Delphi

HotPDF spłaszcza obrót stron PDF przez THotPDF.FlattenLoadedPageRotation: metoda owija treść każdej obróconej strony transformacją cm zgodną z ruchem wskazówek zegara, przepisuje każdą ramkę strony, którą strona faktycznie ma, obraca geometrię adnotacji, macierze wyglądu, jawne cele i otagowaną geometrię struktury o ten sam kąt, a potem ustawia /Rotate na 0. Strona wygląda identycznie w przeglądarce, ale jej układ współrzędnych jest teraz pionowy. To ma znaczenie w chwili, gdy narzędzie dalej w potoku, RIP drukarki albo twój własny kod stemplowania zignoruje /Rotate i położy rzeczy w surowej przestrzeni użytkownika

Typowym wyzwalaczem jest skaner albo mobilna aplikacja skanująca, która zapisuje strony poziome jako pionowy nośnik z /Rotate 90. Każda przeglądarka pokazuje je poprawnie, więc nikt nie zauważa, dopóki ktoś nie ostempluje numeru strony w „prawym dolnym rogu" i nie wyląduje on bokiem wzdłuż lewej krawędzi, albo krok impozycji czytający wyłącznie /MediaBox nie rozplanuje pionowego slotu dla poziomej strony. Spłaszczanie brzmi jak robota na jednej linii macierzy. W praktyce dotyka pięciu ramek strony, trzech rodzajów geometrii adnotacji, celów łączy dokumentu i drzewa struktury, a każdy z tych elementów ma własną regułę w ISO 32000-1

W którą stronę /Rotate obraca stronę PDF?

/Rotate obraca stronę zgodnie z ruchem wskazówek zegara przy wyświetlaniu i drukowaniu, w wielokrotnościach 90 stopni (ISO 32000-1 §7.7.3.3, tabela 30). Przy 90 stopniach lewa krawędź nośnika staje się górą, a górna krawędź prawą stroną, więc w y-down przestrzeni urządzenia mapowanie to X = (y - Bottom) * Scale i Y = (x - Left) * Scale. Przy 270 stopniach prawa krawędź staje się górą. /Rotate to też jeden z zaledwie czterech dziedziczalnych atrybutów strony, obok /Resources, /MediaBox i /CropBox (§7.7.3.4), więc słownik strony bez własnego /Rotate może zostać obrócony przez przodka /Pages. THotPDF.GetLoadedPageRotation przechodzi łańcuch /Parent i normalizuje wynik do 0–359, i to jest wartość, której chcesz, nie surowy klucz na stronie

Kierunek łatwo pomylić w sposób, który przeżywa testy, i wcześniejsze buildy HotPDF zrobiły dokładnie to. Stara macierz strona-urządzenie zamieniała komponenty y dla 90 i 270, co daje odbicie po przekątnej zamiast obrotu: orientacja macierzy przewraca się względem przypadku nieobróconego. Oba kąty nadal „wyglądają obrócone", bitmapa ma zamienione szerokość i wysokość, a cykl ze strony do widoku i z powrotem wraca do punktu startowego, więc kontrole wymiarów i testy cykli wszystkie przechodzą. Jedynym wiarygodnym sprawdzeniem jest to, gdzie wyląduje znacznik rogu, porównany piksel po pikselu z renderem referencyjnym. Ponieważ model przeglądarki, backend renderujący SIMD i mapowanie podświetleń skopiowały tę samą macierz, wszystkie poprawiono razem, a kod spłaszczania używa teraz tej samej konwencji zgodnej z zegarem co renderer

Jak HotPDF spłaszcza obrót strony w Delphi: pionowa strona zapisana z /Rotate 90 wyświetla się zgodnie z zegarem jako poziomy widok 792 na 612, mapowanie urządzenia X = (y - Bottom) * Scale, Y = (x - Left) * Scale przestawia każdy róg, a zamiana komponentów y macierzy daje odbicie, które wyłapie tylko porównanie znaczników rogów
Przeglądarki obracają stronę zgodnie z zegarem przy wyświetlaniu, podczas gdy bajty zostają pionowe — GetLoadedPageRotation najpierw przechodzi łańcuch /Parent, bo /Rotate to jeden z czterech dziedziczalnych atrybutów strony

Jak FlattenLoadedPageRotation przepisuje stronę

FlattenLoadedPageRotation(PageRange, Info) przetwarza każdą stronę w PageRange, której efektywny obrót wynosi 90, 180 albo 270, i zwraca liczbę spłaszczonych stron. Pusty PageRange znaczy wszystkie strony; w przeciwnym razie łańcuch używa zwyczajowej składni jedno-bazowej '1-3,7', a numer strony poza zakresem rzuca wyjątek, zamiast zostać pominięty. Oryginalne strumienie treści nigdy nie są rekodowane. Metoda dokleja z przodu nowy strumień zawierający q 0 -1 1 0 -Bottom Width+Left cm (dla 90 stopni) do /Contents strony, dokleja z tyłu strumień z Q, a na koniec zapisuje jawne /Rotate 0 do słownika strony, żeby odziedziczona wartość na węźle /Pages nie obróciła strony drugi raz

var
  Pdf: THotPDF;
  Info: THPDFRotationFlattenInfo;
  Flattened: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('scanned-batch.pdf') > 0 then
    begin
      // '' = wszystkie strony; strony przy 0 stopni są skanowane, ale zostawione w spokoju
      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;

Rekord THPDFRotationFlattenInfo warto logować, a nie wyrzucać. ScannedPageCount to rozmiar zakresu, FlattenedPageCount równa się wartości zwracanej, a trzy liczniki Transformed... mówią ci, czy dokument miał łącza, zakładki albo otagowaną geometrię celującą w obrócone strony. Partia, w której każdy plik raportuje zero celów, jest w porządku; otagowany plik PDF/UA, który raportuje zero geometrii struktury, gdy spodziewałeś się ramek figur, to sygnał, żeby obejrzeć go ręcznie

Które ramki strony przepisuje spłaszczanie i w jakiej kolejności?

Spłaszczanie przepisuje tylko ramki, które strona już ma, i czyta każdą ramkę, zanim napisze cokolwiek. Kolejność ma znaczenie przez łańcuch domyśleń: GetLoadedPageBox(PageIndex, pbCropBox, ...) zwraca /MediaBox, gdy strona nie ma /CropBox, a /BleedBox, /TrimBox i /ArtBox domyślają się CropBoxa (§14.11.2). Wcześniejsza wersja czytała, transformowała i pisała po jednej ramce naraz. Przepisała najpierw MediaBox, potem przeczytała „CropBox", dostała z powrotem już obrócony MediaBox, obróciła go drugi raz i napisała CropBoxa, którego strona nigdy nie miała, co przycięło poziomą stronę do kwadratu. Reguły dziedziczenia dzielą się tak samo: MediaBox i CropBox są wyszukiwane wzdłuż łańcucha /Parent, podczas gdy Bleed, Trim i ArtBox liczą się tylko, jeśli siedzą na samym słowniku strony, więc zabłąkany /TrimBox na węźle /Pages jest traktowany jako nieobecny i nigdy nie jest kopiowany na stronę

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 nawet bez klucza /TrimBox: wartość spada do CropBoxa, potem MediaBoxa
  if Pdf.GetLoadedPageBox(PageIndex, pbTrimBox, L, B, R, T) then
    Writeln(Format('TrimBox  [%g %g %g %g]', [L, B, R, T]));
  // Ustawiony wstępnie Letter; GetLoadedPageVisibleBox zostawia wyjścia nietknięte przy porażce
  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;

Uruchom ten helper przed spłaszczeniem i po nim, a liczby wytłumaczą się same. Dla strony 90-stopniowej z MediaBox [0 0 612 792] spłaszczony MediaBox staje się [0 0 792 612]; każda przepisywana ramka jest mapowana przez ten sam obrót zgodny z zegarem, względem początku oryginalnego MediaBoxa, więc nowy MediaBox zawsze zaczyna się w początku, a pozostałe ramki zachowują swoją pozycję wewnątrz niego. GetLoadedPageVisibleBox zwraca to, co pokazują przeglądarki i co drukują drukarki: CropBoxa przyciętego do MediaBoxa i znormalizowanego tak, że Left jest mniejsze od Right, i tego samego pudełka używają renderer, eksport SVG, przeglądarka i ścieżka druku HotPDF. Gdy potrzebujesz rozmiaru strony takiego, jaki widzi człowiek, wołaj GetLoadedPageVisibleBox, a nie czytaj /MediaBox

Dlaczego HotPDF czyta każdą ramkę strony przed pisaniem czegokolwiek podczas FlattenLoadedPageRotation: BleedBox, TrimBox i ArtBox domyślają się CropBoxa, który sam spada do MediaBoxa, więc obracanie ramek po jednej sprawiło, że CropBox przeczytał już przepisany MediaBox, a drugi obrót napisał ramkę, której strona nigdy nie miała, przycinając poziomą stronę do kwadratu
Łańcuch domyśleń znaczy, że wyjście jednej ramki jest wejściem drugiej — przeczytaj najpierw wszystko, transformuj względem początku oryginalnego MediaBoxa, potem pisz

Dlaczego adnotacje się psują, gdy obrócisz tylko /Rect?

Adnotacje się psują, bo strumień wyglądu nie jest rysowany prosto do /Rect. Zgodnie z §12.5.5 przeglądarka najpierw transformuje /BBox formularza jego /Matrix, a potem skaluje i przesuwa ramkę ograniczającą tego wyniku do /Rect. Obróć tylko /Rect, a stempel 200 × 40 zostanie wciśnięty w slot 40 × 200, nieczytelny i na boku. FlattenLoadedPageRotation mnoży więc z prawej obrót strony zgodny z zegarem na każdą /Matrix wyglądu (dla 90 stopni [0 -1 1 0 0 0] w konwencji wektorów-wierszy), w obrębie wyglądów /N, /R i /D oraz każdego stanu w ich środku. Jeden strumień wyglądu może być współdzielony przez kilka adnotacji albo stanów, więc każdy strumień jest obracany dokładnie raz na wywołanie. Jedyny przypadek bez czystej odpowiedzi to strumień współdzielony między stronami o różnych obrotach; idzie za pierwszą stroną, która go dosięgnie

Dwie dalsze reguły trzymają pola formularza i karteczki na miejscu. Wpis /MK /R widżetu (§12.5.6.19) to kąt przeciwny do wskazówek zegara, więc kąt strony zgodny z zegarem jest od niego odejmowany modulo 360; pomiń to, a następna regeneracja wyglądu narysuje tekst pola w złym kierunku. Adnotacje z flagą NoRotate (pozycja bitu 5, wartość 16, §12.5.3) zostają pionowe na obróconej stronie i obracają się wokół lewego górnego rogu swojego /Rect, więc spłaszczanie zachowuje ich szerokość, wysokość i pionowy wygląd, a przesuwa tylko ten róg tam, gdzie każe go obrót. Poza adnotacjami metoda obraca też /QuadPoints, /Vertices, /L i /InkList, przepisuje jawne cele wymieniające stronę (punkty /XYZ, prostokąty /FitR oraz /FitH / /FitV zamienione przy 90 i 270 stopniach, §12.3.2.2) i transformuje otagowaną geometrię, jak wpisy /BBox atrybutów elementów struktury, których /Pg to strona

Dlaczego adnotacje się psują, gdy strona HotPDF jest spłaszczana samym obrotem /Rect: stempel 200 na 40 jest skalowany do slotu 40 na 200 i staje się nieczytelny, więc FlattenLoadedPageRotation mnoży z prawej obrót zgodny z zegarem na każdą /Matrix wyglądu w obrębie /N, /R i /D, koryguje przeciwny do zegara /MK /R i obraca adnotacje NoRotate wokół ich lewego górnego rogu
Przeglądarka wciska przetransformowany BBox wyglądu w /Rect, więc sam strumień musi się obrócić — jeden przebieg na współdzielony wygląd, dokładnie raz na wywołanie

Czego spłaszczanie nie obejmuje?

Spłaszczanie to geometryczny przepis własnych obiektów jednej strony, i kilka sytuacji wypada poza nie po cichu, a nie głośno

  • Strony o efektywnym obrocie już równym 0 albo z brakującym MediaBoxem lub zerową szerokością czy wysokością są pomijane bez błędu; porównaj wartość zwracaną z liczbą stron, których zmiany się spodziewałeś
  • Form XObjects wskazywane z zasobów strony zachowują własny /BBox w przestrzeni formularza, bo zewnętrzny cm obraca je już sam; skan drzewa struktury idzie tylko za /K i /A, więc nigdy nie wchodzi w zasoby albo adnotacje strony drugi raz
  • Cele są znajdowane skanem każdego obiektu pośredniego raz na spłaszczaną stronę, więc duży dokument ze setkami obróconych stron płaci za ten spacer przy każdej z nich
  • Renderer stron HotPDF nie rysuje adnotacji, więc wzrokowe sprawdzenie obróconych stempli wymaga najpierw FlattenLoadedAnnotations
// Wypal wyglądy w treść, żeby renderer mógł je pokazać,
// potem renderuj stronę 1 przed i po usunięciu jej /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));
    // Porównaj tu piksele znacznika rogu, nie tylko wymiary
  finally
    After.Free;
  end;
finally
  Before.Free;
end;

Za głębszym tłem — strona adnotacyjna tej historii ciągnie się w artykule o syntezowaniu wyglądów adnotacji przed spłaszczeniem, renderer za porównaniem przed-po jest omówiony w artykule o renderowaniu wczytanej strony PDF do bitmapy, a redakcja i składanie N-up na wczytanych PDF-ach pokazuje tę samą technikę doklejania strumieni treści, na której opierają się prefiks i sufiks obrotu. HotPDF, łącznie z FlattenLoadedPageRotation i czytnikami ramek strony, jest dostępny dla Delphi i C++Buildera na stronie HotPDF Delphi PDF component