เมื่อส่งออกเอกสารจาก Microsoft Word หรือ Excel ด้วยตัวเลือก บันทึกเป็น PDF ไฟล์ที่ได้มักจะเป็นไฟล์ประเภท hybrid-reference ซึ่งเก็บข้อมูล cross-reference ไว้สองครั้ง: ครั้งแรกเป็นตารางความกว้างคงที่แบบคลาสสิกที่อยู่ท้ายไฟล์ PDF ทุกไฟล์จนถึงเวอร์ชัน 1.4 และอีกครั้งเป็นสตรีม cross-reference ที่ถูกบีบอัดซึ่งส่วนใหญ่ของเอกสารต้องพึ่งพา คีย์ส่วนท้ายเดียวคือ /XRefStm เป็นตัวเชื่อมทั้งสองมุมมองเข้าด้วยกัน และการที่เครื่องมือจะมองเห็นเอกสารทั้งหมดหรือไม่นั้นขึ้นอยู่กับว่ามันตามคีย์นั้นไปหรือไม่
บทความนี้จะพิจารณาไฟล์แบบไฮบริดจากฝั่งผู้บริโภค (consuming side): ไบต์ที่ท้ายไฟล์มีลักษณะอย่างไร มุมมองทั้งสองแตกต่างกันอย่างไรเมื่อมีการแก้ไข และไพป์ไลน์ Delphi จะตรวจจับและกำหนดเส้นทางอินพุตไฮบริดได้อย่างไร วิธีที่ตัวโหลดผสานมุมมอง และเหตุใดลำดับจึงไม่สามารถต่อรองได้ เป็นหัวข้อของ บทความ HotPDF ของเราเกี่ยวกับการโหลดไฟล์ hybrid-reference ส่วนบทความนี้จะมุ่งเน้นไปที่การจดจำเค้าโครงในเบื้องต้น
เหตุใดการส่งออกจาก Office จึงเขียนดัชนีสองครั้ง
PDF 1.5 นำเสนอสองฟีเจอร์ที่เปลี่ยนรูปแบบของไฟล์: cross-reference stream ซึ่งเก็บดัชนีอ็อบเจ็กต์เป็นข้อมูลไบนารีที่ถูกบีบอัดแทนที่จะเป็นตารางข้อความธรรมดา และ object stream ซึ่งบรรจุอ็อบเจ็กต์ขนาดเล็กจำนวนมากลงในคอนเทนเนอร์เดียวที่บีบอัดด้วย Flate ตัวเขียนที่ใช้ฟีเจอร์เหล่านี้จะสร้างไฟล์ที่มีขนาดเล็กกว่า แต่โปรแกรมอ่าน PDF 1.4 จะไม่สามารถเปิดไฟล์ผลลัพธ์ได้ เนื่องจากโครงสร้างหลักที่มันอ้างอิงคือคำหลัก xref และพจนานุกรม trailer ได้หายไป
ISO 32000-1 §7.5.8.4 กำหนดข้อตกลงประนีประนอมเอาไว้ ไฟล์ hybrid-reference จะเขียนข้อมูลทั้งสองอย่าง: ตาราง cross-reference แบบคลาสสิกที่ระบุที่อยู่ของอ็อบเจ็กต์ซึ่งโปรแกรมอ่านรุ่นเก่าจำเป็นต้องเข้าถึง (เช่น แค็ตตาล็อกและแผนผังหน้า) และสตรีม cross-reference ที่ทำดัชนีสำหรับทุกอย่างที่เหลือ อ็อบเจ็กต์ที่ถูกพับเก็บไว้ใน object stream จะถูกทำเครื่องหมายว่าว่าง (free) ในตารางแบบคลาสสิก ดังนั้นโปรแกรมอ่าน 1.4 จะข้ามอ็อบเจ็กต์เหล่านี้ไปโดยไม่มีปัญหา ตำแหน่งที่แท้จริงของอ็อบเจ็กต์เหล่านี้จะอยู่ในสตรีมเท่านั้น จากนั้นส่วนท้าย (trailer) แบบคลาสสิกจะมีคีย์ /XRefStm ซึ่งเก็บ byte offset ของสตรีมนั้น โปรแกรมดูไฟล์รุ่นเก่าจะไม่เคยอ่านคีย์นี้และจะเรนเดอร์ไฟล์จากมุมมองของตาราง โปรแกรมดูไฟล์สมัยใหม่จะตามคีย์นี้ไปและเห็นเอกสารที่สมบูรณ์ Word และ Excel ส่งออกเค้าโครงในรูปแบบนี้มานานหลายปีแล้ว นี่จึงเป็นเหตุผลว่าทำไมไฟล์แบบไฮบริดจึงไม่ใช่กรณีศึกษาที่แปลกประหลาด แต่เป็นส่วนใหญ่ของสิ่งที่ไพป์ไลน์ธุรกิจได้รับ
ส่วนท้ายของไฟล์แบบไฮบริดมีลักษณะอย่างไร
เค้าโครงเป็นสิ่งที่เข้าใจง่ายที่สุดเมื่อดูจากไบต์ นี่คือส่วนท้ายของไฟล์ไฮบริดขนาดเล็ก โดยมีการย่อ offset ลง ในการส่งออกจาก Office จริง ค่า /XRefStm มักจะเป็น offset ขนาดใหญ่ใกล้กับส่วนท้ายของไฟล์ ลำดับการอ่านจะเป็นการเดินอ่านจากส่วนท้าย (tail-first walk) ตามที่อธิบายไว้ใน ภาพรวมของโครงสร้างไฟล์ PDF ของเรา: ค้นหา %%EOF, อ่าน startxref, และข้ามไปที่ตาราง
% ... body objects, including object streams and, at byte 116,
% the cross-reference stream (a stream object with /Type /XRef) ...
xref % classic section: what startxref points at
0 4
0000000000 65535 f % slot 0: head of the free list, always present
0000000017 00000 n % object 1: the catalog, visible to any reader
0000000000 65535 f % object 2: marked free -- lives in an object stream
0000000000 65535 f % object 3: same; only the stream view locates it
trailer
<<
/Size 4
/Root 1 0 R
/XRefStm 116 % byte offset of the cross-reference stream
>>
startxref
7164 % byte offset of the 'xref' keyword above
%%EOF
รายละเอียดสองอย่างในข้อมูลดัมพ์นี้อธิบายกลไกทั้งหมด ประการแรก startxref ชี้ไปที่ส่วนแบบคลาสสิกโดยตั้งใจ ซึ่งนั่นคือที่อยู่ที่โปรแกรมอ่านรุ่นเก่าจะต้องเข้าไปอ่าน สตรีม cross-reference สามารถเข้าถึงได้ผ่านคีย์ /XRefStm ที่อยู่ภายในพจนานุกรม trailer เท่านั้น ดังนั้น parser ที่ไม่เคยค้นหาคีย์นี้ก็จะไม่เคยรู้ว่ามีสตรีมนั้นอยู่ ประการที่สอง อ็อบเจ็กต์ 2 และ 3 เป็นเรื่องโกหกที่ไม่มีพิษมีภัย ตารางแบบคลาสสิกประกาศว่าพวกมันว่าง แต่ความจริงแล้วพวกมันเป็นอ็อบเจ็กต์ที่อยู่ในคอนเทนเนอร์ที่ถูกบีบอัด การทำเครื่องหมายว่าว่างนี้เป็นสิ่งที่ป้องกันไม่ให้โปรแกรมอ่าน 1.4 สะดุดกับรายการที่มันไม่สามารถใช้งานได้ ผู้บริโภคที่เชื่อมุมมองแบบคลาสสิกเพียงอย่างเดียวจะสรุปว่าส่วนใหญ่ของเอกสารนี้ไม่มีอยู่จริง
มุมมองทั้งสองแตกต่างกันอย่างไร
ไฟล์แบบไฮบริดที่ส่งออกจาก Word ใหม่ๆ จะมีความสอดคล้องกันภายใน: มุมมองทั้งสองจะอธิบายเอกสารเดียวกัน โดยแต่ละมุมมองจะอยู่ในขอบเขตที่ประกาศไว้ ปัญหาจะเริ่มต้นขึ้นเมื่อมีการแก้ไขไฟล์ด้วยเครื่องมือที่เข้าใจมุมมองใดมุมมองหนึ่งเท่านั้น พิจารณาเครื่องมือประทับตราที่ต่อท้ายการอัปเดตแบบ incremental สไตล์คลาสสิก: มีอ็อบเจ็กต์ใหม่, ส่วน xref ใหม่, สายเชื่อมโยง /Prev ไปยังส่วนก่อนหน้า, และ trailer ใหม่ หาก trailer นั้นทิ้งคีย์ /XRefStm ไป สตรีมวิวจะกลายเป็นสตรีมกำพร้า หากมีการคัดลอกค่าเก่าไปข้างหน้า สตรีมวิวจะยังคงอธิบายเอกสารในแบบที่เป็นก่อนการแก้ไข ไม่ว่าจะเป็นกรณีใด ดัชนีทั้งสองในตอนนี้จะไม่ตรงกันเกี่ยวกับสิ่งที่ไฟล์มีอยู่
ไฟล์ผลลัพธ์จะมีรูปแบบความล้มเหลวที่แตกต่างกัน: อ็อบเจ็กต์ที่มองเห็นในมุมมองหนึ่งจะหายไปหรือเก่าไปในอีกมุมมองหนึ่ง โปรแกรมอ่านที่แก้ไขข้อมูลผ่านสตรีมวิวจะพบเวอร์ชันก่อนแก้ไขของอ็อบเจ็กต์ที่อัปเดตแล้ว หรืออาจไม่พบรายการใดๆ สำหรับอ็อบเจ็กต์ที่ต่อท้าย โปรแกรมอ่านที่พึ่งพามุมมองของตารางจะเห็นการแก้ไขแต่จะไม่สามารถติดตามอ็อบเจ็กต์ที่ถูกบีบอัดซึ่งสตรีมสามารถระบุตำแหน่งได้เพียงอย่างเดียว ในทางปฏิบัติ ปัญหานี้จะปรากฏให้เห็นในรูปแบบของฟิลด์ฟอร์มที่ยังคงอยู่ในโปรแกรมดูไฟล์หนึ่งและหายไปในอีกโปรแกรมหนึ่ง คำอธิบายประกอบที่ดูเหมือนว่ากระบวนการประทับตราถูกลบไป หรือการค้นหาที่นำไปสู่อ็อบเจ็กต์ที่ผิดพลาดโดยสิ้นเชิง
สิ่งที่ทำให้การดีบักไฟล์เหล่านี้มีราคาแพงคือ Adobe Acrobat มักจะเปิดไฟล์เหล่านั้นโดยไม่มีการร้องเรียน: เมื่อดัชนีไม่ตรงกับไบต์ มันจะสร้างข้อมูล cross-reference ขึ้นมาใหม่เงียบๆ โดยการสแกนหาส่วนหัวของอ็อบเจ็กต์ ดังนั้นใครก็ตามที่สร้างไฟล์เสียจะไม่เห็นความผิดปกติ ความล้มเหลวจะปรากฏในภายหลัง เมื่อไฟล์ถูกส่งไปยังผู้บริโภคที่เข้มงวด โปรแกรมตรวจสอบก่อนการพิมพ์ บริการลงลายมือชื่อ หรือการนำเข้าที่เก็บถาวร ซึ่งเชื่อในโครงสร้างที่ประกาศไว้ และรายงานว่ามีอ็อบเจ็กต์สูญหายหรือ cross-reference ไม่ตรงกัน ประโยคที่ว่า "มันเปิดได้ปกติใน Acrobat" มักจะเป็นจุดเริ่มต้นของตั๋วแจ้งปัญหาแบบ hybrid desynchronization เกือบทุกใบ
การตรวจจับไฟล์แบบไฮบริดใน Delphi
การจำแนกประเภทอินพุตไม่จำเป็นต้องใช้ไลบรารี PDF คีย์ /XRefStm สามารถเกิดขึ้นภายในพจนานุกรม trailer แบบคลาสสิกเท่านั้น และ trailer ที่ใช้งานอยู่จะอยู่ภายในไม่กี่กิโลไบต์สุดท้ายของไฟล์ เนื่องจากข้อกำหนดระบุให้ %%EOF ปรากฏใกล้กับส่วนท้ายทางกายภาพ การอ่านและค้นหาหน้าต่างที่มีขอบเขตเฉพาะส่วนท้ายก็เพียงพอแล้วสำหรับการคัดกรองเบื้องต้น:
uses
System.SysUtils, System.Classes, System.StrUtils, System.Math;
function IsHybridReferencePdf(const FileName: string): Boolean;
const
TailWindow = 2048;
var
Stream: TFileStream;
Buf: TBytes;
Tail: string;
Len, TrailerPos, NextPos, KeyPos, StartXrefPos: Integer;
begin
Result := False;
Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
if Stream.Size < 48 then
Exit;
Len := Min(TailWindow, Integer(Stream.Size));
SetLength(Buf, Len);
Stream.Position := Stream.Size - Len;
Stream.ReadBuffer(Buf[0], Len);
finally
Stream.Free;
end;
// Every keyword involved is 7-bit ASCII, so a byte-wise decode is safe
Tail := TEncoding.ANSI.GetString(Buf);
// Find the LAST 'trailer' keyword: with incremental updates,
// the newest trailer is the one that governs the file
TrailerPos := 0;
NextPos := Pos('trailer', Tail);
while NextPos > 0 do
begin
TrailerPos := NextPos;
NextPos := PosEx('trailer', Tail, NextPos + 1);
end;
if TrailerPos = 0 then
Exit; // no classic trailer: a pure xref-stream file, not hybrid
// A hybrid trailer carries /XRefStm between 'trailer' and 'startxref'
KeyPos := PosEx('/XRefStm', Tail, TrailerPos);
StartXrefPos := PosEx('startxref', Tail, TrailerPos);
Result := (KeyPos > 0) and
((StartXrefPos = 0) or (KeyPos < StartXrefPos));
end;
ผลลัพธ์ทั้งสามสอดคล้องกับเค้าโครงทั้งสามรูปแบบ ไฟล์ที่มีเฉพาะแบบคลาสสิกจะมี trailer แต่ไม่มี /XRefStm: ให้ค่า False ไฟล์ที่ใช้งาน cross-reference stream อย่างเต็มรูปแบบจะไม่มีคำหลัก trailer เลย โดยคีย์ trailer ของมันจะอยู่ในพจนานุกรมสตรีม: ให้ค่า False เช่นกัน ซึ่งก็ถูกต้อง เพราะไฟล์ดังกล่าวถูกบีบอัด ไม่ใช่แบบไฮบริด มีเพียงเค้าโครงที่มีดัชนีสองชุดเท่านั้นที่จะคืนค่า True
สำหรับการใช้งานจริง การปรับปรุงความปลอดภัยสองประการนั้นคุ้มค่าที่จะเพิ่มจำนวนบรรทัดของโค้ด: การแยกวิเคราะห์จำนวนเต็มที่อยู่หลัง /XRefStm, ค้นหาที่ offset นั้น และยืนยันว่ามีอ็อบเจ็กต์สตรีมประเภท /Type /XRef อยู่ที่นั่นจริงๆ; ไฟล์ที่ถูกตัดปลายอาจมีคีย์นั้นในขณะที่สตรีมหายไป ซึ่งจัดอยู่ในกลุ่มที่แตกต่างจากไฟล์ไฮบริดที่สมบูรณ์ และจัดการกับขนาดหน้าต่างเป็นพารามิเตอร์: ขนาด 2 KB ครอบคลุมไฟล์ทั่วไปที่ออกจาก Office แต่พจนานุกรม trailer ที่มีขนาดใหญ่ผิดปกติอาจทำให้คำหลักหลุดออกจากช่วง และการขยายหน้าต่างนั้นดีกว่าการประกาศผิดพลาดว่าเป็นไฟล์แบบคลาสสิกโดยบังเอิญ
การกำหนดเส้นทางไฟล์แบบไฮบริดผ่านไพป์ไลน์ Delphi
การตรวจจับช่วยให้คุณสามารถตัดสินใจเกี่ยวกับการกำหนดเส้นทาง สำหรับไฟล์ที่ใช้อ่าน เรนเดอร์ หรือตรวจสอบความถูกต้องเพียงอย่างเดียว ให้ใช้โปรแกรมโหลดที่รวมมุมมองทั้งสอง จากนั้นตรวจสอบพฤติกรรมแทนที่จะตรวจสอบไบต์ คอมโพเนนต์ PDFium จะทำการวิเคราะห์เชน /XRefStm ในระหว่างการโหลด ดังนั้นตารางอ็อบเจ็กต์ที่โค้ดของคุณมองเห็นจะเป็นแบบที่ผสานกันแล้ว และการตรวจสอบที่ได้อธิบายไว้ใน บทความของเราเกี่ยวกับการตรวจสอบอ็อบเจ็กต์และ cross-reference stream สามารถนำมาใช้ได้โดยไม่มีการเปลี่ยนแปลง หากไฟล์ไฮบริดที่ desynchronized ได้รับความเสียหายอย่างรุนแรงจนปฏิเสธการโหลด เอนจินจะรายงานผ่านชุดข้อผิดพลาดของมัน เช่น FPDF_ERR_SUCCESS, FPDF_ERR_UNKNOWN, FPDF_ERR_FILE, FPDF_ERR_FORMAT, FPDF_ERR_PASSWORD, FPDF_ERR_SECURITY และ FPDF_ERR_PAGE โดยที่ FPDF_ERR_FORMAT เป็นข้อผิดพลาดที่เกิดจากความเสียหายทางโครงสร้าง อย่างไรก็ตาม อย่าพึ่งพาสัญญาณนั้นมากนัก: PDFium ถูกออกแบบมาให้มีความยืดหยุ่นและสร้างไฟล์ที่ไม่สอดคล้องกันส่วนใหญ่ขึ้นใหม่โดยไม่แสดงข้อผิดพลาด ดังนั้นการโหลดที่สำเร็จจะพิสูจน์ว่าไฟล์นั้นสามารถกู้คืนได้ ไม่ได้แปลว่ามุมมองทั้งสองมีความสอดคล้องกัน การตรวจสอบความสอดคล้องที่มีความหมายคือการเปรียบเทียบสิ่งที่เจอจากการสำรวจอ็อบเจ็กต์ทั้งหมดกับสิ่งที่ /Size ของ trailer ประกาศไว้
สำหรับไฟล์ที่ไพป์ไลน์ของคุณจะทำการแก้ไข นโยบายที่ปลอดภัยที่สุดคือการหยุดไม่ให้ไฟล์เหล่านั้นเป็นไฮบริดเลย การโหลดแล้วตามด้วยการบันทึกข้อมูลทั้งหมดผ่าน HotPDF จะเขียนเอกสารใหม่ด้วย cross-reference แบบเดียวที่มีความสอดคล้องกันภายใน: ไม่มี /XRefStm, ไม่มีมุมมองที่สองที่จะทำให้เกิดความไม่สอดคล้องกัน (fall out of sync), อ็อบเจ็กต์แต่ละอันจะมีดัชนีเป็นเจ้าของเพียงรายการเดียว การทำ normalization รูปแบบนี้คือสิ่งที่คุณต้องการก่อนที่จะนำเข้าที่เก็บถาวร ก่อนกระบวนการ RIP ขั้นสุดท้ายที่เข้มงวด หรือบริการลงลายมือชื่อ และหลังจากที่มีการแก้ไขใดๆ กับอินพุตแบบไฮบริด มันทำงานได้ดีเนื่องจากตัวโหลดได้ผสานมุมมองอย่างถูกต้องในตอนนำเข้า ซึ่งเป็นกลไกที่บทความเกี่ยวกับ hybrid-reference ของ HotPDF ได้อธิบายไว้อย่างละเอียด
ไฟล์ประเภทหนึ่งที่ควรปล่อยไว้ตามเดิมคือเอกสารที่มีการลงลายมือชื่อดิจิทัล การเขียนไฟล์ใหม่ทั้งหมดจะเป็นการเคลื่อนย้ายทุกไบต์ ซึ่งจะทำให้ลายเซ็นที่คำนวณจากช่วงข้อมูลเดิมไม่ถูกต้อง การเปลี่ยนแปลงในไฟล์ไฮบริดที่มีการลงลายเซ็นแล้วจะต้องดำเนินการผ่านการอัปเดตแบบ incremental อย่างเหมาะสมซึ่งรักษามุมมองทั้งสองไว้ ไฟล์ที่จำเป็นต้องอ่านเพียงอย่างเดียวควรผ่านไปโดยไม่มีการแตะต้อง การทำ normalization มีไว้สำหรับไฟล์ที่คุณเป็นเจ้าของ ส่วนไฟล์ที่มีลายเซ็น คุณทำได้เพียงแค่การต่อท้ายเท่านั้น
ไฟล์ Hybrid-reference PDF ไม่ได้เป็นไฟล์ที่มีข้อบกพร่อง; พวกมันเป็นเพียงสะพานเชื่อมความเข้ากันได้ของรูปแบบไฟล์นั้นๆ และแอปพลิเคชัน Office จะยังคงสร้างไฟล์เหล่านี้ต่อไปตราบใดที่ยังคงมีโปรแกรมอ่าน PDF 1.4 อยู่ในฐานการติดตั้ง ไพป์ไลน์ที่สามารถมองเห็นคีย์ /XRefStm, ตรวจสอบเอกสารที่ผสานกันด้วย คอมโพเนนต์ PDFium, และสร้างผลลัพธ์แบบดัชนีเดียวที่สะอาดขึ้นมาใหม่ด้วย คอมโพเนนต์ HotPDF จะปฏิบัติต่อไฟล์เหล่านี้ตามที่มันควรจะเป็น: คือเป็นเพียงอินพุตธรรมดาที่มีป้ายบอกทางเพิ่มขึ้นมาหนึ่งอันในส่วนท้าย