Technical Article

ส่งออก TDataSet และรายงานเป็น PDF ใน Delphi

สำหรับการส่งออก TDataSet หรือรายงานเป็น PDF ใน Delphi นั้น losLab PDF Library มีสองแนวทางให้เลือก โดย PDFlibTableExport จะปรับเปลี่ยน TDataSet ใดๆ — ไม่ว่าจะเป็นคิวรี FireDAC, ClientDataSet หรือตารางในหน่วยความจำ (in-memory table) — ให้เป็นตาราง PDF ที่มีเว้นการแบ่งหน้า และบริดจ์ (bridge) อีกสามตัวภายใต้ Addons จะส่งต่อรายงาน FastReport, QuickReport หรือ ReportBuilder ที่เตรียมไว้ไปยัง PDF writer ตัวเดียวกัน โดยทั้งสองวิธีจะสร้างไฟล์ PDF จริงโดยไม่ต้องใช้ไดรเวอร์เครื่องพิมพ์และไม่มีหน้าต่างปรากฏให้เห็น

ปัญหาทั้งสองนี้ดูเหมือนจะคล้ายกันแต่จริงๆ แล้วไม่ใช่ DBGrid หรือผลลัพธ์คิวรีแบบดิบไม่มีโครงร่าง (layout) เป็นของตัวเอง ดังนั้นการส่งออกจึงหมายถึงการออกแบบโครงร่างขึ้นมาใหม่ ได้แก่ คอลัมน์, ส่วนหัว และการแบ่งหน้า ในขณะที่เอกสาร FastReport หรือ ReportBuilder มีการออกแบบโครงร่างไว้เรียบร้อยแล้ว ดังนั้นการส่งออกจึงหมายถึงการทำตามคำสั่งการวาดของคนอื่นลงในพื้นที่ของ PDF อย่างถูกต้องแม่นยำ losLab PDF Library แยกการทำงานเหล่านี้ออกเป็นยูนิตที่ต่างกันเนื่องจากโหมดความล้มเหลว (failure modes) มีความแตกต่างกัน และส่วนที่เหลือของบทความนี้จะอธิบายรายละเอียดของแต่ละส่วนรวมถึงจุดที่ระบบอาจเริ่มทำงานได้ไม่เสถียร

จะส่งออก TDataSet เป็น PDF ใน Delphi ได้อย่างไร?

PDFlibTableExport จะแปลงชุดข้อมูล (dataset) ให้เป็นตารางด้วยการเรียกใช้งานเพียงครั้งเดียว ตัวส่งออกจะตรวจสอบรายชื่อฟิลด์ทีละฟิลด์ และข้ามฟิลด์ binary blob โดยอัตโนมัติ — เนื่องจาก ftBlob, ftGraphic, ftBytes และประเภทเดียวกันไม่มีข้อความในเซลล์ที่เป็นประโยชน์ — จัดคอลัมน์ตัวเลขชิดขวา และลงสีลายทางสลับสี (zebra stripe) ตามตัวเลือกที่กำหนดบนแถบส่วนหัวที่กำหนดสไตล์ไว้ เบื้องหลังการทำงานนั้นระบบจะสร้างตารางด้วย CreateTable เติมข้อมูลในเซลล์ด้วย SetTableCellContent และเรนเดอร์ด้วย DrawTableRows ซึ่งเป็น Table API สาธารณะแบบเดียวกับที่คุณใช้งานด้วยตนเอง นอกจากนี้ PDFlibExportDataSet ซึ่งเป็น wrapper อำนวยความสะดวกจะกำหนดค่าหน่วยเป็นมิลลิเมตรและจุดกำเนิดที่มุมบนซ้ายให้คุณ ดังนั้นผู้เรียกใช้จึงเพียงแค่ระบุหน้าเว็บเท่านั้น

uses
  Data.DB, FireDAC.Comp.Client, FireDAC.Stan.StorageBin,
  PDFlibrary, PDFlibTableExport;

var
  MemTable: TFDMemTable;
  PDF: TPDFlib;
  Options: TPDFlibTableExportOptions;
begin
  MemTable := TFDMemTable.Create(nil);
  PDF := TPDFlib.Create;
  try
    MemTable.LoadFromFile('customer.FDS');   // any TDataSet works here

    PDF.SetOrigin(1);
    PDF.SetMeasurementUnits(1);              // millimetres
    PDF.SetPageSize('A4');
    PDF.AddStandardFont(4);

    Options := DefaultTableExportOptions;
    Options.Title := 'Customers';
    Options.ColumnWidth := 32;               // 11 fields fit on A4 at 32 mm
    Options.RepeatHeader := True;            // redraw the header on each page

    PDFlibExportDataSet(PDF, MemTable, 'customers.pdf', Options);
  finally
    PDF.Free;
    MemTable.Free;
  end;
end;

การข้ามฟิลด์ blob อัตโนมัติเป็นเพียงค่าเริ่มต้น ไม่ใช่ข้อจำกัดที่ยืดหยุ่นไม่ได้ เมื่อคุณต้องการควบคุมทีละฟิลด์ — เช่น ป้ายกำกับคอลัมน์แบบกำหนดเอง, ความกว้างที่แคบลง หรือการทิ้งคอลัมน์คีย์ภายในที่ไม่ใช่ blob — ให้สร้าง TPDFlibTableExporter โดยตรงและเชื่อมต่อกับเหตุการณ์ OnFieldFilter ซึ่งจะทำงานหนึ่งครั้งต่อหนึ่งฟิลด์และส่งมอบข้อกำหนดที่เปลี่ยนแปลงได้มาให้คุณ โดยกำหนดค่า Include เป็น False เพื่อทิ้งฟิลด์นั้น หรือกำหนดค่า ColumnWidth และ DisplayLabel เพื่อเขียนทับค่าเริ่มต้น นอกจากนี้ฮุก (hook) นั้นยังเป็นจุดที่คุณใช้คัดแยกคอลัมน์ memo ขนาดใหญ่ออกไปเพื่อไม่ให้ตารางขยายใหญ่เกินหน้ากระดาษ

การแบ่งหน้า: การส่งต่อสถานะการวาดระหว่างหน้า

DrawTableRows จะเก็บสถานะการแบ่งหน้าเอาไว้ และกุญแจสำคัญคือการทำความเข้าใจข้อตกลงในการใช้งาน คุณเรียกใช้ฟังก์ชันนี้ด้วยแถวแรกและแถวสุดท้าย โดยการส่งค่าแถวสุดท้ายที่น้อยกว่า 1 จะหมายถึงการวาดไปจนถึงจุดสิ้นสุดของตารางภายใต้ขอบเขตความสูงที่คุณกำหนดไว้ ฟังก์ชันจะส่งคืนความสูงที่วาดจริง และ GetTableLastDrawnRow จะรายงานแถวสุดท้ายที่ใส่ได้พอดี คู่สถานะนี้เป็นสิ่งที่คุณส่งผ่านจากหน้าหนึ่งไปยังหน้าถัดไป: เมื่อแถวสุดท้ายที่วาดไม่ถึงแถวทั้งหมด คุณจะต้องเปิดหน้าใหม่และดำเนินการต่อจากแถวถัดจากนั้น โดยไม่มีการวัดขนาดใหม่ — การทำงานต่อจะเริ่มจากจุดที่การเรียกใช้ก่อนหน้านี้สิ้นสุดลงทันที

// How the exporter continues a long table across pages
PageHeight := PDF.PageHeight;
Y := PageHeight - Options.Top;
Row := 1;
while Row <= TotalRows do
begin
  DrawHeight := Y - Options.BottomMargin;
  // Last row = 0 means "draw to the end", bounded by DrawHeight
  PDF.DrawTableRows(TableID, Options.Left, Y, DrawHeight, Row, 0);
  LastDrawn := PDF.GetTableLastDrawnRow(TableID);
  if LastDrawn >= TotalRows then
    Break;                       // the whole table fit on this page
  if LastDrawn < Row then
    Break;                       // safety: no forward progress, bail out
  PDF.NewPage;
  Row := LastDrawn + 1;          // continue from the first undrawn row
  Y := PageHeight - Options.Top;
end;

การวนซ้ำ (loop) ยังเป็นจุดที่ใช้จัดการส่วนหัวที่ต้องแสดงซ้ำ ในแต่ละหน้าถัดไป ตัวส่งออกจะวาดแถวที่ 1 (ส่วนหัว) ซ้ำอีกครั้งก่อนแถวข้อมูลตามตัวเลือกที่กำหนด และใช้ความสูงที่ส่งกลับจากการเรียกใช้ DrawTableRows ครั้งแรกเพื่อเลื่อนตำแหน่งเนื้อหาตารางให้อยู่ด้านล่าง นี่คือสาเหตุที่ DrawTableRows ส่งกลับค่าความสูงแทนที่จะเป็นพิกัดแถวถัดไป: ค่าที่ส่งกลับนี้ช่วยให้คุณสามารถวางส่วนหัวที่วาดซ้ำและเนื้อหาที่เหลือซ้อนกันได้โดยไม่ต้องระบุขนาดของส่วนใดส่วนหนึ่งตายตัว

จะส่งออกรายงาน FastReport หรือ ReportBuilder เป็น PDF ได้อย่างไร?

บริดจ์สำหรับรายงานจะใช้วิธีตรงกันข้าม: พวกมันจะไม่สร้างโครงร่างขึ้นใหม่ แต่จะทำตามโครงร่างที่มีอยู่แล้ว บริดจ์แต่ละตัวจะเชื่อมต่อกับเอนจินของตัวเองในจุดส่งออกของเอนจินนั้นๆ PDFlibFRExport เป็นคลาสย่อย (subclass) ของ TfrxCustomExportFilter ซึ่งจะรับวัตถุ memo, picture, shape และ line ของ FastReport และแปลงแต่ละวัตถุเป็น PDFlibPas ดั้งเดิม (primitive) ส่วน PDFlibRBDevice เป็นคลาสย่อยของ TppFileDevice และจะไล่ตามรายการ DrawCommand ของ ReportBuilder ในขณะที่ PDFlibQRExport จะใช้แนวทางที่สามที่แตกต่างออกไปอย่างสิ้นเชิง ซึ่งหัวข้อถัดไปจะกล่าวถึง ทั้งสามตัวนี้ทำงานโดยไม่มีส่วนติดต่อผู้ใช้ (headless) ซึ่งเป็นเหตุผลสำคัญที่เลือกใช้งานบนเซิร์ฟเวอร์

การทำงานแบบ headless มีข้อควรระวัง และ FastReport ก็เป็นกรณีตัวอย่าง TfrxReport.Export จะส่งข้อมูลผ่านหน้าตัวอย่างก่อนพิมพ์ (preview pages) ซึ่งจะตรวจสอบคุณสมบัติ ShowDialog ของตัวกรอง และคุณสมบัตินั้นจะสืบทอดค่าเริ่มต้นเป็น True บนเครื่องที่ไม่มีเดสก์ท็อปแบบโต้ตอบ กล่องโต้ตอบโมดอล (modal dialog) จะส่งคืนผลลัพธ์การยกเลิกและการส่งออกจะล้มเหลวอย่างเงียบๆ ให้กำหนด ShowDialog เป็น False ก่อนที่จะเรียกใช้ Export และรายงานจะถูกเรนเดอร์โดยไม่แสดงหน้าต่างใดๆ สำหรับ ReportBuilder ก็มีสวิตช์ขนานกันคือ — AllowPrintToFile และ ShowPrintDialog — ส่วน QuickReport ซึ่งขับเคลื่อนทุกอย่างผ่าน Prepare ไม่จำเป็นต้องปิดการใช้งานกล่องโต้ตอบเลย

var
  Exporter: TPDFlibFRExport;
begin
  Report.PrepareReport;                  // build the pages first
  Exporter := TPDFlibFRExport.Create(nil);
  try
    Exporter.FileName := 'invoice.pdf';
    Exporter.ShowDialog := False;        // headless: skip the modal, no cancel
    Report.Export(Exporter);
  finally
    Exporter.Free;
  end;
end;

สามเอนจิน สามระบบพิกัด

บริดจ์แต่ละตัวจะคำนวณพิกัดของตัวเอง เนื่องจากเอนจินแต่ละตัวใช้วัดระยะที่แตกต่างกัน PDFlibFRExport จะจัดการตำแหน่งวัตถุของ FastReport เป็นพิกเซลที่ 96 PPI และขยายขนาดด้วยอัตราส่วน 96/25.4 เพื่อแปลงเป็นหน่วยมิลลิเมตร ส่วน PDFlibRBDevice จะอ่านคำสั่งวาดภาพของ ReportBuilder ในหน่วยหนึ่งในพันของมิลลิเมตรและหารด้วย 1000 และตัวมันเองจะลงทะเบียนผ่าน ppRegisterDevice soap การกำหนด ppReport.DeviceType เป็น 'PDFlibPas' และเรียกใช้ Print ก็เพียงพอแล้วที่จะส่งเอาต์พุตผ่านตัวมัน ส่วน PDFlibQRExport ไม่ได้คำนวณพิกัดเลย: หน้ารายงาน QuickReport ที่เตรียมไว้จะเป็น EMF metafile เรียบร้อยแล้ว ดังนั้นบริดจ์จะบันทึกแต่ละหน้าลงในสตรีม (stream) และส่งต่อไปยัง ImportEMFFromStream เพื่อปล่อยให้ device-context and metafile rendering path ของไลบรารีจัดวางแต่ละสัญลักษณ์อักขระ (glyph) และเส้นบรรทัด ในทางตรงกันข้าม บริดจ์ของ FastReport และ ReportBuilder จะส่งออก native vector drawing primitives และข้อความดั้งเดิม

var
  PDF: TPDFlib;
begin
  PDF := TPDFlib.Create;
  try
    PDF.SetOrigin(1);
    // Prepares the report and appends every page in one call; the bridge
    // turns each page's EMF into PDF through ImportEMFFromStream.
    PDFlibQRExportReport(PDF, QuickRep1, 'ledger.pdf');
  finally
    PDF.Free;
  end;
end;

จุดสิ้นสุดของความถูกต้องแม่นยำ

โปรดรับรู้ถึงข้อจำกัดต่างๆ ก่อนที่จะเริ่มใช้งานบริดจ์เหล่านี้ในเวิร์กโฟลว์ของคุณ ตัวส่งออกชุดข้อมูลจะละทิ้งคอลัมน์ไบนารีโดยไม่มีการเตือน ดังนั้นรายงานที่ต้องแสดงรูปภาพฝังตัวจึงต้องใช้วิธีการอื่นนอกเหนือจากการส่งออกตารางปกติ สำหรับแนวทางของ QuickReport นั้น เนื่องจากส่งผ่าน EMF จึงทำให้ข้อความถูกฝังเป็นภาพเวกเตอร์ (vector glyphs) — ซึ่งทำให้หน้ากระดาษดูถูกต้องแต่จะไม่สามารถเลือกหรือค้นหาข้อความได้ และไม่มี โครงสร้าง PDF ที่มีแท็กสำหรับความสามารถในการเข้าถึง (accessibility) ในขณะที่ FastReport และ ReportBuilder จะรักษาข้อความจริงเอาไว้ แต่ตัวจัดการประเภทข้อมูลจะครอบคลุมเฉพาะมุมมองทั่วไปเท่านั้น — เช่น memo, picture, shape, line — และจะกลับไปใช้กล่องขอบเขต (bounding box) หรือข้ามประเภทที่ซับซ้อน ดังนั้นรายงานที่พึ่งพา Rich Text, บาร์โค้ด หรือการไล่สี (gradient fills) อาจสูญเสียรายละเอียดไป สิ่งเหล่านี้ไม่ใช่ความบกพร่องของระบบ แต่เป็นข้อจำกัดที่แท้จริงของเลเยอร์การแปล

ข้อควรระวังในการปฏิบัติงานข้อหนึ่งมีความสำคัญที่สุด เอนจินทั้งสามตัวนี้ไม่ได้รวมอยู่ภายใน losLab PDF Library และบริดจ์จะถูกแยกออกจากการคอมไพล์หลักโดยเจตนา — พวกมันจะคอมไพล์ได้ก็ต่อเมื่อโปรเจกต์มี FastReport VCL 6.x, QuickReport 8 หรือ ReportBuilder 20 อยู่ในพาธการค้นหา (search path) เรียบร้อยแล้ว โครงสร้างของ metafile และ DrawCommand ที่พวกมันทำงานด้วยนั้นยังคงมีความเสถียรในหลายเวอร์ชันหลักของเอนจิน แต่คุณต้องเป็นผู้ดูแลความเข้ากันได้ของเวอร์ชันเอง เมื่อทำได้ถูกต้องแล้ว ทั้งสองแนวทางจะครอบคลุมความต้องการทั้งหมด ตั้งแต่การดัมพ์ข้อมูล DBGrid แบบเฉพาะกิจไปจนถึงการออกแบบใบแจ้งหนี้ ทั้งตัวส่งออกตารางและบริดจ์รายงานจะจัดส่งมาพร้อมกับ losLab PDF Library สำหรับ Delphi และ C++Builder