PDFium มีชื่อเสียงในฐานะเอนจินสำหรับการดูเอกสาร ซึ่งเป็นตัวเรนเดอร์ที่อยู่เบื้องหลังแท็บ PDF ของ Chrome ดังนั้นสิ่งแรกที่ต้องทำความเข้าใจคือ PDFium Component สามารถสร้างเอกสารที่ไม่เคยมีมาก่อนได้เช่นกัน ส่วนของการสร้างนี้จะครอบทับ API page-object ของ PDFium: คุณสร้างเอกสารเปล่า เพิ่มหน้าด้วยขนาดที่กำหนดชัดเจน แล้ววางข้อความ เส้นเวกเตอร์ และรูปภาพลงบนแต่ละหน้าตามพิกัดที่คุณเลือก ไม่จำเป็นต้องเรียนรู้ภาษาคำอธิบายหน้า (page description language) และไม่มีไดรเวอร์เครื่องพิมพ์เข้ามาเกี่ยวข้อง คุณเรียกใช้เมธอด ไลบรารีจะประกอบออบเจ็กต์ PDF และใช้ SaveAs เพื่อบันทึกผลลัพธ์
สิ่งที่คุณจะไม่ได้คือเอนจินจัดหน้า (layout engine) เรื่องนี้สำคัญพอที่จะต้องบอกไว้ก่อน เพราะมันเป็นตัวกำหนดรูปแบบของตัวอย่างทั้งหมดด้านล่าง PDFium Component วางเนื้อหาในจุดที่คุณบอกในพิกัดแบบสัมบูรณ์ (absolute coordinates) และจะไม่อยู่ที่อื่น มันจะไม่ตัดคำขึ้นบรรทัดใหม่ ไม่ดันข้อความข้ามหน้า หรือคำนวณตารางจากแถวและคอลัมน์ งานเหล่านั้นเป็นหน้าที่ของคุณ หากคุณคาดหวังให้มันจัดเรียงข้อความเหมือนที่โปรแกรมประมวลผลคำทำ โปรดปรับความเข้าใจเสียใหม่: นี่คือ API สำหรับจัดวางตำแหน่งระดับต่ำที่แม่นยำ ซึ่งใกล้เคียงกับการวาดบนแคนวาสมากกว่าการจัดเรียงพิมพ์เอกสาร สำหรับการสร้างใบแจ้งหนี้ ใบรับรอง ฉลาก และหน้ารายงานที่คุณรู้ล่วงหน้าว่าแต่ละองค์ประกอบควรอยู่ตรงไหน ความแม่นยำระดับนี้คือสิ่งที่คุณต้องการอย่างแท้จริง
จุดเริ่มต้นขั้นต่ำในการสร้างไฟล์
มีเพียงการเรียกใช้งานสามครั้งเท่านั้นที่คั่นระหว่าง TPdf ว่างเปล่ากับ PDF ที่บันทึกเสร็จแล้ว: สร้างเอกสาร เพิ่มหน้า และเขียนลงดิสก์ สิ่งอื่น ๆ ล้วนเป็นเนื้อหาที่คุณใส่แทรกลงไปในระหว่างนั้น
uses
Vcl.Graphics, // for clBlack and TColor
PDFium; // TPdf lives here
procedure CreateBlankPdf(const FileName: string);
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.CreateDocument; // empty in-memory document
Pdf.AddPage(0, 595, 842); // A4 portrait, in points
Pdf.AddText('First page', 'Arial', 18, 50, 780);
Pdf.SaveAs(FileName); // serialize to disk
finally
Pdf.Active := False;
Pdf.Free;
end;
end;
รายละเอียดหนึ่งที่มักทำให้คนที่เคยเห็นโค้ดเก่าสับสนคือ คุณไม่ต้องกำหนดค่า Pdf.Active := True หลังจากเรียก CreateDocument คุณสมบัติ Active จะรายงานว่ามีแฮนเดิลของเอกสารอยู่หรือไม่ และ CreateDocument ได้สร้างมันขึ้นมาแล้ว ดังนั้นคุณสมบัตินี้จึงเป็น True ทันทีที่เมธอดนั้นทำงานเสร็จ การตั้งค่ามันอีกครั้งในกรณีที่ดีที่สุดคือไม่มีผลอะไร และในกรณีที่แย่ที่สุดคือทำให้คนอื่นที่มาอ่านโค้ดเข้าใจผิด Active จะมีประโยชน์ตอนปิดการทำงาน: การกำหนดค่าเป็น False จะปล่อยเอกสารที่อยู่เบื้องหลังก่อนเรียก Free ซึ่งเป็นลำดับการเคลียร์หน่วยความจำที่สะอาด ให้ถือว่า CreateDocument และการเปิดไฟล์เพื่อโหลดใช้งานร่วมกันไม่ได้ ไลบรารีจะปฏิเสธการสร้างเอกสารใหม่บน TPdf ที่เปิดเอกสารอยู่แล้ว ดังนั้นการนำกลับมาใช้ใหม่หมายถึงต้องปิดเอกสารปัจจุบันก่อนเสมอ
พิกัดเริ่มต้นที่มุมซ้ายล่าง
อาร์กิวเมนต์คู่ที่สองของ AddText และสำหรับการเรียกจัดวางองค์ประกอบอื่น ๆ คือจุดในพื้นที่ผู้ใช้ PDF (PDF user space) จุดเริ่มต้นจะอยู่ที่มุมซ้ายล่างของหน้า ค่า X วิ่งไปทางขวา และค่า Y วิ่งขึ้นด้านบน หนึ่งหน่วยมีค่าเท่ากับหนึ่งพอยต์ (1/72 นิ้ว) ดังนั้นหน้า A4 จะมีขนาด 595 x 842 หน่วย และ US Letter คือ 612 x 792 หน่วย ค่า Y ที่วิ่งขึ้นด้านบนนี้เป็นต้นเหตุอันดับหนึ่งของความสับสนประเภท "ข้อความของฉันหลุดออกนอกหน้า" เพราะพิกัดหน้าจอและบิตแมปทั่วไปจะวางจุดเริ่มต้นไว้ด้านบนโดยที่ค่า Y จะเพิ่มขึ้นเมื่อลงด้านล่าง บนหน้าที่สูง 842 พอยต์ หัวข้อที่อยู่ใกล้ขอบบนจะอยู่ที่ประมาณ Y 780 ไม่ใช่ Y 60 เมื่อข้อความของคุณไปปรากฏในจุดที่ไม่คาดคิด ความสูงของหน้าลบด้วยค่า Y ที่คุณใส่ มักจะเป็นตัวเลขที่คุณต้องการจริง ๆ เสมอ
AddPage รับตำแหน่งการแทรกเป็นอาร์กิวเมนต์แรก โดยนับเริ่มจาก 1 และมี 0 เป็นทางลัดที่สะดวกสำหรับ "จุดเริ่มต้นของเอกสาร" ส่งค่า 0 หรือ 1 สำหรับหน้าแรกและหน้านั้นจะถูกแทรกไว้ด้านหน้า; ส่งค่าที่ตรงกับจำนวนหน้าทั้งหมดที่คุณกำลังต่อท้ายเพื่อเพิ่มหน้าเข้าไปด้านหลัง หน้าที่เพิ่งเพิ่มเข้ามาจะกลายเป็นหน้าปัจจุบันด้วย ซึ่งคำสั่งวาดภาพในลำดับถัดไปจะทำงานบนหน้านั้น ดังนั้นจึงไม่มีขั้นตอน "เลือกหน้านี้" แยกต่างหากหลังจากที่เพิ่มหน้า หากคุณเพิ่มหลายหน้าและจำเป็นต้องกลับไปวาดบนหน้าก่อนหน้าในภายหลัง ให้ตั้งค่า PageNumber เพื่อย้ายเคอร์เซอร์; ในขณะที่คุณกำลังเติมหน้าตามลำดับตอนสร้าง คุณสามารถปล่อยค่านี้ไว้ได้เลย
การเขียนข้อความ และกฎของฟอนต์ที่อาจทำให้เกิดปัญหาอย่างเงียบ ๆ
procedure WriteHeader(Pdf: TPdf; const Title, Author: string);
begin
// Title in black, default opacity, no rotation
Pdf.AddText(Title, 'Arial', 20, 50, 780);
// A lighter byline 24 points below it
Pdf.AddText('By ' + Author, 'Arial', 11, 50, 756, clGray);
// A faint diagonal draft stamp across the page
Pdf.AddText('DRAFT', 'Arial', 64, 180, 380, clGray, $30, 45.0);
end;
ไบต์แอลฟา (alpha byte) มีค่าตั้งแต่ $00 (มองไม่เห็น) ไปจนถึง $FF (ทึบแสง) ซึ่งเป็นสิ่งที่ทำให้การประทับตราฉบับร่างเป็นลายน้ำแทนที่จะเป็นบล็อกทึบ: $30 คิดเป็นความทึบประมาณ 19 เปอร์เซ็นต์ ซึ่งเพียงพอที่จะอ่านเนื้อหาทะลุผ่านได้ มุมองศาจะหมุนข้อความทวนเข็มนาฬิการอบจุดยึด ดังนั้น 45 องศาจึงให้ตราประทับมุมถึงมุมแบบคลาสสิก ทั้งหมดนี้ไม่ต้องการฟีเจอร์ลายน้ำแยกต่างหาก ลายน้ำก็เป็นเพียงการเรียก AddText ขนาดใหญ่ แบบกึ่งโปร่งใส และถูกหมุน และการวาดมันก่อนหรือหลังเนื้อหาหลัก จะเป็นตัวกำหนดว่ามันจะอยู่ด้านหลังหรือด้านบนของเนื้อหา
เรื่องฟอนต์ควรได้รับคำอธิบายอย่างระมัดระวัง เพราะเมื่อมันล้มเหลว มันจะเกิดขึ้นอย่างเงียบ ๆ เมื่อคุณส่งชื่อฟอนต์ PDFium Component จะร้องขอข้อมูล TrueType ของฟอนต์นั้นจากระบบปฏิบัติการและฝังมันลงในเอกสาร ซึ่งเป็นเหตุผลที่ไฟล์ที่สร้างบนเครื่องของคุณจะเรนเดอร์ออกมาเหมือนกันเป๊ะบนเครื่องที่ไม่เคยติดตั้งฟอนต์นั้นมาก่อน ข้อควรระวังคือจะเกิดอะไรขึ้นเมื่อชื่อนั้นหาไม่พบ: อาจเป็นการพิมพ์ผิด หรือฟอนต์หน้านั้นไม่มีอยู่ในเครื่องที่ใช้สร้าง จะไม่มี exception แจ้งเตือน ไลบรารีจะถอยกลับไปสร้างออบเจ็กต์ข้อความที่เก็บชื่อนั้นไว้เป็นเพียงเลเบลเท่านั้น โดยไม่มีการฝังใด ๆ และปล่อยให้โปรแกรมดู PDF (viewer) เลือกฟอนต์แทนที่มาให้ตามที่มันคิดว่าใกล้เคียง ข้อความจะปรากฏขึ้นในการทดสอบของคุณ ดูเหมือนจะใช้งานได้ และจะเลื่อนระยะห่างหรือเปลี่ยนรูปอักขระทันทีที่เปิดไฟล์นั้นในที่ที่มีฟอนต์ติดตั้งแตกต่างออกไป จงใช้ชื่อที่คุณมั่นใจว่ามีอยู่ในเครื่องที่สร้างไฟล์ จัดการรายชื่อฟอนต์ให้เป็นเหมือน dependency ในการติดตั้ง (deployment dependency) และเปิดตัวอย่างใน viewer บนเครื่องที่เพิ่งติดตั้งระบบใหม่ (clean system) ก่อนที่คุณจะไว้วางใจผลลัพธ์นั้น
รูปร่างเวกเตอร์: สร้างเส้นทาง แล้วยืนยัน
เส้นตรง สี่เหลี่ยม และพื้นที่เติมสีล้วนทำผ่านเส้นทาง (path) คุณเปิดเส้นทางด้วย CreatePath ซึ่งจะตั้งค่าจุดเริ่มต้นและสไตล์ทั้งหมดในครั้งเดียว ได้แก่ โหมดการเติมสี สีเติมและสีเส้นขอบพร้อมไบต์แอลฟาของแต่ละสี ความกว้างของเส้นขอบ รวมถึงลักษณะปลายเส้นและรอยต่อ จากนั้นคุณต่อยอดมันด้วย LineTo, BezierTo และ ClosePath และท้ายที่สุด AddPath จะทำการยืนยันเส้นทางที่เสร็จสมบูรณ์ลงบนหน้า ขั้นตอนการยืนยันนี้ลืมได้ง่ายมากและจะไม่มีอะไรถูกวาดออกมาเลยหากคุณข้ามมันไป
procedure DrawDivider(Pdf: TPdf; X, Y, Width: Single);
begin
// A thin horizontal rule. The rectangle overload sets a box directly:
// X, Y, Width, Height, then fill mode and colors.
Pdf.CreatePath(X, Y, Width, 0.5, fmNone, clBlack, $FF,
True, clBlack, $FF, 1.0);
Pdf.AddPath;
end;
procedure DrawTriangle(Pdf: TPdf);
begin
// Point overload: start at the first vertex, line to the rest, close.
Pdf.CreatePath(200, 300, fmWinding, clBlue, $80, True, clNavy, $FF, 2.0);
Pdf.LineTo(300, 300);
Pdf.LineTo(250, 400);
Pdf.ClosePath;
Pdf.AddPath; // nothing is drawn until this runs
end;
โอเวอร์โหลดสองตัวครอบคลุมกรณีทั่วไป รูปแบบสี่พิกัดรับค่า X, Y, ความกว้าง และความสูง แล้วให้สี่เหลี่ยมที่ขนานกับแกนในคำสั่งเดียว ซึ่งเป็นสิ่งที่คุณจะใช้เมื่อวาดเส้นกั้น ขอบเซลล์ หรือแผงพื้นหลังที่มีสีเติม รูปแบบสองพิกัดจะกำหนดเพียงจุดเริ่มต้น และคุณจะต้องลากเส้นขอบที่เหลือด้วยตัวคุณเองผ่าน LineTo และ BezierTo โหมดการเติมสีควบคุมวิธีที่พื้นที่ทับซ้อนกันจะถูกระบายสี: fmWinding (nonzero winding) เหมาะสำหรับรูปร่างทึบส่วนใหญ่, fmAlternate (even-odd) จัดการช่องเจาะและการตัดกันของเส้นขอบภายในตัวเอง, และ fmNone จะทิ้งไว้เพียงเส้นทางที่มีเฉพาะเส้นขอบโดยไม่มีสีเติม ซึ่งเป็นโหมดที่เส้นกั้นในตัวอย่างด้านบนใช้งาน
ตารางประกอบจากเส้นทางและข้อความ โดยประกอบด้วยมือ
เนื่องจากไม่มีโครงสร้างตาราง (table primitive) ตารางจึงเป็นการวนลูป คุณต้องกำหนดค่าออฟเซ็ตแกน X ของคอลัมน์และความสูงของแถว เขียนแต่ละเซลล์ด้วย AddText และวาดเส้นกั้นตารางด้วยเส้นทางสี่เหลี่ยม การคำนวณตัวเลขเป็นหน้าที่ของคุณ แต่มันตรงไปตรงมา และเมื่อเขียนเสร็จแล้ว มันก็สามารถนำไปประยุกต์ใช้กับตารางใด ๆ ที่คุณต้องการได้
procedure DrawTable(Pdf: TPdf; Left, Top: Double);
const
ColX: array[0..2] of Double = (0, 110, 210); // column offsets
RowH = 20;
var
Y: Double;
Row: Integer;
begin
// Header row
Pdf.AddText('Item', 'Arial', 10, Left + ColX[0], Top);
Pdf.AddText('Qty', 'Arial', 10, Left + ColX[1], Top);
Pdf.AddText('Price', 'Arial', 10, Left + ColX[2], Top);
// Rule under the header
Pdf.CreatePath(Left, Top - 5, 260, 0.5, fmNone, clBlack, $FF);
Pdf.AddPath;
// Data rows, stepping Y downward each iteration
Y := Top;
for Row := 1 to 3 do
begin
Y := Y - RowH;
Pdf.AddText('Item ' + IntToStr(Row), 'Arial', 9, Left + ColX[0], Y);
Pdf.AddText(IntToStr(Row * 2), 'Arial', 9, Left + ColX[1], Y);
Pdf.AddText('$' + IntToStr(Row * 10) + '.00', 'Arial', 9, Left + ColX[2], Y);
end;
end;
สังเกตค่า Y ที่ลดลงตามความสูงของแถวในแต่ละรอบ เพราะทิศทางขึ้นมีค่าเป็นบวก และนี่ก็เป็นจุดที่แสดงให้เห็นถึงการขาดการวัดขนาดข้อความ: ไม่มีอะไรหยุดไม่ให้ชื่อสินค้ายาว ๆ ล้นเข้าไปในคอลัมน์ถัดไป เพราะไลบรารีไม่รู้ว่าข้อความที่คุณเรนเดอร์มีความกว้างเท่าใด สำหรับผลลัพธ์รูปแบบคงที่ที่คุณควบคุมข้อมูลได้ คุณสามารถกำหนดขนาดคอลัมน์ให้กว้างเผื่อไว้แล้วทำงานต่อ สำหรับเนื้อหาที่เปลี่ยนแปลงได้จริง ๆ คุณมีทางเลือกเพียงจำกัดอินพุตหรือวัดความกว้างของอักขระด้วยตัวเองก่อนที่จะจัดวาง ซึ่ง ณ จุดนี้ไลบรารีสำหรับการจัดองค์ประกอบหน้าเฉพาะทาง (composition library) จะเริ่มคุ้มค่าขึ้นมา
รูปภาพและการจัดการหลายหน้า
เนื้อหาแบบแรสเตอร์เข้ามาผ่านตัวช่วยรูปภาพ AddPicture รับค่า TPicture ที่โหลดมาแล้ววางมันลงบนจุด พร้อมระบุความกว้างและความสูงเป็นตัวเลือกเสริมเพื่อปรับสเกล; AddImage รับพาธไฟล์หรือ TBitmap โดยตรง และ AddJpegImage จะสตรีมไบต์ของ JPEG โดยไม่ต้องผ่านขั้นตอนการแปลงไปมาของบิตแมป เช่นเดียวกับสิ่งอื่น ๆ พิกัดการจัดวางคือมุมซ้ายล่างของภาพในพื้นที่ผู้ใช้ (user space) และความกว้างกับความสูงคือขนาดบนหน้าที่มีหน่วยเป็นพอยต์ ไม่ใช่มิติพิกเซลของภาพต้นฉบับ
procedure CreateMultiPageReport(const FileName: string; PageCount: Integer);
var
Pdf: TPdf;
P: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.CreateDocument;
for P := 1 to PageCount do
begin
Pdf.AddPage(P, 595, 842); // append; the new page becomes current
Pdf.AddText('Page ' + IntToStr(P) + ' of ' + IntToStr(PageCount),
'Arial', 10, 50, 30); // footer near the bottom edge
// ... draw this page's body here ...
end;
Pdf.SaveAs(FileName);
finally
Pdf.Active := False;
Pdf.Free;
end;
end;
เอกสารที่มีหลายหน้าก็คือรูปแบบของหน้าเดียวที่ถูกนำมาวนลูป AddPage แต่ละครั้งจะต่อท้ายหน้าใหม่และทำให้เป็นหน้าปัจจุบัน ดังนั้นส่วนเนื้อหาและส่วนท้ายหน้า (footer) ที่คุณวาดในลำดับถัดไปจะไปอยู่บนหน้าที่คุณเพิ่งเพิ่ม คุณไม่จำเป็นต้องกำหนดค่า PageNumber ใหม่ภายในลูปนี้ เพราะการเพิ่มหน้าได้ย้ายเคอร์เซอร์ไปที่นั่นแล้ว; คุณจะใช้ PageNumber ก็ต่อเมื่อคุณต้องกลับไปยังหน้าที่ไม่ได้เรียงตามลำดับการสร้าง เรียกใช้ SaveAs เพียงครั้งเดียวในตอนจบ หลังจากหน้าสุดท้ายถูกเติมเต็ม หากคุณต้องการโปรไฟล์สำหรับการจัดเก็บเอกสารระยะยาวแทนที่จะเป็นไฟล์ธรรมดา ออบเจ็กต์เอกสารตัวเดียวกันก็มี SaveAsPdfA และตัวเลือกย่อยอื่น ๆ ที่เป็นไปตามมาตรฐาน (conformance variants) ให้ใช้งาน ดังนั้นการเลือกมาตรฐานผลลัพธ์จึงอยู่ที่การเรียกใช้เมธอดในการบันทึก ไม่ได้แปลว่าจะต้องเปลี่ยนวิธีการสร้างเอกสารทั้งหมด
ตำแหน่งและบทบาทของเครื่องมือนี้
หากจะพูดกันตรง ๆ ก็คือ API การสร้าง (authoring API) ของ PDFium Component เป็นชั้นบาง ๆ ที่ซื่อสัตย์ต่อโมเดล page-object ของ PDFium: มันให้การสร้างเอกสารที่แท้จริง ฟอนต์ที่ฝังในตัวจริง เนื้อหาเวกเตอร์และแรสเตอร์จริง และแปลงข้อมูลให้เป็นไฟล์ที่ได้มาตรฐาน มันไม่ใช่ และไม่ได้พยายามทำตัวเป็น เอนจินสำหรับการจัดรูปแบบเอกสาร (reflowing document engine) เส้นแบ่งที่ชัดเจนก็คือการจัดวางข้อความ (text layout) หากผลลัพธ์ของคุณเป็นเทมเพลต เช่น ใบแจ้งหนี้ ใบรับรอง ฉลาก หรือแดชบอร์ดที่เรนเดอร์บนกริดที่คงที่ โมเดลพิกัดสัมบูรณ์จะตรงไปตรงมา รวดเร็ว และโค้ดยังคงอ่านง่าย แต่หากผลลัพธ์ของคุณเป็นข้อความยาว ๆ ที่ต้องตัดคำและขึ้นหน้าใหม่ได้เอง คุณจะต้องสร้างเอนจินจัดหน้าซ้อนทับลงบนคำสั่งเรียกใช้เหล่านี้ ซึ่งนั่นเป็นการเลือกใช้เครื่องมือผิดประเภท การรู้ว่าคุณอยู่ฝั่งใดของเส้นแบ่งนี้ ถือเป็นส่วนสำคัญที่สุดของการตัดสินใจ
เมธอดการสร้างที่อธิบายไว้ที่นี่เป็นส่วนหนึ่งของ PDFium Component สำหรับ Delphi ซึ่งได้นำเส้นทางการสร้างเหล่านี้มาจับคู่กับฟีเจอร์การเรนเดอร์และการดึงข้อความ ซึ่งเป็นจุดเด่นที่ทำให้คนรู้จัก PDFium เป็นอย่างดี