Việc kết xuất một trang PDF thành tệp JPEG là hai hoạt động mà mọi người thường gộp chung lại để chạy và sau đó gỡ lỗi riêng biệt. Đầu tiên, bạn raster hóa trang thành một bitmap pixel ở độ phân giải bạn chọn. Sau đó, bạn giao bitmap đó cho một bộ mã hóa JPEG và chọn chất lượng. Component PDFium sở hữu nửa đầu thông qua RenderPage; nửa sau là VCL thuần túy, TJPEGImage từ Vcl.Imaging.jpeg. Ranh giới giữa chúng là nơi tồn tại những quyết định thú vị, bởi vì độ phân giải bạn chọn ở phía kết xuất và chất lượng bạn chọn ở phía mã hóa đánh đổi với nhau và với kích thước tệp theo những cách rất dễ làm sai
Điều cần ghi nhớ trước khi viết bất kỳ mã nào: một trang PDF không có pixel. Nó được mô tả bằng điểm, trong đó một điểm bằng 1/72 inch và trang là một bản vẽ vector được đo bằng các điểm đó. Khi bạn yêu cầu PDFium kết xuất, bạn đang chọn xem bản vẽ đó sẽ chiếu lên bao nhiêu pixel, và lựa chọn đó chính là DPI. Làm sai phép toán số học và bạn sẽ kết xuất một hình thu nhỏ bị mờ khi bạn muốn có bản gốc để in, hoặc bạn sẽ cấp phát một bitmap 200 megapixel cho một thứ gì đó vốn được dự định là bản xem trước 120 pixel
Từ DPI đến kích thước pixel
RenderPage muốn các giá trị pixel số nguyên cho Width và Height, không phải là một DPI. Vì vậy, công việc đầu tiên là chuyển đổi. Một trang báo cáo kích thước của nó bằng điểm thông qua PageWidth và PageHeight (cả hai đều là Double) và quá trình chuyển đổi cũng giống như bất kỳ trình raster hóa nào sử dụng: số pixel bằng số điểm nhân với DPI mục tiêu chia cho 72. Một trang US Letter có kích thước là 612 x 792 điểm. Ở 150 DPI, nó trở thành 1275 x 1650 pixel; ở 72 DPI, nó giữ nguyên 612 x 792, một pixel cho mỗi điểm, đây là trường hợp mà mọi người quên rằng chỉ là đồng nhất
// Pdf.PageNumber must already point at the page you want.
PixelW := Round(Pdf.PageWidth * Dpi / 72);
PixelH := Round(Pdf.PageHeight * Dpi / 72);
Bitmap := Pdf.RenderPage(0, 0, PixelW, PixelH, ro0, [], clWhite);
// ... use Bitmap ...
Bitmap.Free; // the function-form RenderPage hands you ownership
Hai chi tiết trong bốn dòng đó quyết định mã có chính xác hay không. Điều đầu tiên là hình thức chức năng của RenderPage trả về một TBitmap mà bạn sở hữu. PDFium đã cấp phát nó và bước đi; nếu bạn không Free nó trong mỗi lần lặp lại, một đợt hàng loạt trên vài trăm trang sẽ rò rỉ vài trăm bitmap và quá trình này phình to cho đến khi có thứ gì đó đổ vỡ. Chi tiết thứ hai là đối số Color, ở đây là clWhite. Các trang PDF thường được vẽ giả định một nền trắng đục và một trang có độ trong suốt được hiển thị trên sai màu nền sẽ tạo ra các cạnh có màu bùn hoặc các quầng tối lộn xộn. Trắng là mặc định đúng cho hầu hết mọi tài liệu; tham số tồn tại cho những trường hợp hiếm hoi không phải vậy
Các tham số 0, 0 là độ lệch Left và Top vào trang, trong không gian tọa độ được chia tỷ lệ và bạn để chúng bằng 0 trừ khi bạn đang cắt xén. Tham số ro0 là phép xoay: hãy để nó ở mức không và PDFium tôn trọng bất kỳ phép xoay nào mà trang đã khai báo trong mục /Rotate của nó, vì vậy một trang được thiết kế hướng ngang sẽ ra hướng ngang mà bạn không cần phải làm gì cả
Mã hóa bitmap thành JPEG
Một khi bitmap đã tồn tại, JPEG là phần dễ dàng và nó là Delphi thuần túy. TJPEGImage.Assign sao chép bitmap vào, CompressionQuality đặt chất lượng theo thang đo từ 1 đến 100 và SaveToFile ghi tệp. Quy tắc sắp xếp duy nhất là phải đặt chất lượng trước khi bạn lưu, bởi vì nó chi phối quá trình mã hóa mà SaveToFile kích hoạt
uses
Vcl.Graphics, Vcl.Imaging.jpeg, PDFium;
procedure SavePageAsJpeg(Pdf: TPdf; PageNumber, Dpi, Quality: Integer;
const FileName: string);
var
Bitmap: TBitmap;
Jpeg: TJPEGImage;
begin
Pdf.PageNumber := PageNumber;
Bitmap := Pdf.RenderPage(0, 0,
Round(Pdf.PageWidth * Dpi / 72),
Round(Pdf.PageHeight * Dpi / 72),
ro0, [], clWhite);
try
Jpeg := TJPEGImage.Create;
try
Jpeg.Assign(Bitmap);
Jpeg.CompressionQuality := Quality; // 1..100
Jpeg.SaveToFile(FileName);
finally
Jpeg.Free;
end;
finally
Bitmap.Free;
end;
end;
Khối try/finally lồng nhau đó trông có vẻ cầu kỳ đối với một trình trợ giúp một trang và nó hoàn toàn phù hợp cho một lô. Khối bên trong giải phóng bộ mã hóa, khối bên ngoài giải phóng bitmap và nếu một trong hai khối kích hoạt ngoại lệ thì nó vẫn giải phóng tài nguyên nó sở hữu. Thu gọn chúng thành một khối và một ngoại lệ trong quá trình mã hóa có thể khiến bitmap bị kẹt. Trong một thời gian dài, đó là sự khác biệt giữa một trình chuyển đổi hoàn tất và một trình chuyển đổi chết ở trang 300 với tệp bị hỏng và hộp thoại hết bộ nhớ
Chọn đồng thời DPI và chất lượng
Hai núm điều chỉnh này không độc lập với mục đích của đầu ra và sai lầm phổ biến là vặn cả hai lên vì lý do thận trọng. Hình thu nhỏ web được hiển thị ở 300 DPI và được lưu ở chất lượng 95 có kích thước vài trăm kilobyte nhưng lại giả vờ là hình ảnh 120 pixel; trình duyệt sẽ ném đi gần như toàn bộ hình ảnh đó khi thu nhỏ lại. Kết hợp độ phân giải với số pixel mà đầu ra thực sự cần, sau đó chọn chất lượng sống sót qua quá trình nén có tổn hao của JPEG mà không có các lỗi lạ có thể nhìn thấy
| Đầu ra | DPI | Chất lượng JPEG |
|---|---|---|
| Danh sách hình thu nhỏ | 72 | 60-70 |
| Xem trước trên màn hình | 96-150 | 80-85 |
| Xem chi tiết độ nét cao | 200-300 | 85-95 |
| Bản gốc in | 300-600 | 90-100 |
Chất lượng JPEG xứng đáng với một lời cảnh báo riêng. Nó không phải là một mặt số tuyến tính. Việc tăng từ 70 lên 85 sẽ đem lại sự cải thiện rõ rệt về mặt hình ảnh cho mức tăng kích thước tệp khiêm tốn; việc tăng từ 95 lên 100 gần như tăng gấp đôi kích thước tệp với một sự khác biệt mà hầu như không ai có thể nhận ra, vì chất lượng 100 vẫn không phải là chất lượng không bị giảm đi (lossless), nó chỉ dừng việc loại bỏ nhiều thứ. Đối với các trang chứa nhiều văn bản, quá trình nén dựa trên khối của JPEG làm mờ các cạnh sắc nét của các glyph thành các đường viền mờ nhạt (ringing), đó là lý do tại sao chất lượng dưới 80 làm cho văn bản trông giống như được quét trên một trang đáng lẽ phải là kết quả đầu ra rõ ràng. Nếu các trang hầu hết là văn bản và bạn có thể thay đổi định dạng, định dạng PNG sẽ kết xuất văn bản đó mà không gặp phải hiệu ứng đường viền mờ; định dạng JPEG xứng đáng có chỗ đứng đối với nội dung nhiếp ảnh và hỗn hợp, nơi hiệu quả nén của nó thực sự nhỏ hơn
Hình thu nhỏ nhanh hơn, nhỏ hơn
Khi mục tiêu là hình thu nhỏ thay vì bản sao trung thực, bạn có thể yêu cầu trình kết xuất thực hiện ít công việc hơn. Tham số Options lấy một tập hợp các cờ TRenderOption và một số cờ trong số đó đánh đổi độ trung thực lấy tốc độ chính xác theo cách mà một bản xem trước nhỏ mong muốn. reGrayscale bỏ màu sắc, vừa kết xuất nhanh hơn vừa tạo ra bitmap nhỏ hơn để mã hóa. reNoSmoothImage và reNoSmoothPath bỏ qua bước làm mờ chống răng cưa dù sao cũng không thể nhìn thấy ở tỷ lệ hình thu nhỏ
function RenderThumbnail(Pdf: TPdf; PageNumber, MaxW, MaxH: Integer): TBitmap;
var
Scale: Double;
begin
Pdf.PageNumber := PageNumber;
// Fit the page inside MaxW x MaxH while preserving aspect ratio.
Scale := Min(MaxW / Pdf.PageWidth, MaxH / Pdf.PageHeight);
Result := Pdf.RenderPage(0, 0,
Round(Pdf.PageWidth * Scale),
Round(Pdf.PageHeight * Scale),
ro0, [reGrayscale, reNoSmoothImage], clWhite);
end;
Trường hợp hình thu nhỏ cũng cho thấy một cách suy nghĩ rõ ràng hơn về việc định kích cỡ. Thay vì thông qua DPI, hãy tính toán một hệ số tỷ lệ duy nhất phù hợp với trang bên trong hộp giới hạn và duy trì tỷ lệ khung hình, đó là điều mà giá trị Min của hai tỷ lệ sẽ thực hiện. Cả trang dọc và trang ngang đều kết thúc bên trong cùng một hộp mà không bị bóp méo, và bạn không bao giờ phải suy luận xem DPI nào tương ứng với việc "vừa vặn trong không gian 200 x 280." Một cảnh báo với reGrayscale: nó chuyển đổi nội dung hình ảnh raster sang màu xám, nhưng các hình tô vector và văn bản vẫn giữ nguyên giá trị màu sắc của chúng trong công cụ, vì vậy một trang phần lớn là nghệ thuật vector có thể trả về một kết quả không đơn sắc như tên gọi của cờ gợi ý. Để có được kết quả toàn thang xám thực sự, việc chuyển đổi bitmap đã kết xuất bằng hàm GrayscalePdfBitmap là con đường đáng tin cậy
Xử lý hàng loạt toàn bộ tài liệu
Việc kết hợp nó với nhau cho toàn bộ tài liệu là một vòng lặp trên PageCount, với PageNumber di chuyển mỗi lần một trang. Các trang dựa trên chỉ số 1: trang một là PageNumber := 1 và vòng lặp chạy đến giá trị bao gồm PageCount, chứ không phải PageCount - 1. Một điều khác mà quá trình chạy hàng loạt phải tuân thủ là nguyên tắc tải im lặng. Việc thiết lập Active := True không bao giờ gây lỗi đối với tệp bị hỏng hoặc sai mật khẩu; nó chỉ để giá trị của Active ở False. Hãy kiểm tra giá trị của nó trước khi bạn hiển thị một trang duy nhất, nếu không lệnh RenderPage đầu tiên sẽ làm việc trên một tài liệu chưa bao giờ được mở
procedure ExportAllPages(const PdfPath, OutDir: string; Dpi, Quality: Integer);
var
Pdf: TPdf;
I, Digits: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := PdfPath;
Pdf.Active := True;
if not Pdf.Active then
raise Exception.Create('Could not open ' + PdfPath);
Digits := Length(IntToStr(Pdf.PageCount)); // zero-pad so files sort right
for I := 1 to Pdf.PageCount do
SavePageAsJpeg(Pdf, I, Dpi, Quality,
Format('%s\page_%.*d.jpg', [OutDir, Digits, I]));
finally
Pdf.Active := False;
Pdf.Free;
end;
end;
Việc thêm số 0 ở đầu thông qua Digits là một việc nhỏ giúp tiết kiệm được cả buổi chiều sau này. Việc đặt tên tệp từ page_1.jpg đến page_10.jpg và dùng bất kỳ công cụ nào phân loại chúng dưới dạng chuỗi sẽ xếp page_10 nằm ngay sau page_1, làm xáo trộn thứ tự. Thêm vùng đệm (padding) tới chiều rộng của số trang cao nhất, vì vậy một tài liệu 300 trang sẽ tạo ra page_001.jpg, giữ cho thứ tự phân loại và thứ tự trang giống hệt nhau ở mọi nơi sau này
Đối với các tài liệu đủ lớn mà quá trình chuyển đổi mất một khoảng thời gian đáng chú ý, hãy chạy lệnh đó ngoài luồng giao diện người dùng (UI) hoặc chạy truyền tin nhắn (pump messages) giữa các trang để ứng dụng vẫn duy trì tính phản hồi và cho người dùng một cách để dừng lệnh. Nếu bạn đang kết xuất các trang rất lớn và muốn một tính năng hủy lệnh chen ngang chừng ngay giữa trang thay vì chỉ dừng ở ranh giới giữa các trang, thì Component PDFium có một quy trình kết xuất tiến triển (progressive render path) cùng với token để hủy lệnh; đó là một cơ chế nặng hơn cơ chế mà hầu hết các lệnh kết xuất hàng loạt cần đến, nhưng nó có sẵn khi chính bản thân một trang đơn ở 600 DPI đã đủ chậm để chặn ứng dụng
Một điểm cuối cùng cần biết. Việc raster hóa một trang sẽ loại bỏ lớp văn bản của nó: JPEG là dạng điểm ảnh (pixels), và những từ ngữ trong đó sẽ không còn được chọn hay tìm kiếm được nữa. Khi bạn cần cả hình ảnh và văn bản gốc đính kèm, hãy kết xuất hình ảnh và trích xuất riêng phần chữ, phần được đề cập tới trong một bài viết chia sẻ về việc trích xuất văn bản từ tệp PDF bằng Component PDFium. Cả các kiểu quá tải (overloads) RenderPage và các tuỳ chọn kết xuất hiển thị tại đây là một phần của Component PDFium dành cho Delphi và C++Builder