คอมโพเนนต์ HotPDF Component สามารถค้นหาและแทนที่ข้อความภายในไฟล์ PDF ที่มีอยู่ได้จาก Delphi และ C++Builder ฟังก์ชัน SearchLoadedPageText และ SearchLoadedDocumentText จะระบุตำแหน่งทุกครั้งที่พบสตริงข้อมูลด้วยความแม่นยำระดับ glyph และฟังก์ชัน ReplaceLoadedPageText และ ReplaceLoadedDocumentText จะทำการเขียนไบต์ข้อมูลที่ตรงกันทับลงในตำแหน่งเดิม — โดยมีข้อแม้ว่าอักขระใหม่ที่จะแทนที่ทุกตัวต้องสามารถถูกเข้ารหัสกลับ (re-encode) ผ่านฟอนต์ดั้งเดิมได้ ซึ่งเป็นข้อจำกัดทางกายภาพที่บทความนี้อธิบายตรงไปธรรมดาแทนที่จะซ่อนไว้ในเชิงอรรถ
เบื้องหลังความต้องการของคุณสมบัตินี้มักเป็นเรื่องปกติทั่วไป เช่น บริษัทเปลี่ยนชื่อใหม่แต่ใบแจ้งหนี้สามพันใบที่จัดเก็บไว้ยังมีชื่อเดิม สัญญาตัวอย่างที่ถูกส่งออกไปพร้อมวันหมดอายุของปีที่แล้ว หรือรหัสผลิตภัณฑ์ถูกยกเลิกการใช้งานและเอกสารข้อมูลทุกแผ่นที่อ้างถึงรหัสเดิมจำเป็นต้องแก้ไขเป็นรหัสใหม่ทดแทน ในโปรแกรมประมวลผลคำ งานเหลานี้ใช้เวลาเพียงสามสิบวินาทีก็เสร็จสิ้น แต่สำหรับไฟล์ PDF นี่เป็นปัญหาที่ยากมาก และการเข้าใจสาเหตุคือกุญแจสำคัญระหว่างการใช้งาน API ได้อย่างมีประสิทธิภาพกับการแจ้งรายงานปัญหาการทำงานที่จริงๆ แล้วเป็นพฤติกรรมตามข้อกำหนดของไฟล์
ทำไมการแทนที่ข้อความใน PDF จึงเป็นเรื่องยากมาก?
การแทนที่ข้อความในไฟล์ PDF เป็นเรื่องยากเนื่องจากหน้ากระดาษ PDF ไม่ได้เก็บข้อความที่แก้ไขได้ — แต่มันเก็บภาพ glyph ที่ถูกจัดวางตำแหน่งไว้ ภายใต้โมเดลแสดงข้อความของ ISO 32000-1 §9.4 สตรีมเนื้อหาจะขับเคลื่อนตัวดำเนินการอย่าง Tj และ TJ ที่ทำหน้าที่วาดลำดับของรหัสอักขระ ณ พิกัดที่กำหนดโดยเมทริกซ์ข้อความ รหัสเหล่านั้นไม่ใช่ Unicode แต่เป็นดัชนีชี้ไปยังการเข้ารหัสใดๆ ที่ฟอนต์ของหน้านั้นประกาศไว้ และแผนผังการแปลงกลับเป็นตัวอักษรที่อ่านได้อาจอยู่ในตาราง /ToUnicode CMap, อาร์เรย์ของผลต่างการเข้ารหัส หรือโซ่จับคู่ CID ในเอกสารไม่มีวัตถุส่วนย่อยย่อหน้า (paragraph object) ไม่มีการไหลของข้อความ และไม่มีสิ่งใดรับประกันว่าคำหนึ่งคำที่เรามองเห็นจะถูกเก็บไว้เป็นสตริงเดียวกัน
การค้นหาข้อความ: การค้นหาระดับ glyph พร้อมระบบติดตามออฟเซ็ตไบต์
ฟังก์ชัน SearchLoadedDocumentText ของ HotPDF จะค้นหาคำค้นแต่ละครั้งที่พบโดยจับคู่กับลำดับ Unicode glyph ที่ถอดรหัสออกมาแล้วของแต่ละหน้า แทนการตรวจสอบกับไบต์สตรีมดิบ ดังนั้นคำที่ตรวจพบจึงมีความถูกต้องโดยไม่คำนวณตามวิธีการเข้ารหัสของฟอนต์ โครงสร้างเบื้องหลังนี้ถูกเพิ่มเข้ามาในเวอร์ชัน v2.251.0: ตัวแยกโทเค็นสตรีมเนื้อหาจะบันทึกช่วงไบต์ StartOfs/EndOfs สำหรับทุกอาร์กิวเมนต์สตริง — รวมถึงเครื่องหมายวรรคตอนอย่าง ( ) หรือ < > — และ glyph แต่ละตัวที่ถอดรหัสได้จะเก็บชุดข้อมูลสามส่วน ได้แก่ TokenIndex/ItemIndex/ByteOffset ที่ชี้กลับไปยังอาร์กิวเมนต์ที่สอดคล้องกัน รายการอาร์เรย์ TJ และหน่วยรหัสที่เป็นตัวสร้างมันขึ้นมา กลไกแปลความหมาย glyph ชุดเดียวกันนี้ยังเป็นรากฐานให้กับ API การแยกข้อความที่กล่าวถึงในการแยกข้อความจาก PDF ที่โหลดใน Delphi การค้นหาเพียงแค่ทำหน้าที่เก็บรักษาความสัมพันธ์ที่ขั้นตอนการแยกข้อความโยนทิ้งไป
var
Pdf: THotPDF;
Matches: THPDFTextMatchArray;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('invoices-2025.pdf') > 0 then
begin
if Pdf.SearchLoadedDocumentText('Acme Corp', False, Matches) then
for I := 0 to Length(Matches) - 1 do
WriteLn(Format('page %d at (%.1f, %.1f): "%s"',
[Matches[I].PageIndex, Matches[I].X, Matches[I].Y,
Matches[I].Text]));
end;
finally
Pdf.Free;
end;
end;
มีส่วนการตัดสินใจออกแบบอย่างหนึ่งที่ควรระบุไว้ เมื่อตั้งค่า CaseSensitive เป็น False การเปรียบเทียบจะทำเฉพาะกับตัวอักษรภาษาอังกฤษ ASCII เท่านั้น ซึ่งนี่เป็นการออกแบบโดยเจตนา: เนื่องจากพฤติกรรมการเปรียบเทียบตัวอักษรใหญ่เล็ก (case folding) ของ Unicode มีความแตกต่างกันในแต่ละชุดเครื่องมือ Delphi 5 ถึง XE ที่ HotPDF รองรับ และ API การค้นหาที่ให้ผลลัพธ์แตกต่างกันไปตามชนิดของคอมไพเลอร์ที่ใช้สร้างแอปพลิเคชันย่อมแย่กว่าการมีข้อจำกัดที่ระบุไว้อย่างชัดเจนและสามารถคาดเดาผลลัพธ์ได้ สำหรับข้อความทางธุรกิจที่เป็นภาษาละตินทั่วไป — เช่น ชื่อ รหัส วันที่ — การทำ folding เฉพาะอักษร ASCII ก็นับว่าครอบคลุมการใช้งานจริงแล้ว
การแทนที่ข้อความ: การเข้ารหัสย้อนกลับและการตัดต่อข้อมูลอย่างละเอียด
ฟังก์ชัน ReplaceLoadedDocumentText ที่เพิ่มเข้ามาใน HotPDF เวอร์ชัน v2.252.0 จะเขียนข้อมูลทับทุกครั้งที่พบข้อความเป้าหมายโดยรันกลไกถอดรหัสในทิศทางย้อนกลับ ฟังก์ชัน HPDFEncodeUnicode ทำหน้าที่ย้อนกลับของตัวถอดรหัสรหัสอักขระ: มันจะค้นหาผ่านสายโซ่กลยุทธ์เดียวกันในลำดับย้อนกลับ — ได้แก่ การค้นหา bfchar และ bfrange ใน /ToUnicode, การจับคู่ CID ของสตรีมเข้ารหัส, การจับคู่เอกลักษณ์ของ Type0 และตาราง WinAnsi และ MacRoman ที่กำหนดไว้ล่วงหน้า — เพื่อเปลี่ยนอักขระทดแทนแต่ละตัวกลับไปเป็นไบต์รหัสอักขระที่ฟอนต์ดั้งเดิมคาดหวัง จากนั้นไบต์ที่เข้ารหัสใหม่จะถูกเปลี่ยนเป็นข้อความธรรมดา (string literal) หรือข้อความฐานสิบหกที่ถูกต้องตามโครงสร้าง โดยเลียนแบบกฎการแปลงโทเค็นของระบบเพื่อให้แน่ใจว่าการแปลงข้อมูลไป-กลับมีความเสถียร
การตัดต่อข้อมูลได้รับการทำอย่างประณีตและเจาะจงเฉพาะจุดแทนการเขียนทับทั้งหมด มีเพียงช่วงรหัสไบต์ที่ตรวจพบเท่านั้นที่จะถูกแทนที่ภายในอาร์กิวเมนต์สตริง ส่วนไบต์ที่ไม่ตรงกันในอาร์กิวเมนต์เดียวกัน ช่องว่างระหว่างโทเค็น และตัวดำเนินการรอบข้างทั้งหมดจะได้รับการคงไว้ตามเดิม ไบต์ต่อไบต์ การแทนที่ bca ภายในคำว่า abcabc จะส่งผลลัพธ์เป็น a + คำแทนที่ + bc โดยไม่ทำให้อาร์กิวเมนต์เสียหาย คำแทนที่อาจจะสั้นหรือยาวกว่าคำเดิมก็ได้ — สตริงจะถูกสร้างขึ้นใหม่และปรับปรุงค่า /Length ของสตรีมข้อมูลตามความเหมาะสม — และสตรีม /Contentsแต่ละตัวของหน้าที่มีหลายสตรีมจะถูกประมวลผลแยกอิสระจากกันเพื่อให้มั่นใจว่าหน้านั้นยังคงมีโครงสร้างที่ถูกต้อง
var
Pdf: THotPDF;
ReplaceCount: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('contract-draft.pdf') > 0 then
begin
if Pdf.ReplaceLoadedDocumentText('2025-12-31', '2026-12-31',
True, ReplaceCount) then
WriteLn(Format('%d ตัวดำเนินการเขียนใหม่สำเร็จ', [ReplaceCount]));
Pdf.SaveLoadedDocument('contract-final.pdf');
end;
finally
Pdf.Free;
end;
end;
โปรดสังเกตในสิ่งที่ API นี้ไม่ได้กระทำ: มันไม่ได้ทำการจัดหน้าอักษรใหม่ (typeset) หน้ากระดาษ PDF ไม่มีพฤติกรรมการไหลของข้อความใหม่ ดังนั้นคำแทนที่ซึ่งมีความกว้างทางสายตามากกว่าคำดั้งเดิมจะกินพื้นที่แนวนอนเพิ่มขึ้นและอาจจะไปบดบังทับข้อมูลอื่นๆ ที่อยู่ทางขวาได้ การแทนที่ด้วยคำที่มีความยาวเท่ากันหรือใกล้เคียงกัน — เช่น วันที่ สตริงเวอร์ชัน รหัสชิ้นส่วน หรือการแก้ไขชื่อสะกดผิด — จึงเป็นกรณีการใช้งานที่เหมาะสมที่สุด การปรับเปลี่ยนข้อความใหม่ทั้งหมดควรทำตั้งแต่ขั้นตอนการทำเอกสารต้นฉบับ ไม่ใช่การมาแก้ไขในไฟล์ PDF
ทำไมคุณจึงไม่สามารถแทนที่ข้อความด้วยอักขระที่ฟอนต์ชุดย่อยไม่เคยบรรจุไว้?
คุณไม่สามารถแทนที่ข้อความด้วยอักขระที่ฟอนต์ชุดย่อยที่ฝังมาไม่เคยบรรจุไว้ได้ เนื่องจากไม่พบลำดับไบต์สำหรับเลือกอักขระดังกล่าวในตารางจับคู่ของฟอนต์ เมื่อโปรแกรมสร้าง PDF ฝังฟอนต์แบบชุดย่อย ตาราง /ToUnicode CMap และโครงสร้างการเข้ารหัสของมันจะครอบคลุมเฉพาะ glyph ที่เอกสารต้นฉบับใช้งานจริงเท่านั้น ฟังก์ชัน HPDFEncodeUnicode สามารถทำงานย้อนกลับได้เฉพาะการจับคู่ที่มีอยู่จริงในไฟล์: หากเอกสารไม่เคยใช้อักษร E ในฟอนต์ดังกล่าว ย่อมไม่มีรหัสอักขระของ E ให้ระบบแปลงย้อนกลับไปใช้ นี่เป็นคุณสมบัติทางกายภาพของไฟล์ ไม่ใช่ข้อจำกัดของไลบรารีเฉพาะ — ไม่มีเครื่องมือใดสามารถบันดาลแผนผังการจับคู่ glyph ที่ไม่เคยฝังมาในไฟล์ตั้งแต่แรกได้
HotPDF จะจัดการกับข้อผิดพลาดนี้อย่างรัดกุม หากมีตัวอักษรใดตัวหนึ่งของคำแทนที่ไม่สามารถเข้ารหัสย้อนกลับได้ การแทนที่คำนั้นทั้งคำจะถูกข้ามไป — โดยไม่มีการเกิดข้อยกเว้น ไม่มีข้อความขยะบางส่วนปรากฏ และคำนั้นจะไม่ถูกนับรวมในผลลัพธ์ ReplaceCount ผลในทางปฏิบัติคือ: ให้ตรวจสอบ ReplaceCount เทียบกับจำนวนคำที่ค้นพบจากการค้นหาก่อนหน้า และใช้ผลต่างนั้นเป็นสัญญาณบ่งชี้ ในตัวอย่างการแก้ไขวันที่ข้างต้น ตัวเลข 6 จะต้องเคยปรากฏอยู่ที่ใดที่หนึ่งในเนื้อหาของเอกสารด้วยฟอนต์เดียวกันนั้นเพื่อให้ขั้นตอนเขียนทับสำเร็จ — ซึ่งอาจจะพบได้ในใบแจ้งหนี้ทั่วไปแต่ก็ไม่มีหลักประกันเสมอไป เมื่ออักขระที่คุณต้องการไม่มีให้ใช้งานและเป้าหมายคือการลบข้อมูลที่ละเอียดอ่อนแทนที่จะเป็นการเปลี่ยนถ้อยคำ การทำเซนเซอร์เพื่อนำข้อมูลออกจริงจะเป็นเครื่องมือที่เหมาะสมกว่า โปรดอ่านเพิ่มเติมเกี่ยวกับเส้นทางดังกล่าวในการทำเซนเซอร์และการปรับโครงสร้างไฟล์ PDF ที่โหลดใน Delphi
var
Matches: THPDFTextMatchArray;
Expected, Replaced: Integer;
begin
Pdf.SearchLoadedDocumentText('Acme Corp', True, Matches);
Expected := Length(Matches);
Pdf.ReplaceLoadedDocumentText('Acme Corp', 'Apex Corp', True, Replaced);
if Replaced < Expected then
WriteLn(Format('%d รายการที่ถูกข้ามไป: อักขระขาดหาย จากฟอนต์ชุดย่อย หรือคำค้นคาบเกี่ยวหลายอาร์กิวเมนต์',
[Expected - Replaced]));
end;
เงื่อนไขการข้ามข้อที่สองในข้อความดังกล่าวคือขอบเขตการทำงานอีกจุดหนึ่งที่บันทึกไว้: นั่นคือคำค้นหาคาบเกี่ยวหลายอาร์กิวเมนต์สตริง — ตัวอย่างเช่น คำว่า Hello ถูกแบ่งเก็บเป็น [(He)(llo)] TJ ซึ่งกรณีนี้ตัวค้นหาจะพบคำเนื่องจากการค้นหาจับคู่กับลำดับ glyph ที่ถอดรหัสแล้ว แต่จะถูกข้ามไปในกระบวนการแทนที่ เนื่องจากกระบวนการเขียนทับข้ามขอบเขตอาร์กิวเมนต์จำเป็นต้องทำควบรวมช่วงไบต์ที่อยู่ติดกัน การค้นหาแล้วตรวจสอบความถูกต้องก่อนจะช่วยให้ผู้ใช้รับรู้ขอบเขตทั้งสองข้อนี้ได้ดีกว่าการทำงานเงียบๆ
มีอะไรเปลี่ยนแปลงในไฟล์บ้างเมื่อสั่งบันทึก?
สตรีม /Contents ที่ถูกแทนที่ข้อมูลจะถูกบันทึกแบบไม่บีบอัด สตรีมข้อมูลบีบอัดแบบ FlateDecode จะถูกคลายออกเพื่อแก้ไข และเมื่อ HotPDF เขียนบันทึกไบต์ที่สร้างใหม่ มันจะละทิ้งข้อมูลรายการ /Filter ของสตรีมข้อมูลนั้นไป และทำการปรับปรุงค่า /Length ใหม่แทนการบีบอัดข้อมูลกลับไปเป็นแบบเดิม ไฟล์ PDF ผลลัพธ์ที่ได้ยังคงมีความถูกต้องสมบูรณ์และสามารถเปิดดูได้ปกติในโปรแกรมอ่านทั่วไป ข้อแลกเปลี่ยนคือไฟล์จะมีขนาดใหญ่ขึ้นสำหรับสตรีมที่ผ่านการแก้ไข สำหรับไพป์ไลน์แบบแบทช์ที่ต้องประมวลผลเอกสารนับพันรายการ ให้เตรียมแผนรองรับขนาดไฟล์ที่เพิ่มขึ้นนี้หรือรันกระบวนการบีบอัดข้อมูลแยกต่างหากในขั้นตอนถัดไป วิธีที่ออบเจกต์ที่เขียนใหม่ทำงานร่วมกับโครงสร้างอ้างอิงไขว้ของเอกสารเมื่อสั่งบันทึกเป็นหัวข้อเฉพาะที่อธิบายในออบเจกต์สตรีมและการอัปเดตแบบเพิ่มหน่วยใน HotPDF
ข้อมูลส่วนอื่นๆ ทั้งหมดของไฟล์จะไม่มีการแตะต้อง สตรีมข้อมูลที่ไม่ถูกแก้ไขจะคงสถานะการบีบอัดข้อมูลตามเดิม ฟอนต์และรูปภาพจะไม่ถูกเขียนใหม่ และการตัดต่อข้อมูลในระดับอาร์กิวเมนต์หมายความว่าสตรีมที่ถูกแก้ไขจะมีความแตกต่างจากไฟล์ดั้งเดิมเฉพาะบริเวณจุดที่พบข้อความเป้าหมายเท่านั้น ความระมัดระวังนี้เป็นการออกแบบโดยตั้งใจ: ยิ่งไลบรารีทำการเขียนข้อมูลในเอกสารใหม่มากเท่าใด ก็ยิ่งมีโอกาสที่จะทำให้เกิดความผิดพลาดกับพฤติกรรมเฉพาะของระบบสร้างไฟล์ที่เราไม่ได้คาดการณ์ไว้มากเท่านั้น
การค้นหาและแทนที่ข้อความทำงานร่วมกับระบบการแยกข้อความ การเซนเซอร์ข้อความ และการเรนเดอร์หน้ากระดาษเป็นชุดเครื่องมือจัดการเอกสารของ HotPDF ซึ่งทั้งหมดขับเคลื่อนด้วยตัวแปลสตรีมเนื้อหาเดียวกัน และใช้งานได้ตั้งแต่ Delphi 5 จนถึงรุ่น RAD Studio ในปัจจุบันโดยไม่มีการพึ่งพาโมดูลภายนอก รายการอ้างอิง API ฉบับเต็มและตัวดาวน์โหลดรุ่นทดลองใช้งานมีให้บริการบนหน้าผลิตภัณฑ์ HotPDF Component