פורמט ה-RTF קיים מספיק זמן כך שהוא צץ במקומות שאיש לא תכנן: מחוללי דוחות מדור קודם, תהליכי מיזוג דואר (mail merge), וארכיונים של מסמכים משפטיים שקדמו למעבדי תמלילים מודרניים. המרתו ל-PDF בזמן אמת היא דרישה חוזרת ונשנית, והגישה שבאמת עובדת ב-Windows אינה מנתח RTF ייעודי אלא נתיב הרינדור ש-Windows עצמה כבר מספקת דרך TRichEdit ו-EM_FORMATRANGE. המהדורה של losLab PDF Library כקובץ DLL חושפת הקשר התקן (virtual device context) וירטואלי שמשתלב ישירות לתוך הצינור הזה
המנגנון: DC וירטואלי ו-EM_FORMATRANGE
פקדי Rich Edit יכולים לעבר (paginate) את התוכן שלהם עבור כל הקשר התקן, לא רק עבור מדפסת פיזית. ההודעה EM_FORMATRANGE אומרת לפקד לפרוס טווח של תווים לתוך DC נתון ומחזירה את המיקום של התו האחרון שהיא הצליחה להתאים. קרא לה שוב ושוב, תוך קידום cpMin בכל פעם, ותקבל פלט דף אחר דף. הפונקציה GetCanvasDC של losLab PDF Library מספקת DC בזיכרון בגודל של כל מידות דף שתציין; לאחר רינדור דף לתוכו, LoadFromCanvasDc לוכדת את התוצאה כדף PDF. זה כל התהליך
דבר אחד שצריך לעשות נכון מראש: הפקד TRichEdit חייב להיות בגודל שיתאים לדף היעד. אם הפקד קטן או גדול ממידות ה-DC, העימוד לא יתיישר עם מה שבסופו של דבר יהיה ב-PDF. עבור פלט A4 הגישה הסטנדרטית היא להגדיר את מידות הפיקסלים של הפקד כך שיתאימו ל-210 x 297 מ"מ ב-96 DPI לפני טעינת קובץ ה-RTF, תוך שימוש באותם עוזרי קנה מידה שבהם תשתמש כדי לשנות את גודל ה-DC
מימוש ב-Delphi
להלן שימוש ביחידת הייבוא PDFlibAX_TLB, אשר עוטפת את מהדורת ה-DLL של הספרייה. הטופס מארח TRichEdit וכפתור; מנהל האירוע OnCreate של הטופס קובע את גודל הפקד וטוען את ה-RTF, ולחיצת הכפתור מניעה את לולאת ההמרה
unit MainUnit;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, ComCtrls, PDFlibAX_TLB, ActiveX;
type
TForm1 = class(TForm)
RichEdit1: TRichEdit;
Button1: TButton;
procedure FormCreate(Sender: TObject);
procedure Button1Click(Sender: TObject);
private
function PrintRtfBox(hDc: HDC; rtfBox: TRichEdit;
FirstChar: Integer): Integer;
end;
var
Form1: TForm1;
PdfDoc: TPDFLibrary;
implementation
{$R *.dfm}
procedure TForm1.FormCreate(Sender: TObject);
begin
PdfDoc := TPDFLibrary.Create(Self);
// Size the control to A4 at screen DPI so pagination matches the DC
RichEdit1.Width := Round(ScaleX(210, mmPixel));
RichEdit1.Height := Round(ScaleY(297, mmPixel));
RichEdit1.Lines.LoadFromFile(
ExtractFilePath(Application.ExeName) + 'document.rtf');
end;
procedure TForm1.Button1Click(Sender: TObject);
var
Dc: HDC;
PageNumber, LastChar, PdfDocId: Integer;
begin
PageNumber := 1;
LastChar := 0;
repeat
// Obtain a virtual DC sized to A4
Dc := PdfDoc.GetCanvasDC(
Round(ScaleX(210, mmPixel)),
Round(ScaleY(297, mmPixel)));
// Render the next page of RTF content into the DC
LastChar := PrintRtfBox(Dc, RichEdit1, LastChar);
// Capture the DC contents as a PDF document
PdfDoc.LoadFromCanvasDc(96, 0);
PdfDocId := PdfDoc.SelectedPdfDocument;
PdfDoc.SaveToFile(
ExtractFilePath(Application.ExeName)
+ 'Output' + IntToStr(PageNumber) + '.pdf');
PdfDoc.RemovePdfDocument(PdfDocId);
Inc(PageNumber);
until LastChar = 0;
end;
function TForm1.PrintRtfBox(hDc: HDC; rtfBox: TRichEdit;
FirstChar: Integer): Integer;
var
RcDrawTo, RcPage: TRect;
Fr: TFormatRange;
NextCharPosition: Integer;
begin
RcPage.Left := 0;
RcPage.Top := 0;
RcPage.Right := rtfBox.Left + rtfBox.Width + 100;
RcPage.Bottom := rtfBox.Top + rtfBox.Height + 100;
RcDrawTo.Left := rtfBox.Left;
RcDrawTo.Top := rtfBox.Top;
RcDrawTo.Right := rtfBox.Left + rtfBox.Width;
RcDrawTo.Bottom := rtfBox.Top + rtfBox.Height;
Fr.hdc := hDc;
Fr.hdcTarget := hDc;
Fr.rc := RcDrawTo;
Fr.rcPage := RcPage;
Fr.chrg.cpMin := FirstChar;
Fr.chrg.cpMax := -1;
NextCharPosition :=
SendMessage(rtfBox.Handle, EM_FORMATRANGE, 1, LPARAM(@Fr));
if NextCharPosition < Length(rtfBox.Text) then
Result := NextCharPosition
else
Result := 0; // signals last page
end;
end.
מה הלולאה עושה
PrintRtfBox ממלאת את מבנה ה-TFormatRange ומעבירה אותו לפקד Rich Edit דרך SendMessage. הפקד מרנדר תווים החל מ-cpMin, ועוצר כאשר ה-DC מתמלא, ומחזיר את המיקום של התו הראשון שלא התאים. כאשר ערך ההחזרה שווה לאורך הטקסט הכולל או עולה עליו, כל תו רונדר והפונקציה מחזירה אפס, מה שמסיים את הלולאה repeat...until
כל איטרציה מפיקה קובץ PDF אחד בשם Output1.pdf, Output2.pdf, וכן הלאה. אם אתה רוצה מסמך יחיד מרובה דפים במקום זאת, ממשק ה-API להוספת דפים של הספרייה מאפשר לך להרכיב אותם בדיעבד, או שאתה יכול לבנות מחדש את הלולאה כדי לקרוא ל-AddPage בתוך סשן מסמך יחיד. תבנית ה-SaveToFile לכל איטרציה ואחריה RemovePdfDocument שלמעלה שומרת את שיא הזיכרון מוגבל לתוכן של דף אחד, דבר שחשוב עבור קובצי RTF ארוכים מאוד
פרטי קביעת גודל שמכשילים אנשים
הארגומנט 96 DPI ל-LoadFromCanvasDc אומר לספרייה באיזו רזולוציית מסך רונדר ה-DC, כך שהיא תוכל לחשב את המיפוי הנכון מנקודה לפיקסל עבור דף ה-PDF. אם תטעה בכך, הטקסט יופיע בגודל הלא נכון בפלט למרות שהתמונה נראית נכון על המסך
ה-+100 שנוסף ל-RcPage.Right ול-RcPage.Bottom הוא שוליים קטנים מעבר לקצה הגלוי של הפקד. Rich Edit משתמש במלבן rcPage כדי להחליט היכן לפצל דפים; ללא השוליים, שורה שנופלת בדיוק על הגבול עלולה להשתכפל על פני שני דפים. זהו אינו קבוע קסם: אתה רוצה שהוא יהיה גדול מספיק כדי שגבול הדף יפול בצורה נקייה בתוך אזור הפריסה של הפקד ולא על הפיקסל האחרון
לבסוף, הפקד חייב להיות מחובר כבר לחלון טופס גלוי כאשר FormCreate רץ, כך שידית החלון שלו תהיה תקפה לפני הקריאה הראשונה ל-SendMessage. פקד TRichEdit שנוצר באופן דינמי בזמן ריצה זקוק לקריאה מפורשת ל-HandleNeeded לפני שלולאת הרינדור מתחילה אם הטופס טרם הוצג
טיפול בגופנים ותכונות RTF
מכיוון שהרינדור נעשה על ידי מנוע Rich Edit של Windows, החלפת הגופנים פועלת על פי אותם כללים בהם הוא משתמש לתצוגה ולהדפסה. גופנים אליהם יש הפניה בקובץ ה-RTF ושמותקנים על המכונה ירונדרו בנאמנות; גופנים חסרים יוחלפו בשקט, מה שיכול להזיז אורכי שורות ועימוד. עבור המרת אצווה בייצור כדאי לבדוק זאת במפורש: טען מסמך עם כל גופן שבו מקורות ה-RTF שלך משתמשים וודא שספירת הדפים בפלט תואמת למה שאתה מצפה מתצוגה מקדימה להדפסה ידנית
טבלאות, תמונות מוטבעות ורוב תכונות העיצוב של Rich Text עובדות ללא כל טיפול נוסף מכיוון ש-Rich Edit מרנדר אותן באופן מובנה (natively). התחום האחד שיכול להפתיע הוא טקסט המשתמש במרווחי פסקאות מותאמים אישית או כניסות של השורה הראשונה המבוטאות ב-twips: מערכת הקואורדינטות הפנימית של Rich Edit היא ב-twips (1/1440 אינץ'), בעוד קואורדינטות ה-DC שאתה מגדיר ב-TFormatRange הן בפיקסלים ב-DPI הנוכחי. הפקד ממיר באופן פנימי, אך אם אתה בונה את ה-RTF בצורה תכנותית עליך לוודא שערכי השוליים שלך נמצאים ביחידה הנכונה
מודעות ל-DPI וצגים בעלי רזולוציה גבוהה (high-DPI)
בתצוגה הפועלת בקנה מידה של 150% (144 DPI), הפונקציה ScaleX(210, mmPixel) תחזיר ספירת פיקסלים גדולה יותר מאשר בתצוגה של 100%. ספריית ה-PDF רושמת אילו מידות פיקסלים תעביר ל-GetCanvasDC ומשתמשת בארגומנט ה-DPI ב-LoadFromCanvasDc כדי לחשב לאחור את הגודל הפיזי של הדף ב-PDF. כל עוד ערך ה-DPI שאתה מעביר תואם ל-DPI שבו פועל היישום שלך, גודל דף הפלט יהיה נכון ללא קשר לשינוי קנה המידה של התצוגה
אם היישום שלך אינו מודע ל-DPI (ברירת המחדל הישנה), Windows משנה את קנה המידה של ה-DC של המסך וחישובי הפיקסלים שלך יהיו שגויים במכונות high-DPI. התיקון הפשוט ביותר הוא להצהיר על מודעות ל-DPI (DPI awareness) במניפסט של היישום; היישום אז מקבל פיקסלים אמיתיים של המכשיר ויש להחליף את ה-96 שאתה מעביר ל-LoadFromCanvasDc ב-DPI האמיתי של התצוגה המתקבל מ-GetDeviceCaps(GetDC(0), LOGPIXELSX). דוגמת הקוד שלמעלה משתמשת בקוד קשיח של 96 מכיוון שהיא מתאימה לסביבת שינוי קנה מידה של 100% ושומרת על הדוגמה קצרה
מבנה הפלט: קובץ אחד לכל דף לעומת מסמך משולב
הלולאה שלמעלה כותבת כל דף לקובץ PDF נפרד. האם זה מה שאתה רוצה תלוי בשימוש בהמשך. מערכות ליצירת דוחות זקוקות לרוב לדפים בודדים מכיוון שהן מרכיבות את המסמך הסופי מאוחר יותר על ידי מיזוג או סידור מחדש של הדפים. אם אתה רוצה PDF יחיד מההתחלה, הספרייה מאפשרת לך ליצור מסמך עם מספר דפים בסשן יחיד: צור את המסמך פעם אחת מחוץ ללולאה, קרא למתודת הוספת הדפים במקום SaveToFile בתוך הלולאה, ושמור את המסמך השלם לאחר היציאה מהלולאה. פעולה זו מונעת את יצירת קובצי הביניים והיא המבנה הנכון עבור רוב תרחישי ההמרה של מסמך יחיד
עבור קובצי RTF גדולים כדאי להוסיף משוב התקדמות בתוך הלולאה, מכיוון שקצב ההמרה פרופורציונלי בערך לספירת הדפים ומסמך בן 200 דפים יכול לקחת מספר שניות. מבנה ה-repeat...until קל להרחבה: עקוב אחר היסט התווים בעדכון סרגל התקדמות לאחר כל איטרציה, תוך שימוש ב-LastChar מחולק בספירת התווים הכוללת מ-RichEdit1.GetTextLen
המתודות GetCanvasDC ו-LoadFromCanvasDc המוצגות כאן הן חלק מ-losLab PDF Library עבור Delphi ו-C++Builder