Seorang pembaca low-vision tidak dapat mengenali teks hitam di atas halaman putih pada kontras default, sehingga mereka meminta sebuah dark mode. Jawaban yang naif adalah membalik (invert) setiap piksel dari halaman yang dirender. Fitur itu dikirimkan dalam satu minggu dan rusak keesokan harinya: foto hasil pindai kembali terlihat seperti negatif film, tanda highlighter kuning milik pembaca berubah menjadi coretan biru yang tidak terbaca, dan seseorang bertanya mengapa hasil cetakannya keluar hitam pekat. Fitur ini sungguh-sungguh layak dibangun dan sungguh-sungguh mudah dikerjakan setengah benar, dan celah antara kedua hasil itu adalah satu gagasan: setiap keputusan warna berada pada titik tertentu dalam render pipeline, dan inversi adalah alat yang salah yang diterapkan pada tahap yang salah. Kode di sini menggunakan PDFium Component, viewer berbasis PDFium untuk Delphi, C++Builder, dan Lazarus, yang rendering API-nya mengekspos tahapan-tahapan itu secara terpisah
Filter adalah presentation state, bukan document state
Satu aturan mencegah kategori bug terburuk di sini: sebuah reading mode mengubah cara bitmap diproduksi atau dipasca-proses, dan tidak ada yang lain. Byte PDF tetap tidak tersentuh, setiap mode dapat dibalik (reversible) dengan me-render ulang, dan "save" tidak pernah menuliskan sebuah tampilan yang sudah difilter kembali ke dalam file. Ini terdengar jelas sampai seorang peninjau hukum mencetak sebuah kontrak di bawah filter yang aktif dan mengarsipkan versi yang terbalik itu. Pada titik itu, pertanyaan "apakah pencetakan menggunakan tampilan asli dokumen atau tampilan layar" ternyata layak mendapat jawaban eksplisit dalam spek Anda, bukan sekadar kebetulan dari jalur kode. Simpan pengaturan filter dalam viewer state, terapkan itu saat render, dan buat setiap jalur ekspor menyatakan tampilan mana yang digunakannya
Aturan ini terbayar dua kali lipat. Reversibilitas datang secara gratis, karena berpindah mode me-render ulang dari sumber yang tidak berubah: tidak ada undo stack yang harus dipelihara dan tidak ada cara bagi rentetan perubahan mode untuk menurunkan kualitas halaman. Skenario multi-jendela tetap koheren karena alasan yang sama. Dua tampilan dari satu dokumen dapat menjalankan mode yang berbeda, karena setiap tampilan memiliki presentation state-nya sendiri sementara objek dokumen tetap dibagikan bersama
Render dulu, transform kemudian
Pola yang didukung adalah pemrosesan bitmap pasca-render: RenderPage menghasilkan raster halaman, lalu sebuah pass transform menyesuaikannya. Komponen ini menyertakan tiga transform sebagai operasi bitmap in-place, InvertPdfBitmap, DuotonePdfBitmap, dan GrayscalePdfBitmap, yang menjadikan pergantian mode sebuah fungsi dua tahap yang bersih:
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); // bg gelap, teks amber
rmGrayscale: GrayscalePdfBitmap(Result);
end;
// rmNormal jatuh ke bawah (fall through): dokumen mempertahankan warnanya sendiri
end;
Dua hal mengikuti dari desain ini. Pertama, biaya transform sebanding dengan ukuran bitmap, sehingga pekerjaan itu sebaiknya berada di mana pun hasil render Anda di-cache: filter bitmap yang di-cache satu kali, bukan pada setiap paint. Kedua, karena transform berjalan pada raster yang sudah jadi, ia mengenai teks, vector art, gambar, dan tampilan anotasi dengan cara yang sama. Keseragaman itu justru yang salah dilakukan oleh inversi biasa terhadap foto. Itulah alasan mengapa transform duotone menjadi default yang lebih baik untuk dokumen padat teks, karena ia memetakan luminance ke sebuah ramp warna gelap-ke-terang pilihan, bukan menegasikan hue; inversi tetap tersedia sebagai pilihan eksplisit bagi pembaca yang menginginkannya. Tepi glyph yang lebih tajam adalah tuas yang terpisah. Opsi render reNoSmoothText mematikan anti-aliasing teks saat render dan berpasangan baik dengan mode high-contrast pada zoom besar
Dua grayscale yang tidak sepakat
Opsi render mencakup reGrayscale, yang terlihat seperti jalan pintas melewati langkah pasca-proses. Itu bukan operasi yang sama:
// Level-engine: grayscale diterapkan selama rasterisasi
GrayA := Pdf.RenderPage(0, 0, W, H, ro0, [reGrayscale]);
// Pasca-proses: render dalam warna, konversi bitmap yang sudah jadi
GrayB := Pdf.RenderPage(0, 0, W, H);
GrayscalePdfBitmap(GrayB);
Opsi level-engine berlaku pada output raster dari konten gambar tetapi tidak menjangkau fill vector atau warna teks, sehingga sebuah halaman dengan heading berwarna bisa kembali dengan foto berwarna abu-abu dan heading biru yang tetap membandel. GrayscalePdfBitmap pada bitmap yang sudah jadi mengonversi segalanya, tanpa syarat. Opsi render itu tetap mendapat tempatnya ketika Anda ingin gambar didesaturasi sambil mempertahankan warna teks sebagai sinyal, yang secara khusus disukai oleh sebagian pembaca low-vision. Tetapi jika kebutuhannya berbunyi "halaman grayscale", pasca-proses adalah versi yang memenuhinya. Jalur mana pun yang Anda pilih, ingatlah kedua gaya overload RenderPage. Bentuk fungsinya mengembalikan sebuah bitmap yang dimiliki oleh si pemanggil dan harus dibebaskan, dan itu menjadi penting begitu filter melipatgandakan jumlah bitmap yang dirender secara bersamaan
Latar belakang, tanda seleksi, dan jebakan PageColor
Tidak setiap penyesuaian kenyamanan adalah sebuah transform. Mengganti latar belakang halaman putih dengan nuansa hangat sering kali sudah cukup dengan sendirinya bagi pembaca yang sensitif terhadap silau, dan ada properti khusus untuk itu. Properti ini membawa sebuah aturan scope yang sering menjebak orang:
// Hanya memengaruhi tampilan di layar
PdfView.PageColor := $00D9EDF2; // nuansa kertas hangat di belakang konten halaman
// Output RenderPage mengabaikan PageColor; oper warnanya secara eksplisit
Bmp := Pdf.RenderPage(0, 0, W, H, ro0, [], $00D9EDF2);
PageColor mengubah apa yang ditampilkan oleh TPdfView, tetapi bitmap yang dihasilkan melalui RenderPage mempertahankan warna putih default kecuali parameter Color menyatakan sebaliknya. Gejalanya konsisten: layar menampilkan halaman yang bernuansa warna, pengguna mengekspor atau mencetak, dan hasilnya kembali ke putih. Catat itu di bawah keputusan kebijakan-ekspor yang sama dari bagian pertama
Properti warna yang tersisa mendefinisikan tanda overlay: HighlightColor untuk hasil pencarian, SelectionColor untuk seleksi teks pengguna, ReadingWordColor untuk kursor kata-yang-diucapkan. Setiap satu di antaranya harus diperiksa ulang di bawah setiap filter yang Anda tawarkan. Sebuah kursor pembacaan amber yang bekerja pada warna putih menghilang setelah inversi; sebuah seleksi biru pucat lenyap ke dalam latar belakang high-contrast. Pelihara palet overlay per mode, bukan satu set global, dan uji kombinasi-kombinasinya dengan sengaja. Filter ditambah text-to-speech adalah konfigurasi normal bagi pembaca yang dilayani oleh fitur ini, bukan sebuah edge case. Mesin overlay itu sendiri dibahas dalam artikel pembaca yang mudah diakses
Angka, verifikasi, dan pertanyaan seputar pencetakan
WCAG 2.1 mengubah fitur ini menjadi sesuatu yang dapat Anda ukur. Kriteria keberhasilan 1.4.3 meminta rasio kontras 4,5:1 untuk teks tubuh, dan 1.4.6 menaikkannya menjadi 7:1 untuk kontras yang ditingkatkan. Periksa sampel mode high-contrast Anda terhadap rasio-rasio tersebut dengan sebuah contrast analyzer yang dijalankan pada output hasil render yang sesungguhnya. Teks di atas gambar dan teks pada field form adalah tempat rasio-rasio itu diam-diam gagal bahkan ketika teks tubuh lolos
Pencetakan layak mendapat keputusannya sendiri, dan default yang dapat dipertahankan adalah tampilan asli dokumen, dengan "cetak sebagaimana ditampilkan" ditawarkan sebagai pilihan pengguna yang eksplisit. Sebuah halaman yang dicetak adalah bukti dalam lebih banyak alur kerja daripada yang cenderung diperkirakan oleh penulis viewer, dan sebuah hasil cetak terbalik dari sebuah kontrak adalah insiden dukungan dengan nuansa hukum. Satu pasangan lagi penting untuk performa: rendering yang difilter menggandakan pekerjaan bitmap pada setiap pergantian mode, jadi jangan menerapkan sebuah transform pada setiap pesan paint. Cache bitmap yang sudah difilter dan jalankan ulang transform hanya ketika halaman, zoom, atau mode benar-benar berubah. Strategi caching yang membuat ini murah ada dalam artikel render cache dan performa zoom
Satu hal yang perlu diselesaikan dalam UI Anda, bukan dalam kode Anda: mode mana yang menjadi default yang tepat. Tidak ada satu jawaban tunggal, jadi tawarkan seluruh kumpulan mode dan biarkan pembaca memilih. High contrast cocok untuk sebagian besar bacaan padat teks, inversi cocok bagi pembaca yang secara khusus menginginkan light-on-dark, grayscale memangkas noise warna, dan nuansa latar belakang menangani sensitivitas silau. Simpan pilihan itu per pengguna, pulihkan saat startup, dan sediakan jalur satu-ketukan-tombol kembali ke normal, karena seorang pembaca yang mendarat di sebuah mode yang tidak dapat mereka baca membutuhkan jalan keluar yang cepat
Opsi render, transform bitmap, dan properti warna tampilan yang digunakan di sini disertakan dengan PDFium Component untuk Delphi, C++Builder, dan Lazarus/FPC, dengan source lengkap sehingga implementasi transform dapat diaudit atau diperluas