Artikel Teknis

Identity Tm di Content Stream PDF: Peephole Removal Aman

PDF Library for Delphi menghapus operator identity text matrix, 1 0 0 1 0 0 Tm, dalam optimasi peephole content stream saat save hanya ketika text matrix dan text line matrix sudah berupa identity: tepat setelah BT, atau tepat setelah identity Tm sebelumnya. Identity cm tetap selalu dibuang, karena cm mengalikan CTM sedangkan Tm mengganti kedua text matrix sekaligus. Sejak v3.539.28 setiap identity Tm lainnya tetap berada di stream

Bug yang diperbaiki ini tipe yang senyap. Sebuah report generator mengeluarkan BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET, mengandalkan identity Tm untuk mengirim string kedua kembali ke origin text space sebelum menerapkan logika posisinya sendiri. Optimizer lama melihat enam angka yang membentuk identity matrix, menyimpulkan operator itu mustahil mengubah apa pun, dan menghapusnya. Tak ada yang gagal, tak ada yang mencatat warning, dan halaman tersimpan menggambar "Total" tepat setelah "Invoice" pada baseline yang sama — persis kelas cacat yang tak disadari siapa pun sampai pelanggan mencetak PDF-nya

Kenapa 1 0 0 1 0 0 Tm tak selalu no-op?

Identity Tm hanya no-op kalau ia akan mengganti dua matrix yang sudah berisi identity, dan itu properti dari operator sebelumnya, bukan dari operannya sendiri. ISO 32000-1 §9.4.1 menyatakan BT menginisialisasi text matrix (Tm) dan text line matrix (Tlm) ke identity, dan §9.4.2 mendefinisikan Tm sebagai penyetel keduanya ke nilai yang diberikan, bukan penggabung ke atasnya. Bandingkan dengan cm (§8.4.4) yang mengalikan kanan current transformation matrix: perkalian dengan identity membiarkan CTM mana pun tak berubah, jadi 1 0 0 1 0 0 cm aman dihapus di mana saja. Di dalam text object gambarnya berbeda. Td, TD, T*, dan Tm non-identity semuanya menggerakkan Tlm, dan setiap operator penampil teks (Tj, TJ, ', ") memajukan Tm selebar glyph yang digambar. Setelah salah satu dari itu, identity Tm adalah reset sungguhan ke origin. Kalau Anda pernah menelusuri posisi teks secara manual dengan state tracker CTM dan text matrix content stream, ini pembedaan yang sama antara menggabungkan state dan menggantinya

PDFlibPas memperlakukan 1 0 0 1 0 0 cm dan 1 0 0 1 0 0 Tm secara berbeda: cm mengalikan kanan CTM dan no-op di mana saja, sedangkan Tm mengganti Tm dan Tlm sekaligus, dan setiap Tj memajukan Tm selebar yang digambar, jadi identity Tm setelah teks tampil adalah reset sungguhan
Report generator mengandalkan reset itu: menghapus identity Tm menggambar Total tepat setelah Invoice pada baseline yang sama, dan tak ada yang gagal, tercatat, atau memperingatkan sampai ke printer pelanggan

Cara backward scan memutuskan identity Tm mana yang dibuang

TPDFContentPeepholeOptimizer.RemoveIdentityMatrices kini menelusuri mundur dari setiap identity Tm dan membuangnya hanya kalau scan mencapai BT atau identity Tm lain lebih dulu. Identity Tm sebelumnya tetap terhitung entah ia disimpan atau baru saja dijadwalkan untuk dihapus, karena bagaimanapun ia meninggalkan kedua matrix pada identity, persis seperti yang dilakukan BT. Aturannya mengelompokkan setiap operator yang bisa ditemui ke salah satu dari dua grup:

  • Berhenti dan simpan Tm-nya: Td, TD, T*, Tm non-identity, Tj, TJ, ', ", ET, operator apa pun yang tak dikenali parser, atau awal stream
  • Lewati dan lanjutkan scan: operator yang tak pernah menyentuh Tm maupun Tlm, seperti Tf, Tc, color setter, gs, operator marked-content, dan cm
RemoveIdentityMatrices milik PDFlibPas menelusuri mundur dari setiap identity Tm: Tf, Tc, color setter, gs dan cm dilewati, sementara Td, TD, T*, Tm non-identity, Tj, TJ, operator tak dikenal, atau ET menghentikan scan dan menyimpan Tm-nya, dan BT mengesahkan pembuangan itu
Identity Tm sebelumnya juga menghentikan scan, entah disimpan atau sudah dijadwalkan dihapus ia meninggalkan kedua matrix pada identity — bagaimanapun optimizer tak pernah menggeser glyph

Kasus-kasus konservatif itu disengaja. Operator tak dikenal bisa berupa apa saja, jadi scan menolak menalar melewatinya. ET menutup text object, sehingga Tm setelahnya tak punya BT yang mengesahkan nilai matrix-nya. Scan juga bekerja pada satu content stream dalam satu waktu, yang penting untuk halaman yang /Contents-nya berupa array: layer yang mulai di tengah text object, tanpa BT miliknya sendiri, menyimpan identity Tm-nya meski layer sebelumnya membuatnya redundan. Itu biaya beberapa byte pada file aneh dan tak pernah menggeser glyph. Kalau Anda menyunting teks halaman di level instruksi, seperti di walkthrough mapping karakter-ke-byte konten, model TPDFContentProgram hasil parse yang sama itulah yang ditulis ulang oleh optimizer

uses
  PDFlibContentModel, PDFlibContentOptimize;

function OptimizeSnippet(const Source: AnsiString): AnsiString;
var
  Prog: TPDFContentProgram;
  Optimizer: TPDFContentPeepholeOptimizer;
begin
  Result := Source;
  Prog := TPDFContentProgram.Create;
  try
    if not Prog.Parse(Source) then
      Exit; // stream rusak: biarkan byte-nya apa adanya
    Optimizer := TPDFContentPeepholeOptimizer.Create(Prog);
    try
      Optimizer.Run; // mengembalikan jumlah instruksi yang dihapus
    finally
      Optimizer.Free;
    end;
    Result := Prog.Emit; // satu instruksi per baris
  finally
    Prog.Free;
  end;
end;

// Dihapus: Tm tepat setelah BT, yang kedua dari dua identity Tm beruntun
//   OptimizeSnippet('BT /F1 12 Tf 1 0 0 1 0 0 Tm (hello) Tj ET')
// Disimpan: Tm setelah Td, setelah Tj, setelah non-identity Tm, atau di luar BT
//   OptimizeSnippet('BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET')

Jalankan helper pada stream invoice dari pembukaan dan identity Tm-nya selamat, karena backward scan menabrak Tj sebelum mencapai BT. Selipkan /F1 12 Tf, 2 Tc, dan 0 g di antara BT dan identity Tm, dan tetap terbuang, karena tak satu pun dari itu menyentuh text matrix. Urutan seperti BT 10 20 Td 1 0 0 1 0 0 Tm 1 0 0 1 0 0 Tm kehilangan tepat satu operator: identity Tm pertama mereset matrix yang digerakkan Td, dan hanya yang kedua yang redundan

Kapan peephole optimizer benar-benar berjalan?

Optimizer hanya berjalan pada compression pass, di dalam TPDFPageTree.Compress, dan hanya pada content stream yang belum terkompresi Flate. TPDFlib.SetOptimizeContentStreams(1) adalah defaultnya, dan switch yang sama diekspos sebagai field OptimizeContentStreams milik TPDFlibSaveOptions; CompressContent maupun CompressPage sama-sama menghormatinya. Stream yang /Filter-nya sudah /FlateDecode dilewati sepenuhnya, jadi memuat PDF terkompresi yang sudah ada lalu menyimpannya lagi tidak menulis ulang operatornya. Kalau stream gagal di-parse, byte hasil decode aslinya dikompresi apa adanya. TPDFlib.NormalizeContentStreams mem-parse dan menulis ulang konten dengan spacing dan angka kanonik tapi tak pernah memanggil optimizer, yang menjadikannya baseline yang berguna saat Anda ingin melihat seberapa besar kontribusi aturan peephole terhadap ukuran, di samping kemenangan yang lebih besar yang dibahas di optimasi ukuran file PDF dengan font subsetting

PDFlibPas menjalankan peephole optimizer hanya di dalam compression pass saat save: TPDFPageTree.Compress menghormati SetOptimizeContentStreams, stream yang sudah terfilter /FlateDecode dilewati sepenuhnya, stream yang tak terparse dikompresi dengan byte aslinya tanpa diubah, dan NormalizeContentStreams tak pernah memanggil optimizer sama sekali
Stream terkompresi yang dilewati adalah bagian senyapnya: muat PDF yang sudah ada, simpan lagi, dan operatornya keluar tanpa tersentuh karena optimizer hanya menulis ulang stream yang dia decode lebih dulu
var
  Lib: TPDFlib;
  Options: TPDFlibSaveOptions;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('report.pdf', '') <> 1 then
      Exit;
    // Stream tak terkompresi melewati aturan peephole, lalu Flate
    Lib.SetOptimizeContentStreams(1);
    Lib.CompressContent;
    Lib.SaveToFile('report-optimized.pdf');

    // Pilihan yang sama lewat save options bawaan; False menolak
    Options.CompressContent := True;
    Options.CompressFonts := True;
    Options.CompressImages := True;
    Options.Linearize := False;
    Options.KeepModDate := False;
    Options.OptimizeContentStreams := False;
    Options.GarbageCollect := False;
    Options.PackObjectStreams := True;
    Lib.SaveToFileOptions('report-plain.pdf', Options);
  finally
    Lib.Free;
  end;
end;

Apa yang sebenarnya dijamin regression test lama?

Regression test lama hanya menjamin satu bentuk: identity Tm tepat setelah BT dihapus. Peephole_RemovesIdentityTextMatrix memberikan BT 1 0 0 1 0 0 Tm (hello) Tj ET ke optimizer dan meng-assert bahwa tak ada Tm tersisa. Rilis sebelumnya sudah mencatat bahwa membuang identity Tm tak aman ketika Tlm bukan identity, lalu tetap mempertahankan perilakunya karena test "mengunci"-nya. Dibaca ulang dengan cermat, test itu tak berkata apa-apa soal identity Tm setelah Td atau setelah teks tampil; memperlakukan cakupan satu sampel sebagai kontrak seluruh aturan itulah kesalahan sebenarnya. Perbaikannya menjaga kasus asli itu tetap lolos dan menambah enam kasus yang mengunci bentuk yang boleh dibuang maupun yang disimpan, termasuk sebuah Tm di luar text object mana pun dan satu yang mengikuti ET

Trade-off-nya mudah diterima begitu dituliskan. Generator yang membungkus setiap text object dengan BT 1 0 0 1 0 0 Tm ... tetap mendapatkan operator redundan itu dihapus, dan di situlah hampir semua penghematan berasal. Yang dilepas optimizer adalah identity Tm sesekali di tengah text object, segenggam byte per halaman sebelum Flate melihatnya, sebagai ganti jaminan yang dinyatakan terang di header modul: setiap transform ekuivalen output dan tak pernah mengubah halaman yang terlihat. Size optimizer yang menggeser teks bukan optimizer, itu rendering bug dengan rasio kompresi yang bagus

Parser content stream, peephole optimizer, dan opsi kompresi saat save yang dijelaskan di sini semuanya tersedia di PDF Library for Delphi dan C++Builder, yang juga mengekspos NormalizeContentStreams, CompressContent, dan TPDFlibSaveOptions untuk menyetel cara setiap dokumen ditulis