PDF Library for Delphi gỡ bỏ một toán tử text matrix identity, 1 0 0 1 0 0 Tm, trong bước tối ưu content-stream kiểu peephole lúc lưu, chỉ khi text matrix và text line matrix đã là identity: ngay sau BT, hoặc ngay sau một Tm identity đứng trước đó. Một cm identity thì vẫn luôn bị vứt, vì cm nhân vào CTM trong khi Tm thay thẳng cả hai text matrix. Kể từ v3.539.28, mọi identity Tm khác đều được giữ nguyên trong stream
Bug mà bản sửa này xử lý thuộc kiểu thầm lặng. Một trình sinh báo cáo phát BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET, dựa vào identity Tm để đưa chuỗi thứ hai về gốc tọa độ text-space trước khi áp logic định vị của chính nó. Bộ tối ưu cũ nhìn thấy sáu con số vẽ nên matrix identity, kết luận toán tử này chẳng thể thay đổi điều gì, và xóa nó. Chẳng gì hỏng, chẳng gì log cảnh báo, và trang lưu ra vẽ "Total" ngay sau "Invoice" trên cùng một baseline, đúng cái loại khuyết tật chẳng ai để ý cho tới khi khách hàng đưa PDF đi in
Vì sao 1 0 0 1 0 0 Tm không phải lúc nào cũng là no-op?
Một Tm identity chỉ là no-op khi nó sắp thay thế hai matrix đang là identity, và đó là tính chất của các toán tử đứng trước nó, không phải của chính toán hạng của nó. ISO 32000-1 §9.4.1 nói BT khởi tạo cả text matrix (Tm) lẫn text line matrix (Tlm) về identity, còn §9.4.2 định nghĩa Tm là gán cả hai về giá trị cho trước, chứ không nối vào chúng. So với cm (§8.4.4), thứ nhân-phải vào current transformation matrix: nhân với identity để nguyên mọi CTM, nên 1 0 0 1 0 0 cm xóa ở đâu cũng an toàn. Bên trong một text object thì câu chuyện khác. Td, TD, T* và một Tm không identity đều dời Tlm, còn mọi toán tử hiển thị văn bản (Tj, TJ, ', ") đều đẩy Tm tiến theo bề rộng các glyph nó vừa vẽ. Sau bất kỳ toán tử nào trong số đó, một identity Tm là một phép reset thật sự về gốc. Nếu bạn đã từng lần tay theo vị trí văn bản với bộ theo dõi trạng thái CTM và text matrix của content stream, thì đây chính là sự phân biệt giữa nối trạng thái và thay trạng thái
Phép quét ngược quyết định identity Tm nào bị xóa ra sao?
TPDFContentPeepholeOptimizer.RemoveIdentityMatrices giờ đi ngược từ mỗi identity Tm và chỉ xóa nó nếu phép quét chạm BT hay một identity Tm khác trước. Identity Tm đứng trước được tính cả khi nó được giữ lẫn khi nó vừa được đưa vào danh sách xóa, vì rốt cuộc cách nào nó cũng để cả hai matrix ở trạng thái identity, y như BT. Luật này xếp mọi toán tử nó có thể gặp vào một trong hai nhóm:
- Dừng và giữ Tm:
Td,TD,T*, mộtTmkhông identity,Tj,TJ,',",ET, bất kỳ toán tử nào parser không nhận ra, hoặc điểm mở đầu của stream - Bước qua và quét tiếp: những toán tử không bao giờ đụng tới Tm hay Tlm, như
Tf,Tc, các bộ đặt màu,gs, các toán tử marked-content vàcm
Nữa các ca bảo thủ là có chủ đích. Một toán tử lạ có thể là bất cứ gì, nên phép quét từ chối suy luận vượt qua nó. ET đóng text object, nên một Tm đứng sau nó chẳng có BT nào bảo chứng cho giá trị matrix. Phép quét cũng chỉ làm việc trên một content stream mỗi lần, điều quan trọng với các trang mà /Contents là một mảng: một layer mở đầu giữa chừng một text object, không có BT riêng, sẽ giữ identity Tm của nó kể cả khi layer trước đó khiến nó dư thừa. Cái giá là vài byte trên những tệp lạ và chẳng bao giờ dời một glyph. Nếu bạn sửa văn bản trang ở mức instruction, như trong hướng dẫn ánh xạ ký tự sang content byte, thì chính mô hình TPDFContentProgram đã parse này là thứ optimizer viết lại
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 hỏng: cứ để nguyên các byte
Optimizer := TPDFContentPeepholeOptimizer.Create(Prog);
try
Optimizer.Run; // trả về số instruction đã bị gỡ
finally
Optimizer.Free;
end;
Result := Prog.Emit; // mỗi dòng một instruction
finally
Prog.Free;
end;
end;
// Đã gỡ: Tm đứng ngay sau BT, cái thứ hai trong hai identity Tm liền nhau
// OptimizeSnippet('BT /F1 12 Tf 1 0 0 1 0 0 Tm (hello) Tj ET')
// Giữ lại: Tm sau Td, sau Tj, sau một Tm không identity, hay ngoài BT
// OptimizeSnippet('BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET')
Chạy helper lên stream hóa đơn ở phần mở đầu thì identity Tm sống sót, vì phép quét ngược đụng Tj trước khi tới BT. Đặt /F1 12 Tf, 2 Tc và 0 g vào giữa BT và identity Tm thì nó vẫn bị dọn, vì chẳng toán tử nào trong số đó đụng tới các text matrix. Một chuỗi kiểu BT 10 20 Td 1 0 0 1 0 0 Tm 1 0 0 1 0 0 Tm mất đúng một toán tử: identity Tm đầu tiên reset lại matrix mà Td vừa dời, và chỉ cái thứ hai mới là dư thừa
Peephole optimizer thực sự chạy khi nào?
Optimizer chỉ chạy trong bước nén, bên trong TPDFPageTree.Compress, và chỉ trên các content stream chưa được nén Flate sẵn. TPDFlib.SetOptimizeContentStreams(1) là mặc định, và cùng công tắc đó được lộ ra dưới dạng trường OptimizeContentStreams của TPDFlibSaveOptions; cả CompressContent lẫn CompressPage đều tôn trọng nó. Một stream mà /Filter đã là /FlateDecode bị bỏ qua trọn vẹn, nên nạp một PDF đã nén có sẵn rồi lưu lại không viết lại các toán tử của nó. Nếu stream parse hỏng, các byte gốc đã giải mã được nén nguyên trạng. TPDFlib.NormalizeContentStreams parse và phát lại content với khoảng cách cùng con số chuẩn hóa nhưng không bao giờ gọi optimizer, khiến nó thành một baseline hữu ích khi bạn muốn thấy phần chênh lệch kích thước mà các luật peephole đóng góp là bao nhiêu, bên cạnh các khoản thắng lớn hơn được nói trong tối ưu kích thước tệp PDF với font subsetting
var
Lib: TPDFlib;
Options: TPDFlibSaveOptions;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('report.pdf', '') <> 1 then
Exit;
// Stream chưa nén đi qua các luật peephole, rồi tới Flate
Lib.SetOptimizeContentStreams(1);
Lib.CompressContent;
Lib.SaveToFile('report-optimized.pdf');
// Cùng lựa chọn đó qua bộ save options đi kèm; False là từ chối
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;
Test hồi quy cũ thực sự bảo đảm điều gì?
Test hồi quy cũ chỉ bảo đảm đúng một hình dạng: một identity Tm đứng ngay sau BT sẽ bị xóa. Peephole_RemovesIdentityTextMatrix nạp BT 1 0 0 1 0 0 Tm (hello) Tj ET cho optimizer và assert rằng chẳng còn Tm nào sót lại. Một bản phát hành trước đã ghi nhận việc xóa identity Tm là không an toàn khi Tlm không phải identity, rồi vẫn giữ nguyên hành vi vì test đã "khóa" nó. Đọc lại cho kỹ, test chẳng nói gì về một identity Tm sau Td hay sau văn bản đã hiển thị; việc coi phạm vi phủ của một mẫu đơn lẻ là hợp đồng của cả luật mới là lỗi thật sự. Bản sửa giữ ca gốc vẫn pass và thêm sáu ca ghim chặt cả các hình dạng được xóa lẫn các hình dạng được giữ, kể cả một Tm nằm ngoài mọi text object và một cái đứng sau ET
Sự đánh đổi dễ chấp nhận ngay khi được viết ra. Những generator gói mọi text object thành BT 1 0 0 1 0 0 Tm ... vẫn thấy toán tử dư thừa đó bị dọn, và đó là nơi gần như toàn bộ khoản tiết kiệm đến từ. Cái optimizer từ bỏ là những identity Tm thỉnh thoảng nằm giữa chừng text object, vài byte mỗi trang trước khi Flate kịp nhìn chúng, đổi lấy một cam kết mà header của module nói thẳng: mọi phép biến đổi là output-equivalent và không bao giờ thay đổi trang hiển thị. Một bộ tối ưu kích thước mà dời chữ không phải là optimizer, đó là một bug render với tỉ lệ nén đẹp
Parser content-stream, peephole optimizer và các tùy chọn nén lúc lưu được mô tả trong bài đều đi kèm trong PDF Library for Delphi và C++Builder, thứ cũng lộ ra NormalizeContentStreams, CompressContent và TPDFlibSaveOptions để chỉnh cách mỗi tài liệu được ghi