HotPDF Delphi Component สร้างช่องว่างคำกับการขึ้นบรรทัดใหม่ใน THotPDF.ExtractLoadedPageText ขึ้นจากเรขาคณิตของ glyph ไม่ใช่จากอักขระช่องว่าง ช่องว่างจะถูกใส่เมื่อช่องว่างหลังความกว้างของ glyph เองเกิน 0.15 ของความสูงข้อความ และบรรทัดใหม่เริ่มเมื่อจุดกำเนิดข้อความข้ามทิศทางการเขียนไปเกินครึ่งหนึ่งของความสูงข้อความเท่านั้น ตั้งแต่ v2.768.3 ข้อความของหน้ายังรวมข้อความที่วาดผ่าน Form XObjects และตัด glyph ที่อยู่นอกพื้นที่ crop ที่มองเห็นออกไป ที่เหลือของบทความนี้อธิบายว่าทำไมแต่ละกฎถึงมีรูปแบบแบบนี้ เพราะทุกกฎล้วนแทนที่กฎง่าย ๆ ที่ให้ output ที่ฟังดูเชื่อได้แต่ผิดบนเอกสารจริง
อาการพวกนี้คุ้นเคยกับใครก็ตามที่เคยเอาข้อความ PDF ไปเข้า search index หน้าปกสกัดออกมาเป็น PDFReferenceManualNovember4,1998 แบบฟอร์มภาษีแตกเป็น 156 บรรทัด ลายน้ำเฉียงมาทีละตัวอักษรต่อบรรทัด และ proof ที่ถูกตัดขอบนำด้วย slug line ของเครื่องพิมพ์ที่ viewer ไม่มีตัวไหนแสดงเลย ไฟล์พวกนี้ไม่ได้เสียสักไฟล์ แต่ละไฟล์ใช้วิธีวางข้อความที่ถูกต้องตามกฎหมายเต็ม ๆ ที่ extractor หลง ๆ อ่านผิด
ทำไมข้อความ PDF ที่สกัดมาถึงเสียช่องว่างคำ
ข้อความที่สกัดมาเสียช่องว่างคำเพราะ PDF ไม่เคยถูกบังคับให้มีมัน ผู้ผลิตจะแยกคำด้วยการแสดงอักขระช่องว่างก็ได้ แต่จะเลื่อนปากกาด้วยตัวเลขใน array TJ (ISO 32000-1 §9.4.3) หรือด้วย Td ใหม่ (§9.4.2) ก็ได้เช่นกัน และ output ของ TeX, ไฟล์ Distiller จำนวนมากกับ layout ที่จัดชิดขอบสองข้างส่วนใหญ่ทำแบบหลังเป๊ะ ๆ ก่อน v2.766.76 HPDFAssemblePageText มองแค่การเคลื่อนแนวดิ่ง การขึ้นคำใหม่ที่เกิดจากการจัดตำแหน่งจึงระเหยหายไปเฉย ๆ assembler ตอนนี้วัดระยะตามทิศทางการเขียนของ glyph ก่อนหน้า จากปลายความกว้างของ glyph นั้นไปจุดกำเนิดของ glyph ปัจจุบัน แล้วใส่ช่องว่างหนึ่งตัวเมื่อระยะเกิน 0.15 ของความสูงกล่อง glyph ปัจจุบัน วัดจาก ascent ถึง descent ใน user space ไม่มีช่องว่างถูกเติมเมื่อข้างใดข้างหนึ่งว่างอยู่แล้ว และไม่มีระหว่างอักษร CJK สองตัว เพราะการจัดชิดขอบกางอักษรออกจากกันโดยที่การกางนั้นไม่ได้แปลว่าขอบเขตคำ record ของ glyph เปิดเรขาคณิตชุดเดียวกันออกมา คุณจึงทำซ้ำการตัดสินใจได้เมื่อไฟล์สักไฟล์ทำให้งง
uses
SysUtils, HPDFDoc, HPDFContentStream;
procedure DumpWordGaps(Pdf: THotPDF; PageIndex: Integer);
var
Glyphs: THPDFGlyphArray;
I: Integer;
Height, Gap: Double;
begin
if not Pdf.ExtractLoadedPageGlyphs(PageIndex, Glyphs) then
Exit;
for I := 1 to High(Glyphs) do
begin
// ความสูง ascent ถึง descent ของกล่อง glyph ใน user space
Height := Sqrt(Sqr(Glyphs[I].QuadX[3] - Glyphs[I].QuadX[0]) +
Sqr(Glyphs[I].QuadY[3] - Glyphs[I].QuadY[0]));
// ข้อความแนวนอน: ช่องว่างจากปลายความกว้างของ glyph ก่อนหน้า
Gap := Glyphs[I].BaselineStartX - Glyphs[I - 1].GlyphEndX;
if (Height > 0) and (Gap > 0.15 * Height) then
Writeln(Format('U+%.4x gap %.2f height %.2f: space',
[Glyphs[I].Unicode, Gap, Height]));
end;
end;
ทำไมต้องวัดจากความกว้างของ glyph เอง ไม่ใช่ตำแหน่งปากกา
HotPDF วัดช่องว่างคำจาก GlyphEndX / GlyphEndY เพราะตำแหน่งปากกาหลัง glyph หนึ่งมีระยะห่างที่ไม่ใช่ช่องว่างปนอยู่แล้ว ISO 32000-1 §9.4.4 นิยามการเลื่อนแนวนอนว่าคือความกว้าง glyph คูณขนาดฟอนต์ บวก character spacing Tc บวก word spacing Tw คูณสเกลด้วย Tz ทั้งหมด BaselineEndX / BaselineEndY ถือระยะเลื่อนเต็มนั้น ขณะที่ GlyphEndX / GlyphEndY ถือแค่ advance ของฟอนต์กับ Tz ความต่างนี้สำคัญกับผู้ผลิตที่กระชับ tracking ด้วย Tc ติดลบแล้วคืนช่องว่างกลับด้วย adjustment ของ TJ หลังแต่ละ glyph: ถ้าวัดจากตำแหน่งปากกา การคืนกลับจะดูเป็นช่องว่าง และคำจีน “95后” สกัดออกมาเป็น “9 5 后” เกณฑ์ตัดสินถูกผูกกับความสูงกล่อง glyph แทนขนาด Tf ด้วยเหตุผลคล้ายกัน Word เขียน 1 Tf แล้วฝากขนาดจริงไว้ใน Tm ที่สเกลแล้วบ่อย ๆ Tfs จึงบอก 1 ทั้งที่ข้อความสูง 10 พอยต์ กฎที่ผูกกับ Tfs จะตีความสองการสะกดของหน้าเดียวกันต่างกันไป
กฎนี้มีขอบที่ต้องยอมรับตามตรง หัวเรื่องที่จัดด้วย tracking ห่างมาก ที่ Tc อย่างเดียวกางช่องว่างระหว่างตัวอักษรเกิน 0.15 ของความสูงข้อความ จะสกัดออกมามีช่องว่างคั่นทุกตัวอักษร ซึ่งตรงกับที่หน้าดูออกมา แต่คงไม่ใช่สิ่งที่คุณอยากเข้าดัชนี ชิ้นส่วนที่ถูกวาดพลิกลำดับบน baseline เดียวกันให้ช่องว่างติดลบและเกาะกันโดยไม่มีช่องว่าง ทั้งสองกรณีไม่ค่อยเจอในเนื้อหา และบนชุดทดสอบการเปลี่ยนนี้เพิ่มการ match คำกับตัวอ่านอ้างอิงใน 28 หน้าโดยไม่ลดหน้าไหนเลย
HotPDF ขึ้นบรรทัดใหม่ในข้อความที่สกัดเมื่อไร
ตั้งแต่ v2.766.79 บรรทัดใหม่เริ่มเมื่อการเลื่อนจากจุดกำเนิดของ glyph ก่อนหน้ามาที่ตัวปัจจุบัน เมื่อฉายลงบน normal ของทิศทางการเขียนก่อนหน้า เกินครึ่งหนึ่งของความสูงกล่องที่ใหญ่กว่าของ glyph คู่นั้น กฎเดิมเทียบการเลื่อน Y ดิบ ๆ กับครึ่งหนึ่งของ Tfs ซึ่งพังสองทาง ด้วย 1 Tf บวก Tm ที่สเกลแล้ว เกณฑ์หดเหลือครึ่งหน่วย superscript ที่ยกด้วย text rise 0.4 หรือ jitter ของ baseline ธรรมดาจึงตัดบรรทัดไปเอง กฎเดิมยังเมิน X ทิ้งสนิท ข้อความใต้ Tm ที่หมุนจึงเดินลงหน้าทีละ glyph และออกมาทีละตัวอักษรต่อบรรทัด การฉายลง normal ของทิศทางทำให้ run ที่หมุนทำตัวเหมือน run แนวนอน และการเลือกความสูงที่ใหญ่กว่าของสองตัวรั้งคำตัวอย่างใหญ่กับ caption จิ๋วของมันไว้บนบรรทัดเดียวเมื่อแชร์ baseline กัน บนแบบฟอร์มภาษีที่พูดถึงข้างต้น จำนวนบรรทัดลดจาก 156 เหลือ 97 ข้อความแนวตั้งใน writing mode 1 (§9.7.4.3) ใช้เส้นทางแยก: glyph พวกนั้นถูกจัดกลุ่มเป็นคอลัมน์ อ่านจากขวาไปซ้ายและบนลงล่าง โดยขึ้นบรรทัดใหม่ทุกครั้งที่เปลี่ยนคอลัมน์
ข้อความไหนที่ ExtractLoadedPageText รับเข้าหรือตัดออก
ExtractLoadedPageText คืนข้อความที่ viewer แสดง ตั้งแต่ v2.766.80 มันทำงานจาก glyph ที่มองเห็นเท่านั้น ทิ้งทุก glyph ที่จุดกึ่งกลางกล่องตกอยู่นอก GetLoadedPageVisibleBox ซึ่งคือ CropBox ที่ถูกครอบด้วย MediaBox (§14.11.2) สิ่งนี้เอา slug line กับตราเครื่องพิมพ์อื่นที่ตั้งเป็นข้อความนอกพื้นที่ trim ออกไป ExtractLoadedPageGlyphs ยังตั้งใจคืน glyph ทุกตัวของ content stream ของหน้าต่อไป คุณจึงยังหาวัสดุพวกนั้นเจอเมื่อต้องการ ตัวกรองเป็นเทสต์กล่อง ไม่ใช่เทสต์ความมองเห็น: ข้อความที่ถูก clipping path ซ่อน วาดด้วยสีขาวหรือโดนภาพทับยังถูกสกัดออกมาเหมือนเดิม
var
Pdf: THotPDF;
Glyphs: THPDFGlyphArray;
PageText: UnicodeString;
L, B, R, T: Single;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('trimmed-proof.pdf');
if Pdf.GetLoadedPageVisibleBox(0, L, B, R, T) then
Writeln(Format('Visible box: %.1f %.1f %.1f %.1f', [L, B, R, T]));
// glyph ทุกตัวของ content stream ของหน้า รวม slug line ด้วย
if Pdf.ExtractLoadedPageGlyphs(0, Glyphs) then
Writeln(Length(Glyphs), ' glyphs in the page content stream');
// เฉพาะที่หน้าแสดง โดยประกอบข้อความจาก Form XObject เข้ามาด้วย
if Pdf.ExtractLoadedPageText(0, PageText) then
Writeln(PageText);
finally
Pdf.Free;
end;
end;
ข้อความที่วาดผ่าน Form XObjects เป็นส่วนหนึ่งของข้อความหน้าตั้งแต่ v2.768.3 หัวกระดาษ ตราปั๊มและลายน้ำมักอาศัยอยู่ในฟอร์ม และเอกสารมาตรฐานบางชุดเสียตัวอักษรไป 30 ถึง 35 เปอร์เซ็นต์ก่อนการเปลี่ยนนี้ THotPDF.InterpretContentWithForms จด Do แต่ละตัวพร้อม CTM ที่มีผลอยู่ ตีความฟอร์มที่ /Matrix ของมันคูณ CTM นั้น (§8.10.1) แล้วประกอบ glyph ของฟอร์มเข้าที่ตำแหน่งของ Do โดยเวียนลึกเข้าฟอร์มซ้อนด้วย ฟอร์มที่ไม่มี /Resources ของตัวเองยืมของ stream ที่วาดมัน ตามที่ §7.8.3 อนุญาต glyph ของฟอร์มแบก TokenIndex = -1 และ ExtractLoadedPageGlyphs ยังคืนเฉพาะ glyph ของ page-stream เพราะการค้นหา แทนที่และ redact เขียนการเปลี่ยนกลับผ่าน TokenIndex และจะแก้ผิดไบต์ถ้า glyph ของฟอร์มหลุดเข้าไป มีสองการตัดทอนที่สมควรรู้: ข้อความฟอร์มไม่ถูกครอบด้วย /BBox ของฟอร์ม และการเวียนลึกหยุดที่ 12 ชั้นแทนการจับวงจรย้อนกลับ ฟอร์มเพี้ยนที่วาดตัวเองจึงพ่นข้อความซ้ำจนถึงเพดานนั้น
ทำไมข้อความหลัง operator Q ถึงถอดรหัสออกมาเป็นขยะ
ข้อความหลัง Q เคยถอดรหัสเพี้ยนก่อน v2.766.73 เพราะตัว extractor เก็บแค่ CTM บน q พารามิเตอร์ของ text state คือฟอนต์ ขนาด Tc, Tw, Tz, TL, โหมด render กับ rise อยู่ใน graphics state (§9.3.1) Q จึงต้องคืนมันพร้อมทุกอย่างอื่นบน stack (§8.4.2) รายงานอุตสาหกรรมหนึ่งเลือกฟอนต์ Identity-H สองไบต์ไว้ใน q … Q แล้วแสดงข้อความ WinAnsi หนึ่งไบต์ต่อโดยไม่มี Tf ของตัวเอง extractor คงฟอนต์ชั้นในไว้ อ่านจุดนำกับคำ “Adobe” บนสารบัญเป็นรหัสสองไบต์ และทิ้งตัวอักษรของหน้าไป 15% stack ของ q/Q ของตัวตีความตอนนี้ถือ text state เต็ม ๆ กฎการสกัดที่เล่ามาทั้งหมดใช้กับทุกหน้า เอกสารทั้งฉบับจึงลงไฟล์ได้ในการเรียกเดียว
var
Output: TFileStream;
Pages: Integer;
begin
Output := TFileStream.Create('report.txt', fmCreate);
try
// ช่วงว่าง = ทุกหน้า; form feed คั่นระหว่างหน้า; UTF-8 BOM
Pages := Pdf.ExtractLoadedPagesTextToStream(Output, '', #12, True);
Writeln(Pages, ' pages extracted');
finally
Output.Free;
end;
end;
ควรใช้ API ข้อความตัวไหนของ HotPDF
ExtractLoadedPageText คงลำดับของ content stream ไว้ ซึ่งเป็นค่าเริ่มต้นที่ถูกต้องสำหรับการค้นหากับการทำดัชนี ห่วงโซ่การถอดรหัสที่อยู่ใต้มันเล่าไว้ในการสกัดข้อความจาก PDF ที่โหลดไว้ด้วย HotPDF สำหรับเอกสารแบบ tagged ที่ลำดับการเขียนสำคัญ การสกัดข้อความตามลำดับโครงสร้างเดินต้นไม้โครงสร้างแทนการเดาจากเรขาคณิต และสำหรับข้อมูลที่ขังอยู่ในตาราง การสกัดตารางแบบมีชนิดข้ามการขึ้นหน้าใหม่คืน cell แทนบรรทัด API reference เต็มกับ build ทดลองอยู่ที่หน้าผลิตภัณฑ์ HotPDF Delphi PDF Component