Τεχνικό Άρθρο

HotPDF Resolution: μονάδες σχεδίασης και UserWidth

Στο HotPDF Component, το THotPDF.Resolution ορίζει τη μονάδα σχεδίασης: κάθε συντεταγμένη X και Y, κάθε περιθώριο, το μέγεθος που περνάτε στο SetFont, και τα αποτελέσματα των TextWidth και GetWideTextWidth μετρούνται σε 1/Resolution ίντσα. Τα THPDFPage.Width και Height δεν το ακολουθούν και μένουν σε points, οπότε τα όρια διάταξης πρέπει να βγαίνουν από τα read-only UserWidth και UserHeight. Η συνηθισμένη αιτία να αγγίξετε το Resolution είναι ένα port: μια μηχανή αναφορών που ήδη σκέφτεται σε 1/96 ή 1/144 ίντσα μετακομίζει πιο εύκολα όταν η πλευρά PDF μιλά την ίδια μονάδα παρά όταν κάθε call site παίρνει έναν συντελεστή μετατροπής. Δουλεύει καλά, εφόσον ξέρετε ποιοι αριθμοί μετακόμισαν στη νέα μονάδα και ποιοι έμειναν πίσω

Τι αλλάζει πραγματικά το THotPDF.Resolution;

Το THotPDF.Resolution αλλάζει μόνο το πώς διαβάζει το HotPDF τους αριθμούς που περνάτε· το PDF που γράφει είναι το ίδιο. Ο setter είναι δύο γραμμές: το SetResolution αποθηκεύει την τιμή και θέτει DocScale := Value / 72. Από εκεί και πέρα, τα XProjection και YProjection διαιρούν κάθε συντεταγμένη με το DocScale στον δρόμο προς το content stream, και το SetFont διαιρεί το μέγεθος με τον ίδιο τρόπο πριν το καταγράψει. Το user space του PDF έχει default 1/72 ίντσα (ISO 32000-1 §8.3.2.3), οπότε στο default Resolution του 72 η προβολή είναι η ταυτότητα και στο 144 μία μονάδα σχεδίασης είναι μισό point. Καμία εγγραφή /UserUnit δεν γράφεται. Εκείνο το attribute σελίδας, προσθεμένο στο PDF 1.6, είναι ξεχωριστό πράγμα που το HotPDF εκθέτει ως THPDFPage.SetUserUnit. Μια λεπτομέρεια που πιάνει όσους έρχονται από τα μαθήματα TextOut: οι συντεταγμένες σελίδας τρέχουν από την πάνω αριστερή γωνία με το Y να μεγαλώνει προς τα κάτω, επειδή το YProjection υπολογίζει την κορυφή του MediaBox μείον το κλιμακωμένο Y, και αυτό μένει αληθές σε κάθε Resolution

Πώς το THotPDF.Resolution ορίζει τη μονάδα σχεδίασης στο Delphi: ο setter αποθηκεύει DocScale ως Resolution δια 72, μετά τα XProjection, YProjection και SetFont διαιρούν κάθε συντεταγμένη και μέγεθος στον δρόμο προς το content stream, οπότε το Resolution 72 είναι ταυτότητα και το Resolution 144 κάνει μία μονάδα σχεδίασης μισό point ενώ η σελίδα εξακολουθεί να τρέχει από πάνω αριστερά με το Y προς τα κάτω
Τίποτα στο αρχείο εξόδου δεν μετακινείται — αλλάζει μόνο η σημασία των αριθμών που περνάτε, γι' αυτό το ίδιο content stream βγαίνει στο 72 και στο 144
var
  Pdf: THotPDF;
  Page: THPDFPage;
  Margin: Single;
  Title: WideString;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'invoice.pdf';
    Pdf.Resolution := 144;               // 1 μονάδα σχεδίασης = 1/144 ίντσα
    Pdf.BeginDoc;
    Page := Pdf.CurrentPage;             // A4: Width = 595, UserWidth = 1190
    Margin := 144;                       // μία ίντσα σε μονάδες σχεδίασης
    Page.SetFont('Arial', [fsBold], 28); // 28/144 ίντσα, γραμματοσειρά 14 pt
    Title := 'INVOICE 2026-0417';
    // Δεξιά στοίχιση ως προς την ακμή σελίδας μετρημένη στην ίδια μονάδα
    Page.TextOut(Page.UserWidth - Margin - Page.GetWideTextWidth(Title),
      Margin, 0, Title);
    Page.SetLineWidth(2);                // γραμμή 1 pt
    Page.MoveTo(Margin, Margin + 48);
    Page.LineTo(Page.UserWidth - Margin, Margin + 48);
    Page.Stroke;
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Γιατί το Page.Width διαφωνεί με τις συντεταγμένες μου στο Resolution 144;

Τα THPDFPage.Width και Height αναφέρουν τη σελίδα σε points ό,τι κι αν είναι το Resolution του εγγράφου, ενώ οι συντεταγμένες σας είναι σε 1/Resolution ίντσα, οπότε στο 144 η σελίδα φαίνεται το μισό πλάτος από όσο πραγματικά είναι. Μια σελίδα A4 διαβάζει Width = 595 και Height = 842 σε Resolution 72 και διαβάζει ακόμα 595 και 842 στο 144, όπου η δεξιά ακμή είναι στην πραγματικότητα στο X = 1190. Τα UserWidth και UserHeight, προσθεμένα στο v2.766.0, επιστρέφουν Width * DocScale, που είναι το μέγεθος σελίδας στη μονάδα που σχεδιάζετε. Πριν υπήρχαν, η βιβλιοθήκη ανάμειχνε τα δύο εσωτερικά, και τα συμπτώματα στο Resolution 144 ήταν δραματικά: οι παράγραφοι τυλίγονταν μετά από κάθε χαρακτήρα, το THPDFTable.Render σπρώχνε κάθε γραμμή σε νέα σελίδα, και τόσο ο HTML importer όσο και ο XFA flattener σχεδίαζαν το content τους στο μισό μέγεθος, με την επιπεδωμένη φόρμα στριμωγμένη στην πάνω αριστερή γωνία. Η διάταξη παραγράφων, η απόδοση πινάκων, το HTML import, το κεντράρισμα EMF, το page clip WMF, και η διαγνωστική διάταξης διαβάζουν πλέον όλα το μέγεθος σε user units. Ο δικός σας κώδικας διάταξης πρέπει επίσης: ό,τι συγκρίνεται με συντεταγμένη σχεδίασης (ένα δεξί περιθώριο, ένα τεστ αλλαγής σελίδας, ένας υπολογισμός κεντραρίσματος) ανήκει στα UserWidth και UserHeight, ποτέ στα Width και Height

Παγίδα ένα: ανάθεση Width ή Height γυρνά τη σελίδα σε points

Ορίζοντας Page.Width ή Page.Height αλλάζει αθόρυβα τη σελίδα σε UserDefined, και μια σελίδα UserDefined αγνοεί ολότελα το DocScale, οπότε ό,τι σχεδιάζετε πάνω της μετά είναι σε points, όχι σε 1/Resolution ίντσα. Ο setter είναι παλιός και παίρνει points εξ ορισμού, γι' αυτό η σημασία του αφέθηκε όπως ήταν. Η προβολή για σελίδα UserDefined είναι σκέτο X + MinX, και το SetFont αποθηκεύει το μέγεθος αμετάβλητο. Στο Resolution 144 το αποτέλεσμα είναι μια σελίδα της οποίας το content βγαίνει ξαφνικά διπλάσιο από τη σελίδα πριν από αυτήν. Η βιβλιοθήκη έκανε ακριβώς αυτό το λάθος η ίδια: οι σελίδες συνέχειας παραγράφων αντέγραφαν παλιά το μέγεθος της προηγούμενης σελίδας μέσω Width, και κάθε σελίδα υπερχείλισης γύριζε σε points. Εκείνες οι σελίδες αντέγουν πλέον Size, Orientation, και το Resolution της σελίδας, και γυρνούν σε Width και Height μόνο όταν η αρχική σελίδα ήταν ήδη UserDefined

Δύο έξοδοι, ανάλογα με τι χρειάζεστε. Αν μια τυποποιημένη κόλλα κάνει, ορίστε Page.Size και Page.Orientation και συνεχίστε να σχεδιάζετε στη μονάδα Resolution σας. Αν πραγματικά θέλετε προσαρμοσμένο μέγεθος σελίδας, δεχτείτε ότι είναι σελίδα points και σχεδιάστε σε points· το UserWidth ισούται με Width εκεί, οπότε κώδικας διάταξης που πάντα διαβάζει UserWidth εξακολουθεί να δουλεύει και στα δύο είδη σελίδας. Το unit test το καρφώνει: στο Resolution 144 μια σελίδα A4 αναφέρει UserWidth 1190, αλλά μετά από Width := 500 και Height := 400 αναφέρει 500 και 400. Οι φορτωμένες σελίδες συμπεριφέρονται το ίδιο, επειδή μια σελίδα ξαναχτισμένη από υπάρχον PDF ξέρει μόνο το MediaBox της σε points και σχεδιάζει σε points. Σελίδες που δημιούργησε αυτό το έγγραφο κρατούν τις δικές τους μονάδες όταν φεύγετε και γυρνάτε μέσω CurrentPageNumber, που ισχύει από το v2.766.26

Γιατί το Page.Width διαφωνεί με τις συντεταγμένες σας στο Resolution 144 στο HotPDF: Width και Height μένουν σε points ενώ η σχεδίαση χρησιμοποιεί 1/144 ίντσα, οπότε μια σελίδα A4 διαβάζει 595 αλλά η δεξιά της ακμή βρίσκεται στο UserWidth 1190, και η ανάθεση Width γυρνά τη σελίδα σε UserDefined που αγνοεί το DocScale, οπότε παράγραφοι τυλίγονται ανά χαρακτήρα, πίνακες σπάνε ανά γραμμή και τα μεγέθη SetFont υποδιπλασιάζονται
Ό,τι συγκρίνεται με συντεταγμένη σχεδίασης ανήκει στα UserWidth και UserHeight — σε σελίδα UserDefined points τα δύο συμπίπτουν, οπότε ο ίδιος κώδικας διάταξης επιζεί και στα δύο

Παγίδα δύο: γιατί τα μεγέθη γραμματοσειρών βγαίνουν μισά;

Μέγεθος γραμματοσειράς που ξεκίνησε ως points βγαίνει μισό στο Resolution 144 επειδή το SetFont μεταχειρίζεται το όρισμα μεγέθους του ως μονάδες σχεδίασης και το μετατρέπει σε points πριν το αποθηκεύσει. Εσωτερικά, το SetFont αποθηκεύει ASize / DocScale * DPI στο τρέχον αντικείμενο γραμματοσειράς, οπότε η αποθηκευμένη τιμή είναι πάντα points. Η βιβλιοθήκη σκόνταψε σε αυτό δύο φορές: τόσο το font fallback στο WideTextOutBoxEx όσο και η σελίδα συνέχειας παραγράφων παρέδιδαν εκείνη την αποθηκευμένη τιμή points πίσω στο SetFont, που την κλιμάκωνε δεύτερη φορά και μισούσε το κείμενο. Ο κώδικάς σας δεν μπορεί να διαβάσει το αποθηκευμένο μέγεθος, αλλά το ίδιο bug αναδύεται όποτε μια τιμή points από αλλού φτάνει στο SetFont: ένα TFont.Size από φόρμα VCL, ένα μέγεθος σε ορισμό αναφοράς, ένα μήκος pt CSS. Μετατρέψτε το πρώτα, και συμπεριλάβετε στο συντελεστή το δικό του Resolution της σελίδας και την περίπτωση UserDefined, όπως κάνει η αναπαραγωγή metafile όταν ξαναπαίζει το Canvas της σελίδας (δείτε το πώς το HotPDF εισάγει vector γραφικά EMF και WMF για εκείνη τη διαδρομή):

// Μονάδες σχεδίασης ανά point στην τρέχουσα σελίδα. Καθρεφίζει την προβολή
// που χρησιμοποιεί το HotPDF: 1 σε σελίδα διαστασιολογημένη μέσω Width/Height, αλλιώς
// (document Resolution / 72) * (page Resolution / 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 είναι σε points· το SetFont περιμένει μονάδες σχεδίασης
  Pdf.CurrentPage.SetFont(AnsiString(Font.Name), Font.Style,
    Font.Size * UnitsPerPoint(Pdf));
end;

Η βιβλιοθήκη εφαρμόζει τον ίδιο κανόνα στις δικές της σταθερές points. Η γραμματοσειρά 12 points με την οποία ξεκινά κάθε νέα σελίδα πολλαπλασιάζεται πλέον με τον εσωτερικό συντελεστή μονάδων-ανά-point, οπότε είναι 12 points σε κάθε Resolution. Το DrawChart, του οποίου τα περιθώρια, τα μεγέθη ετικετών και τα πλάτη γραμμών είναι όλα hard-coded points, τώρα τρέχει με την κλίμακα προσωρινά σε 1. Αυτό που μένει σε μονάδες σχεδίασης, επίτηδες, είναι τα defaults δημόσιων παραμέτρων όπως το μέγεθος module του DrawQRCode και το default μέγεθος γραμματοσειράς πινάκων: είναι μέρος του συμβολαίου API, οπότε στο Resolution 144 σημαίνουν το μισό από ό,τι στο 72. Αν διαστασιολογείτε αναφορές από template, ο οδηγός για έξοδο αναφορών με γραμματοσειρές και εικόνες στο HotPDF καλύπτει από πού έρχονται συνήθως εκείνες οι τιμές

Πώς επαληθεύετε ότι μια διάταξη είναι ανεξάρτητη από Resolution;

Ο πιο αξιόπιστος έλεγχος είναι μια σύγκριση bytes: αποδώστε την ίδια σελίδα σε Resolution 72 και ξανά σε 144 με κάθε συντεταγμένη και μέγεθος διπλασιασμένα, και τα ασυμπίεστα content streams πρέπει να είναι πανομοιότυπα. Και τα δύο τρεξίματα προσγειώνονται στις ίδιες τιμές points μετά την προβολή, οπότε κάθε διαφορά είναι μια τιμή που προσπέρασε τη μετατροπή. Έτσι ελέγχει η σουίτα τεστ του HotPDF παραγράφους, πίνακες, HTML import, επιπέδωση XFA, τόξα, metafiles και εικόνες. Η ίδια τεχνική δουλεύει για τον δικό σας κώδικα αναφορών με σχεδόν καθόλου εξοπλισμό:

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 streams
    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) and RenderPage('r144.pdf', 144, 2)
// πρέπει να παράγουν byte-πανομοιότυπα content streams σελίδας
Πώς επαληθεύετε ανεξαρτησία Resolution σε κώδικα HotPDF Delphi: αποδώστε την πανομοιότυπη διάταξη δύο φορές, μία στο Resolution 72 με κλίμακα 1 και μία στο 144 με κάθε συντεταγμένη και μέγεθος γραμματοσειράς διπλασιασμένα, μετά απαιτήστε byte-πανομοιότυπα ασυμπίεστα content streams — μια αναντιστοιχία δείχνει σελίδα γυρισμένη σε UserDefined μέσω Width ή τιμή points μη μετατραπείσα που φτάνει στο SetFont
Και τα δύο τρεξίματα προσγειώνονται στις ίδιες τιμές points μετά την προβολή, οπότε κάθε διαφορά είναι ένας αριθμός που προσπέρασε τη μετατροπή του — ο ίδιος εξοπλισμός στον οποίο βασίζεται η σουίτα τεστ του HotPDF

Ελέγξτε τους τελεστές που κουβαλούν αριθμούς: Td, Tm, Tf, re, w και τους πίνακες TJ. Τα bytes σε επίπεδο αρχείου θα διαφέρουν ακόμα στην ημερομηνία δημιουργίας και στο /ID, οπότε συγκρίνετε τα streams, όχι ολόκληρα αρχεία. Μια αναντιστοιχία σχεδόν πάντα δείχνει σε μία από τις δύο παγίδες παραπάνω: μια σελίδα που άλλαξε μέγεθος μέσω Width, ή μια τιμή points που πέρασε κατευθείαν στο SetFont. Αν είστε νέοι στις ίδιες τις κλήσεις σχεδίασης, ξεκινήστε από το walkthrough TextOut του HotPDF για μέγεθος, στυλ και περιστροφή, μετά επιστρέψτε και αλλάξτε το Resolution μόλις η διάταξή σας διαβάζει UserWidth. Πλήρεις λεπτομέρειες API και trial downloads βρίσκονται στη σελίδα HotPDF Delphi PDF component