บทความเทคนิค

การแปลงไฟล์ RTF เป็น PDF ใน Delphi ด้วยไลบรารี losLab PDF

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