Articolo tecnico

Importazione vettoriale EMF su Free Pascal con PDFlibPas

PDFlibPas converte i metafile avanzati in vero contenuto di pagina PDF record per record, invece di rasterizzarli, il che è ciò che mantiene nitido un grafico importato o un disegno CAD a qualsiasi zoom. Quel convertitore conta circa 6500 righe ed è stato scritto contro la VCL, quindi quando la libreria ha acquisito un target Free Pascal è stato classificato come non portabile ed è stato sostituito da uno stub. Quella classificazione era sbagliata, e il modo in cui era sbagliata è una lezione utile su come auditare una dipendenza prima di decidere di riscrivere tutto attorno

La superficie VCL realmente usata da quelle 6500 righe si è rivelata piccola: una classe bitmap impiegata per il formato pixel, il salvataggio su stream, l'handle, la canvas e le scanline; una classe metafile impiegata per larghezza, altezza e handle; e il tipo colore con due costanti. Ognuno di questi era già fornito dall'unità grafica della libreria stessa, che esiste proprio perché la build non VCL abbia equivalenti. Il convertitore non era affatto bloccato sulla VCL. Era bloccato sull'unità Windows di Free Pascal

Dividere sull'asse da cui il codice dipende davvero

Quindi la modifica non è stata una reimplementazione. È stata una sola condizionale: da "compila lo stub quando si compila senza la VCL" a "compila lo stub quando la build non è per Windows". Questo è l'asse corretto, e dichiarare il perché rende la differenza evidente. Un metafile avanzato è un contenitore Windows. Il convertitore è un parser di record GDI di Windows dall'inizio alla fine. Se l'applicazione ospite usa la VCL, un altro widget set o nessun widget set non c'entra nulla con la possibilità di interpretare quei record; se il target è Windows c'entra tutto

Le conseguenze della scelta dell'asse giusto vengono gratis. Le build C++Builder, che non definiscono il simbolo della piattaforma Windows in questa libreria, mantengono lo stub che genera eccezioni e si comportano esattamente come prima. macOS mantiene lo stub, correttamente, perché lì non ci sono record GDI da interpretare. Le build Delphi VCL restano intoccate. E una build Windows con un widget set non VCL guadagna l'importazione vettoriale EMF come effetto collaterale, che nessuno ha dovuto implementare. Una condizionale allineata alla dipendenza reale trasforma il lavoro di piattaforma in una modifica di una riga; una condizionale allineata a quella sbagliata lo trasforma in una riscrittura che non verrà mai pianificata

La condizionale di importazione EMF riassettata dall'appartenenza alla VCL alla piattaforma Windows, mantenendo gli stub altrove e dando alle build Windows non VCL l'importazione vettoriale
Riassettare la condizione dello stub sulla piattaforma Windows conserva ogni comportamento esistente delle build e dà ai target Windows non VCL l'importazione vettoriale EMF gratis

Il divario di Free Pascal erano le dichiarazioni, non la logica

Quello che mancava davvero erano le dichiarazioni Win32 che l'unità Windows di Delphi fornisce e quella di Free Pascal no. Raccoglierle in un'unica unità di compatibilità invece di spargere condizionali per il convertitore ha mantenuto il parser leggibile. L'elenco è istruttivo perché mostra quanto disomogenea sia la copertura degli header tra le due RTL: 113 costanti di tipo record metafile, due flag di output testo esteso, tre costanti di modalità gradient fill, un tipo puntatore per la handle table, alias per i record vertice del gradiente e primitivi, e tre tipi record che Free Pascal non dichiara affatto, relativi ad alpha blending, blitting trasparente e modalità di color management

Nulla di ciò è interessante singolarmente. Tutto deve essere corretto prima che il parser compili, e un'unità di compatibilità è la sede naturale perché può essere confrontata con la documentazione degli header nel suo insieme

Dichiarazioni Win32 assenti dall'unità Windows di Free Pascal, raccolte in un'unica unità di compatibilità per il convertitore vettoriale da EMF a PDF
Costanti di record, flag, alias e tre tipi record assenti vivono tutti in un'unica unità di compatibilità confrontabile con la documentazione degli header

Quella che disegna silenziosamente l'immagine sbagliata

Due di quelle dichiarazioni non sono soltanto assenti: sono presenti e sbagliate per questo scopo, e questa è la parte che vale la pena ricordare anche se non si tocca mai un metafile

Free Pascal dichiara il record di creazione del pennello con la struttura run-time del pennello incorporata, e il record della penna estesa con la struttura run-time della penna incorporata. Entrambe le strutture run-time dichiarano il proprio membro hatch come un intero della dimensione di un puntatore, perché in una chiamata GDI attiva quel membro può trasportare un handle. Un metafile, però, memorizza sempre la forma a 32 bit, perché il layout dei record è parte del formato file serializzato e non cambia con la bitness del processo

Nelle build a 32 bit le due forme coincidono e non succede nulla. Su Win64 il membro della dimensione di un puntatore è di otto byte dove il file ne ha quattro, quindi ogni campo successivo al membro hatch viene letto dall'offset sbagliato. Non c'è eccezione, né errore di parsing, né avvertimento. Il metafile semplicemente viene reso male: colori dai byte sbagliati, spessori delle penne dai byte sbagliati, e un'immagine che sembra un bug di rendering e non un bug di layout della struttura. Delphi fornisce varianti esplicitamente a 32 bit di entrambe le strutture proprio per questo motivo, e l'unità di compatibilità le ridichiara allo stesso modo

Layout in byte del record pennello EMF che mostra un campo hatch della dimensione di un puntatore che sposta i campi successivi di quattro byte su Win64 rispetto al layout fisso a 32 bit
Il record serializzato memorizza sempre un hatch a 4 byte, quindi la struttura run-time della dimensione di un puntatore legge male in silenzio ogni campo successivo su Win64
// Sbagliato su Win64: Hatch è della dimensione di un puntatore, il
// file memorizza 32 bit, e ogni campo successivo si sposta di quattro
// byte senza alcun errore
type
  TLogBrushRuntime = record
    lbStyle: UINT;
    lbColor: COLORREF;
    lbHatch: ULONG_PTR;      // 8 byte in un processo a 64 bit
  end;

// Giusto: il layout serializzato, larghezza fissa indipendentemente
// dalla bitness
type
  TLogBrush32 = record
    lbStyle: UINT;
    lbColor: COLORREF;
    lbHatch: DWORD;          // sempre 4 byte, come memorizzato nel metafile
  end;

La regola generale: qualsiasi struttura che compare sia come argomento API run-time sia come layout di campi serializzato richiede due dichiarazioni, e quella serializzata deve usare tipi a larghezza fissa ovunque. I membri della dimensione di un puntatore in un formato file sono sempre un bug in attesa di una build a 64 bit

Le differenze di firma appartengono a un wrapper, non a ogni call site

Le differenze restanti erano comuni mismatch di firma, e il modo di assorbirle è un wrapper di inoltro piuttosto che una condizionale su ciascun call site. La funzione di combinazione delle trasformazioni accetta puntatori sotto Free Pascal dove Delphi accetta parametri per riferimento, quindi il wrapper prende riferimenti e passa indirizzi. Copia inoltre entrambi gli argomenti sorgente in variabili locali prima, perché il convertitore ha call site in cui la matrice di destinazione è contemporaneamente una delle sorgenti, e passare lo stesso indirizzo due volte a una funzione che scrive mentre legge produce una trasformazione leggermente sbagliata in un modo che si manifesta solo su contenuto ruotato

function CombineTransformCompat(var Dest: TXForm;
  const A, B: TXForm): BOOL;
var
  SrcA, SrcB: TXForm;
begin
  // Copiare prima: i chiamanti passano legittimamente Dest come A o B
  SrcA := A;
  SrcB := B;
{$IFDEF FPC}
  Result := Windows.CombineTransform(@Dest, @SrcA, @SrcB);
{$ELSE}
  Result := Windows.CombineTransform(Dest, SrcA, SrcB);
{$ENDIF}
end;

I tipi rettangolo e punto sono l'altro caso. Free Pascal tratta i record rettangolo e punto del metafile come tipi distinti da quelli grafici generali, quindi otto siti di assegnazione hanno richiesto un cast esplicito tra record di layout identico. Entrambi i compilatori accettano la forma con cast, quindi quei siti non portano alcuna condizionale, il che vale un po' di bruttezza

Cosa cambia per un deployment Free Pascal

L'importazione vettoriale EMF funziona su Windows sotto Free Pascal, producendo lo stesso contenuto di pagina della build Delphi: i path come path, i gradienti come contenuto pattern, il testo come testo. Fuori da Windows la strada raster resta la risposta, e quella è una limitazione del formato più che del porting. Lo stato di coordinate e clipping che il convertitore alimenta è descritto nell'articolo sul tracker di CTM e clipping del content stream, e i primitivi vettoriali che emette sono trattati in grafica vettoriale, shader e gradienti

Se Lei sta esaminando la propria codebase per la stessa occasione, l'esercizio utile è quello che ha dato inizio a tutto: elencare i membri che si usano davvero dal framework da cui si crede di dipendere. La risposta è spesso molto più corta di quanto suggerisca la lista degli import, e il vincolo reale di solito sta in un posto completamente diverso. Le vie di importazione basate sul device context in generale sono descritte nell'articolo su print preview e device context, e la copertura di piattaforme e toolchain è elencata nella pagina di prodotto di losLab PDF Developer Library