Seorang pengguna tunanetra membuka laporan kuartalan di viewer Delphi baru Anda yang mengilap, menyalakan NVDA, dan mendengar footer halaman, lalu sebuah kolom angka, lalu judul yang seharusnya dibaca lebih dulu oleh pembaca yang bisa melihat. Atau tidak mendengar apa-apa sama sekali. Halamannya terlihat sempurna di layar, dan itulah tepatnya jebakannya: rendering dan pembacaan adalah dua masalah berbeda yang diselesaikan oleh kode yang berbeda pula. Urutan sebuah PDF melukis glyph-nya tidak berkewajiban sama sekali untuk sesuai dengan urutan yang seharusnya didengar seseorang, sehingga sebuah viewer yang hanya dibangun di atas panggilan rendering menghasilkan gambar yang sempurna dan narasi yang tidak berguna. PDFium Component, wrapper VCL/LCL di sekitar mesin PDFium untuk Delphi, C++Builder, dan Lazarus, membawa sekumpulan API pembacaan yang terpisah karena alasan ini. API penggambaran tidak bisa memulihkan sebuah urutan baca yang memang tidak pernah diberikan kepadanya
Sebuah reader yang aksesibel berdiri atau roboh pada tiga hal. Ia harus mengekstrak sebuah urutan yang bisa diucapkan screen reader, menjaga kursor kata yang terlihat tetap terpaku pada apa pun yang sedang diucapkan suara itu, dan mengakui ketika sebuah dokumen tidak pernah di-tag alih-alih menebak-nebak dan berpura-pura. Masing-masing punya API yang jelas untuk dipakai dan sebuah kegagalan yang menggigit jika Anda melewatkan detailnya
Urutan Baca Hidup di Structure Tree, Bukan di Urutan Paint
ISO 32000-1 §14.8 mendefinisikan struktur logis sebagai sebuah tree elemen yang berlapis di atas konten halaman. PDF/UA (ISO 14289-1) melangkah lebih jauh dan menjadikan tree itu wajib: setiap bagian konten sungguhan harus bisa dijangkau melaluinya dalam urutan baca, dengan artifact halaman ditandai sebagai artifact dan dilewati. Sebuah laporan yang di-tag dengan benar tahu bahwa "Quarterly Results" adalah sebuah heading level dua dan bahwa grid totals adalah sebuah tabel dengan sel header. Sebuah laporan yang tidak di-tag hanyalah tumpukan glyph run yang diposisikan dan kebetulan terlihat seperti sebuah dokumen
ReadablePageContent menyusuri struktur itu ketika ada, dan mengembalikan fragmen yang ditandai dengan sebuah Kind semantik, nilai-nilai seperti cfHeading dan cfParagraph, sehingga UI bisa mengatakan "heading" sebelum kata-katanya, alih-alih membaca sebuah baris tebal sebagai teks body biasa. Tanpa tree yang bisa dipakai, panggilan yang sama jatuh kembali ke analisis layout heuristik: mendeteksi kolom, mengelompokkan baseline, mengurutkan dari kiri ke kanan dan atas ke bawah. Fallback itu baik-baik saja untuk sebuah memo satu kolom dan goyah untuk sebuah newsletter, sebuah form multi-kolom, apa pun yang punya sidebar atau pull quote. Yang penting adalah mengetahui hasil mana yang Anda dapatkan, dan API-nya memberitahukan itu secara terus terang. Record TPdfReadableContent membawa sebuah field Source yang diset ke rosStructure ketika urutannya berasal dari tree yang di-tag, atau rosHeuristic ketika itu disimpulkan dari geometri. Tampilkan sebuah urutan tebakan seolah-olah sudah terverifikasi dan Anda telah mengirim versi aksesibilitas dari sebuah lencana lolos pada sebuah build yang tidak pernah dijalankan siapa pun
Langkah murah pada saat pembukaan adalah membaca IsTagged dan memanggil ValidatePdfUa sekali saja, lalu meng-cache jawabannya. Sebuah pemeriksaan PDF/UA yang gagal bukan alasan untuk menolak filenya. Itu alasan untuk menaruh "urutan baca perkiraan" di status bar, sehingga ketika seorang pelanggan mengirim keluhan soal narasi yang kacau, tim support sudah tahu apakah mereka sedang menghadapi masalah tagging di dalam file atau sebuah bug di kode Anda
Dari Halaman ke Speech Queue dengan ReadingUnits
Untuk text-to-speech, ReadingUnits yang melakukan pekerjaan beratnya. Ia mengembalikan sebuah array record TPdfReadingUnit untuk halaman aktif, masing-masing memegang teks yang harus diucapkan, peran semantiknya, dan rectangle yang menempatkannya di halaman. Ada sebuah pendamping tingkat dokumen, DocumentReadingUnits, untuk saat Anda menginginkan pembacaan berkelanjutan lintas halaman. Satu unit langsung masuk ke satu slot dari sebuah speech queue:
procedure TReaderForm.QueuePageSpeech(PageNumber: Integer);
var
Units: TPdfReadingUnits;
i: Integer;
begin
Pdf.PageNumber := PageNumber; // ReadingUnits bekerja pada halaman aktif
Units := Pdf.ReadingUnits;
FSpeechQueue.Clear;
for i := Low(Units) to High(Units) do
FSpeechQueue.Add(Units[i]); // teks + semantik + rect highlight
FCurrentPage := PageNumber;
SpeakNextUnit;
end;
Ada dua hal dalam loop itu yang mudah salah. Jaga agar queue-nya per halaman dan bangun ulang setiap kali pengguna bernavigasi, karena reading unit membawa rectangle dalam ruang halaman; sebuah queue sisa dari halaman tiga akan melukis highlight-nya ke halaman empat. Dan perlakukan sebuah array Units yang kosong pada halaman yang jelas-jelas punya konten sebagai detektor image-only Anda. Sebuah halaman hasil scan adalah piksel tanpa lapisan teks di bawahnya, dan jawaban yang benar adalah mengucapkan sebuah peringatan ("halaman ini tidak memiliki teks yang bisa diekstrak") alih-alih diam saja dengan cara yang tidak bisa dibedakan pendengarnya dari sebuah hang
Kursor Kata yang Mengikuti Suara
Menyorot satu paragraf penuh sekaligus terasa lamban bagi pengguna low-vision yang mengikuti kata-kata dengan mata sementara kata itu dibacakan. Highlighting tingkat kata, efek karaoke, membutuhkan dua bagian: geometri dari setiap kata, dan sebuah cara untuk memetakan laporan progres mesin TTS ke geometri itu. PageWordBoxes memberi Anda geometrinya sebagai record TPdfWordBox, masing-masing dengan teks kata, offset karakternya, jumlah karakternya, dan sebuah rectangle dalam ruang halaman. TrackReadingWordAt memberi Anda pemetaannya. Berikan posisi karakter yang sudah dilaporkan oleh event word-boundary milik SAPI, dan ia akan menyelesaikan offset itu menjadi sebuah index dalam array word-box lalu melukis kursor pada kata yang cocok dalam satu panggilan
procedure TReaderForm.PrepareKaraoke(PageNumber: Integer);
begin
// Word box milik view berasal dari halaman yang ditampilkan view.
// Menyetel Pdf.PageNumber saja tidak akan menggerakkan view
PdfView.PageNumber := PageNumber;
FWordBoxes := PdfView.PageWordBoxes;
end;
procedure TReaderForm.OnTtsWordBoundary(Sender: TObject; CharIndex: Integer);
var
WordIdx: Integer;
begin
// TrackReadingWordAt memetakan offset SEKALIGUS menggambar kursor kata
WordIdx := PdfView.TrackReadingWordAt(FCurrentPage, CharIndex);
if WordIdx < 0 then
PdfView.ClearReadingWord; // boundary melewati akhir teks halaman
end;
Kontraknya murah hati pada satu hal dan tidak memberi ampun pada hal lain. Bagian yang murah hati: TrackReadingWordAt menyimpan cache word-box-nya sendiri untuk halaman yang sedang dilacaknya, sehingga tidak ada apa pun yang perlu di-pre-load, dan tidak ada rendering yang terjadi sama sekali karena word box-nya berasal dari lapisan teks. Sebuah layanan speech headless tanpa window yang terlihat pun tetap bisa melacak posisi. Bagian yang tidak memberi ampun: index karakternya harus menunjuk ke teks yang diekstrak komponen ini, bukan ke sebuah string bersih yang Anda bangun sendiri. Ketika CharIndex melewati akhir teks halaman, fungsi ini mengembalikan -1, bukan melempar error, yang terjadi sepanjang waktu ketika sebuah mesin TTS memicu satu event boundary terakhir untuk tanda baca di akhir. Baca -1 sebagai "bersihkan kursornya", jangan pernah sebagai sebuah error
Di sisi tampilan, ReadingWordColor menetapkan warna kursor. Warna amber default-nya bertahan baik di sebagian besar background halaman, tetapi ujilah di bawah setiap filter tampilan yang ditawarkan viewer Anda. Sebuah kursor amber bisa lenyap sepenuhnya di bawah inversi warna, dan inversi yang berjalan berdampingan dengan speech itulah tepatnya cara kerja seorang pengguna low-vision, sehingga satu kombinasi yang paling perlu Anda perbaiki justru satu-satunya yang tidak pernah dijalankan sebuah demo cepat. Setel ReadingWordFollow ke True dan view akan men-scroll kata yang sedang diucapkan ke dalam pandangan dengan sendirinya, sesuatu yang tidak bisa Anda lewati pada sebuah halaman yang di-zoom hingga meluber lintas layar. Perhatikan satu aturan cakupan: SetReadingWord hanya melukis pada halaman TPdfView yang aktif. Putuskan sejak awal apakah scrolling manual menjeda speech atau perilaku follow itu yang menang, karena tidak memilih keduanya membuat suaranya terus membaca sementara kursornya duduk di suatu tempat di luar layar
Dokumen-Dokumen yang Merusak Reader Anda
Segelintir bentuk input mengalahkan sebuah implementasi naif dengan cukup konsisten sehingga layak menjadi sampel permanen dalam regression suite, bukan sekadar bug sekali pakai yang Anda perbaiki lalu lupakan
- File yang tidak di-tag tetapi kaya teks. Urutan heuristik cenderung benar untuk laporan linear dan salah begitu sebuah sidebar atau pull quote masuk. Tandai urutannya sebagai perkiraan, baik di UI maupun di log diagnostik Anda, sehingga kegagalannya bisa dibaca nanti
- Scan image-only. Sama sekali tidak ada lapisan teks. Tangkap lewat reading unit yang kosong dan arahkan pengguna ke sebuah langkah OCR di hulu, alih-alih membiarkan reader menarasikan halaman kosong
- Karakter combining dan skrip campuran. Tanda combining Unicode tidak selalu meluruh satu-lawan-satu menjadi kata visual, sehingga jumlah word-box bisa melenceng dari apa pun yang diharapkan tokenizer Anda sendiri. Jangan meng-index array word-box dengan offset yang Anda hitung sendiri dari memecah teks; gunakan hanya index yang dikembalikan
TrackReadingWordAt
Uji Seperti Seorang Auditor, Bukan Seperti Sebuah Demo
"Program itu membacakan sampel saya dengan suara keras" tidak membuktikan apa-apa. Sebuah kelulusan yang bisa Anda pertahankan menjalankan tiga file melalui build jadi dengan NVDA terpasang: satu file yang diketahui sudah di-tag, di mana heading diumumkan sebagai heading dan sebuah tabel dibaca dalam urutan baris; satu file yang diketahui belum di-tag, di mana indikator urutan-perkiraan terlihat; dan sebuah scan, di mana peringatan tanpa-teks sungguh-sungguh diucapkan. Masing-masing menguji sebuah jalur yang dilewati oleh happy case
Dari situ, pastikan kursor kata tetap terkunci pada kecepatan speech dua kali lipat dan setengahnya, serta bahwa scrolling ReadingWordFollow tidak berebut dengan scrolling milik pengguna sendiri. Lalu jalankan speech sambil Anda berputar melalui setiap filter warna dan amati bahwa kursornya tidak pernah menghilang. Artikel filter warna low-vision membahas jalur rendering itu secara mendetail, dan pembahasan mendalam kursor speech kata mengurai timing TTS-nya
API reading-unit dan word-box yang dipakai di atas disertakan dalam PDFium Component untuk Delphi dan C++Builder (VCL) serta Lazarus/FPC (LCL). Halaman produk menautkan referensi API lengkap, termasuk layout record untuk reading unit dan word box di balik contoh-contoh ini