מאמר טכני

Resolution ב-HotPDF לדלפי: יחידות ציור ו-UserWidth

ברכיב HotPDF, ה-THotPDF.Resolution מגדיר את יחידת הציור: כל קואורדינטת X ו-Y, כל שוליים, הגודל שמועבר אל SetFont, והתוצאות של TextWidth ושל GetWideTextWidth נמדדות ב-1/Resolution אינץ. ה-THPDFPage.Width וה-Height לא הולכים אחריו ונשארים בנקודות, ולכן גבולות הפריסה חייבים לבוא מה-UserWidth וה-UserHeight הקריאות-בלבד. הסיבה הרגילה לגעת ב-Resolution היא העברת פורט: מנוע דוחות שכבר חושב ב-1/96 או ב-1/144 אינץ קל יותר להעביר כשהצד של ה-PDF מדבר באותה יחידה מאשר כשכל אתר קריאה מקבל גורם המרה. זה עובד היטב, כל עוד אתם יודעים אילו מספרים עברו ליחידה החדשה ואילו נשארו מאחור

מה ה-THotPDF.Resolution באמת משנה?

ה-THotPDF.Resolution משנה רק איך HotPDF קורא את המספרים שאתם מעבירים; ה-PDF שהוא כותב הוא אותו דבר. ה-setter הוא שתי שורות: ה-SetResolution מאחסן את הערך ומגדיר DocScale := Value / 72. מאותו רגע, ה-XProjection וה-YProjection מחלקים כל קואורדינטה ב-DocScale בדרך אל זרם התוכן, וה-SetFont מחלק את הגודל באותה צורה לפני שהוא רושם אותו. מרחב המשתמש של PDF מוגדר כברירת מחדל 1/72 אינץ (ISO 32000-1 §8.3.2.3), כך שב-Resolution ברירת המחדל של 72 ההקרנה היא הזהות וב-144 יחידת ציור אחת היא חצי נקודה. שום רשומת /UserUnit אינה נכתבת. אותה תכונת עמוד, שנוספה ב-PDF 1.6, היא עניין נפרד ש-HotPDF חושף כ-THPDFPage.SetUserUnit. פרט אחד שתופס באי-כוח ממדריכי ה-TextOut: קואורדינטות העמוד רצות מפינת השמאלי-העליון כשה-Y גדל כלפי מטה, כי ה-YProjection מחשב את ראש ה-MediaBox פחות ה-Y המוכפל, וזה נכון בכל Resolution

איך ה-THotPDF.Resolution מגדיר את יחידת הציור בדלפי: ה-setter מאחסן את ה-DocScale כ-Resolution חלקי 72, ואז ה-XProjection, ה-YProjection וה-SetFont מחלקים כל קואורדינטה וגודל בדרך אל זרם התוכן, כך ש-Resolution 72 הוא מיפוי זהות ו-Resolution 144 הופך יחידת ציור אחת לחצי נקודה בזמן שהעמוד עדיין רץ מלמעלה-משמאל עם Y כלפי מטה
שום דבר בקובץ הפלט לא זז — רק המשמעות של המספרים שאתם מעבירים משתנה, ולכן אותו זרם תוכן מופיע ב-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/144 אינץ
    Pdf.BeginDoc;
    Page := Pdf.CurrentPage;             // A4: Width = 595, UserWidth = 1190
    Margin := 144;                       // אינץ אחד ביחידות ציור
    Page.SetFont('Arial', [fsBold], 28); // 28/144 אינץ, פונט של 14 נקודות
    Title := 'INVOICE 2026-0417';
    // יישור לימין מול קצה העמוד הנמדד באותה יחידה
    Page.TextOut(Page.UserWidth - Margin - Page.GetWideTextWidth(Title),
      Margin, 0, Title);
    Page.SetLineWidth(2);                // קו של 1 נקודה
    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 מדווחים על העמוד בנקודות בלי קשר ל-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 וגם משטח ה-XFA ציירו את התוכן שלהם בחצי גודל, הטופס המשוטח דחוס אל פינת השמאלי-העליון. פריסת פסקאות, רינדור טבלאות, ייבוא HTML, מרכוז EMF, ה-clip של עמוד WMF ואבחוני פריסה כולם קוראים עכשיו את הגודל ביחידות-משתמש. גם קוד הפריסה שלכם צריך: כל דבר שמשווה מול קואורדינטת ציור (שוליים ימניים, בדיקת מעבר עמוד, חישוב מרכוז) שייך אל ה-UserWidth וה-UserHeight, לעולם לא אל ה-Width וה-Height

מלכודת ראשונה: הצבת Width או Height מעבירה את העמוד לנקודות

הצבת Page.Width או Page.Height משנה בשקט את העמוד ל-UserDefined, ועמוד UserDefined מתעלם לגמרי מה-DocScale, כך שכל מה שתציירו עליו אחר כך הוא בנקודות, לא ב-1/Resolution אינץ. ה-setter ישן ומקבל נקודות בכוונה, ולכן משמעותו הושארה כמו שהיא. ההקרנה עבור עמוד UserDefined היא X + MinX פשוט, וה-SetFont מאחסן את הגודל ללא שינוי. ב-Resolution 144 התוצאה היא עמוד שהתוכן שלו יוצא פתאום פי שניים מהעמוד שלפניו. הספרייה ביצעה בעצמה בדיוק את הטעות הזאת: עמודי ההמשך של פסקאות העתיקו פעם את גודל העמוד הקודם דרך ה-Width, וכל עמוד גלישה עבר לנקודות. העמודים האלה מעתיקים עכשיו את ה-Size, את ה-Orientation ואת ה-Resolution של העמוד במקום, ונופלים חזרה אל Width ו-Height רק כשהעמוד המקורי כבר היה UserDefined

שתי דרכי יציאה, לפי מה שאתם צריכים. אם גיליון תקני יספיק, הגדירו Page.Size ו-Page.Orientation והמשיכו לצייר ביחידת ה-Resolution שלכם. אם אתם באמת צריכים גודל עמוד מותאם, קבלו שזהו עמוד נקודות וציירו בנקודות; ה-UserWidth שווה שם ל-Width, ולכן קוד פריסה שתמיד קורא UserWidth ממשיך לעבוד על שני סוגי העמודים. בדיקת היחידה נועצת את זה: ב-Resolution 144 עמוד A4 מדווח UserWidth של 1190, אבל אחרי Width := 500 ו-Height := 400 הוא מדווח 500 ו-400. עמודים טעונים מתנהגים באותה צורה, כי עמוד שנבנה מחדש מ-PDF קיים מכיר רק את ה-MediaBox שלו בנקודות ומצייר בנקודות. עמודים שהמסמך הזה יצר שומרים על היחידות שלהם כשאתם עוברים הלאה וחוזרים דרך ה-CurrentPageNumber, וזה המצב מאז v2.766.26

למה ה-Page.Width חולק על הקואורדינטות שלכם ב-Resolution 144 ב-HotPDF: ה-Width וה-Height נשארים בנקודות בזמן שהציור משתמש ב-1/144 אינץ, כך שעמוד A4 קורא 595 אבל הקצה הימני שלו יושב ב-UserWidth 1190, והצבת Width מעבירה את העמוד ל-UserDefined שמתעלם מה-DocScale, כך שפסקאות עוטפות על כל תו, טבלאות נשברות על כל שורה וגדלי SetFont מתחלקים לשניים
כל דבר שמשווה מול קואורדינטת ציור שייך אל ה-UserWidth וה-UserHeight — על עמוד נקודות UserDefined השניים מצטלבים, ולכן אותו קוד פריסה שורד את שניהם

מלכודת שנייה: למה גדלי פונטים יוצאים בחצי גודל?

גודל פונט שהתחיל כנקודות יוצא בחצי גודל ב-Resolution 144 כי ה-SetFont מתייחס אל ארגומנט הגודל שלו כיחידות ציור וממיר אותו לנקודות לפני האחסון. בפנים, ה-SetFont מאחסן ASize / DocScale * DPI באובייקט הפונט הנוכחי, כך שהערך המאוחסן תמיד נקודות. הספרייה נתקלה בזה פעמיים: נתיב הפונט החלופי ב-WideTextOutBoxEx ועמוד ההמשך של הפסקה שניהם העבירו את ערך הנקודות המאוחסן הזה חזרה אל ה-SetFont, שהכפיל אותו בקנה מידה פעם שנייה וחצה את הטקסט. הקוד שלכם לא יכול לקרוא את הגודל המאוחסן, אבל אותו באג צץ בכל פעם שערך נקודות ממקום אחר מגיע אל ה-SetFont: ה-TFont.Size מטופס VCL, גודל בהגדרת דוח, אורך pt של CSS. המירו אותו קודם, וכללו בגורם את ה-Resolution של העמוד עצמו ואת המקרה של UserDefined, כפי שהשמעת מטאפיילים עושה כשהיא משמיעה את ה-Canvas של העמוד (ראו איך HotPDF מייבא גרפיקה וקטורית של EMF ו-WMF עבור הנתיב הזה):

// יחידות ציור לנקודה בעמוד הנוכחי. משקף את ההקרנה
// ש-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 הוא בנקודות; ה-SetFont מצפה ליחידות ציור
  Pdf.CurrentPage.SetFont(AnsiString(Font.Name), Font.Style,
    Font.Size * UnitsPerPoint(Pdf));
end;

הספרייה מיישמת את אותו כלל על קבועי הנקודות שלה. הפונט בן 12 הנקודות שכל עמוד חדש מתחיל איתו מוכפל עכשיו בגורם הפנימי של יחידות-לנקודה, כך שהוא 12 נקודות בכל Resolution. ה-DrawChart, שהשוליים, גדלי התוויות ורוחבי הקו שלו כולם נקודות קשיחות בקוד, רץ עכשיו עם קנה המידה מוגדר זמנית ל-1. מה שנשאר ביחידות ציור, בכוונה, הם ברירות המחדל של פרמטרים ציבוריים כמו גודל המודול של ה-DrawQRCode וגודל הפונט של הטבלה כברירת מחדל: הם חלק מהחוזה של ה-API, ולכן ב-Resolution 144 הם אומרים חצי ממה שהם אומרים ב-72. אם אתם ממדדים דוחות מתבנית, המדריך לפלט דוחות עם פונטים ותמונות ב-HotPDF מכסה מאיפה הערכים האלה בדרך כלל מגיעים

איך מאמתים שפריסה היא בלתי תלוית-Resolution?

הבדיקה האמינה ביותר היא השוואת בייטים: רנדרו את אותו עמוד ב-Resolution 72 ושוב ב-144 עם כל קואורדינטה וגודל מוכפלים, וזרמי התוכן הלא דחוסים חייבים להיות זהים. שתי הריצות נוחתות על אותם ערכי נקודות אחרי ההקרנה, ולכן כל הפרש הוא ערך שדילג על ההמרה. כך סוויטת הבדיקות של HotPDF בודקת פסקאות, טבלאות, ייבוא HTML, שיטוח XFA, קשתות, מטאפיילים ותמונות. אותה טכניקה עובדת עבור קוד הדוחות שלכם עם כמעט אפס harness:

procedure RenderPage(const FileName: string; Res: Integer; K: Single);
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.Compression := cmNone;       // זרמי תוכן קריאים
    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)
// חייבים להניב זרמי תוכן עמוד זהים בייט-בייט
איך מאמתים עצמאות מ-Resolution בקוד HotPDF לדלפי: מרנדרים את הפריסה הזהה פעמיים, פעם ב-Resolution 72 עם קנה מידה של 1 ופעם ב-144 עם כל קואורדינטה וגודל פונט מוכפלים, ואז דורשים זרמי תוכן לא דחוסים זהים בייט-בייט — אי-התאמה מצביעה על עמוד שהועבר ל-UserDefined דרך Width או על ערך נקודות לא מומר שהגיע אל ה-SetFont
שתי הריצות נוחתות על אותם ערכי נקודות אחרי ההקרנה, ולכן כל הפרש הוא מספר שדילג על ההמרה שלו — אותו harness שסוויטת הבדיקות של HotPDF נשענת עליו

בדקו את האופרטורים שנושאים מספרים: ה-Td, ה-Tm, ה-Tf, ה-re, ה-w ומערכי ה-TJ. בייטים ברמת הקובץ עדיין ישונו בתאריך היצירה וב-/ID, ולכן השוו את הזרמים, לא קבצים שלמים. אי-התאמה כמעט תמיד מצביעה על אחת משתי המלכודות שלמעלה: עמוד שהוגדר מחדש דרך ה-Width, או ערך נקודות שהועבר ישר אל ה-SetFont. אם אתם חדשים בקריאות הציור עצמן, התחילו בהליכת ה-TextOut של HotPDF עבור גודל, סגנון וסיבוב, ואז חזרו והחליפו את ה-Resolution ברגע שהפריסה שלכם קוראת UserWidth. פרטי API מלאים והורדות ניסיון נמצאים בעמוד רכיב HotPDF ל-Delphi PDF