คุณต้องการพจนานุกรมตัวเดียวจาก PDF ขนาด 2 GB แต่เครื่องมือกลับขยายตาราง cross-reference ทั้งหมดเป็นอาร์เรย์ที่ขนาดถูกกำหนดด้วย trailer /Size ก่อน PDFiumPas แทนขั้นตอนนั้นด้วยดัชนีออบเจกต์แบบ sparse lazy: เก็บเฉพาะ descriptor ของส่วน xref resolve เลขออบเจกต์เดียวตามต้องการผ่านหน้าต่างที่มีขอบเขต และแคชเฉพาะรายการที่คุณแตะจริง
รูปทรงเดิมของโค้ดนี้ใน FPdfCompress ซื่อสัตย์แต่แพง ApplyDefaultOpenAction อ่านไฟล์ทั้งไฟล์เข้า TBytes หนึ่งตัว แล้วจองอาร์เรย์ TPdfActiveXrefEntries หนาแน่นที่มีช่องหนึ่งช่องต่อเลขออบเจกต์ถึง /Size สองเรื่องพังตอนขยายสเกล ต้นทุนการอ่านโตเชิงเส้นตามขนาดเอกสารแม้ผู้เรียกต้องการแค่สี่พจนานุกรม และอาร์เรย์หนาแน่นชนกับงบของ parser: TPdfParserResourceBudget.Default ตั้ง MaxObjects เป็น 4,000,000 ไฟล์ที่ถูกต้องสมบูรณ์ที่เลขออบเจกต์สูงสุดเกินเพดานนั้นจึงถูกปฏิเสธด้วยเหตุผลหน่วยความจำ ไม่ใช่เหตุผลความถูกต้อง
ทำไม API สาธารณะของ PDFium จึงตอบคำถามนี้ไม่ได้?
เพราะข้อมูลนั้นมีอยู่ข้างใน PDFium แต่ไม่เคยข้ามพรมแดน C CPDF_Parser ดูแลตาราง cross-reference การเป็นสมาชิกของ object stream กับลำดับความสำคัญของ revision ไว้ภายใน ทว่า header ที่เผยแพร่ไม่มีจุดเรียกใดที่รับเลขออบเจกต์แล้วคืน offset ดิบ generation revision ที่ชนะ หรือ ObjStm ที่มันอาศัยอยู่ ฝั่งเซฟก็ปิดเท่ากัน: FPDF_SaveAsCopy กับ FPDF_SaveWithVersion ยัด callback เขียนแบบลำดับให้คุณเท่านั้น การแพตช์ระดับ byte ใด ๆ ต่อ catalogue หลังเซฟแบบ native จึงต้องถูกสร้างในชั้น Pascal นี่คือเหตุที่ PDFiumPas parse โครงสร้างเหล่านี้เอง แทนการใช้ซ้ำจาก DLL
ดัชนี sparse เก็บอะไรในหน่วยความจำกันแน่?
Descriptor ไม่ใช่รายการ สำหรับตารางแบบคลาสสิก (ISO 32000-1 §7.5.4) TPdfSparseXrefSubsection เก็บเลขออบเจกต์แรก จำนวนออบเจกต์ byte offset ที่แถวรายการเริ่ม และความกว้างรายการที่วัดมาไว้ ตัวรายการเองยังอยู่ในไฟล์ ความกว้างถูกวัดจากแถวแรก แทนการสมมติว่าเป็น 20 byte เพราะผู้ผลิตไม่เห็นด้วยเรื่องการจบบรรทัด; PDFiumPas ยอมรับ 18 ถึง 64 และปฏิเสธทุกค่านอกแถบนั้น พร้อม subsection ใด ๆ ที่จำนวนประกาศไว้วิ่งทะลุปลาย stream สำหรับ cross-reference stream (§7.5.8) ส่วนนั้นเก็บความกว้างสามฟิลด์ของ /W แต่ละฟิลด์ถูกจำกัดที่ 0 ถึง 8 คู่ /Index ที่ถูกแบน และ byte รายการที่ถอดรหัสแล้ว ซึ่งความยาวที่คาดหวังถูกคำนวณจาก /W กับ /Index ก่อน byte หนึ่ง byte ใดถูกขยาย
ดัชนีทั้งชุดถูกสร้างโดย Initialize จากหน้าต่างท้ายไฟล์ไม่เกิน 1 MiB ซึ่งเป็นจุดที่พบ startxref และการอ่านออบเจกต์ทุกครั้งถัดไปใช้หน้าต่างออบเจกต์ 1 MiB เพดาน stream ดิบคือ 64 MiB และบรรทัด xref หนึ่งบรรทัดห้ามเกิน 1024 byte ถ้าคุณเคยอ่านบันทึกของเราเรื่อง การตรวจสอบ object กับ cross-reference stream ด้วย PDFiumPas วินัยเรื่องความกว้างฟิลด์ชุดเดียวกันใช้ที่นี่เช่นกัน เพียงแต่ตอนนี้มันถูกใช้ระบุรายการหนึ่งรายการ แทนการตรวจสอบตารางทั้งตาราง
uses
FPdfCompress;
var
Source: TFileStream;
Revision: TPdfSparseRevisionInfo;
begin
Source := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
{ เดินเฉพาะ startxref, ห่วงโซ่ /Prev กับ catalogue }
if ReadPdfSparseRevisionInfo(Source, Revision) then
begin
Writeln('root ', Revision.RootObjectNumber, ' ',
Revision.RootGeneration);
Writeln('max obj ', Revision.MaximumObjectNumber);
Writeln('xref str ', Revision.UsesXrefStream);
Writeln('encrypted ', Revision.HasEncrypt);
Writeln(string(Revision.CatalogDictionary));
end;
finally
Source.Free;
end;
end;
การ lookup หนึ่งครั้งเข้าถึงออบเจกต์หนึ่งตัวอย่างไร?
ด้วยเลขคณิต ในทั้งสอง layout subsection แบบคลาสสิกมีแถวกว้างคงที่ ที่อยู่ของรายการจึงคือจุดเริ่ม subsection บวก offset ออบเจกต์คูณความกว้างที่วัดไว้; PDFiumPas แล้วอ่านบรรทัดเดียวนั้น parse offset สิบหลักกับ generation ห้าหลัก เช็ค generation กับเพดาน 65535 จาก §7.5.4 และจัดหมวดคำสุดท้ายเป็น axkDirect หรือ axkFree cross-reference stream ต้องการอีกหนึ่งขั้น เพราะ subsection ของ /Index ถูกต่อกันในลำดับ byte ที่ถอดรหัสแล้ว ดัชนีจึงรวมจำนวนของ subsection ก่อนหน้าก่อนคูณด้วยความกว้าง /W ที่รวมกัน type 1 ให้ offset type 2 ให้เลข object stream กับ index สมาชิก และอย่างอื่นกลายเป็น axkUnknown แทนการเดา
{ ตารางคลาสสิก ISO 32000-1 มาตรา 7.5.4 }
EntryOffset := Subsection.EntryOffset +
Int64(ObjectNumber - Subsection.FirstObject) * Subsection.EntryWidth;
{ cross-reference stream ISO 32000-1 มาตรา 7.5.8 }
EntryWidth := Section.Widths[0] + Section.Widths[1] + Section.Widths[2];
EntryPosition := Integer((PriorCount + ObjectNumber -
Section.IndexValues[I]) * EntryWidth);
ไม่มีอะไรในทั้งสองเส้นทางแปรผันตาม /Size นั่นคือจุดหมายทั้งหมดของการเขียนใหม่ ค่าขนาดใน trailer ถูกพาไปต่อในฐานะ metadata และถูกใช้ตอนเขียน revision แบบเพิ่มหน่วย แต่ไม่เคยขับเคลื่อนการจองหน่วยความจำ suite ทดสอบ regression ตะหนักจุดนี้ด้วย fixture ที่ page tree อยู่ที่ออบเจกต์ 1,000,000,000 กับ 1,000,000,001 ภายใต้ trailer ประกาศ /Size 1000000002 โครงสร้างหนาแน่นรุ่นเก่าปฏิเสธไฟล์นั้น; ดัชนี sparse resolve reference ทั้งสองและเก็บขนาดที่ประกาศไว้ใน trailer ของผลลัพธ์
Revision แบบ hybrid ห่วงโซ่ /Prev กับกล่องป้องกันรอบตัวมัน
ลำดับความสำคัญของ revision คือจุดที่ดัชนี lazy แบบไร้ประสบการณ์พลาด PDFiumPas เดินห่วงโซ่จาก startxref โดยใหม่สุดมาก่อน และหยุด lookup ที่ส่วนแรกที่ตอบได้ ซึ่งสร้างกฎลำดับความสำคัญขึ้นมาโดยไม่ต้องยัดตารางรวมเข้าหน่วยความจำ ไฟล์ hybrid-reference (§7.5.8.4) ถูกจัดการภายในกิ่งคลาสสิก: เมื่อ trailer แบก /XRefStm ส่วน stream เสริมถูกลงทะเบียนก่อนส่วนคลาสสิกที่อ้างมัน ออบเจกต์บีบอัดที่ตารางธรรมดามองไม่เห็นจึงยังถูกพบ ขณะที่รายการคลาสสิกคงสถานะเดิม revision เก่ากว่าถูกตามต่อผ่าน /Prev
กล่องป้องกันสองตัวจำกัดการเดินนั้น และทั้งคู่สำคัญบนไฟล์ที่เสียหาย ทุก offset ที่เยี่ยมถูกบันทึก /Prev ที่ชี้ย้อนเข้าห่วงโซ่จึงจบการทำงานแทนการหมุนติด และความลึกการไต่ถูกตัดที่ MaxRecursionDepth ซึ่ง default เป็น 1024 flag การเข้ารหัสถูกรวมตลอดห่วงโซ่ แทนการอ่านจาก trailer ใหม่สุดตัวเดียว เพราะเอกสารที่ trailer ล่าสุดละ /Encrypt อาจยังถูกเข้ารหัสอยู่ที่ถอยไปข้างหลัง ผู้เรียกที่เติม revision ต่อท้ายพึ่ง flag นั้นเพื่อปฏิเสธการเขียนออบเจกต์ plaintext ลงไฟล์ที่ถูกเข้ารหัส
รายการ type-2: ทำไม object stream ถึงรอ
รายการ type-2 เรียกชื่อ object stream และ PDFiumPas ไม่แตะ stream นั้นจนกว่าผู้เรียกขอสมาชิกของมัน เมื่อถึงตอนนั้นจริง ๆ /Type /ObjStm ถูกตรวจ /N ถูกเช็คกับงบออบเจกต์ /First กับเพดาน byte ที่ถอดรหัส และ /N ถูกเช็คความสมเหตุสมผลกับ /First เพราะคู่ header แต่ละคู่ต้องการอย่างน้อยสี่ byte เฉพาะหลังจากนั้น stream ถึงถูกขยาย และการกวาด header หยุดที่สมาชิกที่ถูกขอกับตัวถัดไป แทนการสร้างตารางสมาชิกเต็ม object stream ที่ถอดรหัสแล้วหนึ่งตัวถูกเก็บไว้ครั้งละตัว ซึ่งเป็นการแลกเปลี่ยนที่ถูกทางเมื่อกิ่งหนึ่งของ page tree รวมกลุ่มเป็น ObjStm เดียว บันทึกของเราเรื่อง การถอดรหัส object stream กับ predictor ใน Delphi เล่าว่าข้างในขั้น inflate นั้นมีอะไรเกิดขึ้น (§7.5.7)
var
Reader: TPdfSparseDictionaryReader;
Generation: Integer;
Dict: AnsiString;
begin
{ ดัชนีที่เก็บไว้หนึ่งตัว การอ่านหลายครั้งที่รู้เรื่อง generation }
Reader := TPdfSparseDictionaryReader.Create(Source);
try
if Reader.Valid and
Reader.ReadLatestDictionary(PageObjectNumber, Generation, Dict) then
HandlePage(PageObjectNumber, Generation, Dict);
finally
Reader.Free; { Source ยังเป็นของคุณต่อไป }
end;
end;
จุดที่แคชเลิกให้คำสัญญา
ดัชนีนี้เป็น snapshot และควรพูดให้เห็นกันจะ ๆ ส่วนถูก parse ครั้งเดียวใน Initialize; ถ้า stream พื้นฐานถูกแก้หลังจากนั้น รายการที่แคชไว้ทุกรายการล้าสมัย และคลาสจะไม่สังเกตเห็น TPdfSparseDictionaryReader เก็บดัชนีไว้ตลอดชีวิตของแหล่งข้อมูลที่ผู้เรียกเป็นเจ้าของ ซึ่งเป็นสิ่งที่การไต่แบบเวียนเกิดเหนือ page tree ต้องการพอดี และเป็นสิ่งที่ห้ามทำข้ามการเขียนใหม่ แคชรายการเป็นอาร์เรย์แบนที่ค้นแบบเชิงเส้น และมันเก็บผลลบด้วย การ lookup ไม่กี่ร้อยครั้งจึงถูก แต่ไม่กี่แสนครั้งไม่ถูก ReadDictionary เรียกการจับคู่ generation เป๊ะ ๆ ขณะที่ ReadLatestDictionary resolve ตัวที่ทำงานอยู่ ความต่างนี้ตั้งใจ: การ resolve reference ต้องการแบบแรก การตรวจ catalogue ต้องการแบบหลัง ที่ที่ขอบเขตเหล่านี้ให้เกียรติไม่ได้ หน่วยรอบ ๆ ตกไปใช้ parser ทั้งไฟล์รุ่นเดิม แทนการลดชุดไฟล์ที่ยังใช้งานได้ ซึ่งเป็นแบบแผนเดียวกับที่เราใช้กับ การ stream PDF ขนาดใหญ่ตามต้องการ
regression ข้ามคอมไพเลอร์ครอบพฤติกรรมชุดเดียวกันบนเครื่องมือทั้งสามชุด รวมการยืนยันว่าแหล่ง 2 MiB ไม่เคยเห็นการอ่านเดียวที่ใหญ่กว่า 1 MiB ถ้าคุณดูแลโค้ด Delphi, C++Builder หรือ Lazarus ที่จับโครงสร้าง PDF ตรง ๆ และเบื่อจ่ายต้นทุน parse ทั้งไฟล์เพื่อสี่พจนานุกรม ดัชนี sparse กับรอยต่อสาธารณะรอบมันมาพร้อม PDFiumPas Delphi PDFium component