คอมโพเนนต์ HotPDF Component สามารถแยกข้อความ Unicode จาก PDF ใดๆ ที่คุณโหลดใน Delphi ผ่านการเรียกใช้ฟังก์ชันสองตัว: ExtractLoadedPageText จะส่งกลับข้อความตามลำดับการอ่านของหน้า และ ExtractLoadedPageTextLayout (เพิ่มเข้ามาในเวอร์ชัน v2.263.0) จะสร้างการจัดวางภาพของหน้าเอกสารขึ้นใหม่เป็นข้อความธรรมดา เพื่อให้คอลัมน์ การเยื้อง และการจัดตำแหน่งตารางยังคงอยู่ในผลลัพธ์ ทั้งสองฟังก์ชันนี้สามารถทำงานกับเอกสารที่ HotPDF ไม่ได้สร้างขึ้นได้ ซึ่งเป็นกรณีการใช้งานจริงที่สำคัญที่สุด: เช่น ใบแจ้งหนี้ที่ลูกค้าส่งอีเมลมาหาคุณ รายงานจากสำนักสแกนเอกสาร หรือสัญญาที่สร้างขึ้นจากซอฟต์แวร์ที่ไม่มีใครจำชื่อได้แล้ว
การจะทำเช่นนั้นได้ต้องอาศัยกลไกการทำงานที่ซับซ้อนกว่าที่การเรียกใช้ทั้งสองแบบแสดงให้เห็น เนื่องจาก PDF ไม่ได้จัดเก็บข้อความในลักษณะเดียวกับไฟล์ข้อความทั่วไป บทความนี้จะนำคุณไปทำความเข้าใจโหมดการแยกข้อความทั้งสองแบบ จากนั้นจะเปิดเผยส่วนประกอบสามส่วนที่อยู่เบื้องหลัง — ได้แก่ ตัวอ่าน CMap ตัวตีความสตรีมเนื้อหา และห่วงโซ่การถดถอยกลับของการถอดรหัสฟอนต์ — เพราะการเข้าใจวิธีการแมปข้อมูลนี้คือสิ่งที่จะสร้างความแตกต่างระหว่างการมองผ่านข้อความผลลัพธ์ที่เป็นขยะกับการวิเคราะห์หาสาเหตุที่แท้จริง
ทำไมการแยกข้อความจึงยากกว่าการอ่านสตริงธรรมดาจากไฟล์?
สตรีมเนื้อหาของ PDF จะบันทึกรหัสอักขระ ไม่ใช่อักขระจริง ตัวดำเนินการ Tj และ TJ (ISO 32000-1 §9.4.3) จะเก็บสตริงของไบต์ซึ่งความหมายของมันจะขึ้นอยู่กับฟอนต์ที่เลือกโดยคำสั่ง Tf ก่อนหน้า: ไบต์ 0x41 อาจหมายถึงอักษร A ภายใต้ WinAnsi, อาจเป็น glyph ทั่วไปในฟอนต์ชุดย่อย หรือเป็นครึ่งหนึ่งของ CID ขนาดสองไบต์ในฟอนต์ภาษา CJK มาตรฐาน ISO 32000-1 §9.10 ได้จำกัดความการแยกข้อความว่าเป็นปัญหาของการถอดรหัสนี้ — นั่นคือการจับคู่รหัสแต่ละตัวกลับไปเป็น Unicode โดยใช้ข้อมูลใดๆ ที่พจนานุกรมฟอนต์มีให้ — และมาตรฐานดังกล่าวระบุไว้อย่างชัดเจนว่าไฟล์ที่เป็นไปตามข้อกำหนดไม่จำเป็นต้องให้ข้อมูลที่เพียงพอต่อการดำเนินการดังกล่าว
วรรคสุดท้ายนั้นช่วยอธิบายรายงานปัญหาประเภท "ทำไมการคัดลอกและวางจาก PDF นี้จึงกลายเป็นอักษรขยะ" ที่คุณเคยพบเจอมาทั้งหมด ตัวสร้างเอกสารที่ฝังฟอนต์ชุดย่อยโดยไม่มีตาราง /ToUnicode จะทำให้ได้ไฟล์ที่แสดงผลได้อย่างสมบูรณ์แบบแต่แยกข้อความออกมาเป็นขยะที่ไม่สมเหตุสมผล เนื่องจากมีแผนผังการจับคู่ระหว่างรหัสกับ glyph แต่ไม่เคยส่งแผนผังระหว่างรหัสกับ Unicode มาด้วย ดังนั้น API การแยกข้อความที่ตรงไปตรงมาจึงเป็นห่วงโซ่ความพยายามอย่างดีที่สุดในการย้อนกลับข้อมูล และคำถามที่มีประโยชน์คือห่วงโซ่นี้ลึกเพียงใด
การแยกข้อความตามลำดับการอ่านด้วย ExtractLoadedPageText
สำหรับการทำดัชนีเพื่อค้นหา การค้นหาด้วยคำสำคัญ หรือการส่งข้อความไปยังไพป์ไลน์การวิเคราะห์ ฟังก์ชัน ExtractLoadedPageText คือสิ่งที่คุณต้องการ รูปแบบคือ function ExtractLoadedPageText(PageIndex: Integer; out AText: UnicodeString): boolean — ดัชนีหน้ากระดาษเริ่มจากศูนย์ ผลลัพธ์จะถูกส่งกลับมาเป็น UnicodeString ในตัวของ Delphi และฟังก์ชันจะส่งกลับค่า False เมื่อหน้านั้นไม่มีสตรีมเนื้อหาที่สามารถอ่านได้ แทนที่จะสร้างข้อยกเว้น (exception)
var
Pdf: THotPDF;
PageCount, I: Integer;
PageText, AllText: UnicodeString;
begin
Pdf := THotPDF.Create(nil);
try
PageCount := Pdf.LoadFromFile('invoice.pdf');
AllText := '';
for I := 0 to PageCount - 1 do
if Pdf.ExtractLoadedPageText(I, PageText) then
AllText := AllText + PageText + #13#10;
// ตอนนี้ AllText จะเก็บข้อความตามลำดับการอ่านของเอกสาร
finally
Pdf.Free;
end;
end;
การขึ้นบรรทัดใหม่ในข้อความผลลัพธ์มาจากการประเมินแบบง่ายๆ โดยเจตนา: เมื่อพิกัดแกนตั้งของ glyph มีการเปลี่ยนแปลงมากกว่าครึ่งหนึ่งของขนาดฟอนต์ปัจจุบัน — ซึ่งเป็นลักษณะเฉพาะของการเปลี่ยนคำสั่ง Td หรือ T* ในสตรีมเนื้อหา — จะทำการขึ้นบรรทัดใหม่ ตัวอักษรที่ตัวถอดรหัสไม่สามารถแปลความหมายได้จะกลายเป็นช่องว่างแทนที่จะหายไปทั้งหมด ดังนั้นขอบเขตของคำจึงยังคงอยู่แม้ว่า glyph แต่ละตัวจะไม่สามารถแปลงได้ก็ตาม สิ่งที่โหมดนี้ไม่ได้พยายามทำคือการจัดกลุ่มตามลำดับการอ่านหรือการตรวจหาหน้ากระดาษที่มีหลายคอลัมน์: หน้ากระดาษแบบสองคอลัมน์จะถูกนำออกมาปะปนกันตามลำดับของสตรีมเนื้อหา ซึ่งโดยทั่วไปมักจะเป็นลำดับทางสายตาแต่อาจไม่ใช่แบบนั้นเสมอไป
เมื่อใดที่คุณควรใช้การแยกข้อความแบบคงเค้าโครงเดิมแทน?
ฟังก์ชัน ExtractLoadedPageTextLayout เป็นตัวเลือกที่ถูกต้องเมื่อใดก็ตามที่ตำแหน่งของข้อมูลมีความหมาย: เช่น ตาราง แบบฟอร์ม รายการโค้ด หรือข้อมูลใดๆ ที่คุณต้องการนำไปวิเคราะห์ความต่าง (diff) ใช้ grep ค้นหา หรือแยกวิเคราะห์ตามคอลัมน์ แทนที่จะทำให้ glyph แบนราบเป็นสตรีมข้อมูล มันจะจัดกลุ่ม glyph เหล่านั้นให้อยู่ในแนวเดียวกัน เรียงลำดับแต่ละแนวตามแกน X และสร้างช่องว่างแนวนอนและแนวตั้งขึ้นใหม่บนตารางอักขระความกว้างคงที่ (monospaced) ซึ่งมีขนาดวัดจากค่ามัธยฐานของการเลื่อน glyph และขนาดฟอนต์ ช่องว่างขนาดใหญ่ระหว่างข้อความในแนวเดียวกันจะกลายเป็นช่องว่าง ส่วนช่องว่างขนาดใหญ่ระหว่างแนวจะกลายเป็นบรรทัดว่าง ทำให้ผลลัพธ์ที่ได้สามารถอ่านได้เหมือนกับหน้ากระดาษที่ปรากฏทางสายตา
var
Grid: UnicodeString;
begin
if Pdf.ExtractLoadedPageTextLayout(0, Grid) then
TFile.WriteAllText('page1.txt', Grid, TEncoding.UTF8);
// คอลัมน์ การเยื้อง และการจัดตำแหน่งตารางยังคงอยู่เป็น
// ช่องว่างและบรรทัดว่างบนตารางอักขระ
end;
ทั้งสองโหมดนี้ใช้กลไกการถอดรหัสร่วมกันทุกส่วน และแตกต่างกันเฉพาะวิธีการจัดวาง glyph ที่ถอดรหัสได้เท่านั้น ดังนั้นการเลือกจึงไม่มีผลต่อความแม่นยำของข้อมูล เลือก ExtractLoadedPageText เมื่อคุณต้องการเฉพาะถ้อยคำ และเลือก ExtractLoadedPageTextLayout เมื่อการจัดวางมีความสำคัญ ส่วนการตรวจหาลำดับการอ่านแบบหลายคอลัมน์ยังคงอยู่นอกเหนือขอบเขตการทำงานของทั้งสองโหมด — ตารางแสดงผลของหน้าแบบสองคอลัมน์จะแสดงให้คุณเห็นทั้งสองคอลัมน์เคียงข้างกันอย่างถูกต้อง ซึ่งเหมาะอย่างยิ่งสำหรับการตรวจสอบความต่าง แต่จะไม่เหมาะสำหรับการไหลของข้อความประโยคต่อเนื่อง
HotPDF ถอดรหัสอักขระกลับไปเป็น Unicode อย่างไร?
คอมโพเนนต์ HotPDF Component จะแปลความหมายของรหัสอักขระแต่ละตัวผ่านห่วงโซ่การถดถอยตามลำดับความสำคัญ: เริ่มต้นจากตาราง /ToUnicode CMap ที่ฝังอยู่ในฟอนต์เป็นอันดับแรก จากนั้นเป็นข้อมูลรายการ /Encoding (สตรีม หรือ CMap ที่ระบุชื่อ) และสำหรับฟอนต์คอมโพสิต — จะตรวจสอบไฟล์ CMap มาตรฐานของ Adobe สำหรับการรวบรวมอักขระ เช่น Adobe-GB1, Adobe-CNS1, Adobe-Japan1 และ Adobe-KR และขั้นตอนสุดท้ายคือตาราง WinAnsi และ MacRoman ที่ติดตั้งมาในตัวสำหรับฟอนต์แบบธรรมดา หากขั้นตอนใดขั้นตอนหนึ่งไม่สามารถให้คำตอบได้ ระบบจะถดถอยไปยังขั้นตอนถัดไปอย่างเงียบๆ แทนที่จะแจ้งข้อผิดพลาด และหากไม่พบคำตอบจากทุกขั้นตอนในห่วงโซ่ จะส่งกลับค่าเป็น 0 เพื่อให้ผู้เรียกใช้สามารถนับจำนวนข้อผิดพลาดแทนที่จะเป็นการเดาข้อมูล
ตาราง /ToUnicode CMap (ISO 32000-1 §9.10.3) อยู่เป็นลำดับแรกเนื่องจากเป็นแผนผังการจับคู่ที่ผู้สร้างเขียนขึ้นมาโดยเฉพาะสำหรับการแยกข้อมูล เส้นทาง CMap มาตรฐานของ Adobe มีความสำคัญสำหรับเอกสารภาษา CJK ที่ใช้ CMap ที่กำหนดไว้ล่วงหน้า เช่น UniGB-UTF16-H แทนที่จะฝังไว้: HotPDF จะส่งไฟล์เหล่านั้นมาให้ภายใต้ไดเรกทอรี resources\CMap โดยจะค้นหาตามความสัมพันธ์กับไฟล์รันไทม์ และทำการแคชแผนผังแต่ละอันแยกเป็นรายกระบวนการ — ซึ่งข้อนี้มีประโยชน์เนื่องจากแผนผังที่มีขนาดใหญ่ที่สุดอย่าง Adobe-GB1 จะมีขนาดข้อความประมาณ 2 MB ซึ่งคุณคงไม่ต้องการให้วิเคราะห์ใหม่ในทุกๆ หน้า หากไม่มีไดเรกทอรีนี้ ตัวถอดรหัสจะข้าม CMaps ที่พึ่งพาดิสก์ไป และทำงานเฉพาะตารางที่ฝังควบคู่กับตัวเข้ารหัสในตัว นี่คือกุญแจสะท้อนฝั่งการอ่านของปัญหาเรื่องการจัดรูปร่างตัวอักษรที่กล่าวไว้ในการจัดรูปร่างข้อความอักษรซับซ้อนด้วย HotPDF ซึ่งมีการแยกความแตกต่างระหว่างรหัสกับ glyph เช่นเดียวกันในฝั่งผู้เขียน
กับดักทางไวยากรณ์ของ CMap สองจุดที่ควรระวัง
ไฟล์ CMap ดูเหมือนจะวิเคราะห์ไวยากรณ์ได้ง่ายแต่จริงๆ แล้วไม่ใช่ และมีรายละเอียดสองประการที่เป็นสาเหตุของความล้มเหลวในการสร้างตัววิเคราะห์ครั้งแรก ประการแรกคือจำนวนเรกคอร์ดจะอยู่ ก่อน คำสำคัญของส่วนข้อมูล: เช่น ส่วนข้อมูลจะเขียนว่า 2 beginbfchar ไม่ใช่ beginbfchar 2 ตัววิเคราะห์ที่คาดหวังจะพบจำนวนเรกคอร์ดหลังคำสำคัญจะรับรู้ตัวเลขนั้นเป็นโทเค็นส่วนเกิน แล้วประเมินว่ามีรายการเป็นศูนย์ในทุกส่วน วิธีการแก้ไขที่ทนทาน — ซึ่งตัวอ่านของ HotPDF เลือกใช้ — คือการละเว้นจำนวนเรกคอร์ดทั้งหมดและวนลูปจนกว่าจะพบคีย์เวิร์ด endbfchar / endbfrange ที่สอดคล้องกัน ซึ่งช่วยให้ทำงานกับไฟล์จริงที่มีค่าจำนวนเรกคอร์ดผิดพลาดได้อย่างราบรื่น
กับดักข้อที่สองคือเป้าหมายของ bfchar และ bfrange เป็นสตริง UTF-16BE สตริง ไม่ใช่จำนวนเต็ม ค่าปลายทาง <D83DDE00> หมายถึง U+1F600 — ซึ่งเป็นคู่ซูโรเกต (surrogate pair) ที่ต้องรวมกลับเป็นหนึ่งโค้ดพอยต์ (code point) และการอ่านไบต์ทั้งสี่นี้เป็นจำนวนเต็มแบบบิ๊กเอนเดียน (big-endian) จะทำให้ได้ค่าที่ไม่มีความหมายในทุกจุดรหัสที่อยู่นอก Basic Multilingual Plane อีโมจิใน PDF ไม่ใช่เรื่องแปลกใหม่อีกต่อไป ดังนั้นตัวถอดรหัสที่ข้ามขั้นตอนการรวมคู่ซูโรเกตจะล้มเหลวในการทำงานกับไฟล์จริงของผู้ใช้ของคุณ HotPDF จะแปลงข้อความฐานสิบหกเป็นไบต์ดิบก่อน จากนั้นจึงประกอบหน่วยรหัส UTF-16BE กลับคืน ซึ่งจะครอบคลุมถึงอักษรควบ (ligature) ที่สร้างจากหลายอักขระอีกด้วย
การเข้าถึงระดับ glyph ด้วย ExtractLoadedPageGlyphs
ฟังก์ชันข้อความทั้งสองถูกสร้างขึ้นบน ExtractLoadedPageGlyphs และโครงสร้าง THPDFGlyphArray ที่อยู่เบื้องหลังยังคงสามารถใช้งานได้ในรหัสของคุณเช่นกัน THPDFGlyphRecord แต่ละรายการจะเก็บค่าโค้ดพอยต์ Unicode ที่แปลผลแล้ว ควบคู่กับรหัสอักขระดิบ ขนาดความกว้างไบต์ของรหัส (1, 2 หรือ 4 ตามขนาด codespacerange ของ CMap) คีย์ทรัพยากรฟอนต์และขนาดที่ใช้งาน พิกัดจุดเริ่มต้น X และ Y และระยะเลื่อนแนวนอน ซึ่งเพียงพอสำหรับการนำไปประยุกต์ตรวจหาขอบเขตของคำ การเน้นสีข้อความตามพิกัด หรือสร้างตรรกะการจัดวางโครงสร้างแบบกำหนดเองได้โดยตรง
var
Glyphs: THPDFGlyphArray;
I, Unresolved: Integer;
begin
if Pdf.ExtractLoadedPageGlyphs(0, Glyphs) then
begin
Unresolved := 0;
for I := 0 to High(Glyphs) do
if Glyphs[I].Unicode = 0 then
Inc(Unresolved);
if Unresolved > 0 then
ShowMessageFmt('%d จากทั้งหมด %d glyphs ไม่มีรายละเอียดการจับคู่ Unicode',
[Unresolved, Length(Glyphs)]);
end;
end;
การนับเรกคอร์ดที่ Unicode = 0 ดังแสดงข้างต้น เป็นวิธีการที่ตรงไปตรงมาที่สุดในการวัดคุณภาพการแยกข้อความในเอกสารที่กำหนด ก่อนที่คุณจะนำข้อความนั้นไปประมวลผลต่อในระบบ เรกคอร์ดของ glyph ยังช่วยยึดโยงอักขระแต่ละตัวเข้ากับโอเปอแรนด์ต้นทางในสตรีมเนื้อหา ซึ่งทำให้การค้นหาและการแทนที่ข้อความในเอกสารที่โหลดขึ้นมาของ HotPDF ทำงานได้บนรากฐานเดียวกันนี้
PDF แบบใดที่ไม่สามารถแยกข้อความออกมาได้?
ไฟล์บางประเภทอาจทำให้ไม่สามารถสกัดข้อความใดๆ ออกมาได้เลย และจะดีกว่าหากเราตรวจพบปัญหาเหล่านั้นล่วงหน้า เอกสารสแกนเป็นกรณีที่ชัดเจนที่สุด: หน้ากระดาษที่เป็นภาพขนาดใหญ่เพียงภาพเดียวจะไม่มีคำสั่งจัดการข้อความใดๆ เลย ดังนั้นการแยกข้อความจึงส่งกลับค่าเป็นสตริงว่างอย่างถูกต้อง — ทางแก้ไขคือการทำ OCR และการแยกรูปภาพออกจากไฟล์ PDF ที่โหลดขึ้นมา จะเป็นขั้นตอนแรกของระบบนั้น ฟอนต์ชุดย่อยที่ไม่มีตาราง /ToUnicode จะเป็นกรณีที่ยากกว่า: หากไม่มีข้อมูลเส้นทาง /Encoding และตาราง CMaps มาตรฐานด้วย glyph เหล่านั้นจะถูกระบุค่าเป็น 0 และแสดงผลออกมาเป็นช่องว่างในระหว่างการเรียกใช้ข้อความ เอกสารที่เข้ารหัสไว้จะแยกข้อความได้ตามปกติหากคุณโหลดเอกสารเหล่านั้นพร้อมรหัสผ่านผ่านทาง LoadFromFile เพื่อถอดรหัสสตรีมข้อมูลก่อนที่ตัวแปลคำสั่งจะเข้ามาจัดการประมวลผล
ข้อจำกัดที่ควรชี้แจงให้ชัดเจนคือ: ห่วงโซ่การถอดรหัสจะอ่าน CMap และสตรีมเนื้อหาผ่านทางเดิน Flate ของ HotPDF ดังนั้นฟอนต์ที่สตรีม ToUnicode ใช้ตัวกรองที่ไม่คุ้นเคยจะถูกปรับลดระดับการทำงานไปยังกลวิธีถัดไปแทนที่จะทำผิดพลาดทั้งหน้ากระดาษ ในทางปฏิบัติ FlateDecode ครอบคลุมการทำงานเกือบทั้งหมดที่สร้างขึ้นในช่วงสองทศวรรษที่ผ่านมา และการลดระดับนี้จะเป็นไปอย่างเงียบเชียบโดยเจตนา — เพื่อให้คุณได้รับข้อความที่ดีที่สุดเท่าที่ไฟล์จะเอื้ออำนวยแทนการเกิดข้อยกเว้น กลไกวัตถุฝั่งการอ่านที่ช่วยแปลพจนานุกรมฟอนต์ในจุดนี้ยังทำหน้าที่เป็นกลไกขับเคลื่อนการแก้ไขเมทาดาตาในเอกสารที่โหลดขึ้นมา เพื่อให้ไพป์ไลน์การประมวลผลเอกสารสามารถแยกข้อมูล ตรวจสอบ และเขียนบันทึกเพิ่มเติมได้ในคราวเดียว
การแยกข้อความ การเรนเดอร์แบบคงเค้าโครงเดิม การเข้าถึงในระดับ glyph และฟีเจอร์การค้นหาและแทนที่ข้อมูล ทั้งหมดนี้เป็นส่วนหนึ่งของมาตรฐาน HotPDF Component สำหรับ Delphi และ C++Builder — ไม่จำเป็นต้องใช้ DLLs ภายนอก ไม่มีบริการข้อความจาก OS มีเพียง Object Pascal ที่คุณสามารถดีบักตรวจสอบการทำงานทีละบรรทัดได้เมื่อมีไฟล์แปลกๆ เข้ามาในคิวประมวลผล