Articolo tecnico

HotPDF Resolution in Delphi: unità di disegno e UserWidth

In HotPDF Component, THotPDF.Resolution definisce l'unità di disegno: ogni coordinata X e Y, ogni margine, la dimensione passata a SetFont, e i risultati di TextWidth e GetWideTextWidth si misurano in pollici 1/Resolution. THPDFPage.Width e Height non la seguono e restano in punti, quindi i limiti del layout devono venire dai valori di sola lettura UserWidth e UserHeight. La ragione abituale per toccare Resolution è un porting: un motore di report che ragiona già in pollici 1/96 o 1/144 si sposta più facilmente quando il lato PDF parla la stessa unità che quando ogni call site si prende un fattore di conversione. Funziona bene, purché tu sappia quali numeri sono passati alla nuova unità e quali sono rimasti indietro

Che cosa cambia davvero THotPDF.Resolution?

THotPDF.Resolution cambia solo come HotPDF legge i numeri che passi tu; il PDF che scrive è lo stesso. Il setter è due righe: SetResolution memorizza il valore e imposta DocScale := Value / 72. Da lì in poi, XProjection e YProjection dividono ogni coordinata per DocScale sulla via dell'ingresso nel content stream, e SetFont divide la dimensione allo stesso modo prima di registrarla. Lo user space PDF è di default 1/72 di pollice (ISO 32000-1 §8.3.2.3), quindi alla Resolution di default di 72 la proiezione è l'identità e a 144 un'unità di disegno è mezzo punto. Nessuna voce /UserUnit viene scritta. Quell'attributo di pagina, aggiunto nel PDF 1.6, è una cosa separata che HotPDF espone come THPDFPage.SetUserUnit. Un dettaglio che becca chi arriva dai tutorial TextOut: le coordinate di pagina partono dall'angolo in alto a sinistra con Y che cresce verso il basso, perché YProjection calcola il top del MediaBox meno la Y scalata, e questo vale a ogni Resolution

Come THotPDF.Resolution definisce l'unità di disegno in Delphi: il setter memorizza DocScale come Resolution diviso 72, poi XProjection, YProjection e SetFont dividono ogni coordinata e dimensione sulla via dell'ingresso nel content stream, così Resolution 72 è una mappatura identità e Resolution 144 fa di un'unità di disegno mezzo punto mentre la pagina parte comunque dall'alto a sinistra con Y verso il basso
Niente nel file di output si muove — cambia solo il significato dei numeri che passi, ed è per questo che lo stesso content stream appare a 72 e a 144
var
  Pdf: THotPDF;
  Page: THPDFPage;
  Margin: Single;
  Title: WideString;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'invoice.pdf';
    Pdf.Resolution := 144;               // 1 unità di disegno = 1/144 di pollice
    Pdf.BeginDoc;
    Page := Pdf.CurrentPage;             // A4: Width = 595, UserWidth = 1190
    Margin := 144;                       // un pollice in unità di disegno
    Page.SetFont('Arial', [fsBold], 28); // 28/144 di pollice, un font da 14 pt
    Title := 'INVOICE 2026-0417';
    // Allinea a destra al bordo pagina misurato nella stessa unità
    Page.TextOut(Page.UserWidth - Margin - Page.GetWideTextWidth(Title),
      Margin, 0, Title);
    Page.SetLineWidth(2);                // filetto da 1 pt
    Page.MoveTo(Margin, Margin + 48);
    Page.LineTo(Page.UserWidth - Margin, Margin + 48);
    Page.Stroke;
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Perché Page.Width non è d'accordo con le tue coordinate a Resolution 144?

THPDFPage.Width e Height riportano la pagina in punti a prescindere dalla Resolution del documento, mentre le tue coordinate sono in pollici 1/Resolution, quindi a 144 la pagina sembra larga la metà di quanto sia davvero. Una pagina A4 legge Width = 595 e Height = 842 a Resolution 72 e legge ancora 595 e 842 a 144, dove il bordo destro sta in realtà a X = 1190. UserWidth e UserHeight, aggiunte nella v2.766.0, restituiscono Width * DocScale, che è la dimensione della pagina nell'unità con cui disegni. Prima che esistessero, la libreria mescolava le due cose internamente, e i sintomi a Resolution 144 erano drammatici: i paragrafi andavano a capo dopo ogni carattere, THPDFTable.Render spingeva ogni riga su una nuova pagina, e sia l'importer HTML sia il flattener XFA disegnavano il proprio contenuto a metà dimensione, la form appiattita ammucchiata nell'angolo in alto a sinistra. Layout dei paragrafi, rendering delle tabelle, import HTML, centraggio EMF, clip di pagina WMF e diagnostica del layout ora leggono tutti la dimensione in user unit. Anche il tuo codice di layout dovrebbe farlo: qualsiasi cosa confronti con una coordinata di disegno (un margine destro, un test di salto pagina, un calcolo di centraggio) appartiene a UserWidth e UserHeight, mai a Width e Height

Trappola uno: assegnare Width o Height passa la pagina ai punti

Impostare Page.Width o Page.Height cambia in silenzio la pagina in UserDefined, e una pagina UserDefined ignora del tutto DocScale, quindi tutto ciò che disegni su di essa in seguito è in punti, non in pollici 1/Resolution. Il setter è vecchio e prende punti per progetto, ecco perché il suo significato è stato lasciato stare. La proiezione per una pagina UserDefined è semplice X + MinX, e SetFont memorizza la dimensione invariata. A Resolution 144 il risultato è una pagina il cui contenuto esce all'improvviso il doppio più grande della pagina precedente. La libreria ha fatto esattamente questo errore da sola: le pagine di continuazione dei paragrafi copiavano la dimensione della pagina precedente attraverso Width, e ogni pagina di overflow passava ai punti. Quelle pagine ora copiano Size, Orientation e la Resolution di pagina, e ripiegano su Width e Height solo quando la pagina originale era già UserDefined

Due vie d'uscita, a seconda di cosa ti serve. Se va bene un foglio standard, imposta Page.Size e Page.Orientation e continua a disegnare nella tua unità di Resolution. Se davvero ti serve una dimensione di pagina personalizzata, accetta che è una pagina a punti e disegna in punti; UserWidth eguaglia Width lì, quindi il codice di layout che legge sempre UserWidth continua a funzionare su entrambi i tipi di pagina. Il test unitario lo blocca: a Resolution 144 una pagina A4 riporta una UserWidth di 1190, ma dopo Width := 500 e Height := 400 riporta 500 e 400. Le pagine caricate si comportano allo stesso modo, perché una pagina ricostruita da un PDF esistente conosce solo il proprio MediaBox in punti e disegna in punti. Le pagine che questo documento ha creato conservano le proprie unità quando passi ad altre e torni attraverso CurrentPageNumber, ed è così dalla v2.766.26

Perché Page.Width non è d'accordo con le tue coordinate a Resolution 144 in HotPDF: Width e Height restano in punti mentre il disegno usa pollici 1/144, così una pagina A4 legge 595 ma il suo bordo destro sta a UserWidth 1190, e assegnare Width passa la pagina a UserDefined, che ignora DocScale, quindi i paragrafi vanno a capo per carattere, le tabelle si rompono per riga e le dimensioni di SetFont si dimezzano
Qualsiasi cosa confronti con una coordinata di disegno appartiene a UserWidth e UserHeight — su una pagina a punti UserDefined le due coincidono, quindi lo stesso codice di layout sopravvive a entrambe

Trappola due: perché le dimensioni dei font escono a metà?

Una dimensione di font nata come punti esce dimezzata a Resolution 144 perché SetFont tratta il proprio argomento dimensione come unità di disegno e lo converte in punti prima di memorizzarlo. Internamente, SetFont memorizza ASize / DocScale * DPI nell'oggetto font corrente, quindi il valore memorizzato è sempre in punti. La libreria ci è cascata due volte: il fallback dei font in WideTextOutBoxEx e la pagina di continuazione dei paragrafi restituivano entrambe quel valore in punti memorizzato a SetFont, che lo scalava una seconda volta e dimezzava il testo. Il tuo codice non può leggere la dimensione memorizzata, ma lo stesso bug ricompare ogni volta che un valore in punti da qualche altra parte arriva a SetFont: una TFont.Size da una form VCL, una dimensione in una definizione di report, una lunghezza CSS in pt. Convertilo prima, e includi nel fattore la Resolution della pagina stessa e il caso UserDefined, come fa la riproduzione dei metafile quando riproduce il Canvas della pagina (vedi come HotPDF importa grafica vettoriale EMF e WMF per quel percorso):

// Unità di disegno per punto sulla pagina corrente. Rispecchia la proiezione
// che HotPDF usa: 1 su una pagina dimensionata con Width/Height, altrimenti
// (Resolution del documento / 72) * (Resolution della pagina / 72)
function UnitsPerPoint(Pdf: THotPDF): Single;
begin
  if Pdf.CurrentPage.Size = UserDefined then
    Result := 1
  else
    Result := (Pdf.Resolution / 72) * (Pdf.CurrentPage.Resolution / 72);
end;

procedure SetFontFromVcl(Pdf: THotPDF; Font: TFont);
begin
  // TFont.Size è in punti; SetFont si aspetta unità di disegno
  Pdf.CurrentPage.SetFont(AnsiString(Font.Name), Font.Style,
    Font.Size * UnitsPerPoint(Pdf));
end;

La libreria applica la stessa regola alle proprie costanti in punti. Il font da 12 punti con cui inizia ogni nuova pagina ora viene moltiplicato per il fattore interno unità-per-punto, quindi è 12 punti a qualunque Resolution. DrawChart, i cui margini, dimensioni delle etichette e spessori di linea sono tutti punti hard-coded, ora gira con la scala temporaneamente impostata a 1. Ciò che resta in unità di disegno, di proposito, sono i default dei parametri pubblici come la dimensione del modulo di DrawQRCode e la dimensione di default del font delle tabelle: fanno parte del contratto dell'API, quindi a Resolution 144 valgono la metà di ciò che valgono a 72. Se dimensioni i report da un template, la guida su output dei report con font e immagini in HotPDF copre da dove quei valori di solito vengono

Come verifichi che un layout sia indipendente dalla Resolution?

Il controllo più affidabile è un confronto a byte: renderizza la stessa pagina a Resolution 72 e di nuovo a 144 con ogni coordinata e dimensione raddoppiata, e i content stream non compressi devono essere identici. Entrambe le esecuzioni atterrano sugli stessi valori in punti dopo la proiezione, quindi qualsiasi differenza è un valore che ha saltato la conversione. È così che la suite di test di HotPDF controlla paragrafi, tabelle, import HTML, flattening XFA, archi, metafile e immagini. La stessa tecnica funziona per il tuo codice di report con harness quasi nullo:

procedure RenderPage(const FileName: string; Res: Integer; K: Single);
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.Compression := cmNone;       // content stream leggibili
    Pdf.FileName := FileName;
    Pdf.Resolution := Res;
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 10 * K);
    Pdf.CurrentPage.TextOut(36 * K, 36 * K, 0, 'Line 1');
    Pdf.CurrentPage.Rectangle(36 * K, 60 * K, 200 * K, 40 * K);
    Pdf.CurrentPage.Stroke;
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

// RenderPage('r72.pdf', 72, 1) e RenderPage('r144.pdf', 144, 2)
// devono produrre content stream di pagina identici al byte
Come verificare l'indipendenza dalla Resolution nel codice Delphi HotPDF: renderizza due volte lo stesso layout, una a Resolution 72 con scala 1 e una a 144 con ogni coordinata e dimensione font raddoppiata, poi esigi content stream non compressi identici al byte — una discrepanza punta a una pagina passata a UserDefined attraverso Width o a un valore in punti non convertito che raggiunge SetFont
Entrambe le esecuzioni atterrano sugli stessi valori in punti dopo la proiezione, quindi qualsiasi differenza è un numero che ha saltato la sua conversione — lo stesso harness su cui si affida la suite di test di HotPDF

Controlla gli operatori che portano numeri: Td, Tm, Tf, re, w e gli array TJ. I byte a livello di file differiranno comunque nella data di creazione e nel /ID, quindi confronta gli stream, non i file interi. Una discrepanza punta quasi sempre a una delle due trappole qui sopra: una pagina ridimensionata attraverso Width, o un valore in punti passato dritto a SetFont. Se sei nuovo delle chiamate di disegno in sé, comincia con il percorso guidato TextOut di HotPDF per dimensione, stile e rotazione, poi torna e gira la Resolution quando il tuo layout legge UserWidth. Dettagli API completi e download di prova sono sulla pagina di HotPDF Delphi PDF component