ทีมประมวลผลการเคลมต้องจัดการกับไฟล์กระดาษที่มีมานานถึงสามสิบปี ซึ่งกำลังถูกป้อนผ่านเครื่องสแกนเอกสาร เครื่องสแกนพ่นไฟล์ JPEG ออกมาหนึ่งไฟล์ต่อหนึ่งหน้าลงในโฟลเดอร์ โดยตั้งชื่อเป็น 0001.jpg, 0002.jpg ไปเรื่อย ๆ แต่สิ่งที่ฝ่ายจัดเก็บเอกสาร (archive) ต้องการจริง ๆ คือไฟล์ PDF หนึ่งไฟล์ต่อหนึ่งแฟ้มคดี โดยมีการจัดเรียงหน้าอย่างเป็นระเบียบ เพื่อให้ผู้ตรวจสอบสามารถเปิดอ่านเอกสารเพียงไฟล์เดียวแทนที่จะต้องคอยคลิกดูรูปขนาดย่อ (thumbnails) นับร้อยรูป ขั้นตอนสุดท้ายนี้เอง คือการเปลี่ยนกองภาพสแกนที่มีหมายเลขกำกับให้กลายเป็นไฟล์ PDF เดียวที่มีการจัดเรียง ซึ่งนั่นคืองานของเราในตอนนี้
PDFium Component สามารถจัดการเรื่องนี้ได้โดยตรง นอกเหนือจากการเรนเดอร์และการดึงข้อความ (text extraction) ตัวคอมโพเนนต์สามารถสร้างไฟล์ PDF ขึ้นมาจากศูนย์ได้: คือเริ่มสร้างเอกสารเปล่า เพิ่มหน้าว่างเข้าไปในขนาดใดก็ได้ตามต้องการ วางภาพลงบนหน้านั้นโดยใช้พิกัดของพื้นที่ผู้ใช้ (user-space coordinates) จากนั้นจึงทำการบันทึก ขั้นตอนการทำงาน (pipeline) ทั้งหมดอาศัยคอมโพเนนต์ TPdf ดังนั้นตัวแปลงไฟล์แบบชุด (batch converter) จึงเป็นเพียงแค่การวนซ้ำ (loop) บนชื่อไฟล์ ผนวกกับการโทรเรียกใช้งาน (calls) อีกเพียงหยิบมือเดียว
รูปแบบของการแปลงไฟล์
มีสามสิ่งที่ต้องเกิดขึ้นสำหรับแต่ละภาพสแกน คุณเป็นคนตัดสินใจขนาดหน้ากระดาษ คุณวางภาพลงไปในหน้ากระดาษโดยเว้นระยะขอบ (margin) ไว้ และคุณก็ก้าวไปยังหน้ากระดาษถัดไป PDFium Component มอบเมธอดสำหรับแต่ละงานเหล่านี้: AddPage สร้างหน้าว่างที่ขนาดตามกำหนด, AddImage (หรือ AddPicture หากคุณมี TPicture ถือไว้อยู่แล้ว) ทำหน้าที่วาดบิตแมปลงบนหน้ากระดาษปัจจุบัน และ PageNumber เป็นตัวบอกคอมโพเนนต์ว่าคำสั่งเรียกวาดในครั้งต่อไปจะเจาะจงเป้าหมายที่หน้ากระดาษใด
รายละเอียดหนึ่งที่มักจะทำให้ผู้คนสะดุดล้มก็คือระบบพิกัด พื้นที่ผู้ใช้ (user space) ของ PDF วางจุดกำเนิด (origin) ไว้ที่มุมซ้ายล่างของหน้ากระดาษ โดยที่แกน Y จะมีค่าเพิ่มขึ้นเมื่อชี้ขึ้นด้านบน ซึ่งตรงข้ามกับพิกัดหน้าจอที่นักพัฒนา Delphi มักจะคุ้นชินเป็นปฏิกิริยาสะท้อนกลับ ค่า X, Y ที่คุณส่งไปยัง AddImage คือมุมซ้ายล่างของสี่เหลี่ยมรูปภาพ และ Width, Height คือขนาดสำหรับการจัดวางในหน่วยพอยต์ (points) ไม่ใช่ขนาดพิกเซลของไฟล์ต้นฉบับ หากเข้าใจเรื่องนี้กลับหัวกลับหาง ภาพสแกนของคุณก็จะไปตกอยู่นอกหน้ากระดาษหรือกลับหัวเมื่อเทียบกับตำแหน่งที่คุณคาดหวังให้มันอยู่
การสร้างเอกสารและหนึ่งหน้ากระดาษต่อภาพสแกน
เริ่มต้นด้วยเอกสารเปล่า CreateDocument จัดสรรพื้นที่สำหรับไฟล์ PDF ใหม่และตั้งให้คอมโพเนนต์เริ่มทำงานเลย จึงไม่มีขั้นตอนการเปิดไฟล์ที่แยกต่างหาก จากนั้นคุณเดินตามรายการของไฟล์ที่สแกน และสำหรับแต่ละไฟล์คุณต้องเพิ่มหน้ากระดาษ ทำให้หน้านั้นเป็นหน้าปัจจุบัน และวางรูปภาพลงไป มิติขนาดของหน้ากระดาษที่ใช้ในที่นี้คือ A4 ในหน่วยพอยต์ (กว้าง 595 × สูง 842 ในแนวตั้ง) ซึ่งเป็นขนาดแผ่นมาตรฐานสำหรับการจัดเก็บเอกสารประเภทจดหมายโต้ตอบ
procedure TArchiveForm.ScansToPdf(const Files: TStrings; const OutputPath: string);
const
PageW = 595.0; // A4 width in points
PageH = 842.0; // A4 height in points
Margin = 36.0; // half-inch border around each scan
var
I: Integer;
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.CreateDocument; // new, empty, already active
for I := 0 to Files.Count - 1 do
begin
Pdf.AddPage(I + 1, PageW, PageH); // 1-based page index
Pdf.PageNumber := I + 1; // make the new page current
PlaceScan(Pdf, Files[I], PageW, PageH, Margin);
end;
Pdf.SaveAs(OutputPath);
finally
Pdf.Free;
end;
end;
การวนซ้ำในแต่ละรอบจะสร้างหน้ากระดาษขึ้นมา และตั้งค่า PageNumber ไปที่หน้านั้นในทันที บรรทัดที่สองนั้นมีความสำคัญ: AddPage จะแทรกหน้ากระดาษลงไปแต่บรรดาเมธอดการวาดจะกระทำกับหน้าใดก็ตามที่เป็นหน้าปัจจุบัน ดังนั้นการตั้งค่า PageNumber จึงเป็นสิ่งที่คอยเล็งเป้าหมายของ AddImage ไปยังหน้ากระดาษที่คุณเพิ่งจะสร้างขึ้น หากข้ามมันไป ภาพของคุณก็จะไปกองซ้อนกันอยู่บนหน้าที่บังเอิญถูกโหลดไว้ก่อนหน้านั้น
มีข้อสมมติฐานหนึ่งอย่างซ่อนอยู่ในลูปนั้น: ลำดับของ Files เครื่องสแกนเอกสารจะตั้งชื่อหน้ากระดาษตั้งแต่ 0001.jpg ไปจนถึง 0100.jpg แต่การอ่านรายชื่อไฟล์ในไดเรกทอรีอาจไม่ได้เรียงลำดับส่งกลับมาเสมอไป และในตอนที่คุณไปเจอ page9.jpg อยู่ติดกับ page10.jpg การเรียงลำดับสตริงแบบธรรมดาจะเอาหน้า 10 ไปไว้ก่อนหน้า 9 จงเรียงลำดับรายชื่ออย่างชัดเจนก่อนเข้าสู่ลูป และเลือกใช้รูปแบบชื่อที่มีเลขศูนย์นำหน้า (zero-padded names) ในตอนสแกนเพื่อที่ลำดับทางศัพท์ (lexical order) จะได้ตรงกันกับลำดับหน้ากระดาษ ลำดับของหน้ากระดาษเป็นสิ่งเดียวที่ผู้ตรวจสอบจะสังเกตเห็นได้ในทันที และมันเป็นความผิดพลาดที่มีต้นทุนต่ำที่สุดในการป้องกัน
การจัดวางภาพสแกนและรักษาอัตราส่วนภาพเอาไว้
ภาพสแกนนั้นแทบจะไม่เคยมีรูปทรงเดียวกับหน้ากระดาษ หากคุณยืดภาพเพื่อให้เต็มแผ่น คุณก็จะทำให้ข้อความบิดเบี้ยว; แต่หากคุณวางมันลงไปด้วยขนาดพิกเซลเต็มที่ มันก็จะล้นขอบ วิธีแก้คือการปรับสัดส่วน (scale) ด้วยอัตราส่วนที่เล็กกว่าระหว่างสองค่านี้ คือความพอดีแบบกว้าง (width-fit) หรือความพอดีแบบสูง (height-fit) และจัดกึ่งกลางให้กับพื้นที่ส่วนที่เหลือ เนื่องจากจุดกำเนิด (origin) ตั้งอยู่ที่มุมซ้ายล่าง การจัดกึ่งกลางจึงหมายถึงการแบ่งพื้นที่ที่เหลืออย่างเท่า ๆ กันและบวกเพิ่มเข้าไปในทั้ง X และ Y
procedure TArchiveForm.PlaceScan(Pdf: TPdf; const FileName: string;
PageW, PageH, Margin: Double);
var
Pic: TPicture;
AvailW, AvailH, Scale, DrawW, DrawH, X, Y: Double;
begin
Pic := TPicture.Create;
try
Pic.LoadFromFile(FileName); // BMP, JPG, PNG, etc. via the VCL graphics units
AvailW := PageW - 2 * Margin;
AvailH := PageH - 2 * Margin;
// Fit inside the margins without distorting the scan.
Scale := Min(AvailW / Pic.Width, AvailH / Pic.Height);
DrawW := Pic.Width * Scale;
DrawH := Pic.Height * Scale;
// Center: leftover space split evenly. Y measured from the page bottom.
X := (PageW - DrawW) / 2;
Y := (PageH - DrawH) / 2;
Pdf.AddImage(FileName, X, Y, DrawW, DrawH);
finally
Pic.Free;
end;
end;
โค้ดนี้จะโหลดไฟล์หนึ่งครั้งเพื่ออ่านมิติขนาดพิกเซลของภาพ คำนวณหาค่าสเกลที่เป็นรูปแบบเดียวกัน (uniform) ค่าเดียว แล้วก็ส่งรูปสี่เหลี่ยมผืนผ้าที่เป็นจุดจัดวางภาพต่อไปยัง AddImage โดย AddImage จะยอมรับเส้นทางไฟล์โดยตรงและส่งมันไปตามเส้นทางภาพแบบเดียวกันกับ AddPicture ดังนั้นรูปแบบใดก็ตามที่ยูนิตกราฟิก (graphics units) ของ VCL รู้จักก็จะสามารถใช้งานได้โดยไม่ต้องมีกรณีพิเศษ (special-casing) หากคุณถอดรหัสภาพมาไว้ใน TPicture จากแผงหน้าต่างพรีวิวแล้ว ก็สามารถเรียกใช้งาน AddPicture(Pic, X, Y, DrawW, DrawH) ด้วยรูปสี่เหลี่ยมจัดวางพื้นที่เดียวกันนี้และข้ามการอ่านไฟล์รอบที่สองไปได้เลย
การข้ามขั้นตอนการถอดรหัสสำหรับภาพสแกนแบบ JPEG
เครื่องสแกนมักจะพ่นไฟล์ JPEG ออกมาแทบจะเสมอ การโหลด JPEG เข้าไปใน TPicture จะเป็นการถอดรหัสออกมาเป็นภาพบิตแมป และจากนั้น PDFium ก็จะทำการเข้ารหัสกลับเข้าไปใหม่ในตอนบันทึก ซึ่งทั้งสองรอบไปกลับนี้ทำให้เกิดการสูญเสียคุณภาพ (lossy round trips) ที่คุณไม่ต้องการเลย AddJpegImage จะทำการฝังไบต์ที่ถูกบีบอัดมาจากต้นฉบับตรงลงไปยังหน้ากระดาษผ่านทางสตรีม ซึ่งรวดเร็วกว่าและสะอาดตากว่าสำหรับการทำงานแบบชุดที่มีจำนวนข้อมูลมหาศาล
var
Stream: TFileStream;
begin
// ... after AddPage + PageNumber for the current page ...
Stream := TFileStream.Create(FileName, fmOpenRead);
try
// Embeds the JPEG bytes as-is; no decode/re-encode cycle.
Pdf.AddJpegImage(Stream, X, Y, DrawW, DrawH);
finally
Stream.Free;
end;
end;
คุณยังคงต้องคำนวณหา X, Y, DrawW และ DrawH ด้วยวิธีเดียวกัน เนื่องจากคุณต้องใช้มิติพิกเซลในการปรับสัดส่วน ให้อ่านค่าเหล่านั้นจากตัวไฟล์หรือใช้วิธีวิเคราะห์ข้อมูลส่วนหัวไฟล์ (header parse) อย่างรวดเร็ว จากนั้นก็ส่งสตรีมดิบนั้นให้กับ AddJpegImage สำหรับการสแกนแบบ PNG หรือ TIFF เส้นทางการเรียก AddImage ถือว่าเป็นทางที่ถูกต้องแล้ว; ขอให้สงวนทางลัดในการจัดการ JPEG ไว้กับรูปแบบที่มันรองรับใช้งานได้จริง ๆ ก็พอ
การติดป้ายกำกับในแต่ละหน้ากระดาษ
เอกสารภาพสแกนที่ถูกจัดเก็บถาวรจะถูกตรวจสอบประเมินได้ง่ายกว่าหากแต่ละหน้ากระดาษมีชื่อไฟล์ต้นทางติดอยู่ด้วย AddText ใช้สำหรับวาดสตริงข้อความที่พิกัดตามพื้นที่ของผู้ใช้ (user-space coordinate) ดังนั้นคำอธิบายภาพจะถูกจัดวางอยู่ด้านล่างภาพพอดิบพอดี อย่าลืมเรื่องแกน Y ที่กลับหัว: หากจะแปะป้ายกำกับไว้ใต้ภาพสแกน คุณต้องลบออกจากขอบล่างของภาพแทนที่จะบวกเพิ่มเข้าไป
// Caption below the scan: Y decreases toward the page bottom.
Pdf.AddText('File: ' + ExtractFileName(FileName), 'Helvetica', 9,
X, Y - 14, clGray);
ประเด็นสุดท้ายเกี่ยวกับการบันทึก SaveAs คือฟังก์ชันที่จะคืนค่าตรรกะ (Boolean) ออกมา ดังนั้นในโค้ดสำหรับการผลิตระดับโปรดักชัน โปรดตรวจสอบผลลัพธ์ของมันแทนที่จะด่วนสรุปไปเองว่าการเขียนไฟล์สำเร็จเสร็จสิ้นแล้ว; ฮาร์ดดิสก์ที่เต็มพิกัด หรือเส้นทางไฟล์ปลายทางที่ถูกล็อกเอาไว้ จะพังทลายล้มเหลวไปแบบเงียบ ๆ มิฉะนั้นแล้ว เมื่อการวนลูปสิ้นสุดลงและไฟล์ถูกเขียนเรียบร้อย คุณก็จะได้ไฟล์ที่ฝ่ายเก็บเอกสารต้องการอย่างแน่นอน นั่นคือ: ไฟล์ PDF ที่มีการเรียงลำดับหน้าหนึ่งไฟล์ต่อหนึ่งแฟ้มคดี หน้ากระดาษถูกปรับขนาดให้พอดี พร้อมที่จะเปิดอ่านในโปรแกรมดูไฟล์ตัวใดก็ได้
บล็อกส่วนประกอบในลักษณะเดียวกันครอบคลุมการทำงานอื่น ๆ ที่เกี่ยวโยงกัน ลองสลับกฎเกณฑ์ของการจัดขนาดแต่ละหน้ากระดาษ คุณก็จะได้สมุดภาพถ่ายที่มีหนึ่งภาพต่อกระดาษหนึ่งแผ่น; คงขั้นตอนลูปเอาไว้แต่เปลี่ยนไปอ่านจากแหล่งข้อมูลที่รองรับ TIFF หลายหน้าแทน และคุณก็จะได้ตัวแปลงสำหรับจัดเก็บเครื่องโทรสาร หากคุณต้องการเห็นภาพรวมที่กว้างกว่าเกี่ยวกับการสร้างเอกสาร PDF ผ่านการเขียนโปรแกรม ลองดู การสร้างเอกสาร PDF จากศูนย์ด้วย PDFium Component; หรือหากต้องการเรนเดอร์ผลลัพธ์กลับไปยังหน้าจอในภายหลัง ลองดู การแปลงหน้าเอกสาร PDF เป็นรูปภาพ JPEG ด้วย PDFium Component
PDFium Component จาก loslab.com ได้ทำการมัดรวมชุดเครื่องมือ API ทางด้านการสร้าง การเรนเดอร์ และการจัดการข้อความ ที่ถูกนำมาใช้ตลอดซีรีส์ชุดนี้