Una pagina PDF non memorizza pixel e non memorizza un albero di oggetti forma come fa SVG. Memorizza un programma. Ogni linea, curva, riempimento e immagine posizionata sulla pagina è il risultato dell'esecuzione di una sequenza di operatori in un content stream, dall'alto verso il basso, contro uno stato grafico in esecuzione. Comprendi quel singolo fatto e la maggior parte del comportamento del formato smette di sorprendere: perché un riempimento ha bisogno di un operatore di pittura separato dopo che il tracciato è stato costruito, perché i colori e le larghezze delle linee trapelano da una forma alla successiva a meno che non li si racchiuda, perché lo stesso codice di disegno può atterrare in punti completamente diversi dopo una singola trasformazione delle coordinate. Questo è un tour di quel modello di esecuzione come definito nella ISO 32000: gli operatori che incontri quando apri un content stream e le regole che decidono cosa appare sulla pagina
Il content stream è un bytecode postfisso
Un content stream è una sequenza piatta di byte di operandi seguiti da operatori. Gli operandi vengono prima, l'operatore che li consuma viene per ultimo, il che è l'inverso di una chiamata di funzione ed è identico a una macchina a stack: spingi i numeri, quindi emetti il verbo. Non c'è annidamento, nessuna sintassi di espressione, nessuna variabile. Il contorno di un triangolo è composto da queste cinque righe:
100 100 m % moveto: inizia un nuovo sottotracciato a (100, 100)
200 200 l % lineto: aggiungi un segmento a (200, 200)
300 100 l % lineto: aggiungi un segmento a (300, 100)
h % closepath: riconnettiti all'inizio
S % stroke: dipingi il contorno del tracciato
Gli operatori sono volutamente concisi. Una vera pagina è composta da migliaia di questi, di solito compressi con FlateDecode. Il costo di quella compattezza è che lo stream non porta alcuna struttura che tu possa interrogare: un visualizzatore non può chiedere "dov'è l'intestazione in questa pagina", può solo eseguire il programma e vedere quale inchiostro atterra dove. Questa è la ragione principale per cui l'estrazione del testo da PDF arbitrari è difficile
L'origine è in basso a sinistra e la Y cresce verso l'alto
Prima che qualsiasi coordinata abbia senso devi sapere dove si trova (0, 0). Il PDF pone l'origine nell'angolo in basso a sinistra della pagina, con X che aumenta verso destra e Y che aumenta verso l'alto, misurati in punti a 72 punti per pollice (ISO 32000-2 §8.3.2). Su una pagina Lettera USA il bordo superiore si trova a y = 792, non a y = 0. Chiunque arrivi dalla grafica su schermo, dove l'origine è in alto a sinistra e la Y cresce verso il basso, affronta la cosa al contrario al primo tentativo e disegna la prima riga fuori dalla parte inferiore della pagina. L'unità è anche indipendente dal mezzo: 72 unità è un pollice sia che la pagina venga resa sullo schermo di un telefono o su una fotounità
La maggior parte delle librerie di disegno di pagine eredita questa convenzione direttamente. In HotPDF, ad esempio, TextOut e le chiamate di tracciato misurano tutte dal basso a sinistra in punti, quindi un valore vicino all'altezza della pagina posiziona il contenuto in alto:
// HotPDF, Delphi: y misurata dal bordo inferiore verso l'alto, in punti
Pdf.CurrentPage.SetLineWidth(2.0);
Pdf.CurrentPage.MoveTo(100, 700); // vicino alla parte superiore della pagina
Pdf.CurrentPage.LineTo(300, 700);
Pdf.CurrentPage.Stroke; // emette gli operatori moveto/lineto/stroke
Quella sequenza di chiamate viene compilata esattamente negli operatori m, l e S sopra. La libreria è un dattilografo per il content stream, niente di più, e sapere cosa emette è ciò che ti consente di ragionare sull'output quando una forma atterra in un punto che non ti aspettavi
Costruisci il tracciato, poi dipingilo
PDF separa la costruzione del tracciato dalla pittura del tracciato e la separazione non è pedanteria. Prima descrivi una forma con operatori di costruzione che non aggiungono nulla di visibile, quindi emetti un singolo operatore di pittura che decide cosa fare con il tracciato accumulato. Lo stesso triangolo può essere un contorno, un riempimento solido o entrambi, a seconda solo del verbo con cui finisci
Gli operatori di costruzione sono pochi. m avvia un nuovo sottotracciato in un punto. l aggiunge un segmento diritto. c aggiunge una curva di Bezier cubica da sei operandi, due punti di controllo e un punto finale. re è una scorciatoia che aggiunge un intero rettangolo da una quadrupla x, y, larghezza, altezza. h chiude il sottotracciato corrente al suo inizio. Nessuno di essi mette inchiostro sulla pagina; si limitano ad accumulare geometria
200 250 m % inizia il sottotracciato
300 350 400 450 500 250 c % Bezier cubica: due punti di controllo, poi punto finale
150 200 re % un rettangolo 150 x 200, aggiunto come proprio sottotracciato
h % chiudi
L'esempio originale utilizzava la variante y ora obsoleta dell'operatore di curva; c con i suoi tre punti espliciti è la forma che vedrai nella pratica e quella a cui ricorrere. Una volta che il tracciato esiste, un operatore di pittura lo finisce. Il vocabolario è piccolo e vale la pena memorizzarlo, perché ogni forma su ogni pagina finisce con uno di questi:
Straccia il contorno del tracciato usando la larghezza della linea e il colore del tratto correntifriempie l'interno usando il colore di riempimento corrente e la regola di avvolgimento non zero (nonzero winding rule)f*riempie usando la regola pari-dispari (even-odd rule), che è importante per le forme auto-intersecanti e per le forme con buchiBriempie e poi traccia in un'unica operazione;bchiude prima il tracciatonnon dipinge nulla, ed è così che un tracciato diventa una regione di ritaglio (clip region) senza lasciare un segno visibile
La regola di avvolgimento è la parte che le persone sbagliano. La regola non zero (f, B) conta gli attraversamenti con segno di un raggio dal punto di test e riempie ovunque il conteggio non sia zero, quindi un buco rimane vuoto solo se il suo sottotracciato si avvolge in senso opposto a quello esterno. Il pari-dispari (f*, B*) si attiva ad ogni attraversamento indipendentemente dalla direzione. Se una forma a "ciambella" esce solida, il cerchio interno è avvolto nello stesso modo di quello esterno, e devi invertirlo o passare a pari-dispari
Il colore è una modalità, non un parametro
Il colore in un content stream è appiccicoso. Imposti un colore e rimane impostato finché non ne imposti un altro o ripristini uno stato precedente, motivo per cui un cambiamento di colore non racchiuso colora silenziosamente tutto ciò che viene disegnato dopo di esso. PDF mantiene anche il colore di riempimento e il colore del tratto come due impostazioni indipendenti, con operatori minuscoli per il riempimento e maiuscoli per il tratto. Gli spazi colore del dispositivo hanno ciascuno la propria abbreviazione:
0.5 g % riempimento DeviceGray, grigio medio (0 = nero, 1 = bianco)
0.2 0.6 0.8 rg % riempimento DeviceRGB
0.8 0.2 0.1 RG % tratto DeviceRGB (maiuscolo = tratto)
0.2 0.8 0.0 0.1 k % riempimento DeviceCMYK
DeviceRGB è adatto all'output su schermo, DeviceCMYK è ciò che si aspetta la produzione di stampa e DeviceGray è la scelta più piccola per i contenuti monocromatici. Gli spazi del dispositivo sono convenienti ma non calibrati: la stessa tripla RGB può rendere in modo diverso su due monitor, ed è il problema che gli spazi colore basati su ICC e gli intenti di output PDF/A esistono per risolvere. Per lavori critici sul colore selezioni uno spazio calibrato con cs e CS e imposti i componenti con sc e scn, ma per i documenti ordinari le abbreviazioni del dispositivo si fanno carico del lavoro. Una libreria le avvolge in chiamate tipizzate. HotPDF, ad esempio, accetta un singolo TColor ed emette gli operatori corrispondenti:
Pdf.CurrentPage.SetRGBFillColor(clRed);
Pdf.CurrentPage.Rectangle(100, 100, 200, 150); // x, y, larghezza, altezza
Pdf.CurrentPage.Fill;
Pdf.CurrentPage.SetRGBFillColor(RGB(0, 255, 0));
Pdf.CurrentPage.Circle(150, 400, 50); // x, y, raggio
Pdf.CurrentPage.Fill;
Lo stato grafico e lo stack q/Q
Tutto ciò che non è il tracciato stesso vive nello stato grafico: l'attuale matrice di trasformazione, i colori di riempimento e del tratto, la larghezza della linea, il modello di tratteggio, la regione di ritaglio, l'alfa. Lo stato è globale e mutabile, quindi l'unico modo sicuro per apportare una modifica locale è salvare l'intera cosa, modificarla, disegnare e ripristinarla. È ciò che fanno q e Q. q inserisce (push) una copia dello stato corrente in uno stack; Q la estrae (pop), scartando ogni modifica apportata dal q corrispondente
q % salva l'intero stato grafico
2 0 0 2 100 100 cm % concatena una trasformazione: scala 2x, trasla in (100,100)
0.8 g % riempimento grigio, circoscritto a questo blocco
% ... disegna contenuti in scala, grigi ...
Q % ripristina: la trasformazione e il colore ritornano
I q e Q non bilanciati sono un modo comune in cui un content stream costruito a mano o cucito va storto. Un q isolato senza un Q corrispondente lascia lo stack profondo quando la pagina finisce; un Q in più lo fa andare in underflow. In ogni caso, un visualizzatore può mantenere in vigore un vecchio ritaglio o una trasformazione, e il contenuto scompare o atterra nel posto sbagliato. Quando la grafica svanisce senza alcun motivo spiegabile dal tracciato, verifica prima lo stack di stato
La CTM trasforma ogni coordinata
La matrice di trasformazione corrente (CTM) si trova tra i numeri nei tuoi operatori e la pagina effettiva. Ogni coordinata viene moltiplicata per la CTM prima che venga disegnato alcunché, quindi la modifica della matrice cambia dove e come appare tutto il disegno successivo senza toccare una singola coordinata del tracciato. L'operatore cm concatena una nuova matrice su quella corrente, prendendo sei operandi che mappano la matrice affine [a b c d e f]:
1 0 0 1 100 50 cm % trasla di (100, 50): e e f portano l'offset
2 0 0 1.5 0 0 cm % scala x di 2, y di 1.5: a e d sono i fattori di scala
0.707 0.707 -0.707 0.707 0 0 cm % ruota di 45 gradi (cos/sin in a, b, c, d)
Due cose fanno inciampare le persone. Primo, cm si compone piuttosto che sostituire, quindi le trasformazioni si accumulano e l'ordine conta: scalare e poi traslare non è la stessa cosa che traslare e poi scalare. Secondo, la rotazione e il ridimensionamento ruotano attorno all'origine corrente, non al centro della forma, quindi per ruotare qualcosa sul posto lo trasli all'origine, lo ruoti, quindi lo trasli indietro, tutto avvolto in q/Q. Questa stessa matrice è ciò che posiziona le immagini, l'ultimo pezzo che vale la pena vedere
Immagini e contenuti riutilizzabili sono XObjects
Le immagini raster non vivono inline nel content stream. Vengono archiviate come XObjects immagine, oggetti esterni con il proprio dizionario che descrive larghezza, altezza, profondità di bit, spazio colore e filtro di compressione, e il content stream fa solo riferimento ad essi. Una foto supportata da JPEG si dichiara in questo modo:
/Photo <<
/Type /XObject
/Subtype /Image
/Width 640
/Height 480
/BitsPerComponent 8
/ColorSpace /DeviceRGB
/Filter /DCTDecode % i dati dell'immagine sono uno stream JPEG
>>
Un XObject immagine disegna nel quadrato unitario: occupa sempre la regione da (0, 0) a (1, 1) nello spazio utente. Non gli passi una posizione o una dimensione. Invece imposti la CTM in modo che il quadrato unitario si mappi sul rettangolo che desideri, quindi lo richiami con Do. Questo è il motivo per cui posizionare un'immagine è sempre una trasformazione seguita da un'invocazione, avvolta in un salva/ripristina in modo che la scala non confluisca nell'operazione successiva:
q
640 0 0 480 50 300 cm % mappa il quadrato unitario a un box 640x480 a (50, 300)
/Photo Do % dipingi l'XObject immagine
Q
Lo stesso meccanismo Do guida gli XObjects modulo (form XObjects), che contengono una porzione riutilizzabile di grafica, un logo o un timbro ripetuto, come un proprio content stream con un bounding box. Lo definisci una volta, lo richiami molte volte con una CTM diversa e i byte appaiono nel file solo una volta. La maggior parte delle librerie nasconde questo dietro una singola chiamata di posizionamento: HotPDF registra una bitmap con AddImage e la posiziona con ShowImage, prendendo esplicitamente x, y, larghezza e altezza invece di chiederti di costruire la matrice a mano:
var
Bmp: TBitmap;
ImgIndex: Integer;
begin
Bmp := TBitmap.Create;
try
Bmp.LoadFromFile('logo.bmp');
ImgIndex := Pdf.AddImage(Bmp, icFlate);
// x, y (in basso a sinistra), larghezza, altezza, angolo di rotazione
Pdf.CurrentPage.ShowImage(ImgIndex, 50, 300, 200, 150, 0);
finally
Bmp.Free;
end;
end;
Sotto quella riga la libreria scrive il dizionario dell'XObject immagine, imposta la CTM per dimensionare e posizionare il quadrato unitario, ed emette Do. Il modello sottostante è quello che vale la pena conoscere, perché spiega ogni risultato strano: un'immagine allungata è una CTM con fattori di scala non corrispondenti, un logo identico su quaranta pagine è un form XObject richiamato quaranta volte e un'immagine che viene visualizzata capovolta è un capovolgimento di segno nella matrice, non un file corrotto
Dove porta tutto questo
Il modello grafico è piccolo una volta che ne vedi la forma. Un content stream è un bytecode postfisso che viene eseguito contro uno stato mutabile; le coordinate partono dal basso a sinistra e passano attraverso la CTM; i tracciati vengono costruiti silenziosamente e dipinti con un singolo operatore deliberato; il colore e le impostazioni della linea persistono finché non li racchiudi con q/Q; le immagini e la grafica riutilizzabile sono XObjects posizionati trasformando un quadrato unitario. Quasi ogni risultato di rendering confuso si riduce a una di queste cinque regole. Se vuoi vedere come questi operatori grafici si inseriscono nel modello a oggetti più ampio, nei dizionari di pagina e nella tabella dei riferimenti incrociati che puntano a essi, la panoramica tecnica della struttura del file PDF copre quel livello, e la creazione di un semplice PDF da zero esplora i byte dall'inizio alla fine. Il disegno del testo vive nella sua propria famiglia di operatori e ha le sue insidie, trattate nell'articolo complementare sulla gestione del testo e dei font in PDF
Le chiamate di disegno Delphi mostrate qui, MoveTo, LineTo, Stroke, Rectangle, Fill, SetRGBFillColor, AddImage e ShowImage, fanno parte del HotPDF Component per Delphi e C++Builder, che emette questi operatori del content stream per te