การอัปเดตแบบเพิ่มหน่วยของ PDF ช่วยให้แอปพลิเคชัน Delphi สามารถแก้ไขเอกสารได้โดยการแนบเฉพาะออบเจกต์ที่เปลี่ยนแปลงเข้าไปเพิ่มเติม โดยปล่อยให้ทุกไบต์ดั้งเดิมไม่มีการแตะต้อง losLab PDF Library ใช้งานฟีเจอร์นี้ผ่านทาง AppendToStream ซึ่งจะเขียนเฉพาะส่วนที่เพิ่มขึ้นตามที่กำหนดไว้ใน ISO 32000-1 §7.5.6 ดังนั้นการแก้ไขบุ๊กมาร์กเพียงจุดเดียวในไฟล์ขนาด 2 GB จะใช้พื้นที่ผลลัพธ์เพียงไม่กี่กิโลไบต์แทนที่จะต้องเขียนไฟล์ใหม่ทั้งหมด กลไกเดียวกันนี้ยังเป็นเหตุผลว่าทำไมเอกสารที่ลงลายเซ็นแล้วจึงสามารถอัปเดตได้โดยไม่ทำให้ลายเซ็นเหล่านั้นเสียไป
ปัญหาที่ได้รับการแก้ไขนี้เป็นสิ่งที่จับต้องได้จริง การบันทึกแบบเต็มไฟล์จะเขียนไฟล์ทั้งหมดขึ้นมาใหม่: ออบเจกต์ทุกตัวจะถูกทำให้เป็นอนุกรมใหม่ ระยะออฟเซ็ตของการอ้างอิงไขว้ทั้งหมดจะถูกคำนวณใหม่ และผลลัพธ์ที่ได้จะไม่เหลือความสัมพันธ์ในระดับไบต์กับไฟล์นำเข้าเลย สำหรับใบแจ้งหนี้ขนาด 40 KB นั่นไม่ใช่ปัญหา แต่สำหรับคลังสแกนเอกสารขนาด 2 GB ที่คุณแก้ไขเพียงคำสะกดผิดในชื่อเรื่องของเอกสาร การเขียนข้อมูลใหม่สองกิกะไบต์เพื่อเปลี่ยนข้อมูลเพียงยี่สิบไบต์นั้นเป็นเรื่องที่ไร้เหตุผล — และหากไฟล์นั้นมีลายเซ็นดิจิทัลอยู่ การเขียนทับใหม่นี้ก็ทำลายลายเซ็นนั้นไปแล้ว
ทำไมการบันทึก PDF จึงทำให้ลายเซ็นดิจิทัลเสีย?
ลายเซ็นดิจิทัลของ PDF ไม่ได้เซ็นรับรองเนื้อหาเชิงตรรกะของเอกสาร แต่มันจะเซ็นรับรองช่วงไบต์ของไฟล์จริง ข้อมูลรายการ /ByteRange ในพจนานุกรมลายเซ็นจะบันทึกไว้อย่างชัดเจนว่าการสรุปข้อมูลแบบเข้ารหัสลับ (cryptographic digest) ครอบคลุมพื้นที่ช่วงใดของไฟล์บ้าง การบันทึกใดๆ ที่ทำให้อนุกรมของไบต์เหล่านั้นเปลี่ยนไป — แม้กระทั่งการบันทึกที่สร้างเอกสารที่มีเนื้อหาเหมือนกันทุกประการ — ก็จะเปลี่ยนค่าสรุปข้อมูล และตัวตรวจสอบทุกตัวจะรายงานว่าลายเซ็นนั้นใช้ไม่ได้อีกต่อไป ซึ่งนี่ได้รับการออกแบบมาโดยเจตนา: ลายเซ็นจะรับรองเฉพาะไบต์ที่ผู้ลงลายเซ็นเห็นเท่านั้น ไม่ได้รับรองโมเดลเอกสารที่เป็นนามธรรมใดๆ
การอัปเดตแบบเพิ่มหน่วยคือช่องทางออกที่ข้อกำหนดเฉพาะของ PDF จัดเตรียมไว้ให้ เนื่องจากการบันทึกแบบเพิ่มหน่วยจะแนบข้อมูลใหม่ต่อท้าย %%EOF ดั้งเดิม และจะไม่ไปแตะต้องช่วงไบต์ที่ลงลายเซ็นไว้ ลายเซ็นที่มีอยู่จึงยังคงสามารถตรวจสอบความถูกต้องเทียบกับไบต์ที่ครอบคลุมได้ จากนั้นตัวตรวจสอบจะจัดประเภทการเปลี่ยนแปลงที่แนบมาแยกต่างหาก — เช่น ลายเซ็นที่สอง การกรอกแบบฟอร์ม หรือคำอธิบายประกอบ — และตัดสินใจว่าการแก้ไขเหล่านั้นได้รับอนุญาตหรือไม่ ทุกเวิร์กโฟลว์ที่มีหลายลายเซ็นจะพึ่งพาสิ่งนี้: ผู้ลงนามแต่ละคนจะเพิ่มส่วนที่เพิ่มขึ้นต่อจากคนก่อนหน้า หากคุณกำลังสร้างไพป์ไลน์การลงลายเซ็น บทความคู่ขนานเกี่ยวกับการลงลายเซ็น PAdES และการตรวจสอบความถูกต้องใน Delphi จะครอบคลุมรายละเอียดว่าช่วงไบต์ของลายเซ็นและส่วนที่เพิ่มขึ้นทำงานร่วมกันอย่างไร
การอัปเดตแบบเพิ่มหน่วยทำงานอย่างไรภายใต้ ISO 32000-1 §7.5.6
ISO 32000-1 §7.5.6 กำหนดรูปแบบการทำงานไว้เป็นกฎสามข้อ ข้อแรก เนื้อหาไฟล์ดั้งเดิมจะยังคงอยู่ครบถ้วนโดยไม่มีการเปลี่ยนแปลง — ไม่มีไบต์ใดขยับ ข้อสอง ออบเจกต์ที่เปลี่ยนแปลงและสร้างขึ้นใหม่จะถูกแนบต่อท้ายหลัง %%EOF ตัวล่าสุด โดยแต่ละตัวจะยังคงใช้หมายเลขออบเจกต์เดิมเหมือนก่อนหน้านี้ (ออบเจกต์ที่แก้ไขเพียงแค่ได้คำจำกัดความใหม่กว่ามาทับซ้อนคำจำกัดความเดิม) ข้อสาม ส่วนอ้างอิงไขว้และส่วนท้าย (trailer) ใหม่จะถูกแนบต่อท้าย โดยรายการ /Prev ของส่วนท้ายจะชี้กลับไปยังออฟเซ็ตไบต์ของส่วนอ้างอิงไขว้ก่อนหน้า เกิดเป็นห่วงโซ่ที่โปรแกรมอ่านจะไล่ดูจากใหม่สุดไปยังเก่าสุดเพื่อค้นหาคำจำกัดความล่าสุดของแต่ละออบเจกต์
คุณสมบัติที่มีประโยชน์สองประการเกิดขึ้นจากโครงสร้างนี้ ข้อแรก การอัปเดตจะมีต้นทุนต่ำตามสัดส่วนของสิ่งที่เปลี่ยนแปลง ไม่ใช่ขนาดของเอกสาร — ค่าใช้จ่ายในการแนบคือขนาดของออบเจกต์ที่แก้ไขบวกกับส่วนควบคุม xref/trailer เล็กน้อย และข้อสอง ไฟล์จะกลายเป็นประวัติเวอร์ชันของตัวมันเอง: ทุกการปรับปรุงก่อนหน้านี้ยังคงอยู่ในไฟล์ทางกายภาพ ดังนั้นผู้ตรวจสอบจึงสามารถตัดทอนไฟล์ที่ %%EOF ก่อนหน้า และกู้คืนเอกสารที่เคยมีอยู่ ณ จุดนั้นกลับมาได้อย่างแม่นยำ สำหรับเวิร์กโฟลว์การปฏิบัติตามข้อกำหนดที่ต้องพิสูจน์ว่าเอกสารมีลักษณะอย่างไรก่อนที่จะมีการแก้ไขแต่ละครั้ง เส้นทางการตรวจสอบในตัวนี้มักเป็นข้อโต้แย้งที่ชี้ขาดให้เลือกใช้การบันทึกแบบเพิ่มหน่วย
การเขียนการอัปเดตแบบเพิ่มหน่วยด้วย AppendToStream
losLab PDF Library เปิดให้ใช้งานเอาต์พุตแบบเพิ่มหน่วยผ่านทาง AppendToStream(AppendMode: Integer; OutStream: TStream): Integer ซึ่งจะส่งกลับค่า 1 เมื่อสำเร็จ และ 0 เมื่อล้มเหลว พารามิเตอร์ AppendMode จะเลือกสิ่งที่จะส่งไปยังสตรีมเป้าหมาย โหมด 0 จะเขียนไฟล์แบบสมบูรณ์: ไบต์ต้นฉบับดั้งเดิมจะถูกคัดลอกไปยังสตรีมก่อน จากนั้นส่วนที่เพิ่มขึ้นจะถูกแนบตามไป โหมด 1 จะเขียนเฉพาะส่วนที่เพิ่มขึ้นเท่านั้น — หรือส่วนต่าง (delta) — และข้ามไบต์ต้นฉบับทั้งหมด โหมด 2 จะเขียนส่วนหน้า (prefix) ที่ผู้เรียกใช้งานจัดเตรียมไว้ซึ่งลงทะเบียนผ่าน SetAppendInputFromString เป็นอันดับแรก จากนั้นจึงแนบส่วนอัปเดตต่อท้ายด้านบน
var
Doc: TPDFlib;
Delta: TMemoryStream;
begin
Doc := TPDFlib.Create;
try
if Doc.LoadFromFile('contract.pdf', '') <= 0 then
Exit;
// แก้ไขเล็กน้อย: การเปลี่ยนแปลงประเภทที่ไม่ควร
// กระตุ้นให้ต้องเขียนไฟล์ใหม่ทั้งหมด
Doc.SetInformation(3, 'Amended 2026-07-04'); // คีย์ 3 = /Subject
Delta := TMemoryStream.Create;
try
// AppendMode = 1: เขียนเฉพาะส่วนที่เพิ่มขึ้นเท่านั้น
// ไบต์ดั้งเดิม + Delta = PDF ที่สมบูรณ์และถูกต้อง
if Doc.AppendToStream(1, Delta) = 1 then
Delta.SaveToFile('contract.delta.bin');
finally
Delta.Free;
end;
finally
Doc.Free;
end;
end;
โหมด 1 เป็นโหมดที่น่าสนใจสำหรับการออกแบบระบบ เนื่องจากเดลต้านั้นมีความสมบูรณ์ในตัว คุณจึงสามารถจัดส่งมันแยกต่างหากจากไฟล์ต้นฉบับได้: จัดเก็บแต่ละเวอร์ชันเป็นบล็อบ (blob) แยกกันในพื้นที่จัดเก็บออบเจกต์ จำลองเฉพาะเดลต้าไปยังเซิร์ฟเวอร์ปลายทาง หรือกู้คืนเวอร์ชันใดๆ ขึ้นมาใหม่โดยการนำไฟล์หลักมาต่อกับสายโซ่ของส่วนต่างที่เพิ่มขึ้น กฎในการประกอบเอกสารขึ้นมาใหม่คือการต่อไบต์แบบธรรมดา — เริ่มจากไฟล์ดั้งเดิมก่อน แล้วจึงตามด้วยเดลต้าแต่ละตัวตามลำดับ — เพราะนี่คือโครงร่างที่มาตรฐาน §7.5.6 กำหนดไว้สำหรับไฟล์ที่อัปเดตแบบเพิ่มหน่วย
ไลบรารีคำนวณระยะออฟเซ็ต xref โดยไม่คัดลอกไฟล์ต้นฉบับได้อย่างไร?
ข้อมูลรายการการอ้างอิงไขว้ภายในส่วนที่เพิ่มขึ้นต้องมีค่าออฟเซ็ตไบต์แบบสัมบูรณ์ — ซึ่งเป็นตำแหน่งที่วัดจากจุดเริ่มต้นของไฟล์ทั้งหมด ไม่ใช่จากจุดเริ่มต้นของเดลต้า นั่นทำให้เกิดความท้าทายสำหรับโหมด 1: ฝั่งตัวเขียนไม่ได้ส่งออกไบต์ดั้งเดิมเลย แต่ทุกออฟเซ็ตที่บันทึกไว้จะต้องทำเสมือนว่ามีไบต์ดั้งเดิมเหล่านั้นอยู่ losLab PDF Library แก้ไขปัญหานี้ด้วยตัวปรับต่อสตรีมภายใน (stream adapter) คือ TPDFAppendSectionStream ซึ่งจะเสนอพื้นที่พิกัดเสมือนจริงให้กับตัวทำอนุกรม ตัวปรับต่อจะถูกสร้างขึ้นโดยใช้ความยาวไบต์ของไฟล์ดั้งเดิมเป็นออฟเซ็ตหลัก รายงานตำแหน่งและขนาดเป็นค่าหลักนั้นบวกกับข้อมูลใดๆ ที่ถูกแนบเพิ่มเติมเข้ามา และส่งต่อเฉพาะไบต์ที่เขียนใหม่ไปยังสตรีมเป้าหมายของผู้เรียกใช้งานเท่านั้น
ผลลัพธ์ก็คือ โหมด 1 จะไม่สร้างสำเนาของเอกสารต้นฉบับขึ้นมาเลย — ไม่ว่าจะบนดิสก์หรือในหน่วยความจำ การเขียนระบบแบบพื้นๆ (เขียนไฟล์ทั้งหมดลงในบัฟเฟอร์ชั่วคราวแล้วตัดส่วนท้ายออก) จะทำให้ต้องเก็บสำเนาของ PDF ต้นฉบับทั้งหมดไว้ชั่วคราว ซึ่งสำหรับไฟล์นำเข้าในระดับกิกะไบต์ นั่นคือต้นทุนที่การอัปเดตแบบเพิ่มหน่วยต้องการหลีกเลี่ยง เทคนิคการจำลองออฟเซ็ตนี้มีความคล้ายคลึงอย่างใกล้ชิดกับการเลื่อนตำแหน่งอ้างอิงไบต์ (byte-reference shifting) ที่ใช้ในส่วนอื่นๆ ของไลบรารี บทความเกี่ยวกับการรวม PDF อย่างรวดเร็วด้วยการเลื่อนตำแหน่งอ้างอิงไบต์ จะแสดงแนวคิดเดียวกันนี้ที่นำมาใช้กับการรวมเอกสาร และคู่มือเกี่ยวกับการผสานและแยก PDF ขนาดใหญ่ด้วยการเข้าถึงไฟล์โดยตรง จะครอบคลุมสถาปัตยกรรม I/O สำหรับไฟล์ที่มีขนาดใหญ่เกินกว่าจะเก็บไว้ใน RAM ได้อย่างราบรื่น
การสตรีมการบันทึกแบบเต็มไฟล์ด้วย SaveToStream
เอาต์พุตแบบเพิ่มหน่วยเป็นเพียงครึ่งหนึ่งของเรื่องราวการสตรีม อีกครึ่งหนึ่งคือสิ่งที่จะเกิดขึ้นเมื่อทำการบันทึกแบบเต็มไฟล์ ฟังก์ชัน SaveToStream ใน losLab PDF Library จะขับเคลื่อนตัวแปลงเอกสารให้เป็นอนุกรมส่งตรงไปยังสตรีมเป้าหมาย แทนที่จะต้องเรนเดอร์เอกสารทั้งหมดลงใน AnsiString ตัวกลางก่อนแล้วจึงเขียนบัฟเฟอร์นั้นออกไปในการเรียกใช้ครั้งเดียว วิธีการแบบเก่าสามารถทำงานได้แต่นั่นหมายความว่าทุกๆ การบันทึกแบบเต็มไฟล์จะต้องการหน่วยความจำชั่วคราวเพื่อเก็บสำเนาสมบูรณ์ชุดที่สองของผลลัพธ์ — ซึ่งอาจไม่มีผลเสียที่ขนาด 10 MB แต่จะเป็นปัญหาที่ 500 MB และเป็นกำแพงขวางกั้นสำหรับเอาต์พุตระดับหลายกิกะไบต์ในกระบวนการแบบ 32 บิต การแปลงเป็นอนุกรมโดยตรงจะช่วยให้หน่วยความจำสูงสุดติดตามเฉพาะโครงสร้างออบเจกต์ของเอกสาร แทนที่จะเป็นความยาวหลังการแปลงเป็นอนุกรม
var
Doc: TPDFlib;
Output: TFileStream;
begin
Doc := TPDFlib.Create;
try
if Doc.LoadFromFile('archive.pdf', '') <= 0 then
Exit;
// ... การแก้ไขข้อมูลที่สมควรแก่การเขียนใหม่ทั้งหมด ...
Output := TFileStream.Create('archive-rewritten.pdf', fmCreate);
try
if Doc.SaveToStream(Output) = 0 then
Writeln('การบันทึกล้มเหลว ข้อผิดพลาด ', Doc.LastErrorCode);
finally
Output.Free;
end;
finally
Doc.Free;
end;
end;
บทเรียนเรื่องโหมดการแบ่งปัน: เมื่อ AppendToFile ส่งกลับค่า 0
มีปัญหาการถดถอย (regression) อย่างหนึ่งในส่วนนี้ที่คุ้มค่าแก่การนำมาเล่าซ้ำเพราะรูปแบบความล้มเหลวนี้สามารถนำไปปรับใช้ทั่วไปได้ ฟังก์ชัน AppendToFile(FileName) จะแนบการอัปเดตแบบเพิ่มหน่วยลงใน PDF ที่มีอยู่บนดิสก์โดยตรง — ซึ่งเป็นการเรียกใช้ตามปกติสำหรับเวิร์กโฟลว์การตรวจสอบในตำแหน่งเดิม: โหลดไฟล์ ทำการแก้ไข แล้วแนบการเปลี่ยนแปลงลงในเส้นทางเดิม ในเวอร์ชัน v3.71.2 ลำดับขั้นตอนดังกล่าวเริ่มส่งกลับค่า 0 สาเหตุหลักอยู่ที่ตัวโหลดไม่ใช่ตัวเขียน: เพื่อรองรับการอ่านเอกสารขนาดใหญ่แบบออนดีมานด์ ฟังก์ชัน LoadFromFile จะต้องเปิดตัวจัดการไฟล์ต้นฉบับ (file handle) ค้างไว้ตลอดอายุการทำงานของออบเจกต์เอกสาร และตัวจัดการนั้นถูกเปิดด้วยโหมด fmShareDenyWrite เมื่อ AppendToFile พยายามเปิดไฟล์เดิมอีกครั้งเพื่อเขียนข้อมูล โหมดการแบ่งปันของตัวโหลดเองจึงปฏิเสธคำขอนั้น และ API ก็ล้มเหลวก่อนที่จะได้เขียนข้อมูลลงไปแม้แต่ไบต์เดียว
การแก้ไขคือการปรับลดความเข้มงวดของโหมดการแบ่งปันของตัวโหลดลงเป็น fmShareDenyNone ซึ่งปลอดภัยอย่างยิ่งเนื่องจากพฤติกรรมของการแนบต่อท้ายแบบเพิ่มหน่วย: มันจะเขียนข้อมูลต่อท้ายหลังจุดสิ้นสุดของไฟล์เท่านั้น และจะไม่ทำการเขียนทับในพื้นที่ที่ตัวจัดการอ่านข้อมูลระยะยาวกำลังให้บริการอยู่ บทเรียนทั่วไปสำหรับผู้ที่เขียนครอบไลบรารีนี้ — หรือสร้างระบบโหลดสตรีมมิ่งที่คล้ายกัน — คือผู้อ่านข้อมูลที่ต้องเปิดตัวจัดการไฟล์ค้างไว้และตัวเขียนไฟล์เดียวกันนั้นมีความขัดแย้งกัน และโหมดการแบ่งปันที่คุณเลือก ณ เวลาเปิดไฟล์ถือเป็นข้อตกลงของ API ไม่ใช่รายละเอียดทางเทคนิคในการติดตั้ง หากฟังก์ชัน AppendToFile ส่งกลับค่า 0 ในรหัสของคุณ ให้ตรวจสอบก่อนว่ามีสิ่งอื่นใดในโปรเซสของคุณที่ยังคงถือไฟล์เป้าหมายนั้นด้วยโหมดการแบ่งปันที่จำกัดสิทธิ์อยู่หรือไม่
ต้นทุนที่แท้จริง: เมื่อการอัปเดตแบบเพิ่มหน่วยเป็นเครื่องมือที่ไม่ถูกต้อง
การอัปเดตแบบเพิ่มหน่วยเป็นการแลกเปลี่ยนขนาดไฟล์กับประสิทธิภาพการเขียน และการแลกเปลี่ยนนี้อาจไม่ได้ให้ผลดีเสมอไป ทุกๆ เวอร์ชันที่ปรับปรุงจะแนบออบเจกต์ที่แก้ไขเพิ่มเข้ามา ในขณะที่คำจำกัดความที่ถูกเขียนทับไปยังคงค้างอยู่ในไฟล์ ดังนั้นเอกสารที่ถูกแก้ไขหลายร้อยครั้งจะสะสมออบเจกต์ที่เสียเปล่าและสร้างห่วงโซ่ /Prev ที่ยาวเหยียดซึ่งตัวอ่านทุกตัวจะต้องไล่ดู ที่แย่กว่านั้นคือ เนื้อหาที่ "ถูกลบ" ไม่ได้หายไปไหน: ข้อความที่ถูกนำออกไปในเวอร์ชันที่ห้ายังคงปรากฏอยู่ในไบต์ของเวอร์ชันที่สี่ และสามารถกู้คืนได้โดยใครก็ตามที่ทำการตัดทอนไฟล์ ดังนั้น การเซนเซอร์ (redaction) การชำระข้อมูล (sanitization) หรือการลบเนื้อหาที่มีความละเอียดอ่อนใดๆ จึงจำเป็นต้องใช้วิธีบันทึกแบบเขียนใหม่ทั้งหมด — การบันทึกแบบเพิ่มหน่วยสำหรับงานเซนเซอร์ข้อมูลคือการทำให้ข้อมูลรั่วไหลในอีกรูปแบบหนึ่ง
การบันทึกแบบเต็มไฟล์ยังเป็นทางเลือกที่ถูกต้องเมื่อเป้าหมายคือการบีบอัดไฟล์ (การกำจัดส่วนเพิ่มที่สะสมไว้และออบเจกต์ที่ไม่ได้ใช้งาน) เมื่อมีการเปลี่ยนแปลงคุณสมบัติโดยรวม of เอกสาร เช่น การเข้ารหัสไฟล์ — เนื่องจากการเข้ารหัสใหม่จะกระทบกับสตริงและสตรีมทุกตัว จึงไม่มีส่วนใดที่เป็น "แบบเพิ่มหน่วย" อีกต่อไป — หรือเมื่อต้องการสร้างผลงานส่งมอบที่สะอาดเรียบร้อยโดยไม่มีประวัติการแก้ไขติดไปกับไฟล์ กฎที่เหมาะสมคือ: ใช้ AppendToStream หรือ AppendToFile ในขณะที่เอกสารยังคงมีการใช้งานและแก้ไข โดยเฉพาะเมื่อเอกสารนั้นมีลายเซ็นกำกับอยู่ และใช้การเขียนใหม่ทั้งหมดด้วย SaveToStream เมื่อสิ้นสุดวงจรการแก้ไข เมื่อเอกสารถูกนำออกจากระบบของคุณ หรือเมื่อต้องการทำให้ประวัติทั้งหมดของไฟล์ราบเรียบ
การอัปเดตแบบเพิ่มหน่วย การส่งออกเดลต้าออฟเซ็ตเสมือน และการแปลงเป็นอนุกรมส่งตรงไปยังสตรีม ล้วนเป็นส่วนหนึ่งของมาตรฐาน losLab PDF Library สำหรับ Delphi, C# และ VB.NET หน้าผลิตภัณฑ์ได้รวบรวมรายการสำหรับบันทึกและแนบข้อมูลผ่าน API ไว้อย่างครบถ้วน ควบคู่ไปกับฟีเจอร์การลงลายเซ็นและไฟล์ขนาดใหญ่ที่กล่าวถึงข้างต้น