מאמר טכני

גאומטריית תמונות HotXLS ב-Delphi: EMU, cm ו-Scaling

אתם מוסיפים לוגו בגודל 600×400 פיקסלים לכותרת של חשבונית שנוצרת, והוא נראה תקין על מסך הפיתוח שלכם ב-96 DPI, ואז שבוע אחר כך לקוח על מחשב נייד עם DPI גבוה מדווח שהוא מודפס בגודל של בול דואר. הפיקסלים לא השתנו. מה שהשתנה הוא ההנחה שמספר פיקסלים מייצג גודל פיזי, וב-OOXML זה פשוט לא כך. לתמונת גיליון אלקטרוני יש ממדים ב-EMU, ועד שלא תחשבו ב-EMU או ביחידות אמיתיות שממפות אליו בצורה נקייה, הפריסה שלכם תלויה ב-DPI שהמכונה שמרנדרת אותה מניחה במקרה

HotXLS הוא רכיב גיליונות אלקטרוניים VCL מקורי ל-Delphi ול-C++Builder, שקורא וכותב XLS ו-XLSX בלי Excel ובלי תלות ב-COM. החל מ-v2.91.0 אובייקט התמונה ב-XLSX מפסיק לחייב אתכם לעשות את חישובי היחידות ביד: לצד ה-EMU הגולמי הוא חושף רוחב וגובה בסנטימטרים, באינצ'ים ובנקודות, וגם מתודת Scale שמשנה גודל באחוזים עם נעילת יחס גובה-רוחב אופציונלית. המאמר הזה עוסק במהו EMU באמת, למה DrawingML בחרה בו, ואיך להשתמש בממשק הגאומטריה החדש כדי למקם תמונות לפי גודל פיזי במקום לפי מספר פיקסלים שאי אפשר לסמוך עליו

מהו EMU ולמה DrawingML משתמשת בו

EMU הוא קיצור של English Metric Unit, והוא יחידת האורך הבסיסית של DrawingML, שכבת השרטוט המשותפת לכל משפחת Office Open XML (ECMA-376, Part 1, §20). EMU אחד מוגדר כך שיש בדיוק 914400 EMU לכל אינץ' ו-360000 EMU לכל סנטימטר. שני הקבועים האלה הם הסיבה כולה לקיומה של היחידה. 914400 מתחלק ב-2, 3, 4, 5, 6, 8, 9, 10, 12 ועוד רבים; הפירוק שלו הוא 26 × 32 × 52 × 127. מכיוון ש-1 אינץ' = 2.54 ס"מ בדיוק, בחירה ביחידה שמתחלקת גם ב-360000 וגם בשבר נקי של 914400 מאפשרת לפורמט לייצג אינצ'ים, סנטימטרים ונקודות כ-מספרים שלמים בלי עיגול בגבול היחידה. במקום ש-"1.27 cm" בפלואוט יגלוש, EMU שומר 457200 ונשאר מדויק

היחידה האחרת שחשובה כאן היא הנקודה. נקודה טיפוגרפית היא 1/72 אינץ', ולכן יש 12700 EMU לכל נקודה (914400 / 72). נקודות הן האופן שבו Excel עצמו חושב על גובהי שורות, גופנים ושוליים מתחת למכסה, ולכן חשיפת גאומטריית תמונה בנקודות שימושית כשאתם רוצים שהתמונה תתיישר עם מדדי טקסט ולא עם סרגל מודפס. HotXLS מקודד את כל ארבעת הקשרים האלה כקבועי יחידות בספרייה:

const
  XlsxEmuPerInch  = 914400;  // 1 inch
  XlsxEmuPerCm    = 360000;  // 1 centimetre
  XlsxEmuPerPoint = 12700;   // 1 point (1/72 inch)
  XlsxEmuPerPixel = 9525;    // 1 pixel at 96 DPI (914400 / 96)

השורה האחרונה היא לב באג בול הדואר. לפיקסל יש גודל פיזי רק אחרי שמקבעים DPI, ו-9525 EMU הוא הגודל של פיקסל ב-96 DPI בדיוק. ברירת המחדל של Excel היא 96 DPI, ולכן תמונה ברוחב 100 פיקסלים נוחתת על 100 × 9525 = 952500 EMU ≈ 2.54 ס"מ בתצורת ברירת מחדל - אבל שום דבר בקובץ לא מבטיח שהצרכן אכן משתמש ב-96. כשכותבים ביחידות אמיתיות, העמימות הזאת נעלמת: 4 ס"מ הם 4 ס"מ בין אם המסך הוא 96 או 220 DPI

ממשק הגאומטריה של TXLSXImage

תמונה מוטמעת ב-HotXLS היא TXLSXImage. האחסון הקנוני שלה הוא שני שדות שלמים, WidthEMU ו-HeightEMU, המעוגנים ב-Row וב-Col מבוססי-אחד, כלומר התא השמאלי-עליון שממנו התמונה תלויה. המאפיינים ביחידות אמיתיות הם תצוגות מחושבות מעל שדות ה-EMU האלה, לא מצב נפרד - קריאה של WidthCM מחלקת את ה-EMU ב-360000, וכתיבה אליו מכפילה ומעגלת חזרה. לכן כל ממד שאתם קובעים הוא רק איות אחר של אותו ערך EMU בסיסי:

  • WidthInch / HeightInch - EMU ÷ 914400
  • WidthCM / HeightCM - EMU ÷ 360000
  • WidthPt / HeightPt - EMU ÷ 12700
  • WidthEMU / HeightEMU - מקור האמת השלם

אתם מוסיפים תמונה עם AddImage(ARow, ACol, AData, AFormat), מעבירים את הבתים המקודדים הגולמיים ואת TXLSXImageFormat (xlsxImagePng, xlsxImageJpeg, xlsxImageGif, או xlsxImageBmp); הפונקציה מחזירה את האינדקס מבוסס-האפס בתוך אוסף Images של הגיליון. יש גם AddImageFromFile(ARow, ACol, AFileName), שמסיק את הפורמט מסיומת הקובץ. שימו לב לבסיס האינדקס: AddImage מחזירה אינדקס מבוסס-אפס ו-Images[] הוא מבוסס-אפס, וזהו ניגוד מכוון לרשת Cells[Row, Col] שמבוססת-אחד, לכן לא צריך להניח שהשניים תואמים

var
  Sheet: TXLSXWorksheet;
  Img: TXLSXImage;
  Idx: Integer;
begin
  Sheet := Workbook.Sheets.Add('Images');

  // Anchor a PNG at row 3, column 2; AddImage returns a 0-based index.
  Idx := Sheet.AddImage(3, 2, LogoBytes, xlsxImagePng);

  Img := Sheet.Images[Idx];
  Img.WidthCM := 4.0;    // 4 cm wide  -> 1440000 EMU
  Img.HeightCM := 3.0;   // 3 cm tall  -> 1080000 EMU

  // Same geometry, read back in other units.
  // Img.WidthPt  is now 113.39 pt, Img.WidthInch is 1.5748 in.
end;

תמונה שנוצרת כברירת מחדל היא בגודל 100×100 פיקסלים, כלומר 952500 EMU בריבוע, בערך תיבה של 2.54 ס"מ ב-96 DPI. ברירת המחדל הזו קיימת כדי שהתמונה תהיה גלויה גם אם שכחתם להגדיר לה גודל, אבל בכל פריסה אמיתית כדאי לקבוע גודל פיזי מפורש במקום להסתמך על ברירת המחדל שמבוססת על פיקסלים

Scaling ודגל יחס הגובה-רוחב

כשאתם רוצים לשנות גודל ביחס לממדים הנוכחיים במקום ליעד מוחלט, למשל להקטין תמונת תרשים ל-60% מהגודל שבו יובאה, השתמשו ב-Scale:

procedure Scale(APercent: Double; AKeepAspect: Boolean = True);

APercent הוא אחוז שבו 100 פירושו ללא שינוי, 150 מגדיל בחצי, ו-50 מחלק בשניים. עם AKeepAspect כברירת המחדל True, גם הרוחב וגם הגובה מוכפלים באותו גורם, כך שהפרופורציות נשמרות ותמונה בגודל 4×3 ס"מ הופכת ל-6×4.5 ס"מ אחרי Scale(150). אם מעבירים False רק הרוחב משתנה - הגובה נשאר בדיוק כפי שהיה. הא-סימטריה הזו מכוונת: כשאתם רוצים למתוח ציר אחד בנפרד, הכלי הנכון הוא המגדירים המפורשים WidthCM/HeightCM, והמסלול של Scale ללא שימור יחס מיועד למקרה הצר יותר של התאמת רוחב בלבד. קל לקרוא Scale(150, False) כ"הגדלה חופשית של שני הצירים" ולהופתע, לכן כדאי לפנות למגדירים כשבאמת מתכוונים לשני ממדים בלתי תלויים

Img.WidthCM := 4.0;
Img.HeightCM := 3.0;

Img.Scale(150);          // aspect locked: now 6.0 x 4.5 cm
Img.Scale(100);          // no-op, returns immediately

Img.Scale(50, False);    // width only: 3.0 cm wide, height unchanged at 4.5 cm

פרט קטן שכדאי להכיר: Scale(100) מקצר את הפעולה וחוזר בלי לגעת באף אחד מהשדות, כך שאפשר לקרוא לו בלי תנאים בתוך לולאה שבה האחוז עשוי להיות 100. ומכיוון שהגאומטריה נשמרת כ-EMU שלמים, כל מגדיר מבצע עיגול. לכן מעבר הלוך ושוב דרך סנטימטרים שבריים עלול לסטות בשבריר EMU - הרבה מתחת לכל דבר שניתן לראות, אבל כדאי להכיר זאת אם אי פעם תבדקו שוויון מדויק במבחן. לשליטה ברמת פיקסל, הגדירו ישירות את WidthEMU ו-HeightEMU וותרו לגמרי על המרת היחידות

קריאת הגאומטריה חזרה

אוסף התמונות ניתן לשאילתה, וזה חשוב כשאתם טוענים חוברת עבודה קיימת וצריכים לבדוק או להתאים את מה שכבר נמצא בה במקום את מה שרק הוספתם. Images.Count מונה כל תמונה בגיליון, Images[i] מאנדקס אותן מבוסס-אפס, ו-FindAt(ARow, ACol) מחזירה את התמונה המעוגנת לתא מסוים - או nil אם אין אחת כזו. יש גם IndexOfCell עבור האינדקס במקום עבור האובייקט, ו-DeleteAt / DeleteInRange להסרה

var
  i: Integer;
  Img: TXLSXImage;
begin
  for i := 0 to Sheet.Images.Count - 1 do
  begin
    Img := Sheet.Images[i];
    Writeln(Format('[%d] R%dC%d  %.2f x %.2f cm  (%d x %d EMU)',
      [i, Img.Row, Img.Col, Img.WidthCM, Img.HeightCM,
       Img.WidthEMU, Img.HeightEMU]));
  end;

  Img := Sheet.Images.FindAt(3, 2);   // nil-check before use
  if Img <> nil then
    Img.Scale(80);
end;

מכיוון שמאפייני היחידות האמיתיות הם תצוגות חיות, תמונה שיובאה מכלי אחר בגודל EMU מסוים מדווחת את הגאומטריה שלה בסנטימטרים מיד - בלי שלב המרה מצדכם. זה משתלב באופן טבעי עם מודל השרטוט הרחב יותר; אם אתם ממקמים גם תרשימים וצורות לצד תמונות רסטר, המדריך המלווה על תרשימים, תמונות ושרטוטי Excel ב-Delphi עם HotXLS מכסה את מודל העיגון שהאובייקטים האלה חולקים

שולי הגדרת עמוד מטריים

אותו מתח בין EMU ליחידות אמיתיות מופיע גם רמה אחת החוצה, בעמוד. OOXML ו-Excel שומרים את שולי ההדפסה ב-אינצ'ים, וזה לא נוח אם תבניות הדוחות שלכם מוגדרות במילימטרים כמו ברוב העולם מחוץ לארצות הברית. v2.91.0 מוסיפה מגדירי נוחות בסנטימטרים מעל שולי האינץ': MarginLeftCM, MarginRightCM, MarginTopCM, MarginBottomCM, MarginHeaderCM, ו-MarginFooterCM. כל אחד מהם הוא שכבת נוחות דקה מעל מאפיין האינץ' המתאים, וממיר ביחס המדויק של 1 אינץ' = 2.54 ס"מ

Sheet.MarginLeftCM := 2.0;     // 2 cm  == 0.7874 inch
Sheet.MarginRightCM := 2.0;
Sheet.MarginTopCM := 2.5;
Sheet.MarginBottomCM := 2.5;
Sheet.MarginHeaderCM := 1.0;
Sheet.MarginFooterCM := 1.0;

מאפייני האינץ' (MarginLeft וחבריהם) נשארים האחסון הקנוני, כך שאפשר לערבב בין השניים - להגדיר שול עליון בסנטימטרים ולקרוא אותו חזרה באינצ'ים, או להפך - והקובץ שנכתב לדיסק זהה בשני המקרים. ההמרה היא כפל פשוט ב-2.54, בלי עיגול לרשת גסה, כך ש-2 ס"מ נשארים 2 ס"מ בדיוק מלא של double. זו אותה פילוסופיית נוחות מטרית כמו גאומטריית התמונה: הפורמט מדבר אימפריאלית מתחת למכסה, והספרייה מאפשרת לכתוב בכל יחידה שבה כתובה המפרט שלכם. עבור פריסת הדוח שסביבו - כותרות, בלוקים של מטא-נתונים, וסכומים - ראו תאים ממוזגים ופריסת תבניות דוח ב-HotXLS, שמשתמש בשוליים האלה יחד עם טווחים ממוזגים ואזור הדפסה

הערה על מה שהגאומטריה כן ולא מבטיחה

מאפייני הגאומטריה קובעים את הגודל המוצהר של התמונה בקובץ - הגודל שבו צרכן תואם ירנדר אותה. הם לא מבצעים דגימה מחדש של נתוני התמונה; PNG של 50×50 פיקסלים שיוגדר ל-8 ס"מ יוגדל וייראה גושי, בדיוק כפי שהיה קורה ב-Excel. קביעת גודל היא פעולת פריסה, לא עיבוד תמונה, לכן צריך להזין לתמונה רזולוציית מקור מספקת לגודל הפיזי המתוכנן. הספרייה גם לא מקודדת מחדש פורמטים: הבתים שאתם מעבירים ל-AddImage נשמרים ונכתבים כפי שהם, יחד עם TXLSXImageFormat שהצהרתם עליו. אם מעבירים בתים של JPEG אבל מסמנים xlsxImagePng תפיקו קובץ ש-Excel לא יוכל לפתוח, לכן כדאי לתת ל-AddImageFromFile להסיק את הפורמט מהסיומת כשאפשר

שום דבר מזה אינו אקזוטי אחרי שמפנימים את הרעיון האחד שמאחוריו: ב-OOXML, גודל פיזי הוא הערך האמיתי ופיקסלים הם הצל התלוי ב-DPI שלו. כתבו תמונות ושוליים בסנטימטרים, באינצ'ים או בנקודות, תנו ל-HotXLS למפות אותם ל-EMU מדויק, והחשבוניות והדוחות שלכם יודפסו באותו גודל בכל מחשב שפותח אותם

ממשקי גאומטריית התמונה, ה-Scaling ושולי המדידה המטריים המתוארים כאן מגיעים עם רכיב הגיליונות האלקטרוניים HotXLS ל-Delphi, שקורא וכותב XLS ו-XLSX מ-Delphi ומ-C++Builder בלי צורך בהתקנת Excel