מאמר טכני

ייצוא טווח תאים של Excel כתמונה אחת עם HotXLS

לפעמים התוצר אינו מסמך אלא תמונה של טבלה. בלוק סיכום במייל סטטוס, פאנל KPI מרונדר בדשבורד, תמונה ממוזערת ליד תוצאת חיפוש: כולם רוצים את התאים ואף אחד לא רוצה נייר. TXLSCellImageExporter ב-HotXLS לוקח מלבן תאים קלאסי או של XLSX ומפיק תמונת PNG או JPEG קומפקטית אחת בלי גודל עמוד, בלי שוליים, בלי כותרות עליונות או תחתונות, בלי כותרות הדפסה ובלי מעברי עמוד. רזולוציה, קנה מידה, פורמט ואיכות JPEG ניתנים להגדרה, objects, קווי רשת וגבולות תאים להם מתגים עצמאיים, הרקע יכול להיות צבע או שקוף, וכתיבת הקובץ עוברת החלפה אטומית באותה תיקייה שמשאירה יעד קיים ללא מגע אם משהו נכשל

הסיבה שזה זקוק למייצא משלו ולא לדגל על נתיב ההדפסה היא שדפדוף אינו שכבה אופציונלית שאפשר לכבות. הוא הדבר שצינורית העמודים קיימת כדי לעשות

למה לא לרנדר את הטווח דרך צינורית ההדפסה?

כי צינורית ההדפסה מכניסה עמוד בינכם לבין התאים. גודל הנייר מכריע כמה נכנס, שוליים דוחפים את התוכן פנימה, כותרות עליונות ותחתונות תופסות פסים שלא ביקשתם, כותרות הדפסה חוזרות על שורות שכבר יש לכם, ומעברי עמוד מפצלים את הטווח. בלוק סיכום שנופל על מעבר יוצא כשתי תמונות עם השורה המעניינת חתוכה בחצי. אפשר לפצות על כל זה בהגדרת גודל עמוד מותאם שתואם בדיוק את הטווח, ואנשים עושים זאת, אבל זה אומר לחשב מחדש גאומטריית נייר בכל פעם שהטווח משתנה וזה עדיין משאיר את פס הכותרת ואת לוגיקת כותרות ההדפסה בנתיב

מייצא התאים מודד את המלבן, מקצה bitmap בגודל הזה בדיוק, מצייר אליו את התאים, ומקודד. אין עמוד, ולכן אין מה לכבות בהגדרות. למקרים שבהם דווקא רוצים נייר, נתיב ייצוא ה-PDF הוא הכלי הנכון ומכוסה במאמר ייצוא PDF של גיליון

TXLSCellImageExporter מודד, מצייר ומקודד תמונה אחת לכל טווח תאים בזמן שצינורית ההדפסה מפצלת את הטווח במעברי עמוד
צינורית העמודים מכניסה גאומטריית נייר בינכם לבין התאים; למייצא התאים אין עמוד בשום מקום בנתיב

מדדו לפני שאתם מרנדרים

Measure מחזיר את ממדי הפיקסלים שההגדרות הנוכחיות היו מפיקות בלי לקודד דבר. זה חשוב משתי סיבות. תבנית HTML או מייל בדרך כלל צריכה את ממדי התמונה לפני שהתמונה קיימת, כדי שתוכל לשריין את התיבה ולהימנע מהזזת פריסה. ושירות שמרנדר טווחים שנבחרו על ידי משתמשים צריך דרך לדחות בקשה אבסורדית לפני שמקצים עבורה

uses
  lxHandleX, lxPagination;

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  Exporter: TXLSCellImageExporter;
  Summary: TXLSXRange;
  W, H, Bytes: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('quarter.xlsx');
    Sheet := Book.Sheets.ByPos[0];
    Summary := Sheet.Range['A1:F20'];

    Exporter := TXLSCellImageExporter.Create;
    try
      Exporter.ImageFormat := xpifPng;   // PNG שומר קווים דקים חדים
      Exporter.DPI := 96;
      Exporter.Scale := 2.0;             // פלט בצפיפות retina
      Exporter.IncludeGridlines := False;
      Exporter.IncludeCellBorders := True;
      Exporter.TransparentBackground := True;
      Exporter.MaxPixels := 40 * 1000 * 1000;
      Exporter.MaxBytes := 8 * 1024 * 1024;

      if not Exporter.Measure(Summary, W, H) then
        raise Exception.Create('range exceeds the configured budget');
      // W ו-H ידועים עכשיו; שריינו את תיבת הפריסה לפני הקידוד
      Bytes := Exporter.Save(Summary, 'summary.png');
      if Bytes <= 0 then
        raise Exception.Create('image export failed, previous file kept');
    finally
      Exporter.Free;
    end;
  finally
    Book.Free;
  end;
end;

תקציבים, כי קנה המידה כופל

MaxPixels ו-MaxBytes אינם קישוט הגנתי. מניין הפיקסלים גדל עם ריבוע מקדם קנה המידה ועם ריבוע יחס הרזולוציה, כך שטווח שהוא סביר של 1200 על 800 ב-96 DPI הופך לערך ל-47 מגהפיקסלים ב-600 DPI, ומשתמש שבוחר טווח נציל מלא במקום בלוק סיכום מוסיף סדר גודל נוסף מעל זה. בלי תקרה מצב הכשל הוא הקצאה שהתהליך אינו יכול לספק, שמפילה כל מה שהתהליך הזה עשה אחרת

עם תקרה הבקשה נכשלת והקורא בוחר: לסרב, להפחית את קנה המידה, או לצמצם את הטווח. זו עמדה הרבה יותר טובה עבור שרת דוחות, וזהו אותו היגיון מאחורי התקציבים המפורשים במפענח ה-metafile המתואר במאמר מפענח ה-EMF וה-WMF המסוגר

זרימת תקציב עבור TXLSCellImageExporter ב-HotXLS: Measure מחזיר קודם את גודל הפיקסלים, ואז MaxPixels ו-MaxBytes חוסמים הקצאה וגודל פלט
הסירוב מתרחש לפני ההקצאה, וכשל תקציב בייטים משאיר את התמונה הקודמת ללא מגע עבור הקורא

החלפה אטומית, ולמה התיקייה חשובה

Save אל שם קובץ אינו כותב אל היעד. הוא כותב קובץ זמני באותה תיקייה, מקודד אליו, ורק אז מחליף את היעד. אם הקידוד נכשל, אם התקציב נחרג באמצע הדרך, או אם התהליך נהרג, התמונה הקודמת עדיין שם ועדיין תקפה. דשבורד שמחדש את האריחים שלו לפי לוח זמנים לכן לעולם לא מציג PNG קטוע, שהוא התסמין הרגיל של כתיבה נאיבית שפותחת את היעד ומתחילה להזרים

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

Save של TXLSCellImageExporter מקודד אל קובץ זמני באותה תיקייה ואז מחליף את היעד באופן אטומי; כשלים משאירים את התמונה הקודמת תקפה
הקובץ הזמני חייב לשבת לצד היעד כי החלפה אטומית עובדת רק בתוך כרך אחד

אירועי ציור מציירים על ה-canvas האמיתי

גם מייצא הטווחים וגם מייצא העמודים חושפים אירועי ציור פותחים וסוגרים, והם מקבלים הקשר קריאה-בלבד מלא ולא רק handle של canvas. TXLSPagePaintContext נושא את ה-canvas החי, את גבולות הפיקסלים, את גודל העמוד בנקודות, את הרזולוציה וקנה המידה בפועל, את מספר העמוד במסמך, את מספר העמוד בגיליון, את מניין העמודים הכולל, את שם הגיליון ואת גיליון המקור גם בטעם הקלאסי וגם בטעם XLSX. זה מספיק לצייר סימן מים שמתאים לקנה מידה נכון, או חותמת עמוד שיודעת היכן היא בריצה

procedure TReportJob.StampDraft(Sender: TObject;
  const AContext: TXLSPagePaintContext);
begin
  // מודע קנה מידה, כך שהחותמת נראית זהה ב-1x וב-3x
  AContext.Canvas.Font.Height := Round(-48 * AContext.Scale);
  AContext.Canvas.Font.Color := clSilver;
  AContext.Canvas.Brush.Style := bsClear;
  AContext.Canvas.TextOut(AContext.Bounds.Left + Round(24 * AContext.Scale),
    AContext.Bounds.Top + Round(24 * AContext.Scale), 'DRAFT');
end;

Exporter.AfterPaint := Job.StampDraft;

שלוש התנהגויות שוות הסתמכות. האירועים מנפיקים בדיוק פעם אחת לכל פריים מרונדר, כולל כל פריים של TIFF מרובה עמודים, ולכן מונה שמוגדל במטפל אמין. הם שקטים במהלך מדידה, כך שמטפל עם תופעת לוואי לא רץ פעמיים עבור פלט אחד. ואם האירוע הפותח זורק חריגה, האירוע הסוגר לא מנפיק ולא נכתבים בייטים חלקיים של תמונה, כך שחריגה בקוד הציור שלכם אינה יכולה להפיק קובץ חתום למחצה

בחירת הפורמט

PNG לכל דבר עתיר טקסט. JPEG מחיל טרנספורמציית בלוקים שמפיקה צלצול נראה סביב קווים דקים בעלי ניגודיות גבוהה, וגבולות תאים וטקסט קטן הם בדיוק זה, וה-artefacts שורדים בהגדרות איכות שבהן צילום נראה מושלם. JPEG מרוויח את מקומו כשהטווח נשלט על ידי צילומים מוטמעים וגודל הקובץ חשוב יותר מנאמנות קווי המתאר. רקעים שקופים דורשים PNG, מאחר של-JPEG אין ערוץ alpha, ולכן אריח שמיועד לשבת על משטח צבעוני קבע את הבחירה בשבילכם

אם הטווח שלכם מכיל תאים ממוזגים, בדקו את הפלט מול הגיליון: אזורים ממוזגים מתקשרים עם רוחבי עמודות באופנים שמפתיעים אנשים, וכללי הפריסה מכוסים במאמר תאים ממוזגים ותבניות דוחות. HotXLS קורא וכותב XLS, XLSX, ODS ו-CSV מ-Delphi ו-C++Builder בלי תלות ב-Excel, ומשטח המייצא המלא מתועד בדף המוצר של HotXLS Delphi spreadsheet component