Articolo tecnico

Rendering pattern a piastrelle PDF in Delphi con HotPDF

Un tratteggio che viene renderizzato come un unico blocco grigio piatto è il classico fallimento dei tiling pattern. HotPDF, il componente PDF VCL nativo per Delphi e C++Builder, dipinge PatternType 1 trasformando il percorso corrente in un clip temporaneo e riproducendo il content stream del pattern una volta per ogni tile visibile, con la selezione del pattern mantenuta nello stato grafico e ripristinata da q e Q

I sintomi arrivano in due varianti, e sembrano non correlati finché non ne conosci la causa. Un disegno CAD perde il proprio tratteggio di sezione e torna come riempimenti solidi, perché il renderer ha risolto il pattern in un colore medio e ha dipinto quello. Oppure il tratteggio fuoriesce: un cartiglio che dovrebbe essere bianco semplice raccoglie le linee diagonali di una vista di dettaglio due percorsi prima. Entrambi sono problemi di stato del pattern, e solo uno dei due riguarda effettivamente il disegnare tile

Perché un tiling pattern sanguina sul percorso successivo?

Perché il nome del pattern selezionato fa parte dello stato grafico, non una proprietà dell'operatore che lo ha usato. ISO 32000-1 §8.6.6.2 definisce uno spazio colore Pattern come uno il cui valore colore è un nome di pattern fornito a scn o SCN, e ogni altro componente dello stato colore viene salvato da q e ripristinato da Q. Il nome del pattern deve seguire la stessa regola. HotPDF lo mantiene nel record di stato come FillPatternName e StrokePatternName, accanto alla famiglia di spazio colore di riempimento e tratto, così un Q rimette a posto la selezione precedente esattamente come rimette a posto la CTM precedente

Memorizza invece quel nome in una variabile locale dentro il dispatcher degli operatori, e sopravviverà a ogni Q nello stream. Il fallimento si manifesta poi in un punto inaspettato: un Form XObject disegnato dopo il percorso con pattern eredita una selezione di pattern che il suo stesso content stream non ha mai fatto, e i suoi riempimenti escono tratteggiati. I form annidati peggiorano la situazione, perché ogni livello di annidamento fa push e pop di uno stato che la variabile vagante ignora. Impostare uno spazio colore non-pattern con cs o CS, o emettere un semplice g / rg / k, deve anche azzerare il nome del pattern, altrimenti la selezione obsoleta sopravvive allo spazio colore che le dava significato

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

Un pattern viene dipinto attraverso un clip, mai come riempimento

Il modello corretto è sottrattivo: restringi il clip del dispositivo alla forma che viene dipinta, poi esegui il contenuto del pattern al suo interno. HotPDF non disegna mai prima un'approssimazione solida e la sovradipinge, perché il solido intermedio sarebbe visibile attraverso gli spazi tra i tile e confliggerebbe con qualsiasi trasparenza nel contenuto del tile. §8.7.3.2 descrive un tiling pattern come un content stream replicato a intervalli orizzontali e verticali fissi, e la replicazione ha senso solo contro un clip che ha già la forma giusta. Per i riempimenti la conversione è diretta: HPDFSelectFillPathClip imposta la modalità di riempimento del poligono a ALTERNATE per f*, B* e b* e a WINDING per le varianti nonzero, costruisce il percorso GDI, e lo interseca nel clip con SelectClipPath. Quella singola riga è ciò che fa sì che un riempimento con pattern even-odd lasci gli stessi buchi di un riempimento solido even-odd, che è esattamente ciò di cui ha bisogno una regione tratteggiata a forma di ciambella

I tratti sono la parte facile da sbagliare. Un percorso tratteggiato non ha interno, quindi intersecare il percorso stesso nel clip produce una regione vuota e non viene dipinto nulla. HPDFSelectStrokePathClip quindi costruisce prima una penna geometrica dallo stato corrente, usando PS_GEOMETRIC con il cap finale da J, la giunzione da j, il limite miter da M, e PS_USERSTYLE quando un array di tratteggio è attivo, poi chiama WidenPath per convertire il contorno tratteggiato in una regione riempibile prima del clipping. Comportamento di cap, giunzione, miter e tratteggio su un percorso con pattern tratteggiato corrispondono quindi a un tratto normale per costruzione piuttosto che per una seconda implementazione. Due limiti onesti vivono qui: le larghezze di linea sotto un'unità dispositivo vengono fissate a un pixel, e l'array di tratteggio viene troncato a sedici voci, che è il tetto che ExtCreatePen accetta

Quali tile sono effettivamente visibili?

L'intervallo visibile deriva dall'eseguire la trasformazione all'indietro. Il posizionamento dei tile avviene nello spazio pattern, ma l'unica cosa che sa quanto della pagina viene toccata è il riquadro di clip del dispositivo, che è nello spazio dispositivo. HotPDF compone BaseMatrix := CTM * PatternMatrix, la inverte, e mappa i quattro angoli del riquadro di clip GDI indietro attraverso l'inversa. I confini allineati agli assi di quei quattro angoli mappati danno il rettangolo nello spazio pattern che può essere potenzialmente coperto, e dividere quel rettangolo per XStep e YStep contro il BBox del pattern dà intervalli di indice chiusi. Ogni cella viene poi renderizzata con una CTM di CTM * PatternMatrix * Translate(i * XStep, j * YStep), e viene ritagliata una seconda volta al proprio poligono BBox trasformato. Quel secondo clip conta quando XStep è più piccolo della larghezza del riquadro delimitante, che è il modo in cui vengono espressi i disegni di tile sovrapposti; senza di esso, le celle adiacenti dipingerebbero l'una sull'altra oltre la propria estensione dichiarata. Se il clip per cella risulta NULLREGION, la cella viene saltata senza tokenizzare o eseguire nulla

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

Pattern non colorati e il colore che arriva dall'esterno

Un pattern PaintType 2 porta forma ma nessun colore, e il colore arriva con il nome del pattern. §8.7.3.2 specifica che un pattern non colorato viene usato solo con uno spazio colore Pattern che dichiara uno spazio sottostante, quindi scn riceve prima i valori componente e per ultimo il nome del pattern. HotPDF risolve quei componenti attraverso lo spazio sottostante memorizzato sulla voce dello spazio colore del pattern, il che significa che un tratteggio non colorato può essere tinto con un inchiostro Separation o una combinazione DeviceN esattamente come qualsiasi altro riempimento; la meccanica di quella risoluzione è trattata in rendering dei colori spot Separation e DeviceN. Dentro il tile, i due tipi di pittura divergono nettamente. Per PaintType 2 il renderer imposta un flag di soppressione degli operatori colore per la durata del tile, così qualsiasi g, rg, k o scn nel contenuto del pattern viene ignorato e ogni segno prende il colore fornito esternamente. Per PaintType 1 vale l'opposto: lo stato di riempimento e tratto viene ripristinato ai valori predefiniti PDF, DeviceGray nero con uno spazio colore identità, e il tile si colora da sé. Saltare quel ripristino permette al colore che era corrente in quel momento all'operatore f di trapelare in un pattern che avrebbe dovuto essere autodescrittivo

Perché la profondità dello stack dello stato grafico deve essere ripristinata dopo ogni tile?

Perché a un content stream di pattern è permesso di essere sbilanciato, e il danno si accumula tra le celle. Un tile il cui stream contiene tre operatori q e due operatori Q lascia lo stack un frame più profondo di quando ha iniziato. Ripristina solo il record di stato corrente tra le celle e la profondità continua a crescere, così la cella numero duecento viene eseguita da un frame di stack che appartiene alla cella numero centonovantanove, con qualunque CTM e clip portasse quel frame. HotPDF quindi fa uno snapshot del record di stato e della profondità dello stack prima del ciclo di tile e chiama RestoreSnapshot all'inizio di ogni iterazione, il che tronca lo stack riportandolo alla lunghezza salvata e reinstalla lo stato salvato in un solo passaggio. Il dizionario Resources della pagina e il flag di soppressione degli operatori colore vengono ripristinati allo stesso confine, poiché un tile può fare riferimento alle proprie risorse e non deve consegnarle al proprio vicino. Lo stato di clip GDI riceve lo stesso trattamento tramite una coppia SaveDC / RestoreDC attorno a ogni cella, così un tile che installa il proprio clip W n non può restringere la regione disponibile per quello successivo

Budget, rifiuti, e cosa il renderer non disegnerà

I tiling pattern sono il punto più facile in un PDF per scrivere un file denial-of-service, quindi i limiti sono numeri rigidi piuttosto che euristiche. L'annidamento dei pattern è limitato alla profondità 4, la stessa protezione usata per la ricorsione dei Form XObject, che ferma un pattern che fa riferimento a sé stesso attraverso il proprio dizionario di risorse. Un singolo dipinto di percorso può eseguire al massimo 16.384 tile in totale, contati a scendere attraverso i pattern annidati e azzerati solo quando inizia il dipinto del pattern più esterno. Una griglia di tile il cui conteggio pianificato di celle supera ciò che resta di quel budget viene rifiutata del tutto, prima che venga eseguita anche una sola cella

La geometria degenere viene rifiutata piuttosto che approssimata. Un BBox mancante o ad area zero, uno XStep o YStep la cui grandezza è sotto 1e-6, un prodotto CTM * PatternMatrix senza inversa, coordinate di clip mappate oltre 1e9, o una grandezza di indice oltre un milione causano tutti il ritorno del dipinto del pattern senza disegnare. Il risultato è una regione non dipinta invece di un thread di rendering bloccato, che è il compromesso che vuoi in un convertitore batch. Le prestazioni derivano da una decisione: lo stream del pattern viene tokenizzato una volta per dipinto con HPDFTokenizeContentStream e l'array di token viene riutilizzato per ogni cella visibile, così il conteggio dei tile moltiplica il costo di esecuzione ma mai il costo di lexing

Renderizzare una pagina con pattern da Delphi

Nulla nel supporto ai pattern cambia il codice chiamante. Carica il documento, richiedi una pagina, e il lavoro sui tile avviene dentro l'interprete del content stream che il rendering pagina-a-bitmap già guida. Lo stesso interprete alimenta contesti dispositivo bitmap, metafile e stampante, quindi un disegno tratteggiato che appare corretto in una thumbnail di anteprima stampa con la stessa geometria di tile. I pattern shading PatternType 2 prendono un ramo diverso che condivide il proprio percorso di valutazione con l'operatore sh semplice, descritto in dettaglio sotto rendering di shading assiali e radiali

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;

Quando una regione con pattern appare ancora sbagliata, controlla le tre classi di fallimento in ordine. Una regione interamente vuota di solito significa un rifiuto: ispeziona XStep, YStep e BBox per valori degeneri, oppure conta i tile di cui la griglia avrebbe bisogno contro il tetto di 16.384. Una regione dipinta in un unico colore piatto significa che il nome del pattern non ha mai raggiunto l'operatore di pittura, il che punta all'ordine di cs e scn nello stream. Un pattern che compare dove non appartiene significa ripristino di stato, e il punto da controllare è la gestione di q / Q attorno al form o al percorso che lo ha ereditato

I tiling pattern sono una di quelle funzionalità PDF che restano invisibili finché il file che ne ha bisogno non atterra nella tua casella di posta, e poi diventano l'intero lavoro. Se stai costruendo visualizzatori di disegni, convertitori di documenti tecnici o renderer di report su Delphi o C++Builder, il componente completo e la sua API di rendering sono documentati sulla pagina HotPDF Delphi PDF component