PDFlibPas giải quyết các lệnh gọi Form XObject đệ quy trong content stream PDF Delphi bằng cách theo dõi chuỗi gọi đang hoạt động, không phải một tập hợp đã-thăm toàn cục, nên TPDFlib.EnumPageContentStatesEx có thể duyệt cùng một Form được gọi nhiều lần trên một trang mà không nhầm việc tái sử dụng hợp pháp với một chu trình. Một Form XObject dấu mộc trong một mẫu hóa đơn là trường hợp điển hình: cùng đối tượng đó được gọi từ header, footer, và một lớp watermark trên một trang, và chỉ một chuỗi gọi vòng lại chính nó mới là một chu trình đích thực
ISO 32000-1 §8.10 định nghĩa một Form XObject là một content stream tự chứa mà một trang, hay một Form khác, gọi bằng toán tử Do, đầy đủ với hệ tọa độ riêng của nó trong /Matrix, một ranh giới clip trong hệ tọa độ đó trong /BBox, và tùy chọn từ điển tài nguyên riêng của nó. Không có gì trong đặc tả giới hạn số lần một Form có thể được gọi hay độ sâu các Form có thể gọi lẫn nhau, nên một trình phân tích tuân thủ chuẩn phải chấp nhận việc tái sử dụng hợp pháp và lồng nhau hợp pháp trong khi vẫn tự phòng vệ trước sắp xếp duy nhất mà đặc tả thực sự cấm: một Form mà content stream của nó, trực tiếp hay bắc cầu, tự gọi chính nó. PDFlibPas báo cáo sự phân biệt đó thông qua các giá trị TPDFlibContentFormTraversalStatus đính kèm mỗi bản chụp Do, đáng chú ý nhất là ftsEnumerated cho một lượt đi xuống thành công và ftsCycle cho trường hợp duy nhất thực sự là một vòng lặp
Vì sao việc tái sử dụng cùng một Form XObject không kích hoạt một chu trình giả?
Một tham chiếu Form XObject lặp lại tự nó không phải bằng chứng của bất cứ điều gì sai. ISO 32000-1 cho phép cùng một đối tượng Form được gọi từ bao nhiêu nơi trong một content stream mà tác giả muốn, chính xác là cách một dấu mộc logo, một mẫu tiêu đề thư, hay một footer đánh số trang được tái sử dụng khắp một trang mà không cần nhân bản content stream của nó nhiều lần. Sự bảo vệ ngây thơ chống lại đệ quy chạy trốn là một tập hợp đã-thăm duy nhất được đánh khóa theo số đối tượng: lần đầu tiên một bộ duyệt thấy đối tượng Form 12, nó đánh dấu 12 là đã thấy và từ chối vào lại nó ở bất cứ đâu khác trong cây. Cách tiếp cận đó gãy đổ ngay khi cùng dấu mộc xuất hiện ở hai góc không liên quan của một trang, vì lệnh gọi thứ hai, hoàn toàn hợp pháp, đến sau khi số đối tượng đã được đánh dấu là đã thấy và bị từ chối như thể nó là một vòng lặp
PDFlibPas tránh dương tính giả đó bằng cách giới hạn phạm vi phát hiện chu trình theo chuỗi gọi hiện tại thay vì toàn bộ tài liệu. EnumPageContentStatesEx đẩy stream Form đã giải quyết lên chuỗi gọi đang hoạt động ngay trước khi đi xuống vào nó, sau đó pop chính mục đó ra một lần nữa ngay khi lượt đi xuống trả về, thành công hay không. Một lệnh gọi anh em của chính stream đó chỉ bắt đầu sau khi lệnh gọi đầu tiên đã bị pop, nên chuỗi gọi sạch khỏi stream đó vào thời điểm lệnh gọi anh em kiểm tra nó, và bộ duyệt liệt kê nó đúng như nó sẽ làm với bất kỳ Form nào khác. Một chu trình thật trông khác trên chính chuỗi đó: Form A gọi Form B, B vẫn còn mở trên chuỗi khi nội dung riêng của nó gọi ngược lại vào A, và A vẫn nằm trên chuỗi từ lệnh gọi ngoài chưa trả về — đó là hình dạng duy nhất ftsCycle báo cáo, một stream Form vẫn còn mở ở đâu đó sớm hơn trên chuỗi gọi hiện tại, không chỉ đơn thuần hiện diện ở đâu đó khác trên trang
Đệ quy Form XObject có thể sâu đến mức nào trước khi PDFlibPas dừng nó?
Phát hiện chu trình và giới hạn độ sâu giải quyết hai vấn đề khác nhau, và PDFlibPas giữ chúng như hai kết quả TPDFlibContentFormTraversalStatus khác nhau chính vì lý do đó. Một chuỗi hai mươi Form riêng biệt, mỗi Form gọi Form tiếp theo và không cái nào lặp lại, không phải một chu trình theo bất kỳ định nghĩa nào — kiểm tra chuỗi đang hoạt động không bao giờ tìm thấy một stream lặp lại — nhưng hai mươi cấp lồng trung thực vẫn là hai mươi cấp phân tích, ghép ma trận, và giải quyết tài nguyên mà một PDF lỗi định dạng hay đối kháng có thể đẩy cao tùy ý nếu không có gì khác dừng nó lại. EnumPageContentStatesEx nhận một tham số MaxFormDepth chính vì lý do này và kẹp bất kỳ giá trị nào được truyền vào ở mức tối đa 64, bất kể caller yêu cầu gì. Một độ sâu bằng không là một trường hợp đặc biệt đáng biết riêng: nó hoàn toàn vô hiệu hóa đệ quy Form và tái tạo hành vi phẳng, chỉ-trang của phương thức EnumPageContentStates cũ hơn, đó là lý do vì sao mỗi bản chụp Do trong chế độ đó báo cáo ftsNotRequested thay vì thử làm gì đó
var
Lib: TPDFlib;
States: array of TPDFlibContentGraphicsState;
Count, I: Integer;
begin
Lib:= TPDFlib.Create;
try
if Lib.LoadFromFile('invoice-batch.pdf', '')<> 1 then
Exit;
Lib.SelectPage(1);
Count:= Lib.EnumPageContentStatesEx(True, 8, States); // count only
SetLength(States, Count);
Lib.EnumPageContentStatesEx(True, 8, States); // fill
for I:= 0 to Count- 1 do
if States[I].FormTraversalStatus= ftsCycle then
LogSuspectForm(States[I].XObjectResource, States[I].ContentDepth);
finally
Lib.Free;
end;
end;
Một sub-tracker cho mỗi lần gọi: cô lập trạng thái đồ họa
Mỗi lượt đi xuống vào một Form XObject nhận một bộ theo dõi trạng thái đồ họa riêng thay vì chia sẻ bộ đã đang duyệt trang, vì content stream của một Form được yêu cầu để lại trạng thái đồ họa đúng như nó tìm thấy, và PDFlibPas không thể giả định mọi PDF nó mở thực sự tôn trọng yêu cầu đó. Tracker con bắt đầu từ một bản chụp bất kể CTM, trạng thái màu sắc, và tham số văn bản nào đang hoạt động tại lệnh Do gọi nó, sau đó đặt lại stack lưu-và-khôi phục riêng và việc theo dõi path hiện tại của nó về rỗng trước khi thực thi một lệnh duy nhất của Form. Một q không cân bằng không có Q khớp bên trong một Form cẩu thả hay hỏng, không hiếm gặp trong các PDF được sản xuất bởi công cụ cũ hơn, được chứa gọn bên trong tracker của chính lần gọi đó và không bao giờ rò rỉ vào tracker trang hay vào một lệnh gọi anh em của cùng dấu mộc nằm một dòng sau đó trong content stream
/Matrix của Form kết hợp với CTM có hiệu lực tại Do theo đúng cách một toán tử cm làm, được nhân trước với phép biến đổi hiện tại thay vì thay thế nó, và PDFlibPas có chủ đích tái sử dụng chính đường code đó thay vì duy trì một công thức thứ hai, vì hai triển khai độc lập của cùng đại số ma trận chính xác là loại trùng lặp âm thầm trôi dạt khỏi nhau sau vài vòng ghép tỉ lệ, xoay, và cắt xiên. /BBox sau đó clip trong không gian tọa độ riêng của Form sau khi ma trận đã được áp dụng, và cả bốn góc của hộp đó được biến đổi riêng lẻ thay vì chỉ hai góc đối diện, vì một Form đã xoay hay cắt xiên nếu không sẽ báo cáo một hộp bao bỏ sót nội dung thật nằm ở nơi từng là một góc cực trước khi phép biến đổi di chuyển nó đi nơi khác. Mở rộng vòng lặp từ ví dụ trước trên cùng mảng States đọc trực tiếp các trường đó
for I:= 0 to Count- 1 do
if (States[I].OperatorName= 'Do')and (States[I].XObjectKind= cxkForm)and
States[I].FormBBoxKnown then
Writeln('Form ', States[I].XObjectResource, ' matrix ',
States[I].FormMatrix.M11:0:3, ',', States[I].FormMatrix.M12:0:3,
' bbox ', States[I].FormBBoxLeft:0:1, '..', States[I].FormBBoxRight:0:1);
Hai Form có cùng tên tài nguyên có chia sẻ một font không?
Không. Một tên tài nguyên như /F1 chỉ có ý nghĩa tương đối với từ điển tài nguyên đang hoạt động tại điểm nó được dùng, và hai Form XObject khác nhau tự do định nghĩa hai font hoàn toàn khác nhau dưới cùng tên đó. PDFlibPas giải quyết điều này bằng cách theo dõi một phạm vi tài nguyên cùng với mỗi tên tài nguyên: khi một Form mang từ điển /Resources riêng của nó, từ điển đó trở thành toàn bộ phạm vi tài nguyên cho mọi thứ bên trong nó, không có dự phòng theo-từng-khóa về từ điển trang hay caller cho bất cứ thứ gì từ điển riêng của Form tình cờ bỏ sót. Chỉ một Form hoàn toàn không có khóa /Resources, một khuôn mẫu vẫn được tạo bởi một số bộ sinh PDF cũ hơn, kế thừa toàn bộ từ điển gọi, và đó là một ngoại lệ tương thích có chủ đích thay vì một quy tắc tổng quát đáng dựa vào trong đầu ra mới. Danh tính font trong một bản chụp TPDFlibContentGraphicsState vì vậy là cặp FontResource và FontResourceScope, không phải chỉ riêng tên, với FontObjectNumber sẵn có để xác nhận chính xác đối tượng gián tiếp nào một /F1 cho trước giải quyết thành trong phạm vi cụ thể đó
Cùng việc phân phạm vi đó áp dụng cho mọi tài nguyên có tên khác mà một Form có thể mang, gồm cả các mục ExtGState và các mục XObject lồng nhau, vì cơ chế giải quyết bên dưới không xử lý đặc biệt font — trường hợp font chỉ tình cờ quan trọng nhất, vì một danh tính font không khớp âm thầm tạo ra sai glyph thay vì một lỗi rõ ràng. Code trích xuất nhóm các lượt chạy văn bản chỉ theo tên font, không nhóm cùng theo phạm vi tài nguyên, sẽ gộp hai font trông khác nhau tình cờ chia sẻ một tên, và sai lầm sẽ không lộ ra cho đến khi ai đó nhận ra các chữ số từ sai kiểu chữ nằm trong thứ lẽ ra phải đọc như một font nhất quán
for I:= 0 to Count- 1 do
if (States[I].OperatorName= 'Tj')and States[I].TextAdvanceResolved then
RecordGlyphRun(States[I].FontResource, States[I].FontResourceScope,
States[I].FontObjectNumber, States[I].ContentDepth);
Đọc FormTraversalStatus trong pipeline của riêng bạn
FormTraversalStatus tự biến mỗi bản chụp Do thành một báo cáo chẩn đoán nhỏ, và một pipeline bỏ qua nó đang vứt bỏ chính xác thông tin có thể giải thích một lượt trích xuất chưa hoàn chỉnh. ftsNotApplicable nghĩa là lệnh đó ngay từ đầu chưa bao giờ là một lệnh gọi Form đã giải quyết; ftsNotRequested nghĩa là đệ quy đã bị tắt cho lệnh gọi này; ftsEnumerated nghĩa là Form đã được phân tích và duyệt thành công; ftsDepthLimit và ftsCycle đánh dấu hai cách một lượt đi xuống bị cắt ngắn có chủ đích; và ftsMalformed bao phủ mọi thứ khác dừng lượt duyệt lại — một tham chiếu stream không thể giải quyết, một /Matrix hay /BBox phân tích thất bại, hay một ngoại lệ ném ra trong lúc thực thi nội dung riêng của Form. Trường hợp cuối cùng đó quan trọng về mặt vận hành, vì một lượt duyệt lồng thất bại rollback bất cứ đầu ra một phần nào nó đã tạo ra cho nhánh đó, nên một caller không bao giờ phải đoán liệu một Form có thực sự rỗng hay đơn giản là nổ tung hai lệnh vào content stream của nó
var
Tally: array[TPDFlibContentFormTraversalStatus] of Integer;
Status: TPDFlibContentFormTraversalStatus;
begin
for Status:= Low(Tally) to High(Tally) do
Tally[Status]:= 0;
for I:= 0 to Count- 1 do
Inc(Tally[States[I].FormTraversalStatus]);
if (Tally[ftsCycle]> 0)or (Tally[ftsMalformed]> 0) then
FlagForManualReview(SourceFileName, Tally[ftsCycle], Tally[ftsMalformed]);
end;
Ranh giới, chi phí, và vị trí của điều này
Content stream của một Form được giải mã và phân tích đúng một lần cho mỗi lệnh gọi liệt kê bất kể Form được gọi bao nhiêu lần, vì PDFlibPas cache danh sách lệnh đã phân tích theo đối tượng stream bên dưới thay vì phân tích lại nó ở mỗi lệnh gọi anh em — dấu mộc ba-góc từ ví dụ mở đầu được giải mã một lần và duyệt ba lần, không giải mã ba lần. Điều thực sự được dựng lại ở mỗi lần gọi riêng lẻ là mọi thứ hợp pháp khác nhau giữa một điểm gọi và điểm tiếp theo: tracker con, CTM đã ghép, clip đã giao, và phạm vi tài nguyên. Việc kế toán CTM và clip theo-từng-lần-gọi đó là chính cỗ máy đứng sau bộ theo dõi trạng thái CTM và clip content-stream của PDFlibPas, đáng đọc cùng bài này cho bất kỳ lượt duyệt content-stream nào vượt ra ngoài chính đệ quy Form
Có hai giới hạn đáng đặt kỳ vọng trước khi API này đi vào một pipeline lớn hơn. Trần độ sâu 64 cấp không phải một núm điều chỉnh cho các tài liệu thực sự sâu, vì hóa đơn, sao kê, và mẫu báo cáo thật về cơ bản không bao giờ lồng Form quá ba hay bốn cấp — một tài liệu thực sự chạm ftsDepthLimit nhiều khả năng là lỗi định dạng hay đối kháng hơn là bất thường tinh vi, và đáng ghi log như một tín hiệu chất lượng dữ liệu thay vì âm thầm thử lại với một con số lớn hơn. EnumPageContentStatesEx cũng là một API phân tích phía-đọc: nó báo cáo một content stream làm gì, không phải liệu một Form có nên hiển thị hay không, một câu hỏi riêng biệt được trả lời bởi trạng thái hiển thị Optional Content Group khi một dấu mộc hay Form watermark nằm sau một lớp mà một trình xem có thể đã tắt. Phát hiện chu trình theo chuỗi gọi, cô lập theo-từng-lần-gọi, và phân phạm vi tài nguyên cùng nhau tạo thành một góc của bề mặt kiểm tra content-stream trong component PDFlibPas dành cho Delphi và C++Builder