Teknisk artikel

Rendering av PDF-mosaikmönster i Delphi med HotPDF

Skraffering som renderas som ett platt grått block är det klassiska misslyckandet för mosaikmönster. HotPDF, den native VCL-PDF-komponenten för Delphi och C++Builder, målar PatternType 1 genom att förvandla den aktuella banan till en tillfällig klippning och spela upp mönstrets innehållsström igen en gång per synlig ruta, med mönsterval hållet i grafiktillståndet och återställt av q och Q

Symptomen kommer i två varianter, och de ser orelaterade ut tills du känner till orsaken. En CAD-ritning tappar sin sektionsskraffering och kommer tillbaka som solida fyllningar, eftersom renderaren löste mönstret till en genomsnittsfärg och målade den. Eller skrafferingen läcker: ett titelfält som borde vara rent vitt plockar upp de diagonala linjerna från en detaljvy två banor tidigare. Båda är mönstertillståndsproblem, och bara en av dem handlar alls om att rita rutor

Varför läcker ett mosaikmönster in på nästa bana?

Eftersom det valda mönsternamnet är en del av grafiktillståndet, inte en egenskap hos operatorn som använde det. ISO 32000-1 §8.6.6.2 definierar en Pattern-färgrymd som en vars färgvärde är ett mönsternamn tillhandahållet till scn eller SCN, och varje annan komponent av färgtillståndet sparas av q och återställs av Q. Mönsternamnet måste följa samma regel. HotPDF håller det i tillståndsposten som FillPatternName och StrokePatternName, tillsammans med fyllnings- och strykfärgrymdens familj, så att en Q sätter tillbaka det tidigare valet exakt som den sätter tillbaka den tidigare CTM:en

Lagra det namnet i en lokal variabel inuti operator-dispatchern i stället, och det överlever varje Q i strömmen. Felet dyker då upp någonstans oväntat: ett Form-XObject ritat efter den mönstrade banan ärver ett mönsterval dess egen innehållsström aldrig gjorde, och dess fyllningar kommer ut skraffererade. Nästlade formulär gör det värre, eftersom varje nästlingsnivå trycker och poppar tillstånd som den vilsna variabeln ignorerar. Att sätta en icke-mönster-färgrymd med cs eller CS, eller att utfärda en ren g / rg / k, måste också rensa mönsternamnet, annars överlever det gamla valet färgrymden som gav det mening

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

Ett mönster målas genom en klippning, aldrig som en fyllning

Den korrekta modellen är subtraktiv: begränsa enhetsklippningen till formen som målas, kör sedan mönsterinnehållet inuti den. HotPDF ritar aldrig en solid approximation först och övermålar den, eftersom den mellanliggande solida ytan skulle synas genom mellanrummen mellan rutorna och skulle strida mot all transparens i ruteinnehållet. §8.7.3.2 beskriver ett mosaikmönster som en innehållsström replikerad med fasta horisontella och vertikala intervall, och replikering är bara meningsfull mot en klippning som redan har rätt form. För fyllningar är omvandlingen direkt: HPDFSelectFillPathClip sätter polygonfyllningsläget till ALTERNATE för f*, B* och b* och till WINDING för nollskillnad-varianterna, bygger GDI-banan, och skär den in i klippningen med SelectClipPath. Den enda raden är vad som får en jämn-udda-mönstrad fyllning att lämna samma hål som en jämn-udda-solid fyllning, vilket är precis vad en munkformad skraffererad region behöver

Strykning är delen som är lätt att göra fel. En strykt bana har ingen inre yta, så att skära själva banan in i klippningen ger en tom region och inget målas. HPDFSelectStrokePathClip bygger därför en geometrisk penna från det aktuella tillståndet först, med PS_GEOMETRIC med ändlocket från J, sammanfogningen från j, miter-gränsen från M, och PS_USERSTYLE när en streckarray är aktiv, och anropar sedan WidenPath för att omvandla den strukna konturen till en fyllbar region innan klippning. Cap-, join-, miter- och streckbeteende på en mönsterstrukt bana matchar då en normal stryk genom konstruktion snarare än genom en andra implementation. Två ärliga gränser bor här: linjebredder under en enhetsdevice klämmas till en pixel, och streckarrayen trunkeras vid sexton poster, vilket är taket ExtCreatePen accepterar

Vilka rutor är faktiskt synliga?

Det synliga intervallet kommer från att köra transformen baklänges. Ruteplacering sker i mönsterrymden, men det enda som vet hur mycket av sidan som berörs är enhetens klippningsruta, som är i enhetsrymden. HotPDF sammansätter BaseMatrix := CTM * PatternMatrix, inverterar den, och mappar de fyra hörnen på GDI-klippningsrutan tillbaka genom inversen. De axelanpassade gränserna av de fyra mappade hörnen ger mönsterrymdsrektangeln som möjligen kan täckas, och att dela den rektangeln med XStep och YStep mot mönstrets BBox ger slutna indexintervall. Varje cell renderas sedan med en CTM på CTM * PatternMatrix * Translate(i * XStep, j * YStep), och klipps en andra gång till sin egen transformerade BBox-polygon. Den andra klippningen spelar roll när XStep är mindre än gränsrutans bredd, vilket är hur överlappande rutdesigner uttrycks; utan den skulle angränsande celler måla över varandra utanför sin deklarerade omfattning. Om klippningen per cell kommer tillbaka som NULLREGION hoppas cellen över utan att tokeniseras eller köras

// 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));

Ofärgade mönster och färgen som kommer utifrån

Ett PaintType 2-mönster bär form men ingen färg, och färgen anländer med mönsternamnet. §8.7.3.2 specificerar att ett ofärgat mönster bara används med en Pattern-färgrymd som deklarerar en underliggande rymd, så scn tar emot komponentvärdena först och mönsternamnet sist. HotPDF löser upp de komponenterna genom den underliggande rymden lagrad på mönsterfärgrymdsposten, vilket betyder att en ofärgad skraffering kan tonas med en Separation-färg eller en DeviceN-kombination precis som vilken annan fyllning som helst; mekaniken i den upplösningen täcks i rendering av Separation- och DeviceN-dekorfärger. Inuti rutan divergerar de två målningstyperna skarpt. För PaintType 2 sätter renderaren en färgoperator-undertryckningsflagga för rutans varaktighet, så all g, rg, k eller scn i mönsterinnehållet ignoreras och varje märke tar den externt tillhandahållna färgen. För PaintType 1 gäller det motsatta: fyllnings- och stryktillstånd återställs till PDF-standardvärdena, DeviceGray svart med en identitetsfärgrymd, och rutan färgar sig själv. Att hoppa över den återställningen låter färgen som råkade vara aktuell vid f-operatorn läcka in i ett mönster som skulle vara självbeskrivande

Varför måste grafiktillståndets stackdjup återställas efter varje ruta?

Eftersom en mönsterinnehållsström tillåts vara obalanserad, och skadan förvärras över celler. En ruta vars ström innehåller tre q-operatorer och två Q-operatorer lämnar stacken en ram djupare än den började. Återställ bara den aktuella tillståndsposten mellan celler och djupet fortsätter växa, så cell nummer tvåhundra körs från en stackram som tillhör cell nummer etthundranittionio, med vilken CTM och klippning den ramen bar. HotPDF tar därför en ögonblicksbild av tillståndsposten och stackdjupet innan ruteloppen och anropar RestoreSnapshot i toppen av varje iteration, vilket trunkerar stacken tillbaka till den sparade längden och återinstallerar det sparade tillståndet i ett steg. Sidans Resources-ordbok och färgoperator-undertryckningsflaggan återställs på samma gräns, eftersom en ruta kan referera sina egna resurser och inte får ge dem till sin granne. GDI-klippningstillstånd får samma behandling genom ett SaveDC / RestoreDC-par runt varje cell, så en ruta som installerar sin egen W n-klippning inte kan krympa regionen tillgänglig för nästa

Budgetar, vägringar, och vad renderaren inte kommer rita

Mosaikmönster är den enklaste platsen i en PDF att skriva en fil för överbelastningsattack, så gränserna är hårda siffror snarare än heuristiker. Mönsternästling är begränsad till djup 4, samma vakt använd för Form-XObject-rekursion, som stoppar ett mönster som refererar sig själv genom sin egen resursordbok. En enda bantmålning får utföra som mest 16 384 rutor totalt, nedräknat över nästlade mönster och återställt bara när det yttersta mönstrets målning börjar. Ett rutnät vars planerade cellantal överstiger vad som är kvar av den budgeten avvisas rakt av, innan en enda cell körs

Degenererad geometri avvisas snarare än approximeras. En saknad eller nollytas BBox, en XStep eller YStep vars magnitud är under 1e-6, en CTM * PatternMatrix-produkt utan invers, mappade klippningskoordinater bortom 1e9, eller en indexmagnitud förbi en miljon orsakar alla att mönstermålningen returnerar utan att rita. Resultatet är en omålad region i stället för en hängande renderingstråd, vilket är avvägningen du vill ha i en batch-konverterare. Prestanda kommer från ett beslut: mönsterströmmen tokeniseras en gång per målning med HPDFTokenizeContentStream och token-arrayen återanvänds över varje synlig cell, så rutantalet multiplicerar körningskostnaden men aldrig lexningskostnaden

Att rendera en mönstrad sida från Delphi

Ingenting om mönsterstöd ändrar den anropande koden. Ladda dokumentet, be om en sida, och mosaikarbetet sker inuti innehållsströms-tolken som sida-till-bitmap-rendering redan driver. Samma tolk matar bitmap-, metafil- och skrivarenhetskontexter, så en skraffererad ritning som ser rätt ut i en förhandsvisningsminiatyr skrivs ut med samma rutgeometri. PatternType 2-skuggningsmönster tar en annan gren som delar sin utvärderingsväg med den rena sh-operatorn, beskriven i detalj under rendering av axiella och radiella skuggningar

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;

När en mönstrad region fortfarande ser fel ut, kontrollera de tre felklasserna i ordning. En region som är helt tom betyder vanligtvis en vägran: inspektera XStep, YStep och BBox för degenererade värden, eller räkna rutorna nätet skulle behöva mot 16 384-taket. En region målad i en enda platt färg betyder att mönsternamnet aldrig nådde målningsoperatorn, vilket pekar mot cs- och scn-ordning i strömmen. Ett mönster som dyker upp där det inte hör hemma betyder tillståndsåterställning, och stället att titta är q / Q-hanteringen runt formuläret eller banan som ärvde det

Mosaikmönster är en av de PDF-funktioner som förblir osynliga tills filen som behöver dem landar i din inkorg, och sedan är de hela jobbet. Om du bygger ritningsvisare, tekniska dokumentkonverterare eller rapportrenderare på Delphi eller C++Builder är den fullständiga komponenten och dess renderings-API dokumenterade på sidan HotPDF Delphi-PDF-komponent