HotPDF ממיר חבילות XPS ו־OpenXPS ל־PDF בתוך Delphi ו־C++Builder בלי מנהל התקן הדפסה, ממפה כל קואורדינטת fixed-page של 96 DPI דרך מטריצת עמוד אחת בקנה מידה 0.75 עם היפוך Y, מפרסם כל VisualBrush כ־Form XObject משותף, והופך מצבי אריחה של ImageBrush לתבניות אריחה מקוריות של PDF במקום ציורי תמונה חוזרים
התרחיש שגורר את רוב חנויות ה־Windows לעניין הזה הוא משעמם ובלתי־נמנע. משהו כבר מדפיס ל־Microsoft XPS Document Writer — דוח ERP ישן, טופס חתום, מנת חשבוניות — ומדיניות הארכיון אומרת PDF. XPS הוא פורמט לכידה מצוין ופורמט נוראי למסור למערכת רשומות בעוד עשר שנים. לכן קובץ ה־spool חייב להפוך ל־PDF עמוד־אחר־עמוד, וברגע שמתחילים לכתוב את הממיר מגלים שהחלק המעניין הוא לא ה־XML. הוא ש־XPS ו־PDF חלוקים לגבי איפה ראש הצירים, כמה שווה יחידה, ומה מותר למברשת להיות
מחבילה ל־PDF במעבר אחד
נקודת הכניסה היא רישום מטפלי המסמכים, לא מחלקת XPS מיוחדת. THPDFDocumentHandlerRegistry.RegisterStandardHandlers מתקין את מטפלי ה־XPS, EPUB ו־CBZ; הזיהוי מבוסס תוכן, ולכן חבילה שנושאת [Content_Types].xml ולפחות חלק אחד .fpage מקבלת ציון 95 גם כשסיומת הקובץ משקרת, בזמן שסיומת .xps או .oxps גרידא מקבלת רק 10. הסדר הזה חשוב כשמקבלים העלאות, כי תוקף שמשנה שם של EPUB ל־.xps לא צריך לכוון את הצינור
var
Handled: IHPDFHandledDocument;
Info: THPDFDocumentHandlerInfo;
Options: THPDFDocumentHandlerOptions;
Registry: THPDFDocumentHandlerRegistry;
Output: TFileStream;
begin
Registry := THPDFDocumentHandlerRegistry.Create;
Output := TFileStream.Create('spool.pdf', fmCreate);
try
Registry.RegisterStandardHandlers;
Options := THPDFDocumentHandlerOptions.Default;
if not Registry.OpenFromFile('spool.xps', '', Options, Handled, Info) then
raise Exception.Create('no registered handler recognised the package');
if Handled.Format = hdfXPS then
Handled.WritePDF(Output, Options, Info);
// Info.UnsupportedFeatureCount הוא הציון הכן עבור ההמרה הזאת
finally
Output.Free;
Registry.Free;
end;
end;
כל מה שקשור להמרה תחום בתקציב לפני שמנסים אותו. THPDFDocumentHandlerOptions.Default מגביל רשומות ארכיון ל־10,000, בתי ארכיון מורחבים ל־1 GiB, את יחס הדחיסה ל־200, משאבים ל־4,096 ועמודים ל־10,000, והוא נושא CancellationToken אופציונלי כדי שעבודת צד שרת תוכל להיעצר באמצע חבילה. קראו את Info.UnsupportedFeatureCount אחר כך והתייחסו לערך שאינו אפס כממצא אמיתי: HotPDF סופר בכוונה את מה שלא יכול למפות במקום לצייר קירוב ולשתוק עליו
למה עמוד XPS צריך מטריצה במקום קואורדינטות מכתובות מחדש?
כי כתיבת קואורדינטות מחדש מאבדת את מחסנית הטרנספורמציות. FixedPage של XPS מוגדר ביחידות 96 DPI עם ראש הצירים בפינה השמאלית העליונה ו־Y שגדל מטה; מרחב המשתמש של PDF הוא 72 DPI עם ראש הצירים בפינה השמאלית התחתונה ו־Y שגדל מעלה. התיקון הנאיבי הוא לכפות כל מספר ב־0.75 ולחסר כל Y מגובה העמוד בזמן הפליטה. זה עובד לנתיב שטוח אחד ומתפרק ברגע שנכנס RenderTransform, Canvas מקונן או מטריצה מקומית למברשת, כי הטרנספורמציות האלה מוגדרות במרחב XPS והכתיבה פר־קואורדינטה שלכם כבר עזבה אותו. לכן HotPDF משאיר את ההקרנה כמטריצה ומרכיב אותה. HPDFXPSPageMatrix מחזיר את הקבועים פעם אחת לעמוד, HPDFMultiplyXPSMatrix משרשר אותה עם טרנספורמציית הנתיב המצטברת, והתוצאה נפלטת כאופרטור cm יחיד לפני הגיאומטריה. נתוני הנתיב נכתבים אז במספרי XPS לא משונים, וזו גם הסיבה שתחביר הגיאומטריה המקוצר יכול לשתף את אותו ניתוח חסום המשמש לנתוני נתיב SVG — רק אסימון כלל המילוי F0 או F1 הפותח מטופל על ידי מתאם ה־XPS. אם עקבתם אחר אותו היגיון בייבוא וקטורי של EMF ו־WMF, צורת הטיעון מוכרת: פורמטי ייבוא מומרים במטריצה, לעולם לא באריתמטיקה על קואורדינטות עלה
function HPDFXPSPageMatrix(PageHeight: Double): THPDFXPSMatrix;
begin
Result.A := 0.75; // יחידת XPS של 96 DPI לנקודת PDF של 72 DPI
Result.B := 0;
Result.C := 0;
Result.D := -0.75; // Y של XPS גדל מטה, Y של PDF גדל מעלה
Result.E := 0;
Result.F := PageHeight; // גובה עמוד PDF, בנקודות
end;
// CTM מורכב אחד לכל ויזואל, נפלט לפני כל אופרטור נתיב
Effective := HPDFMultiplyXPSMatrix(HPDFXPSPageMatrix(PageHeightPDF), PathMatrix);
Page.AppendRawContent(HPDFXPSMatrixOperator(Effective));
איך משתמשים שוב ב־VisualBrush בלי לצייר אותו פעמיים?
VisualBrush צובע עץ ויזואלי שרירותי — ילדי Canvas, Path ו־Glyphs — לתוך אזור, ייתכן בחזרות על פניו. HotPDF מרכיב את הויזואל הזה פעם אחת ל־PDF Form XObject ואז ממקם אותו, וזו אותה אסטרטגיית משאבים המתוארת בייבוא SVG דרך Form XObjects. שני פרטים מכריעים האם זה עובד. ראשית, את הויזואל צריך לעבור כילדי XML ישירים: סריקה שטוחה אחר אלמנטים ראויים לאריחה מעלה ויזואלים מקוננים לרמת העמוד העליונה והורסת גם את תחום המשאבים וגם את סדר הצביעה. שנית, התוכן נלכד עם מטריצת העמוד XPS ל־PDF כבר מיושמת, ולכן פרסום ה־Form דורש כפל בהופך של אותה מטריצה, אחרת כל מיקום מיישם שוב את קנה המידה 0.75 ואת היפוך ה־Y. ה־Form גם צריך להחזיק את המשאבים שלו: HotPDF מעתיק רק את הגופנים, ה־XObjects, התבניות, ה־ExtGStates ומרחבי הצבע שזרם התוכן הנלכד באמת מפנה אליהם; שיבוט מילון משאבי העמוד כולו יגרור את ה־Form שמתרשם לתוך גרף המשאבים של עצמו ויבנה מעגל. גופנים נשארים במילון ישיר בעמודים רגילים ומועלים למילון עקיף משותף רק כשהתוכן הנלכד באמת מכיל Tf, כך שמסמך בלי ויזואלים לשימוש חוזר לא משלם על המכונה. שימו לב לגבול מפרט אחד שכדאי להכיר לפני שאתם פותחים באג: מקטע 13.4 של ECMA-388 דורש שגם ViewboxUnits וגם ViewportUnits על VisualBrush יהיו Absolute, ולכן יחידות יחסיות אינן תכונה חסרה — הן קלט שאינו תואם, ו־HotPDF מסרב להמציא להן סמנטיקת קואורדינטות
אריחת ImageBrush: ארבעה מצבים, ארבעה גדלי תא
מצבי האריחה של XPS ממופים לתבניות אריחה של PDF ממקטע 8.7.3 של ISO 32000-1 במקום להתרחב למיקומי תמונה חוזרים על פני האזור המכוסה, מה ששומר על גודל הפלט וזמן ההמרה בלתי־תלויים בכמה מהעמוד המברשת מכסה. המיפוי מכני ברגע שרואים אותו: השתקפות מבוטאת בהצבת מיקומים מראת־מראה בתוך תא תבנית אחד והגדלת התא להתאים
Tile— מיקום אחד, התא נשאר viewport של 1×1FlipX— שני מיקומים, התא מורחב ל־2×1FlipY— שני מיקומים, התא מוגבה ל־1×2FlipXY— ארבעה מיקומים, התא מורחב ל־2×2
כל מיקום נושא מלבן חיתוך משלו, כי מיפוי Viewbox שחורג מהתא־המשנה שלו ידמם אל ההשתקפות השכנה. ה־/Matrix של התבנית הוא החלק שתופס אנשים. תבנית אריחה נעוגה למרחב המשתמש הברירת־מחדל של זרם התוכן ההורה, לא למצב הגרפיקה הנוכחי ברגע בחירת התבנית, ולכן המטריצה צריכה להרכיב את שלוש השכבות במפורש — את הקרנת הקבוע־עמוד, את טרנספורמציית ה־Path ואת ה־Transform המקומי למברשת — במקום לסמוך על CTM סביבתי. HotPDF גם מתקף לפני שהוא מקצה: RegisterImageTilingPattern מגביל תבנית ל־1,024 מיקומים ודוחה חיתוכים ניווניים, מטריצות שאינן הפיכות ואינדקסי תמונה לא תקפים. אם אתם רוצים את המודל הכללי בצד ה־PDF מאחורי זה, תבניות אריחה ומרחב הצבע Pattern מכסה את האופרטורים הבסיסיים
// הקרנת הקבוע־עמוד מקופלת למטריצת התבנית, ואז זו המקומית למברשת
PatternMatrix := HPDFMatFromOps( 0.75 * PathMatrix.A, -0.75 * PathMatrix.B,
0.75 * PathMatrix.C, -0.75 * PathMatrix.D,
0.75 * PathMatrix.E,
PageHeight - 0.75 * PathMatrix.F);
if Brush.HasTransform then
PatternMatrix := HPDFMatMul(PatternMatrix, BrushMatrix);
PatternName := Document.RegisterImageTilingPattern(Resource.ImageIndex,
Brush.Viewport.Left, Brush.Viewport.Top,
Brush.Viewport.Left + CellWidth, Brush.Viewport.Top + CellHeight,
CellWidth, CellHeight, Placements, pttNoDistortion, PatternMatrix);
מה קורה כשגרדיאנט רדיאלי אינו מעגל?
XPS מגדיר RadialGradientBrush עם GradientOrigin, Center, RadiusX ו־RadiusY, ולכן המברשת היא אליפסה. הצללה מסוג 3 של PDF, במקטע 8.7.4.5.4 של ISO 32000-1, מערבבת בין שני מעגלים ואין לה דרך לבטא אליפסה ישירות. מיצוע שני הרדיוסים למספר אחד הוא הקיצור המפתה והוא טעות נראית לעין בכל מברשת שאינה קרובה לעגולה. HotPDF מעביר במקום זאת את הבעיה למערכת הקואורדינטות: הוא מתאים את Y בקנה המידה RadiusY / RadiusX, רושם הצללה עגולה כנה באותו מרחב מתואם, בוחר את התבנית, ומיד פולט את קנה המידה ההפוך כך שגיאומטריית הנתיב שנכתבת אחר כך נשארת במרחב המשתמש המקורי של XPS
ScaleY := Brush.RadiusY / Brush.RadiusX;
PatternName := Document.RegisterMultiStopRadialGradient(
Brush.StartX, Brush.StartY / ScaleY, 0,
Brush.EndX, Brush.EndY / ScaleY, Brush.RadiusX,
StopPositions, StopColours, 3);
if Abs(ScaleY - 1) > 0.000001 then
Page.AppendRawContent('1 0 0 ' + HPDFPDFNumber(ScaleY) + ' 0 0 cm'#10);
Page.SetFillPattern(PatternName); // התבנית לוכדת את ה־CTM בדיוק כאן
if Abs(ScaleY - 1) > 0.000001 then
Page.AppendRawContent('1 0 0 ' + HPDFPDFNumber(1 / ScaleY) + ' 0 0 cm'#10);
הסדר בקטע הקוד הזה הוא כל התרגיל, והוא לא סגנוני. תבנית הצללה של PDF לוכדת את מטריצת הטרנספורמציה הנוכחית ברגע שהיא נבחרת כצבע הנוכחי, ולכן קנה המידה הזמני חייב להיפלט לפני SetFillPattern או SetStrokePattern, וההפוך חייב לבוא אחרי הבחירה אך לפני אופרטורי הנתיב. תשגו את הסדר לרעה באחד הכיוונים ותקבלו גרדיאנט שמתעדף נכון בנתיב הראשון וסוטה בכל הבא אחריו. מגבלה קרובה חלה על מצב קואורדינטות יחסיות: RadiusX ו־RadiusY חייבים להיפתר מול רוחב הנתיב וגובהו בנפרד, כי קנה מידה אחד לשניהם לפי אורך צלע בודד משנה בשקט את יחס הממדים של האליפסה בכל נתיב שאינו מרובע
איפה ההמרה כנה לגבי מגבלותיה
חלק ממבני ה־XPS מומרים בקירוב וחלק לא מומרים בכלל, והבחירה העיצובית לאורך כל הדרך היא לספור אותם ולא לזייף אותם. חלקי TIFF ו־JPEG XR נסרקים לרסטר דרך WIC ואינם נושאים הבטחה על אלפא שנשמר, בזמן ש־PNG עם ערוץ אלפא תקף מפוצל לתמונת בסיס ועוד /SMask. גודלה הטבעי של תמונה נגזר כ־pixel * 96 / DPI, קורא קודם את pHYs של PNG או את צפיפות ה־JFIF של JPEG ונופל אחורה ל־96 DPI, כך שכותרת צפיפות פגומה נוחתת בגודל צפוי ולא בגודל שרירותי. משאבי מטריצה שלא נפתרו, טרנספורמציות יחסיות שאינן סטנדרטיות, ColorConvertedBitmap, מצבי פרישת גרדיאנט שאינם נתמכים וגיאומטריה פגומה — כולם מגדילים את UnsupportedFeatureCount, וקלט פגום נכשל סגור במקום להידרדר לציור שונה בשקט
זו העמדה השימושית לממיר ארכיוני: המרה שמקרבת בשקט גרועה מכזאת שאומרת לכם אילו ארבעה אלמנטים לא יכלה לייצג, כי רק השנייה נותנת לכם על מה לבדוק לפני שהמסמך נחתם למערכת רשומות. אם אתם מעריכים המרת XPS ו־OpenXPS לצד שאר צינור המסמכים — הרכבת עמודים, גופנים, חתימה, פלט PDF/A — עמוד HotPDF Delphi PDF component מפרט את מלוא ערכת התכונות ואת גרסאות Delphi ו־C++Builder הנתמכות