Bài viết kỹ thuật

Bộ lọc màu PDF cho người thị lực kém trong Delphi bằng PDFium

Một người đọc thị lực kém không thể nhìn rõ chữ đen trên trang trắng ở độ tương phản mặc định, vì vậy họ yêu cầu chế độ tối. Câu trả lời ngây thơ là đảo ngược mọi pixel của trang đã render. Nó ra mắt trong một tuần và hỏng ngay ngày hôm sau: ảnh chụp quét lại trông như phim âm bản, các vết đánh dấu highlighter màu vàng của người đọc biến thành vệt xanh khó đọc, và ai đó hỏi tại sao bản in ra lại đen kịt. Tính năng này thực sự đáng để xây dựng và thực sự dễ làm sai một nửa, và khoảng cách giữa hai kết quả đó chỉ là một ý tưởng: mỗi quyết định về màu sắc thuộc về một điểm cụ thể trong pipeline render, và đảo ngược màu là công cụ sai được áp dụng ở giai đoạn sai. Mã nguồn ở đây dùng PDFium Component, viewer dựa trên PDFium cho Delphi, C++Builder và Lazarus, có API render phơi bày các giai đoạn đó một cách tách biệt

Bộ lọc là trạng thái hiển thị, không bao giờ là trạng thái tài liệu

Một quy tắc ngăn chặn loại lỗi tệ nhất ở đây: chế độ đọc thay đổi cách bitmap được tạo ra hoặc xử lý hậu kỳ, và không gì khác. Byte của PDF vẫn giữ nguyên, mọi chế độ đều có thể đảo ngược bằng cách render lại, và "lưu" không bao giờ ghi một hình thức đã lọc trở lại vào file. Điều này nghe có vẻ hiển nhiên cho đến khi một người thẩm định pháp lý in một hợp đồng dưới bộ lọc đang bật và nộp bản đã đảo ngược màu. Đến lúc đó, câu hỏi "in ấn dùng hình thức riêng của tài liệu hay hình thức trên màn hình" hóa ra xứng đáng có một câu trả lời rõ ràng trong đặc tả của bạn, chứ không phải một tai nạn của đường đi mã nguồn. Giữ thiết lập bộ lọc trong trạng thái viewer, áp dụng nó lúc render, và bắt mọi đường xuất khẩu phải khai báo nó dùng hình thức nào

Quy tắc này tự trả giá hai lần. Khả năng đảo ngược có sẵn miễn phí, bởi vì chuyển chế độ sẽ render lại từ nguồn không đổi: không có ngăn xếp undo nào cần duy trì và không có cách nào để một chuỗi thay đổi chế độ làm suy giảm trang. Các kịch bản nhiều cửa sổ vẫn nhất quán vì cùng lý do đó. Hai khung nhìn của một tài liệu có thể chạy các chế độ khác nhau, vì mỗi khung nhìn sở hữu trạng thái hiển thị riêng trong khi đối tượng tài liệu vẫn dùng chung

Render trước, biến đổi sau

Mẫu hình được hỗ trợ là xử lý bitmap sau render: RenderPage tạo ra raster của trang, sau đó một lượt biến đổi điều chỉnh nó. Component phát hành ba phép biến đổi dưới dạng thao tác bitmap tại chỗ, InvertPdfBitmap, DuotonePdfBitmapGrayscalePdfBitmap, khiến việc chuyển chế độ trở thành một hàm hai giai đoạn gọn gàng:

Sơ đồ pipeline chế độ đọc của một trình xem PDF Delphi, nơi một lượt gọi RenderPage PDFium nạp cho bốn chế độ đọc, mỗi chế độ là một phép biến đổi bitmap tại chỗ như InvertPdfBitmap hay DuotonePdfBitmap
RenderPage tạo raster một lần và chế độ đọc đang hoạt động chọn một phép biến đổi bitmap tại chỗ
function TViewerForm.RenderWithMode(W, H: Integer): TBitmap;
begin
  Result := Pdf.RenderPage(0, 0, W, H, ro0, [reAnnotations]);
  case FReadingMode of
    rmInverted:     InvertPdfBitmap(Result);
    rmHighContrast: DuotonePdfBitmap(Result, clBlack, $0000C8FF);  // nền tối, chữ hổ phách
    rmGrayscale:    GrayscalePdfBitmap(Result);
  end;
  // rmNormal đi qua không đổi: tài liệu giữ nguyên màu của chính nó
end;

Hai điều rút ra từ thiết kế này. Thứ nhất, chi phí biến đổi tỷ lệ thuận với kích thước bitmap, vì vậy công việc này nên nằm ở bất cứ đâu bạn cache kết quả render: lọc bitmap đã cache một lần, không phải mỗi lần vẽ. Thứ hai, vì phép biến đổi chạy trên raster hoàn chỉnh, nó tác động đến văn bản, hình vector, hình ảnh và hình thức chú thích theo cùng một cách. Sự đồng nhất đó chính là điều mà phép đảo ngược đơn thuần làm sai đối với ảnh chụp. Đó là lý do phép biến đổi duotone tạo ra mặc định tốt hơn cho tài liệu nhiều chữ, vì nó ánh xạ độ sáng lên một dải màu từ tối đến sáng được chọn thay vì đảo ngược sắc độ; phép đảo ngược vẫn có sẵn như một lựa chọn rõ ràng cho những người đọc muốn dùng nó. Cạnh chữ sắc nét hơn là một đòn bẩy riêng biệt. Tùy chọn render reNoSmoothText tắt khử răng cưa văn bản lúc render và kết hợp tốt với chế độ tương phản cao ở mức zoom lớn

Hai chế độ grayscale không đồng nhất

Các tùy chọn render bao gồm reGrayscale, trông như một đường tắt bỏ qua bước xử lý hậu kỳ. Nó không phải cùng một phép toán:

Sơ đồ so sánh tùy chọn render reGrayscale của PDFium, vốn khử bão hòa ảnh nhưng bỏ sót các tiêu đề màu, với bước hậu xử lý GrayscalePdfBitmap của Delphi chuyển đổi toàn bộ trang
Tùy chọn engine khử bão hòa nội dung ảnh trong khi hậu kỳ chuyển đổi từng pixel của bitmap hoàn chỉnh
// Cấp độ engine: grayscale được áp dụng trong khi raster hóa
GrayA := Pdf.RenderPage(0, 0, W, H, ro0, [reGrayscale]);

// Xử lý hậu kỳ: render bằng màu, chuyển đổi bitmap đã hoàn thành
GrayB := Pdf.RenderPage(0, 0, W, H);
GrayscalePdfBitmap(GrayB);

Tùy chọn cấp độ engine áp dụng cho đầu ra raster của nội dung hình ảnh nhưng không chạm đến các vùng tô vector hay màu chữ, vì vậy một trang có tiêu đề màu có thể quay lại với ảnh chụp màu xám nhưng tiêu đề vẫn ngoan cố màu xanh. GrayscalePdfBitmap áp lên bitmap đã hoàn thành sẽ chuyển đổi mọi thứ, vô điều kiện. Tùy chọn render vẫn xứng đáng có chỗ đứng khi bạn muốn hình ảnh mất bão hòa màu trong khi vẫn giữ màu chữ như một tín hiệu, điều mà một số người đọc thị lực kém đặc biệt ưa thích. Nhưng nếu yêu cầu ghi là "trang grayscale", xử lý hậu kỳ là phiên bản thỏa mãn nó. Dù bạn chọn đường nào, hãy nhớ cả hai kiểu overload của RenderPage. Dạng hàm trả về một bitmap mà bên gọi sở hữu và phải giải phóng, và điều đó có ý nghĩa ngay khi các bộ lọc nhân số lượng bitmap đã render đang tồn tại cùng lúc

Nền, dấu chọn, và cái bẫy PageColor

Không phải mọi điều chỉnh tiện nghi đều là một phép biến đổi. Thay nền trang trắng bằng một tông màu ấm thường đã đủ cho những người đọc nhạy cảm với chói sáng, và nó có một thuộc tính riêng. Thuộc tính này mang theo một quy tắc phạm vi bẫy nhiều người:

Sơ đồ bẫy phạm vi PageColor trong một trình xem PDF Delphi: sắc màu hiện trên màn hình trong khi đầu ra RenderPage vẫn trắng trừ khi màu được truyền tường minh
PageColor nhuộm màu chỉ cho khung nhìn trên màn hình và RenderPage giữ trang trắng trừ khi màu được truyền tường minh
// Chỉ ảnh hưởng đến khung nhìn trên màn hình
PdfView.PageColor := $00D9EDF2;  // tông giấy ấm phía sau nội dung trang

// Đầu ra của RenderPage bỏ qua PageColor; truyền màu một cách rõ ràng
Bmp := Pdf.RenderPage(0, 0, W, H, ro0, [], $00D9EDF2);

PageColor thay đổi những gì TPdfView hiển thị, nhưng các bitmap tạo ra qua RenderPage vẫn giữ màu trắng mặc định trừ khi tham số Color chỉ định khác. Triệu chứng khá đáng tin cậy: màn hình hiển thị trang đã nhuộm màu, người dùng xuất hoặc in, và đầu ra quay về màu trắng. Xếp việc này vào cùng quyết định chính sách xuất khẩu từ phần đầu tiên

Các thuộc tính màu còn lại định nghĩa các dấu phủ lớp: HighlightColor cho kết quả tìm kiếm, SelectionColor cho vùng chọn văn bản của người dùng, ReadingWordColor cho con trỏ theo dõi từ đang được đọc. Mỗi thuộc tính đó phải được kiểm tra lại dưới mọi bộ lọc bạn cung cấp. Một con trỏ đọc màu hổ phách hoạt động tốt trên nền trắng sẽ biến mất sau khi đảo ngược màu; một vùng chọn xanh nhạt biến mất vào nền tương phản cao. Duy trì bảng màu phủ lớp riêng cho từng chế độ thay vì một bộ toàn cục, và cố ý kiểm thử các tổ hợp. Bộ lọc kết hợp với chuyển văn bản thành giọng nói là một cấu hình bình thường đối với những người đọc mà tính năng này phục vụ, không phải một trường hợp biên. Bản thân cơ chế phủ lớp được đề cập trong bài viết về trình đọc hỗ trợ tiếp cận

Con số, xác minh, và câu hỏi về in ấn

WCAG 2.1 biến tính năng này thành thứ có thể đo lường được. Tiêu chí thành công 1.4.3 yêu cầu tỷ lệ tương phản 4,5:1 cho văn bản chính, và 1.4.6 nâng nó lên 7:1 cho tương phản nâng cao. Kiểm tra mẫu chế độ tương phản cao của bạn theo các tỷ lệ đó bằng một công cụ phân tích tương phản chạy trên đầu ra render thực tế. Văn bản đè lên hình ảnh và văn bản trong các trường form là những nơi tỷ lệ âm thầm thất bại ngay cả khi văn bản chính đạt yêu cầu

In ấn xứng đáng có quyết định riêng của nó, và mặc định có thể biện minh được là hình thức riêng của tài liệu, với "in như hiển thị" được cung cấp như một lựa chọn rõ ràng của người dùng. Một trang in là bằng chứng trong nhiều quy trình làm việc hơn những gì tác giả viewer thường mong đợi, và một bản in đảo ngược màu của một hợp đồng là một sự cố hỗ trợ mang màu sắc pháp lý. Còn một sự kết hợp nữa quan trọng đối với hiệu năng: render có lọc nhân đôi khối lượng công việc bitmap ở mỗi lần chuyển chế độ, vì vậy đừng áp dụng phép biến đổi ở mỗi thông điệp vẽ. Cache bitmap đã lọc và chỉ chạy lại phép biến đổi khi trang, mức zoom hoặc chế độ thực sự thay đổi. Chiến lược caching giúp điều này rẻ nằm trong bài viết về cache render và hiệu năng zoom

Một điều cần quyết định trong giao diện của bạn thay vì trong mã nguồn: chế độ nào là mặc định đúng. Không có một câu trả lời duy nhất, vì vậy hãy cung cấp cả bộ và để người đọc chọn. Tương phản cao phù hợp với phần lớn việc đọc nhiều chữ, đảo ngược màu phù hợp với người đọc muốn cụ thể là sáng-trên-tối, grayscale cắt giảm nhiễu màu, và một tông nền phù hợp cho độ nhạy chói sáng. Lưu lại lựa chọn theo từng người dùng, khôi phục nó khi khởi động, và giữ một đường quay về chế độ bình thường chỉ với một phím bấm, vì một người đọc rơi vào một chế độ họ không thể đọc được cần một lối thoát nhanh

Các tùy chọn render, phép biến đổi bitmap và thuộc tính màu khung nhìn dùng ở đây được phát hành cùng PDFium Component cho Delphi, C++Builder và Lazarus/FPC, với đầy đủ mã nguồn để các phần triển khai biến đổi có thể được kiểm tra hoặc mở rộng