Artykuł techniczny

Renderowanie wzorców kafelkowych PDF w Delphi z HotPDF

Kreskowanie renderujące się jako jeden płaski szary blok to klasyczna awaria wzorców kafelkowych. HotPDF, natywny komponent VCL PDF dla Delphi i C++Builder, maluje PatternType 1, zamieniając bieżącą ścieżkę w tymczasowe przycięcie i odtwarzając strumień treści wzorca raz na widoczny kafelek, z wyborem wzorca trzymanym w stanie grafiki i przywracanym przez q i Q

Objawy przychodzą w dwóch odmianach, i wyglądają na niepowiązane, dopóki nie znasz przyczyny. Rysunek CAD traci kreskowanie przekroju i wraca jako lite wypełnienia, ponieważ renderer zredukował wzorzec do koloru uśrednionego i namalował to. Albo kreskowanie ucieka: blok tytułowy, który powinien być czystą bielą, przejmuje ukośne linie z widoku szczegółowego kilka ścieżek wcześniej. Oba to problemy stanu wzorca, i tylko jeden z nich dotyczy w ogóle rysowania kafelków

Dlaczego wzorzec kafelkowy przecieka na kolejną ścieżkę?

Ponieważ wybrana nazwa wzorca jest częścią stanu grafiki, nie właściwością operatora, który jej użył. ISO 32000-1 §8.6.6.2 definiuje przestrzeń kolorów Pattern jako taką, której wartość koloru to nazwa wzorca dostarczona do scn lub SCN, a każda inna składowa stanu koloru jest zapisywana przez q i przywracana przez Q. Nazwa wzorca musi podlegać tej samej regule. HotPDF trzyma ją w rekordzie stanu jako FillPatternName i StrokePatternName, obok rodziny przestrzeni kolorów wypełnienia i obrysu, więc Q przywraca poprzedni wybór dokładnie tak, jak przywraca poprzednią CTM

Przechowaj tę nazwę zamiast tego w lokalnej zmiennej wewnątrz dyspozytora operatorów, a przetrwa ona każde Q w strumieniu. Awaria pojawia się wtedy gdzieś nieoczekiwanym: formularz XObject narysowany po ścieżce z wzorcem dziedziczy wybór wzorca, którego jego własny strumień treści nigdy nie ustawił, a jego wypełnienia wychodzą zakreskowane. Zagnieżdżone formularze pogarszają sprawę, ponieważ każdy poziom zagnieżdżenia odkłada i zdejmuje stan, który zabłąkana zmienna ignoruje. Ustawienie przestrzeni kolorów niebędącej wzorcem przez cs lub CS, albo wydanie zwykłego g / rg / k, musi też wyczyścić nazwę wzorca, inaczej nieaktualny wybór przeżywa przestrzeń kolorów, która nadawała mu znaczenie

q
  /Pattern cs              % pattern colour space, ISO 32000-1 8.6.6.2
  /P1 scn                  % coloured tiling pattern, PaintType 1
  10 10 200 120 re f       % this rectangle is hatched
Q
0 0 300 200 re f           % must be black again, not hatched

q
  /Cs2 cs                  % [/Pattern /DeviceCMYK] array
  0 0.6 1 0 /P2 scn        % uncoloured pattern plus its underlying colour
  20 20 160 90 re f*
Q

Wzorzec jest malowany przez przycięcie, nigdy jako wypełnienie

Poprawny model jest odejmujący: ogranicz przycięcie urządzenia do kształtu, który jest malowany, a potem uruchom treść wzorca wewnątrz niego. HotPDF nigdy nie rysuje najpierw litego przybliżenia i nie przemalowuje go, ponieważ pośrednia lita plama byłaby widoczna przez szczeliny między kafelkami i kłóciłaby się z jakąkolwiek przezroczystością w treści kafelka. §8.7.3.2 opisuje wzorzec kafelkowy jako strumień treści powielany w stałych odstępach poziomych i pionowych, a powielanie ma sens tylko wobec przycięcia, które ma już właściwy kształt. Dla wypełnień konwersja jest bezpośrednia: HPDFSelectFillPathClip ustawia tryb wypełnienia wielokąta na ALTERNATE dla f*, B* i b* oraz na WINDING dla wariantów nonzero, buduje ścieżkę GDI i przecina ją z przycięciem za pomocą SelectClipPath. Ta jedna linia sprawia, że wypełnienie parzyste-nieparzyste ze wzorcem zostawia te same dziury co lite wypełnienie parzyste-nieparzyste, czego dokładnie potrzebuje obszar zakreskowany w kształcie pierścienia

Obrysy to część, którą łatwo zrobić źle. Obrysowana ścieżka nie ma wnętrza, więc przecięcie samej ścieżki z przycięciem daje pusty region i nic się nie maluje. HPDFSelectStrokePathClip buduje więc najpierw geometryczne pióro z bieżącego stanu, używając PS_GEOMETRIC z zakończeniem z J, złączeniem z j, limitem uciosu z M i PS_USERSTYLE, gdy tablica kreskowania jest aktywna, a potem wywołuje WidenPath, by zamienić obrysowany kontur na region nadający się do wypełnienia, zanim nastąpi przycięcie. Zachowanie zakończeń, złączeń, uciosów i kresek na ścieżce obrysowanej wzorcem odpowiada wtedy normalnemu obrysowi z konstrukcji, a nie z drugiej implementacji. Są tu dwa uczciwe ograniczenia: szerokości linii poniżej jednej jednostki urządzenia są przycinane do jednego piksela, a tablica kreskowania jest obcinana do szesnastu wpisów, co jest pułapem akceptowanym przez ExtCreatePen

Które kafelki są faktycznie widoczne?

Widoczny zakres pochodzi z odwrócenia transformacji. Rozmieszczenie kafelków dzieje się w przestrzeni wzorca, ale jedyną rzeczą, która wie, jaka część strony jest dotykana, jest ramka przycięcia urządzenia, znajdująca się w przestrzeni urządzenia. HotPDF składa BaseMatrix := CTM * PatternMatrix, odwraca ją i mapuje z powrotem cztery narożniki ramki przycięcia GDI przez macierz odwrotną. Granice osiowo wyrównane tych czterech zmapowanych narożników dają prostokąt w przestrzeni wzorca, który potencjalnie może być pokryty, a podzielenie tego prostokąta przez XStep i YStep wobec BBox wzorca daje domknięte zakresy indeksów. Każda komórka renderuje się wtedy z CTM równą CTM * PatternMatrix * Translate(i * XStep, j * YStep), i jest przycinana po raz drugi do własnego przetransformowanego wielokąta BBox. To drugie przycięcie ma znaczenie, gdy XStep jest mniejsze niż szerokość ramki granicznej, co jest sposobem wyrażania nakładających się projektów kafelków; bez tego sąsiednie komórki malowałyby się nawzajem poza swoim zadeklarowanym zasięgiem. Jeśli przycięcie danej komórki wraca jako NULLREGION, komórka jest pomijana bez tokenizacji czy wykonywania czegokolwiek

// Map the device clip box back into pattern space through the inverse of
// CTM * PatternMatrix, then convert those bounds into tile index ranges.
BaseMatrix := HPDFMatMul(FGSStack.State.CTM, PatternMatrix);
if not HPDFMatInvert(BaseMatrix, InverseMatrix) then Exit;   // singular: refuse
if GetClipBox(FDC, ClipRect) = ERROR then Exit;

// MinX..MaxY are the axis-aligned bounds of the four mapped clip corners.
I0 := Floor((MinX - BBox[2]) / StepXAbs);
I1 := Ceil ((MaxX - BBox[0]) / StepXAbs);
J0 := Floor((MinY - BBox[3]) / StepYAbs);
J1 := Ceil ((MaxY - BBox[1]) / StepYAbs);

PlannedTiles := Int64(I1 - I0 + 1) * Int64(J1 - J0 + 1);
if (PlannedTiles <= 0) or (PlannedTiles > FPatternTilesRemaining) then Exit;
Dec(FPatternTilesRemaining, Integer(PlannedTiles));

Wzorce bezbarwne i kolor przychodzący z zewnątrz

Wzorzec PaintType 2 niesie kształt, ale nie kolor, a kolor przychodzi wraz z nazwą wzorca. §8.7.3.2 określa, że wzorzec bezbarwny jest używany wyłącznie z przestrzenią kolorów Pattern, która deklaruje przestrzeń bazową, więc scn otrzymuje najpierw wartości składowych, a nazwę wzorca na końcu. HotPDF rozwiązuje te składowe przez przestrzeń bazową zapisaną we wpisie przestrzeni kolorów wzorca, co oznacza, że bezbarwne kreskowanie może być tonowane atramentem Separation albo kombinacją DeviceN dokładnie tak jak każde inne wypełnienie; mechanikę tego rozwiązywania omawia renderowanie kolorów spot Separation i DeviceN. Wewnątrz kafelka oba typy malowania rozjeżdżają się ostro. Dla PaintType 2 renderer ustawia flagę tłumienia operatorów koloru na czas trwania kafelka, więc każdy g, rg, k lub scn w treści wzorca jest ignorowany, a każdy znak przyjmuje kolor dostarczony z zewnątrz. Dla PaintType 1 obowiązuje odwrotność: stan wypełnienia i obrysu jest resetowany do domyślnych wartości PDF, czarnego DeviceGray z tożsamościową przestrzenią kolorów, a kafelek koloruje się sam. Pominięcie tego resetu pozwala, by kolor, który akurat był bieżący przy operatorze f, przeciekł do wzorca, który miał być samoopisujący

Dlaczego głębokość stosu stanu grafiki musi być przywracana po każdym kafelku?

Ponieważ strumień treści wzorca może być niezbalansowany, a szkoda się kumuluje między komórkami. Kafelek, którego strumień zawiera trzy operatory q i dwa operatory Q, zostawia stos o jedną klatkę głębszy niż na początku. Przywróć między komórkami tylko bieżący rekord stanu, a głębokość wciąż rośnie, więc komórka numer dwieście wykonuje się z klatki stosu należącej do komórki numer sto dziewięćdziesiąt dziewięć, z jakąkolwiek CTM i przycięciem, jakie ta klatka niosła. HotPDF robi więc migawkę rekordu stanu i głębokości stosu przed pętlą kafelków i wywołuje RestoreSnapshot na początku każdej iteracji, co obcina stos z powrotem do zapisanej długości i w jednym kroku instaluje ponownie zapisany stan. Słownik zasobów strony i flaga tłumienia operatorów koloru są przywracane na tej samej granicy, ponieważ kafelek może odwoływać się do własnych zasobów i nie może przekazać ich sąsiadowi. Stan przycięcia GDI otrzymuje to samo traktowanie przez parę SaveDC / RestoreDC wokół każdej komórki, więc kafelek instalujący własne przycięcie W n nie może zmniejszyć regionu dostępnego dla kolejnego

Budżety, odmowy i to, czego renderer nie narysuje

Wzorce kafelkowe to najłatwiejsze miejsce w PDF-ie, by napisać plik odmowy usługi, więc limity są twardymi liczbami, nie heurystykami. Zagnieżdżanie wzorców jest ograniczone do głębokości 4, tej samej strażnicy używanej dla rekursji formularzy XObject, co zatrzymuje wzorzec odwołujący się do samego siebie przez własny słownik zasobów. Pojedyncze malowanie ścieżki może wykonać co najwyżej 16 384 kafelki łącznie, liczone w dół przez zagnieżdżone wzorce i resetowane tylko wtedy, gdy zaczyna się malowanie najbardziej zewnętrznego wzorca. Siatka kafelków, której planowana liczba komórek przekracza to, co zostało z tego budżetu, jest odrzucana od razu, zanim uruchomi się choćby jedna komórka

Zdegenerowana geometria jest odrzucana, nie przybliżana. Brakujące albo zerowe pole BBox, XStep albo YStep, którego wielkość jest poniżej 1e-6, iloczyn CTM * PatternMatrix bez odwrotności, zmapowane współrzędne przycięcia powyżej 1e9, albo wielkość indeksu powyżej miliona — wszystko to powoduje, że malowanie wzorca kończy się bez rysowania. Wynikiem jest niepomalowany obszar zamiast zawieszonego wątku renderowania, co jest kompromisem, którego chcesz w konwerterze wsadowym. Wydajność bierze się z jednej decyzji: strumień wzorca jest tokenizowany raz na malowanie za pomocą HPDFTokenizeContentStream, a tablica tokenów jest ponownie używana dla każdej widocznej komórki, więc liczba kafelków mnoży koszt wykonania, ale nigdy koszt leksowania

Renderowanie strony ze wzorcami z Delphi

Nic w obsłudze wzorców nie zmienia kodu wywołującego. Wczytaj dokument, poproś o stronę, a praca kafelkowania dzieje się wewnątrz interpretera strumienia treści, który już napędza renderowanie strony do bitmapy. Ten sam interpreter zasila konteksty urządzenia bitmapy, metapliku i drukarki, więc zakreskowany rysunek, który wygląda dobrze w miniaturce podglądu, drukuje się z tą samą geometrią kafelków. Wzorce cieniowania PatternType 2 idą inną gałęzią, która dzieli swoją ścieżkę oceny z gołym operatorem sh, opisaną szczegółowo pod renderowaniem cieniowań osiowych i promienistych

var
  Pdf: THotPDF;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('assembly-drawing.pdf') > 0 then
    begin
      // Section hatching that previously flattened to a solid block now
      // replays the tile content once per visible cell.
      Bmp := Pdf.RenderLoadedPageToBitmap(0, 200);
      if Assigned(Bmp) then
      try
        Bmp.SaveToFile('sheet1.bmp');
      finally
        Bmp.Free;
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Gdy zakreskowany obszar wciąż wygląda źle, sprawdź po kolei trzy klasy awarii. Obszar całkowicie pusty zwykle oznacza odmowę: sprawdź XStep, YStep i BBox pod kątem zdegenerowanych wartości, albo policz kafelki, jakich potrzebowałaby siatka, wobec pułapu 16 384. Obszar pomalowany jednym płaskim kolorem oznacza, że nazwa wzorca nigdy nie dotarła do operatora malowania, co wskazuje na kolejność cs i scn w strumieniu. Wzorzec pojawiający się tam, gdzie nie powinien, oznacza przywracanie stanu, a miejscem do sprawdzenia jest obsługa q / Q wokół formularza albo ścieżki, która go odziedziczyła

Wzorce kafelkowe to jedna z tych funkcji PDF, które pozostają niewidoczne, dopóki plik, który ich potrzebuje, nie wyląduje w Twojej skrzynce, a wtedy stają się całą pracą. Jeśli budujesz przeglądarki rysunków, konwertery dokumentów inżynieryjnych albo renderery raportów na Delphi lub C++Builder, pełny komponent i jego API renderowania są udokumentowane na stronie komponentu HotPDF Delphi PDF