Hầu hết các trang PDF raster hóa trong vài mili giây và bạn không bao giờ nghĩ đến nó. Rồi một người dùng mở bản vẽ kỹ thuật A1, một trang chứa hàng chục nghìn nét vector, hoặc một poster chật kín nhóm độ trong suốt và soft mask, và lời gọi duy nhất để vẽ nó mất hai hoặc ba giây. Nếu lời gọi đó chạy trên luồng UI, cửa sổ ngừng vẽ lại, thanh tiêu đề mờ đi, và hệ điều hành đề nghị kill ứng dụng. Công việc là hợp lệ. Trang thực sự cần thời gian đó. Lỗi là kết xuất là một lời gọi blocking không thể chia nhỏ, không có cách nào ngoi lên thở và không có cách nào dừng lại
Bài viết này nói về chính xác một trong hai vấn đề đó: hủy một kết xuất trang đơn dài mà không đóng băng UI. Người dùng đã nhấp sang trang tiếp theo, hoặc thu phóng, hoặc đóng tài liệu, và kết xuất đang thực hiện bây giờ là công việc lãng phí nên kết thúc vào cơ hội tiếp theo thay vì chạy đến hoàn thành. Làm mượt cuộn và thu phóng bằng cách lưu vào bộ nhớ đệm những gì đã được raster hóa là một mối quan tâm riêng với thiết kế riêng của nó, được đề cập trong bài viết đồng hành liên kết ở cuối. Ở đây câu hỏi duy nhất là làm thế nào để một kết xuất tiến trình đáp ứng yêu cầu hủy nhanh chóng và sạch sẽ
API kết xuất tiến trình mà PDFium đã tích hợp sẵn
PDFium đã dự đoán được nửa vấn đề đóng băng. Cùng với FPDF_RenderPageBitmap một lần chụp, nó expose một biến thể tiến trình chia một trang thành các khối công việc. Bạn gọi FPDF_RenderPageBitmap_Start một lần để thiết lập kết xuất với một bitmap đích, sau đó gọi FPDF_RenderPage_Continue lặp lại. Mỗi lần Continue raster hóa một lát có giới hạn và trả về trạng thái. FPDF_RENDER_TOBECONTINUED có nghĩa là còn việc phải làm, FPDF_RENDER_DONE có nghĩa là trang đã xong, và FPDF_RENDER_FAILED có nghĩa là nó dừng vì lỗi. Khi vòng lặp kết thúc bạn gọi FPDF_RenderPage_Close để giải phóng trạng thái tiến trình mỗi trang. Vì quyền điều khiển trả về code của bạn giữa các lát, bạn có thể pump messages, cập nhật chỉ báo tiến trình, hoặc kiểm tra xem công việc có còn cần thiết không
Cơ chế PDFium cung cấp để quyết định khi nào nên yield là một callback struct có tên IFSDK_PAUSE. Bạn trao nó cho Start và cho mỗi Continue. Sau mỗi khối PDFium gọi con trỏ hàm NeedToPauseNow của nó, và nếu nó trả về giá trị khác không, Continue hiện tại dừng sớm và trả về quyền điều khiển với FPDF_RENDER_TOBECONTINUED. Struct cũng mang trường version, phải được đặt là 1, và con trỏ user dạng tự do mà PDFium không bao giờ chạm đến và truyền qua nguyên vẹn. Con trỏ chưa được chạm đó là toàn bộ bản lề của thiết kế tiếp theo
Tái dụng pause như cancel
Mục đích ban đầu của NeedToPauseNow là time-slicing. Trả về khác không khi ngân sách frame của bạn đã hết, trả về không để tiếp tục kết xuất, và PDFium dừng để bạn có thể làm gì đó khác trước khi tiếp tục cùng kết xuất đó. PDFium Component tái sử dụng cùng tín hiệu đó cho một hành động khác. Thay vì trả lời "tôi có nên dừng và cho bạn tiếp tục không," callback trả lời "công việc này đã bị hủy chưa." Hai cái ánh xạ lên nhau sạch sẽ vì điều vòng lặp làm khi thấy cờ. Một pause thực sự kỳ vọng một Continue sau đó; một cancel thì không. Khi vòng lặp gọi quan sát thấy token đã bị hủy, nó đóng context kết xuất và không bao giờ gọi Continue lại, vì vậy cùng giá trị khác không mà PDFium đọc như "dừng khối này" trở thành, về thực chất, "dừng hẳn."
Việc hủy được biểu hiện qua một interface, IPdfCancellationToken, thuộc tính IsCancelled của nó lật từ false sang true khi một phần khác của chương trình yêu cầu dừng kết xuất. Cầu nối giữa interface Pascal đó và callback C của PDFium là một con trỏ duy nhất. Tham chiếu interface của token được ghi vào IFSDK_PAUSE.user, và một callback cdecl tĩnh đọc nó ra và truy vấn nó. Đây là vấn đề cổ điển khi để thư viện C gọi ngược vào Pascal: callback phải là một hàm thuần với quy ước gọi C, không phải một phương thức, vì PDFium lưu và gọi một con trỏ hàm trần không biết gì về đối tượng Pascal hay Self
type
TPdfProgressivePause = record
Pause: IFSDK_PAUSE; // PDFium reads this; .user holds the token
Token: IPdfCancellationToken; // strong ref keeps the token alive
end;
function ProgressivePauseCallback(pThis: PIFSDK_PAUSE): FPDF_BOOL; cdecl;
var
Token: IPdfCancellationToken;
begin
Result := 0;
if (pThis = nil) or (pThis^.user = nil) then
Exit;
Token := IPdfCancellationToken(pThis^.user);
if Token.IsCancelled then
Result := 1; // non-zero: PDFium stops this chunk
end;
Callback phục hồi token bằng cách cast pThis^.user trở lại kiểu interface và đọc IsCancelled. Không có gì trong đó phân bổ, khóa, hoặc block, điều này quan trọng vì PDFium gọi nó trên luồng kết xuất sau mỗi khối và bất kỳ công việc nào thực hiện ở đây đều được cộng vào chi phí của bản thân kết xuất. Việc bảo vệ chống struct nil hoặc trường user nil có nghĩa là cùng một hàm có thể cài đặt an toàn ngay cả trên một kết xuất chưa bao giờ được cấp token thực
Giữ token sống xuyên suốt vòng lặp
Cast một con trỏ interface qua một Pointer thô và trở lại là nơi các lỗi lifetime được sinh ra. Một IInterface trong Delphi được đếm tham chiếu, và số đếm chỉ di chuyển khi trình biên dịch có thể thấy một biến kiểu interface đang được gán. Lưu trữ token chỉ như một con trỏ trần bên trong IFSDK_PAUSE.user sẽ ẩn nó hoàn toàn khỏi bộ đếm tham chiếu. Nếu tham chiếu duy nhất khác đến token đó ra khỏi phạm vi trong khi vòng lặp Continue vẫn đang chạy, đối tượng sẽ bị giải phóng bên dưới callback, và khối tiếp theo sẽ dereference một con trỏ dangling
Đó là lý do tại sao descriptor là một bản ghi chứa hai thứ, không phải một. Trường Pause là struct mà PDFium đọc. Trường Token là một tham chiếu kiểu interface thực mà trình biên dịch đếm, và nó tồn tại không vì lý do nào khác ngoài việc ghim token trong bộ nhớ miễn là bản ghi còn sống. Bản ghi là một biến cục bộ trên stack của thủ tục kết xuất, vì vậy nó vẫn hợp lệ trong suốt thời gian của vòng lặp và chỉ bị phá hủy khi thủ tục thoát. Con trỏ trần trong user và tham chiếu được đếm trong Token đặt tên cho cùng một đối tượng; một là thứ PDFium có thể đọc, cái kia là thứ giữ đối tượng đó không bị thu gom
var
Pause: TPdfProgressivePause;
EffectiveToken: IPdfCancellationToken;
begin
// ... choose EffectiveToken ...
// Strong ref first, then publish the same object to PDFium via .user.
Pause.Token := EffectiveToken;
Pause.Pause.version := 1;
Pause.Pause.NeedToPauseNow := ProgressivePauseCallback;
Pause.Pause.user := Pointer(EffectiveToken);
Đóng context kết xuất bất kể vòng lặp kết thúc thế nào
Mỗi lời gọi đến FPDF_RenderPageBitmap_Start phân bổ trạng thái tiến trình mà PDFium liên kết với trang, và trạng thái đó chỉ được giải phóng bởi FPDF_RenderPage_Close. Có ba cách thoát khỏi vòng lặp drive. Trang hoàn thành và trạng thái cuối cùng là FPDF_RENDER_DONE. Token kích hoạt và vòng lặp thoát sớm báo cáo hủy. Có gì đó thất bại và trạng thái là FPDF_RENDER_FAILED. Cả ba đều phải gọi Close, và đường hủy là đường dễ nhất bị sai, vì hình dạng tự nhiên của "thấy cancel, break ra" có xu hướng bỏ qua dọn dẹp trên đường ra. Để Close không được chạm đến rò rỉ trạng thái mỗi trang, và một trình xem cho phép người dùng hủy kết xuất lần lượt sẽ tích lũy rò rỉ đó trên mỗi trang bị hủy
Hình dạng mạnh mẽ đặt vòng lặp và phân loại kết quả bên trong một try và FPDF_RenderPage_Close trong finally tương ứng. Bitmap đích bị phá hủy trong cùng khối. Việc hủy có thể rời vòng lặp qua một Exit sớm và finally vẫn chạy, vì vậy có chính xác một nơi giải phóng trạng thái tiến trình và nó không thể bị bypass
Status := FPDF_RenderPageBitmap_Start(PdfBmp, FPage, Left, Top,
Width, Height, Ord(Rotation), EncodeRenderOptions(Options), Pause.Pause);
try
while Status = FPDF_RENDER_TOBECONTINUED do
begin
if EffectiveToken.IsCancelled then
begin
Result := prsCancelled;
Exit;
end;
Status := FPDF_RenderPage_Continue(FPage, Pause.Pause);
end;
if EffectiveToken.IsCancelled then
Result := prsCancelled
else if Status = FPDF_RENDER_DONE then
Result := prsDone
else
Result := prsFailed;
finally
// Frees the progressive state Start allocated; mandatory on every path.
FPDF_RenderPage_Close(FPage);
FPDFBitmap_Destroy(PdfBmp);
end;
Vòng lặp kiểm tra token trước mỗi Continue cũng như dựa vào callback bên trong nó. Callback rút ngắn khối hiện tại; kiểm tra vòng lặp dừng khối tiếp theo khỏi việc bắt đầu. Cùng nhau chúng giới hạn thời gian một lần hủy có hiệu lực trong khoảng thời gian của một khối
Ba kết quả, và bitmap chứa gì sau khi hủy
Điểm vào công khai là TPdf.RenderPageProgressive, và nó trả về một TPdfProgressiveStatus là một trong prsDone, prsCancelled, hoặc prsFailed. Các giá trị phản ánh các hằng số FPDF_RENDER_* của PDFium theo cách viết Pascal nhưng gấp trường hợp hủy vào như một kết quả hạng nhất thay vì một lỗi
Điểm mà mọi người vấp phải là bitmap đích chứa gì sau prsCancelled. Nó không trống. PDFium kết xuất tiến trình vào cùng một bitmap khối sau khối, vì vậy khi hủy dừng vòng lặp, bitmap chứa bất cứ thứ gì đã được vẽ cho đến thời điểm đó, đó là một hình ảnh một phần: một số dải đã xong, phần còn lại vẫn hiển thị màu tô. Liệu kết quả một phần đó có hữu ích hay không phụ thuộc vào người gọi. Một trình xem chuẩn bị vứt bỏ bitmap vì người dùng đã điều hướng đến nơi khác có thể bỏ qua nó. Một trình xem muốn hiển thị bản xem trước chi phí thấp có thể giữ nó. Điều bạn không được làm là giả định prsCancelled ngụ ý một bitmap trống hoặc không xác định; nó ngụ ý một ảnh chụp trung thực của một kết xuất chưa hoàn chỉnh
var
Bmp: TBitmap;
Token: IPdfCancellationToken;
Status: TPdfProgressiveStatus;
begin
Bmp := TBitmap.Create;
try
// Token starts un-cancelled; flip Token.IsCancelled from elsewhere
// (a UI action, a navigation event) to abort the render in flight.
Status := Pdf.RenderPageProgressive(Bmp, 0, 0, PageW, PageH, Token);
case Status of
prsDone: Image1.Picture.Assign(Bmp); // fully rendered
prsCancelled: ; // partial bitmap, usually discarded
prsFailed: ShowMessage('Render failed');
end;
finally
Bmp.Free;
end;
end;
Token nil và đường callback không có nhánh
Hủy là tùy chọn. Một người gọi chỉ muốn kết xuất tiến trình vì lợi ích pump messages, mà không có ý định hủy, nên có thể truyền nil cho token. Cách ngây thơ để hỗ trợ điều đó là rải "nếu token được cung cấp" kiểm tra qua callback và vòng lặp, có nghĩa là một nhánh trên mỗi khối và một callback phải xử lý cả token thực và sự vắng mặt của nó
Cài đặt tránh điều đó bằng cách thay thế một singleton khi người gọi không truyền gì. Một token nil được hoán đổi cho PdfNoCancellationToken, một interface mà IsCancelled của nó luôn là false. Từ điểm đó callback và vòng lặp có một token để truy vấn trong mọi trường hợp, vì vậy không cần kiểm tra nil và không cần đường đặc biệt. Token không bao giờ hủy chỉ đơn giản luôn trả lời false, callback luôn trả về không, và kết xuất chạy đến hoàn thành chính xác như một cái không thể hủy sẽ làm. Hành vi tùy chọn được mô hình hóa như một token không bao giờ kích hoạt thay vì như sự vắng mặt của token, điều này giữ cho đường hot thống nhất
// nil -> never-cancel singleton, so the callback path is identical
// whether or not the caller opted into cancellation.
if AToken <> nil then
EffectiveToken := AToken
else
EffectiveToken := PdfNoCancellationToken;
Hình dạng nổi lên thì nhỏ và đáng trình bày lại, vì đó là phần có thể tái sử dụng. Một thư viện C hỗ trợ một callback cho bạn chính xác một kênh để truyền trạng thái vào callback đó, con trỏ user opaque. Đặt một tham chiếu interface Pascal được đếm sau con trỏ đó, giữ một tham chiếu thực thứ hai sống bên cạnh struct để đối tượng không thể bị thu gom giữa lần gọi, và đọc interface trở lại bên trong một hàm cdecl tĩnh. Bọc toàn bộ vòng lặp drive trong một try và giải phóng context native trong finally. Cùng mẫu đó chuyển sang bất kỳ thao tác PDFium tiến trình hoặc được điều khiển bởi callback nào mà code Pascal phải giữ quyền kiểm soát lifetime trong khi C giữ một con trỏ
Hủy chỉ là một nửa của trình xem đáp ứng. Nửa kia là không kết xuất lại các trang bạn đã vẽ, và giữ thu phóng và cuộn mượt mà bằng cách phục vụ các bitmap đã lưu trong bộ nhớ đệm, được đề cập trong bài viết của chúng tôi về bộ nhớ đệm kết xuất và hiệu suất thu phóng. Để biết cách kết xuất có thể hủy phù hợp vào một trình xem hoàn chỉnh cùng điều hướng, chọn và tìm kiếm, xem xây dựng trình xem PDF đầy đủ tính năng với PDFium VCL component. Kết xuất tiến trình được mô tả ở đây đi kèm như một phần của PDFium Component cho Delphi và Lazarus cùng với các API tải, kết xuất và biểu mẫu được đề cập ở những nơi khác trên blog này