Bạn gọi AddText để đóng dấu một dòng lên một trang PDF với PDFiumPas, sau đó ngay lập tức gọi FindFirst để xác nhận con dấu đã đáp xuống, và lượt tìm kiếm trả về rỗng. Văn bản có trên trang — Acrobat hiển thị nó — nhưng component TPdf của PDFiumPas giữ một cấu trúc FPDF_TEXTPAGE đã cache riêng biệt, được phân tích một lần từ content stream của trang, và một chỉnh sửa không tự động hồi tố cập nhật cấu trúc đó. Truy vấn nó trước khi nó được làm mới và bạn đọc trang đúng như nó trông trước thay đổi của bạn, không phải sau đó
Vì sao PDFium trả về văn bản cũ ngay sau một chỉnh sửa?
PDFiumPas bọc engine render PDFium của Google cho Delphi và C++Builder, và các lệnh gọi văn bản và chỉnh sửa của nó vươn vào hai hệ thống con khác nhau bên trong engine đó. FPDF_TEXTPAGE thuộc về phía đọc: FPDFText_LoadPage duyệt content stream của trang một lần và dựng trang văn bản — mã ký tự, vị trí, số đo font, ranh giới từ — và PDFiumPas giữ cấu trúc đó trong cache suốt thời gian trang vẫn còn được nạp. Các lệnh gọi chỉnh sửa như FPDFPage_InsertObject hay FPDFPage_GenerateContent hoạt động trên một biểu diễn hoàn toàn khác, đồ thị đối tượng và content-stream của trang, và PDFium không tự đẩy những thay đổi đó vào một trang văn bản đã mở sẵn. Việc dựng lại nó ở mỗi chỉnh sửa sẽ khiến việc chỉnh sửa hàng loạt chậm không chấp nhận được, nên thiết kế đánh đổi chi phí đó lấy một quy tắc thay thế — bất cứ ai giữ handle đóng nó sau một chỉnh sửa thay đổi nội dung, và lượt đọc tiếp theo dựng một handle mới
Bên trong cache văn bản của TPdf: FTextPage, LoadTextPage, và UnloadTextPage
TPdf theo dõi handle đã cache trong một trường private duy nhất, FTextPage, và bọc vòng đời của nó trong hai phương thức. LoadTextPage kiểm tra xem FTextPage có phải nil hay không và, chỉ trong trường hợp đó, gọi FPDFText_LoadPage trên trang hiện tại; nếu một handle đã tồn tại sẵn, LoadTextPage tái sử dụng nó mà không hỏi liệu trang có thay đổi kể từ khi nó được dựng hay không. UnloadTextPage là nửa còn lại: nó đóng handle gốc bằng FPDFText_ClosePage, đặt FTextPage trở lại nil, và cũng loại bỏ danh sách liên kết web đã cache cùng bất kỳ phiên find đang diễn ra nào, vì cả hai đều được suy ra từ cùng trang văn bản và trở nên cũ vì cùng lý do
Hành vi tái-sử-dụng-mà-không-kiểm-tra của LoadTextPage chính là lý do vì sao thứ tự lại quan trọng. Mọi truy vấn văn bản trên TPdf — Text, FindFirst, GetWebLinks — đều dẫn qua LoadTextPage trước, nên chừng nào FTextPage vẫn còn giữ handle trước-chỉnh-sửa, không lệnh gọi nào trong số đó có cách nào biết một thay đổi đã xảy ra. Việc điều hướng trang chưa bao giờ là rủi ro ở đây: UnloadPage, chạy khi chuyển trang, nạp lại, và đóng tài liệu, luôn đóng trang văn bản cùng với chính trang đó. Câu hỏi mở luôn là về các chỉnh sửa áp dụng cho trang bạn vẫn đang ở đó
Những phương thức PDFiumPas nào tự động làm mới cache?
Các phương thức chỉnh sửa trang riêng của TPdf — AddText, SetText, SetTextPositions, AddPath, RemoveObject, và InsertFormObjectFromXObject — mỗi phương thức đều gọi UnloadTextPage trước khi gọi UpdatePage (FPDFPage_GenerateContent của PDFium) để serialize thay đổi vào content stream. Gọi bất kỳ phương thức nào trong số này và lệnh gọi Text, FindFirst, hay GetWebLinks ngay tiếp theo sẽ dựng lại trang văn bản từ nội dung đúng như nó hiện đang đứng, không cần lệnh gọi thêm nào từ phía bạn
var
Pdf: TPdf;
Index: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'invoice.pdf';
Pdf.Active := True;
Pdf.PageNumber := 1;
Pdf.AddText('Reviewed by J. Alvarez', 'Helvetica', 10, 72, 40, clBlack, 255, 0);
// AddText already closed the cached text page, so this FindFirst
// call rebuilds it fresh before it searches
Index := Pdf.FindFirst('Reviewed by J. Alvarez');
if Index >= 0 then
ShowMessage('Stamp confirmed at character ' + IntToStr(Index));
finally
Pdf.Free;
end;
end;
Khuôn mẫu vẫn hỏng: cache handle TextPage thô
TPdf phơi bày handle sống thông qua một thuộc tính TextPage chỉ đọc, cho trường hợp hiếm khi bạn cần gọi một hàm FPDFText_* mà PDFiumPas chưa bọc. Cánh cửa thoát hiểm đó cũng là nơi duy nhất mà việc vô hiệu hóa tự động không thể giúp được: một khi bạn copy giá trị FPDF_TEXTPAGE ra khỏi thuộc tính đó vào một biến cục bộ, PDFiumPas không có cách nào để biết bạn vẫn đang giữ nó, và không có cách nào để cập nhật bản sao của bạn khi UnloadTextPage chạy ở đâu đó khác trong code của bạn
var
Pdf: TPdf;
RawHandle: FPDF_TEXTPAGE;
StaleCount: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
Pdf.PageNumber := 1;
RawHandle := Pdf.TextPage; // FPDFText_LoadPage handle, cached in FTextPage
Pdf.SetText(0, 'Amended Clause 4.2');
// SetText already closed RawHandle and set Pdf.TextPage back to nil.
// Calling any FPDFText_* function against the old value now touches a
// handle PDFium has already freed — undefined behavior, not a bug you
// can catch with a nil check
StaleCount := FPDFText_CountChars(RawHandle);
finally
Pdf.Free;
end;
end;
Việc dùng một handle sau khi FPDFText_ClosePage đã chạy trên nó là hành vi không xác định trong chính PDFium, không phải một quy ước của PDFiumPas mà bạn có thể chọn bỏ qua — nó có thể trả về dữ liệu-đã-biết-cuối-cùng, không trả về gì, hoặc làm crash tiến trình, và điều nào trong số đó xảy ra trên một bản build cho trước không phải thứ mà code ứng dụng nên phụ thuộc vào. Quy tắc an toàn hẹp: đọc Pdf.TextPage mới, ngay trước lệnh gọi FPDFText_* cần đến nó, và không bao giờ giữ một bản sao qua một câu lệnh có thể chỉnh sửa trang
Gộp các chỉnh sửa của bạn thành lô, rồi truy vấn một lần
Không điều nào trong số này có nghĩa là mỗi lệnh gọi AddText hay RemoveObject cần một truy vấn văn bản phòng thủ ngay sau nó để kiểm tra kết quả. Mỗi phương thức chỉnh sửa đã trả chi phí đóng trang văn bản một lần; truy vấn sau mỗi chỉnh sửa đơn lẻ bên trong một vòng lặp trả chi phí đó lần nữa mà không có lợi ích gì, vì FPDFText_LoadPage duyệt lại toàn bộ content stream mỗi lần nó chạy
var
Pdf: TPdf;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'watermarked.pdf';
Pdf.Active := True;
Pdf.PageNumber := 1;
// Strip every text object that looks like a draft watermark. Each
// RemoveObject call already invalidates the cache on its own, so
// nothing needs refreshing by hand between iterations
for I := Pdf.ObjectCount - 1 downto 0 do
if (Pdf.ObjectType[I] = otText) and (Pdf.ObjectBounds[I].Top > 700) then
Pdf.RemoveObject(I, True);
// Query once, after the whole batch is done, not once per removal
if Pdf.FindFirst('DRAFT') < 0 then
ShowMessage('Watermark cleared');
finally
Pdf.Free;
end;
end;
Cùng logic gộp lô đó áp dụng cụ thể cho trạng thái tìm kiếm. FindNext và FindPrevious tiếp tục một phiên đã bắt đầu bởi FindFirst, và phiên đó bị dỡ bỏ bởi UnloadTextPage cùng mọi thứ khác, nên gọi FindNext lại sau một chỉnh sửa — thay vì gọi lại FindFirst — ném ra một ngoại lệ thay vì âm thầm tiếp tục một lượt tìm kiếm trên nội dung không còn tồn tại. Hãy coi mọi chỉnh sửa như một ranh giới cứng cho cả nội dung văn bản lẫn vị trí tìm kiếm, và để một FindFirst mới ở phía bên kia các chỉnh sửa của bạn tiếp tục lượt tìm kiếm
Điều này phù hợp thế nào với công việc trích xuất và chú thích
Trích xuất văn bản thuần túy — đọc văn bản của một trang mà không thay đổi gì — không bao giờ gặp phải bất cứ điều gì trong số này, vì không có gì làm mất hiệu lực một handle mà không chỉnh sửa nào chạm tới. Về cách Text, hình chữ nhật ký tự, và ranh giới từ hoạt động trên một trang chưa sửa đổi, bài viết đồng hành về trích xuất văn bản với PDFiumPas bao phủ nền tảng đó mà không có vòng đời cache trang văn bản mà bài viết này thêm vào
Vòng đời cache quan trọng nhất trong các luồng công việc chỉnh sửa rồi ngay lập tức hành động trên kết quả: đóng dấu một bản sửa và tìm kiếm nó, che khuất một đoạn văn và xác nhận nó đã biến mất, hay định vị một cụm từ để neo một chú thích markup ngay sau khi chèn văn bản gần nó. Trường hợp cuối cùng đó đáng được nêu riêng — các chú thích markup quad-point được định vị từ các hình chữ nhật ký tự đọc từ trang văn bản, nên một chú thích được xây dựng từ tọa độ ghi lại trước một chỉnh sửa kết thúc bằng việc highlight sai chỗ một khi chỉnh sửa đó đáp xuống
API chỉnh sửa và văn bản của TPdf là một phần của PDFium Component dành cho Delphi và C++Builder, và trang sản phẩm mang đầy đủ tài liệu tham khảo phương thức cho các bề mặt chỉnh sửa, trích xuất, và tìm kiếm được nói đến ở đây