Artikel Teknis

Indeks Objek PDF Sparse Lazy di Delphi dengan PDFiumPas

Anda hanya menginginkan satu kamus dari PDF 2 GB dan tool itu lebih dulu membentangkan seluruh tabel cross-reference menjadi array yang ukurannya ditentukan trailer /Size. PDFiumPas mengganti langkah itu dengan indeks objek sparse lazy: ia hanya menyimpan deskriptor seksi xref, me-resolve satu nomor objek sesuai permintaan melalui jendela berbatas, dan meng-cache hanya entri yang benar-benar Anda sentuh

Bentuk lama kode ini di FPdfCompress jujur tetapi mahal. ApplyDefaultOpenAction membaca berkas lengkap ke satu TBytes, lalu mengalokasikan array TPdfActiveXrefEntries padat dengan satu slot per nomor objek hingga /Size. Dua hal menjadi masalah pada skala besar. Biaya baca tumbuh linear dengan ukuran dokumen meski pemanggil hanya menginginkan empat kamus, dan array padat itu berbenturan dengan anggaran parser: TPdfParserResourceBudget.Default menyetel MaxObjects ke 4.000.000, sehingga berkas yang sepenuhnya valid dengan nomor objek tertingginya di atas plafon itu ditolak dengan argumen memori, bukan kebenaran

Indeks objek sparse lazy PDFiumPas di Delphi dibandingkan array cross-reference padat: jalur padat membaca seluruh berkas dan mengalokasikan satu slot per nomor objek hingga ukuran trailer, sedangkan jalur sparse hanya menyimpan deskriptor seksi
Hanya deskriptor yang tinggal di memori, entri tetap di berkas, dan setiap baca lewat satu jendela berbatas satu mebibyte

Mengapa API publik PDFium tidak menjawab pertanyaan ini?

Karena informasinya ada di dalam PDFium tetapi tidak pernah menyeberangi batas C. CPDF_Parser memelihara tabel cross-reference, keanggotaan object stream, dan prioritas revisi secara internal, namun header yang dipublikasikan tidak mengekspos titik masuk apa pun yang menerima nomor objek dan mengembalikan offset mentahnya, generasinya, revisi mana yang menang, atau ObjStm mana tempatnya berada. Sisi simpannya sama tertutupnya: FPDF_SaveAsCopy dan FPDF_SaveWithVersion hanya menyerahkan callback tulis sekuensial. Patch byte-level apa pun ke katalog setelah simpan native karenanya harus dibangun di lapisan Pascal, itulah sebabnya PDFiumPas memparse struktur-struktur ini sendiri alih-alih memakai ulang DLL-nya

Apa yang benar-benar disimpan indeks sparse di memori?

Deskriptor, bukan entri. Untuk tabel klasik (ISO 32000-1 §7.5.4), sebuah TPdfSparseXrefSubsection menyimpan nomor objek pertama, jumlah objek, offset byte tempat baris entri dimulai, dan lebar entri yang diukur. Entri-entrinya sendiri tetap di berkas. Lebarnya diukur dari baris pertama alih-alih diasumsikan 20 byte, karena para produsen tidak sepakat soal akhiran baris; PDFiumPas menerima 18 sampai 64 dan menolak apa pun di luar rentang itu, ditambah setiap subsection yang jumlah deklarasinya akan melewati ujung stream. Untuk cross-reference stream (§7.5.8), seksi menyimpan tiga lebar field /W, masing-masing dibatasi 0 sampai 8, pasangan /Index yang diratakan, dan byte entri hasil dekode, yang panjang diharapkan dihitung dari /W dan /Index sebelum satu byte pun di-inflate

Seluruh indeks dibangun oleh Initialize dari jendela ekor paling besar 1 MiB, tempat startxref ditemukan, dan setiap baca objek berikutnya memakai jendela objek 1 MiB. Plafon stream mentahnya 64 MiB dan satu baris xref tak boleh melebihi 1024 byte. Jika Anda sudah membaca catatan kami tentang memvalidasi object dan cross-reference stream dengan PDFiumPas, disiplin lebar field yang sama berlaku di sini, hanya kini dipakai untuk mengalamati satu entri alih-alih mengaudit satu tabel utuh

uses
  FPdfCompress;

var
  Source: TFileStream;
  Revision: TPdfSparseRevisionInfo;
begin
  Source := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    { hanya menelusuri startxref, rantai /Prev, dan katalog }
    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;

Bagaimana satu lookup menjangkau satu objek?

Dengan aritmetika, pada kedua tata letak. Subsection klasik punya baris berlebar tetap, sehingga alamat sebuah entri adalah awal subsection ditambah offset objek dikali lebar yang diukur; PDFiumPas lalu membaca satu baris itu, memparse offset sepuluh digit dan generasi lima digit, memeriksa generasinya terhadap plafon 65535 dari §7.5.4, dan mengklasifikasikan kata kunci di ekornya sebagai axkDirect atau axkFree. Cross-reference stream butuh satu langkah tambahan karena subsection /Index dirangkai di run byte hasil dekode, sehingga indeks mengakumulasi jumlah subsection-subsection sebelumnya sebelum mengalikan dengan jumlah lebar /W. Tipe 1 menghasilkan offset, tipe 2 menghasilkan nomor object stream dan indeks anggota, dan sisanya menjadi axkUnknown alih-alih tebakan

{ tabel klasik, ISO 32000-1 bagian 7.5.4 }
EntryOffset := Subsection.EntryOffset +
  Int64(ObjectNumber - Subsection.FirstObject) * Subsection.EntryWidth;

{ cross-reference stream, ISO 32000-1 bagian 7.5.8 }
EntryWidth := Section.Widths[0] + Section.Widths[1] + Section.Widths[2];
EntryPosition := Integer((PriorCount + ObjectNumber -
  Section.IndexValues[I]) * EntryWidth);

Tidak ada di kedua jalur itu yang proporsional terhadap /Size. Itulah inti penulisan ulang ini: nilai ukuran trailer dibawa terus sebagai metadata dan dipakai saat menulis revisi inkremental, tetapi tidak pernah menggerakkan alokasi. Regression suite menguncinya dengan fixture yang page tree-nya berada di objek 1.000.000.000 dan 1.000.000.001 di bawah trailer yang mendeklarasikan /Size 1000000002. Implementasi padat lama menolak berkas itu; indeks sparse me-resolve kedua referensi dan mempertahankan ukuran yang dideklarasikan di trailer output

Cara PDFiumPas me-resolve satu nomor objek di Delphi: tabel cross-reference klasik mengalikan lebar baris yang diukur, sedangkan cross-reference stream mengakumulasi jumlah subsection sebelumnya sebelum mengalikan jumlah lebar field dari array /W
Kedua lookup adalah aritmetika murni, sehingga tak satu pun proporsional terhadap jumlah objek yang dideklarasikan di trailer

Revisi hybrid, rantai /Prev, dan penjaga di sekitarnya

Prioritas revisi adalah tempat indeks lazy yang naif keliru. PDFiumPas menelusuri rantai dari startxref dengan urutan terbaru-dahulu dan menghentikan lookup di seksi pertama yang menjawab, yang mereproduksi aturan prioritas tanpa mematerialisasi tabel gabungan. Berkas hybrid-reference (§7.5.8.4) ditangani di dalam cabang klasik: ketika trailer membawa /XRefStm, seksi stream suplemen didaftarkan sebelum seksi klasik yang menyebutnya, sehingga objek terkompresi yang tak terlihat oleh tabel polos tetap ditemukan sementara entri klasik mempertahankan kedudukannya. Revisi lebih lama kemudian diikuti lewat /Prev

Dua penjaga membatasi penelusuran itu, dan keduanya penting pada berkas rusak. Setiap offset yang dikunjungi dicatat, sehingga /Prev yang menunjuk balik ke rantai akan berhenti alih-alih berputar, dan kedalaman traversal dibatasi MaxRecursionDepth, defaultnya 1024. Flag enkripsi diakumulasi lintas seluruh rantai alih-alih dibaca dari trailer terbaru saja, karena dokumen yang trailer terbarunya menghilangkan /Encrypt bisa jadi tetap terenkripsi lebih di belakang; pemanggil yang menambah revisi mengandalkan flag itu untuk menolak menulis objek plaintext ke berkas terenkripsi

Cara PDFiumPas menelusuri rantai revisi PDF hybrid di Delphi: seksi didaftarkan terbaru dahulu dari startxref, seksi suplemen XRefStm mendahului tabel klasik yang menyebutnya, dan penelusuran /Prev dibatasi offset yang dikunjungi serta plafon kedalaman
Lookup berhenti di seksi pertama yang menjawab, yang mereproduksi prioritas revisi tanpa pernah mematerialisasi tabel gabungan

Entri tipe-2: mengapa object stream menunggu

Entri tipe-2 menyebut sebuah object stream, dan PDFiumPas tidak menyentuh stream itu sampai pemanggil meminta salah satu anggotanya. Saat akhirnya dilakukan, /Type /ObjStm diverifikasi, /N diperiksa terhadap anggaran objek dan /First terhadap plafon byte hasil dekode, serta /N dicek kewajarannya terhadap /First karena setiap pasangan header butuh sekurang-kurangnya empat byte. Baru kemudian stream di-inflate, dan pemindaian header berhenti di anggota yang diminta serta penerusnya alih-alih membangun tabel anggota penuh. Satu object stream hasil dekode dipertahankan pada satu waktu, yang merupakan trade yang tepat ketika cabang page tree menggerombol ke satu ObjStm; tulisan kami tentang dekoding object stream dan predictor di Delphi membahas apa yang terjadi di dalam langkah inflate itu (§7.5.7)

var
  Reader: TPdfSparseDictionaryReader;
  Generation: Integer;
  Dict: AnsiString;
begin
  { satu indeks dipertahankan, banyak baca sadar generasi }
  Reader := TPdfSparseDictionaryReader.Create(Source);
  try
    if Reader.Valid and
       Reader.ReadLatestDictionary(PageObjectNumber, Generation, Dict) then
      HandlePage(PageObjectNumber, Generation, Dict);
  finally
    Reader.Free;  { Source tetap milik Anda }
  end;
end;

Di mana cache berhenti menjanjikan

Indeksnya adalah snapshot, dan patut diakui terus terang. Seksi diparse sekali di Initialize; jika stream di bawahnya dimodifikasi setelahnya, setiap entri cache basi dan class itu tidak akan menyadarinya. TPdfSparseDictionaryReader memegang indeks selama lifetime milik pemanggil dari sumbernya, yang persis diinginkan penelusuran rekursif atas page tree dan persis yang tidak boleh Anda lakukan lintas penulisan ulang. Cache entrinya array flat yang dicari linear dan juga menyimpan hasil negatif, sehingga beberapa ratus lookup murah dan beberapa ratus ribu tidak. ReadDictionary menuntut kecocokan generasi persis sementara ReadLatestDictionary me-resolve yang aktif, dan perbedaannya disengaja: resolusi referensi butuh yang pertama, inspeksi katalog butuh yang kedua. Di mana batas-batas ini tak bisa dipenuhi, unit di sekitarnya jatuh kembali ke parser legacy satu berkas penuh alih-alih mempersempit himpunan berkas yang masih bekerja, pola yang juga kami pakai untuk streaming PDF besar on-demand

Regression lintas-kompiler mencakup perilaku yang sama di ketiga toolchain, termasuk asersi bahwa sumber 2 MiB tidak pernah melihat satu baca pun yang lebih besar dari 1 MiB. Jika Anda memelihara kode Delphi, C++Builder, atau Lazarus yang menyentuh struktur PDF secara langsung dan lelah membayar biaya parse satu berkas penuh untuk empat kamus, indeks sparse dan seam publik di sekitarnya tersedia di PDFiumPas Delphi PDFium component