RTF นั้นอยู่มานานมากจนไปโผล่ในที่ที่ไม่มีใครคาดคิด: ระบบสร้างรายงานรุ่นเก่า ไปป์ไลน์ในการรวมจดหมาย (mail merge) คลังเอกสารทางกฎหมายที่มีมาก่อนยุคโปรแกรมประมวลผลคำสมัยใหม่ การแปลงไฟล์พวกนี้เป็น PDF แบบทันทีคือความต้องการที่มักมีเข้ามาอยู่เรื่อย ๆ และวิธีการที่ใช้งานได้จริงบน Windows นั้นไม่ใช่ตัวแจงส่วน (parser) เฉพาะทางของ RTF แต่เป็นการใช้เส้นทางที่ใช้ในการเรนเดอร์ซึ่งตัวระบบปฏิบัติการ Windows ได้มีไว้ให้แล้วนั่นก็คือ ผ่าน TRichEdit และ EM_FORMATRANGE รุ่นไลบรารี losLab PDF DLL นำเสนอ device context (DC) เสมือนที่สามารถแทรกเข้ามาทำงานกับไปป์ไลน์นี้ได้โดยตรง
กลไก: DC เสมือน กับ EM_FORMATRANGE
ตัวควบคุม Rich Edit สามารถจัดแบ่งหน้าสำหรับ device context ได้ทุกระบบ ไม่เฉพาะเพียงแค่เครื่องพิมพ์ทั่วไป EM_FORMATRANGE เป็นข้อความที่ใช้สั่งให้ตัวควบคุมนี้จัดเรียงบรรทัดของกลุ่มตัวอักษรลงเข้าไปใน DC และมันจะส่งพิกัดของตัวอักษรตัวสุดท้ายที่ถูกรับเข้าไปจัดการ การเรียกใช้ในแต่ละครั้งและให้เดิน cpMin ไปข้างหน้า จะทำให้คุณได้รับเนื้อหาการพิมพ์ออกมาแบบทีละหน้า GetCanvasDC จากไลบรารี losLab PDF นำเสนอ in-memory DC โดยวัดระยะจากขนาดหน้ากระดาษที่คุณตั้งให้; และเมื่อเรนเดอร์เอกสารหน้านั้นเสร็จ LoadFromCanvasDc ก็จะดักเก็บกระบวนการเพื่อให้ได้มาเป็นเอกสารในหน้าแบบ PDF แค่นี้ก็ถือว่าเป็นกระบวนการของไปป์ไลน์แบบเบ็ดเสร็จ
สิ่งที่จำเป็นจะต้องทำให้ถูกต้องในครั้งแรก: ตัวควบคุม TRichEdit ต้องมีการจัดขนาดเพื่อให้พอดีกับตัวหน้ากระดาษปลายทางเสมอ หากตัวควบคุมที่ว่านี้เล็กกว่าหรือใหญ่เกินขนาดระยะของฝั่ง DC การจัดเรียงแบ่งหน้า (pagination) ที่ได้ก็จะไม่สอดคล้องกันกับในไฟล์ PDF สำหรับผลลัพธ์แบบ A4 การใช้งานแบบทั่วไปคือการกำหนดมิติขนาดภาพของหน้าต่างทำงานที่ความละเอียด 210 x 297 มิลลิเมตร พร้อมความละเอียดระดับหน้าจอ 96 DPI ก่อนจะทำการโหลด RTF และใช้ตัวช่วยคำนวณตามที่คุณกำหนดร่วมไว้ให้กับหน้าของ DC ด้วยเช่นกัน
การประยุกต์ใช้งานบน Delphi
วิธีการด้านล่างนี้ใช้ส่วนนำเข้าอย่าง PDFlibAX_TLB เป็นตัวทำงานครอบอีกชั้นจากรุ่นไฟล์แบบ DLL ของทางไลบรารี แบบฟอร์มทำหน้าที่คุมทั้ง TRichEdit และอีกหนึ่งปุ่มตัวเลือก; ใน OnCreate ของฟอร์มเป็นส่วนที่กำหนดขนาดแผงควบคุมตามด้วยการนำเข้าตัวข้อมูล (load 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.
ภายในลูป (loop) ทำอะไรบ้าง
PrintRtfBox จะเติมค่าลงในโครงสร้างข้อมูลประเภท TFormatRange แล้วส่งตัวคุมนี้ไปยัง Rich Edit เพื่อทำหน้าที่ประมวลผลใน SendMessage ตัวระบบทำการดึงโครงหน้าของตัวหนังสือเริ่มต้นจากจุด cpMin และพักจุดยุติเมื่อประมวลเนื้อหาของส่วนนั้นบรรจุจนเต็มเข้ากรอบพื้นที่ของระบบหน้า DC และตอบสนองพิกัดหมายเลขเริ่มต้นจากอักขระจุดเดิมที่ไม่ตกอยู่ในร่องเดียวกันเมื่อทำการเช็คเรียบร้อยแล้วว่าอักษรทั้งส่วนตกขอบไป เมื่อตัวรับข้อมูลตอบค่ามากกว่าขนาดตัวพิมพ์ข้อความหรือก็คืองานทุกจุดได้รับการแสดงผลเรียบร้อยหมด ตัวเลขของการตอบกลับจะเป็น 0 ซึ่งทำให้วงจรคำสั่งนี้ของ repeat...until หลุดพ้นในที่สุด
ในลูปการประมวลผลหนึ่งชุดเราจะได้ผลผลิตมาหนึ่งหน้ากระดาษไฟล์แบบ Output1.pdf, Output2.pdf, และต่อ ๆ กันไป ถ้าจุดประสงค์คืออยากจะเรียบเรียงให้อยู่ไฟล์เดียวของเอกสาร ไลบรารีมากับหน้าฟังก์ชันแถมเพิ่มต่อหน้าเพื่อให้ประกอบภายหลัง หรืออาจวางวงลูปด้วยแบบวิธีคำสั่งเพิ่มหน้าอย่าง AddPage ก็ได้ ภายในแต่ละการทดลองรอบใช้ SaveToFile ร่วมกับ RemovePdfDocument จะได้เรื่องความจำที่เก็บแบบเนื้อหาทีละจุด ๆ ซึ่งเหมาะสมกับชิ้นกระดาษไฟล์ที่ใหญ่สำหรับตัวแบบไฟล์ RTF แบบเนื้อหาค่อนข้างยาวยืดเลยทีเดียว
รายละเอียดด้านขนาดที่มีปัญหากันได้บ่อย ๆ
ข้อความพารามิเตอร์ส่งข้อมูลอย่าง 96 DPI ให้เป็นตัวกำหนดหน้าของ LoadFromCanvasDc สำหรับรับรู้ถึงระยะความกว้างยาวตอนรับจากฝั่ง DC หน้าจอสลับออกมาทำงานด้วยขนาดนี้และเปลี่ยนระบบจุดแปลงเข้ากับหน้าของ PDF หากวางระบบในส่วนนี้เพี้ยนไป ขนาดหน้าแบบอักษรก็จะสัดส่วนเพี้ยนไปเลยเช่นกัน ถึงแม้เมื่อนำไปใช้เทียบตัวภาพบนกระดาษอาจดูสมบูรณ์ก็ตาม
+100 ที่เสริมให้กับการสั่งที่จุดของ RcPage.Right กับอีกส่วนตรงจุด RcPage.Bottom คือส่วนพื้นที่จำลองเว้นไว้นอกเหนือกรอบภาพ Rich Edit หยิบ rcPage ตัดหน้าไปตามกระดาษ ขาดพื้นที่พวกนี้บรรทัดในเส้นตรงส่วนท้าย ๆ จะเบิ้ลไปลงถึง 2 ที่ของเนื้อหางานเอกสารทั้งสองแผ่น จุดนี้ไม่ใช่สิ่งตายตัว: พื้นที่ส่วนเพิ่มนั้นควรมากกว่าความยาวของตัวชิ้นงานให้ตกอยู่ในความลึกที่สมมาตร แทนที่จะเป็นไปตามที่ระยะกะสุดท้าย
ขั้นสรุปท้ายสุด ต้องผูกระบบหน้าเข้ากระดานเมื่อเรียกใช้งานของ FormCreate ด้วยเพื่อให้เข้าเงื่อนไขว่าส่งถูกหน้าด้วย SendMessage หน้าแผงปุ่มเปิดการสั่งของ TRichEdit ซึ่งมาทำงานภายหลังระหว่างโปรแกรม ต้องใช้วิธีลงแบบ HandleNeeded ไว้เป็นทางหลักในการจัดเรียงเพื่อช่วยดึงส่วนวงลูปไม่ให้มีการแจ้งเตือนว่าจอเริ่มแสดงการทำงานออกมาแต่อย่างใด
ฟอนต์และการจัดการ RTF ฟีเจอร์อื่น ๆ
เพราะตัวอักษรนั้นอยู่ใต้โปรแกรมส่วนควบคุมงานในระบบ Rich Edit ข้อสังเกตในการจำแนกและการนำฟอนต์ที่ใช้แสดงก็จะมีการวางแบบในเกณฑ์ของตัวอักษรของระบบไว้เรียบร้อยใน Windows เลย ถ้าได้หยิบยืมจากสไตล์อักษรนั้นผ่านแฟ้มชุดข้อมูลนี้มาใช้อย่างแนบเนียนแล้ว แต่กลับขาดของไป ทางระบบจะปรับเลือกเพื่อเอาตัวแปรแทนเสียบเข้าไปให้เงียบ ๆ ตัวอักษรอาจแสดงผลเบี้ยว หรือคลาดขนาดในการจัดแบ่งไป สำหรับงานแบตช์คอนเวิร์ตใหญ่ ๆ คุ้มที่จะตรวจสอบแยก: ดูทีละเอกสาร เช็ครายรูปแบบดูแบบสุ่มจากแหล่งตัวอย่างตามการประมาณเพื่อลองพิมพ์จริงเทียบไปเลย
สำหรับในตาราง, รูปแนบมาด้วย รวมถึงรายละเอียดต่าง ๆ ไม่ได้ต้องปรับแก้อะไรเพิ่มแล้ว Rich Edit สามารถเข้าใจกับเนื้องานแบบมาให้เดิมๆ เลย มีแปลกใจหน่อยถ้างานประเภทตัวหนังสืออย่างช่องปรับระยะบรรทัด (twips) 1/1440 ในความยาวหน่วยนิ้ว แต่ส่วนที่ TFormatRange ปรับตั้งเอาไว้คือแบบจุดพิกเซลที่ความชัดที่ตัวนั้นทำงาน ค่าการปรับเข้าข้างในทำในส่วนตัวระบบอยู่แล้ว แต่เมื่อเริ่มจัดการกับทาง RTF ควรตรวจสอบหน่อยว่าระดับเว้นหน้าว่างตรงตามไซส์พิกเซลด้วย
ความรู้ความเข้าใจเรื่อง DPI และสำหรับจอความละเอียดระดับสูง (High-DPI)
เวลาเปิดหน้ารันที่สัดส่วนสเกลความอัดภาพไป 150% (144 DPI) สั่งเรียก ScaleX(210, mmPixel) ระบบจัดได้เลขมวลหนาและขนาดใหญ่ออกมาเป็นเท่าตัวของ 100% ตัวทำงานเก็บค่าพิกเซลทั้งหมดที่คุณตั้งส่ง GetCanvasDC แล้วจัดการอิง DPI ใส่ไปกับค่าพารามิเตอร์ LoadFromCanvasDc เอาเพื่อนำมาคำนวณแปลงออกมา ตราบเท่าที่ค่าของคุณมันเข้าคู่กับการประมวลผล ภาพหน้าที่ได้ออกมาจะถูกใจในที่สุดต่อให้ภาพพื้นของหน้าปัดค่าที่สูงระดับ High Scale ก็ตาม
สมมุติเอาว่าแอปนี้ตัวยังเป็นจุดเก่าไร้โครงแบบ DPI (ค่ากำหนดรุ่นตั้งต้นในสมัยอดีต) ระบบหน้าต่างจะจัดการตัวแผ่นสเกลเอา DC เองทั้งหมด และคำนวณเอาจากตรงนี้บนอุปกรณ์ไฮเรส (High-DPI) ก็จะพลาดแบบสิ้นเชิง การตั้งแก้ง่ายมากด้วยการให้โปรแกรมมันจับเอาหน้างานใส่หน้าจัดการโครงแอปเข้าไปเลย ตัวระบบจะดูข้อมูลจริงเพื่ออ้างอิงให้เลข 96 เป็นแค่ค่าพารามิเตอร์เก่าสำหรับ LoadFromCanvasDc สามารถเรียกเอาของ GetDeviceCaps(GetDC(0), LOGPIXELSX) สลับใช้งาน โค้ดที่มีการยกให้เป็นค่าเดิมที่ 96 เป็นระดับสเกลมาตรฐานในภาพ 100% ซึ่งเหมาะในการเขียนเพื่อดูตัวอย่างให้ง่ายกะทัดรัดยิ่งกว่า
เปรียบเทียบโหมดเก็บรูปแบบเอาท์พุต: ต่อหน้าหรือทั้งรวม
โครงของฝั่งชุดลูปบนให้เขียน 1 ใบสำหรับแต่ละแผ่นงานของหน้าบันทึก (pdf) ตรงตามใจหรือไม่ ก็ขึ้นอยู่กับระบบงานที่รองรับต่อไป สำหรับการรวมกลุ่มใบประเมินงาน รายงานเอกสาร ต้องการหน้าที่ไม่ได้จัดเรียบเรียงแล้วนำไปประมวลจัดเรียงในขั้นตอนต่อไปทีหลัง ถ้าคุณมีทิศต้องการจัดเรียงเพียง 1 อันเลยล่ะก็ ตัวคำสั่งจัดการรวบหน้ารวมให้มีกระดานแผ่นรวมการพิมพ์ต่อจากรอบของมัน: เพียงรวบรวมของทั้งหมดเตรียมลูป, เปลียนชุดพารามิเตอร์ของการเก็บบันทึกจากการเรียก SaveToFile สำหรับที่ต้องใช้งานข้างในแทน และสั่งบันทึกการจัดครบหลังพ้นโครงลูปไปแล้ว เท่านี้คือหลีกตัวเลือกไฟล์กลางทางไปและทำให้เหมาะเจาะที่สุดแล้วเพื่อสคริปต์แผ่นเดียวครบสมบูรณ์
สำหรับการสร้างไฟล์แบบจำนวนกระดาษเยอะ น่าลองหาทางแปะหน้าตัวนับเอาให้เห็นเป็นเวลาของการทำชุดตัวรันบ้าง เพราะในอัตราตามระยะเวลากระดาษเป็นสัดส่วนหน้างานที่ต้องเดิน ถ้าถึงขั้นระดับ 200 หน้าก็ไปใช้เวลาหลักวินาทีต่อเลย โครงฐาน repeat...until นี่แหละดูประยุกต์จัดระบบไม่ยาก: ดูในจำนวนหน่วยที่ทำงานเสร็จตามการโหลดเป็นความยาวโดยอ้าง LastChar เทียบยอดอักขระของโครงตาม RichEdit1.GetTextLen เอาก็ครบถ้วนแล้ว
คำสั่งอย่าง GetCanvasDC รวมถึงตัวอย่างหน้า LoadFromCanvasDc นำเอาตัวมาแสดงจากส่วนของ losLab PDF Library ซัพพอร์ตทั้ง Delphi กับ C++Builder