Dua keluhan datang di minggu setelah fitur kompresi dirilis: hasil pindai kontrak kini punya huruf bergerigi seperti tangga yang berbulu, dan logo transparan di halaman sampul terkurung dalam halo pucat. PDFiumPas menjawab keduanya di satu tempat. TPdf.OptimizeImages mengukur setiap gambar sebelum mengecilkan ukurannya, lalu memilih kernel resampling dan mengakumulasi warna dalam bentuk yang sadar alpha
Dulu tidak selalu begini. Sebelum v3.100.0, metode yang sama mengecilkan setiap gambar non-bilevel dengan langkah tetap nearest-neighbour, yang justru algoritma pemicu kedua keluhan itu: ia mengambil sampel titik satu piksel sumber per piksel output, dan memperlakukan RGB di bawah piksel yang sepenuhnya transparan seolah pembaca akan melihatnya. Penulisan ulang di v3.100.0 mengganti jalur tunggal itu dengan lima kernel, aturan pemilihan yang terukur, dan anggaran memori kerja yang eksplisit
Mengapa downsampling membuat teks hasil pindai tampak bergerigi?
Karena point sampling menjawab pertanyaan yang salah. Saat pindai 300 DPI ditargetkan ulang ke 150 DPI, setiap piksel output mewakili blok dua-per-dua piksel sumber, dan nearest neighbour menyimpan satu dari empat serta membuang sisanya. Mana yang bertahan ditentukan oleh pembulatan, sehingga tepi goresan yang halus ter-antialias di sumber menjadi lempar koin per piksel. Hasilnya adalah tangga alias klasik di sepanjang tepi glyph, ditambah moiré pada area halftone tempat sampel yang dibuang kebetulan membawa polanya. Ini lebih berarti di PDF daripada di layar karena kerusakannya permanen. Image XObject membawa data sampelnya bersama /Width, /Height, dan /BitsPerComponent (ISO 32000-1 §8.9.5), dan resampling menulis ulang ketiganya di dalam berkas. Zoom buruk di viewer adalah frame yang bisa Anda gambar ulang, dan PDFiumPas punya mesin terpisah untuk itu di render cache dan performa zoom. Downsampling buruk adalah dokumen baru yang Anda serahkan ke pelanggan
Cara PDFiumPas mengukur detail dan memilih kernel
PDFiumPas memutuskan per gambar, bukan per dokumen. Sebelum memilih kernel, ia menghitung skor detail luminansi ternormalisasi dari grid sampling berbatas: langkah horizontal dan vertikalnya adalah (Width + 63) div 64 dan (Height + 63) div 64, sehingga pindai 12000 piksel dan thumbnail 300 piksel sama-sama berbiaya sekitar satu sapuan 64 per 64. Di setiap posisi sampel, ia menjumlahkan selisih absolut ke tetangga di kanan dan tetangga di bawah, pada maksimal tiga kanal, lalu membaginya dengan jumlah sampel dikali 255. Skornya mendarat di 0 sampai 1, di mana grafis bisnis yang datar berada dekat nol dan tekstur foto yang padat menanjak
Tangga pemilihan lalu berjalan dengan urutan tetap. Jika ResampleFilter bukan pirfAdaptive, filter itu dipakai apa adanya. Selebihnya: konten 1-bit memakai pirfBilevel; ContentClass berisi piccLineArt memakai pirfBox; faktor skala 4 atau lebih juga memakai pirfBox, karena pada pengecilan sebesar itu rata-rata area sekaligus jawaban termurah dan paling benar; piccPhoto, skor detail 0.08 atau lebih tinggi, atau PreferredQuality 0.9 atau lebih tinggi memakai pirfLanczos dengan kernel tiga lobus-nya; skala 2 atau lebih atau kualitas 0.7 atau lebih memakai pirfBicubic radius 2; sisanya memakai pirfBilinear. Karena TPdfImageOptimizeOptions.Default menyetel PreferredQuality ke 0.85, eksekusi default tidak pernah jatuh ke bilinear kecuali pengecilannya ringan dan kontennya datar
uses
PDFium;
procedure ShrinkScannedPdf(const InputFile, OutputFile: string);
var
Pdf: TPdf;
Options: TPdfImageOptimizeOptions;
Report: TPdfImageOptimizeReport;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := InputFile;
// Default: TargetDpi 150, MinDpiRatio 1.5, PreserveBilevel True,
// MinDimension 8, pirfAdaptive, piccAuto, kualitas 0.85, anggaran 64 MiB.
Options := TPdfImageOptimizeOptions.Default;
Options.TargetDpi := 150;
Options.MinDpiRatio := 1.5;
Options.ContentClass := piccAuto;
Options.PreferredQuality := 0.85;
if Pdf.OptimizeImages(Options, Report) and (Report.OptimizedCount > 0) then
Pdf.SaveAs(OutputFile);
finally
Pdf.Free;
end;
end;
Sebuah gambar hanya disentuh ketika DPI penempatan terbesarnya, horizontal atau vertikal, dibagi TargetDpi, mencapai MinDpiRatio. Penjaga itu ada supaya foto 160 DPI yang ditujukan ke target 150 DPI tidak dienkode ulang demi keuntungan enam persen yang mengorbankan satu generasi kualitas. Gambar yang di bawah MinDimension pada salah satu sumbu, 8 secara default, dilewati sebagai ikon atau garis
Mengapa logo transparan mendapat fringe putih?
Karena warna di bawah piksel yang sepenuhnya transparan itu arbitrer, dan rata-rata berbobot biasa membiarkannya ikut memberi suara. Ekspor sebuah logo dari desain tool dan margin tak terlihatnya sering kali putih, atau hitam, atau apa pun warna kanvasnya; kanal alpha menyembunyikannya, dan penjumlahan lurus di atas area kernel segera mencampurnya kembali ke tepi yang terlihat. PDFiumPas menghindarinya dengan mengakumulasi sampel BGRA dalam bentuk premultiplied dan membatalkan premultiplikasinya hanya di piksel tujuan
Secara konkret, setiap sampel penyumbang menambahkan channel * alpha * weight ke akumulator warna, alpha * weight ke akumulator alpha, dan weight ke jumlah bobot. Warna tujuan kemudian dibagi dengan akumulator alpha, bukan dengan jumlah bobot, dan di situlah langkah yang menentukan: membagi dengan jumlah bobot akan menyeret warna ke arah piksel tak terlihat, sedangkan membagi dengan alpha terakumulasi merekonstruksi warna yang disepakati sampel-sampel terlihat tersebut. Alpha tujuan adalah kuantitas terpisah, 255 * AlphaSum / WeightSum. Format non-alpha membagi dengan jumlah bobot seperti biasa, byte padding tujuan FPDFBitmap_BGRx ditulis sebagai konstanta 255, dan setiap kanal di-clamp ke rentang 0 sampai 255 sebelum disimpan. Alpha itu lazimnya berasal dari entri soft mask di kamus gambar (ISO 32000-1 §11.4), yang sudah dikomposit PDFium ke buffer BGRA yang diterima resampler
// Bentuk loop akumulasi bagian dalam, per sampel sumber penyumbang
if SrcFormat = FPDFBitmap_BGRA then
Alpha := PByte(PAnsiChar(Pixel) + 3)^ / 255
else
Alpha := 1;
for Channel := 0 to Min(BytesPerPixel, 3) - 1 do
Accumulated[Channel] := Accumulated[Channel] +
PByte(PAnsiChar(Pixel) + Channel)^ * Alpha * Weight;
AlphaSum := AlphaSum + Alpha * Weight;
WeightSum := WeightSum + Weight;
// ... dan di piksel tujuan, unpremultiply terhadap jumlah alpha
if SrcFormat = FPDFBitmap_BGRA then
begin
if Abs(AlphaSum) > 1E-12 then
ValueSum := Accumulated[Channel] / AlphaSum
else
ValueSum := 0;
end
else
ValueSum := Accumulated[Channel] / WeightSum;
Menjaga line art 1-bit keluar dari zona abu-abu
Kernel kontinu apa pun yang diterapkan pada pindai bilevel menghasilkan abu-abu, dan abu-abu justru hal yang tidak boleh ada di gambar bergaya faks. Karena itu PDFiumPas membiarkan gambar 1-bit apa adanya secara default: PreserveBilevel bernilai True di TPdfImageOptimizeOptions.Default, dan gambar demikian mendarat di SkippedCount tanpa disentuh. Setel ke False dan jalur pirfBilevel mengambil alih alih-alih kernel perata. Ia menelusuri persegi sumber persis yang menutupi setiap piksel tujuan, merata-ratakan luminansi dengan bobot 0.114, 0.587, dan 0.299 dalam urutan memori BGR, lalu men-threshold hasilnya di 127.5 menjadi 0 atau 255 yang datar. Tidak ada nilai antara yang bisa ditulis, sehingga tepi tetap tajam dan tidak ada halo abu-abu terbentuk di sekitar goresan tipis; kanal alpha sumber BGRA dirata-ratakan normal, dan tujuan BGRx mendapat konstanta 255. Jika yang Anda butuhkan piksel aslinya, bukan dokumen yang lebih kecil, mengekstrak gambar dari dokumen PDF adalah jalur terpisahnya
Apa yang terjadi saat gambar melebihi anggaran memori kerja?
Ia dibiarkan persis seperti semula, dan dihitung. MaxWorkingBytes defaultnya 64 MiB dan ditegakkan dua kali. Sebelum bitmap tujuan dibuat, PDFiumPas menolak gambar jika lebar kali tinggi kali byte per piksel melebihi anggaran. Setelah FPDFBitmap_CreateEx berhasil, ia memeriksa lagi dengan stride nyata kali tinggi, karena padding baris bisa mendorong alokasi melewati batas yang lolos dari perkalian naifnya. Penolakan mana pun menghancurkan tujuan dan tidak mengembalikan apa pun. Pahami dengan jelas degradasi yang disiratkan: gambar yang melebihi anggaran tidak di-resample dengan kualitas lebih rendah, dan tidak dipecah menjadi tile. Originalnya tetap di dokumen, BudgetExceededCount dan SkippedCount sama-sama bertambah, dan satu eksekusi pun bisa melaporkan sukses padahal dokumen hanya teroptimasi sebagian. Itu perilaku fail-safe yang disengaja, tetapi artinya report bukan bacaan opsional. Mode kegagalan lain juga ada: gambar yang bitmapnya tidak mampu dihasilkan PDFium sama sekali, seperti CMYK, JPX, JBIG2, atau sumber bertopeng, menaikkan FailedCount sebagai gantinya dan sama-sama dibiarkan utuh
procedure OptimizeBatch(const Files: array of string);
var
Pdf: TPdf;
Options: TPdfImageOptimizeOptions;
Report: TPdfImageOptimizeReport;
I: Integer;
begin
Options := TPdfImageOptimizeOptions.Default;
Options.PreserveBilevel := False; // pakai voting area bilevel
Options.ContentClass := piccPhoto; // paksa Lanczos untuk set foto
Options.MaxWorkingBytes := 256 * 1024 * 1024; // ruang sisa untuk pindai besar
Pdf := TPdf.Create(nil);
try
for I := Low(Files) to High(Files) do
begin
Pdf.FileName := Files[I];
if not Pdf.OptimizeImages(Options, Report) then
begin
WriteLn('optimize failed: ', Report.ErrorMessage);
Continue;
end;
if Report.BudgetExceededCount > 0 then
WriteLn(Files[I], ': ', Report.BudgetExceededCount,
' image(s) over budget and kept at full size');
if Report.FailedCount > 0 then
WriteLn(Files[I], ': ', Report.FailedCount,
' image(s) could not be decoded to a bitmap');
if Report.OptimizedCount > 0 then
Pdf.SaveAs(ChangeFileExt(Files[I], '.opt.pdf'));
end;
finally
Pdf.Free;
end;
end;
Membaca report sebelum mengirim berkasnya
TPdfImageOptimizeReport dirancang untuk didiagnosis, bukan sekadar dicatat. Di samping OptimizedCount, SkippedCount, dan FailedCount, ia mengekspos satu counter per kernel, sehingga BoxFilterCount, BilinearFilterCount, BicubicFilterCount, LanczosFilterCount, dan BilevelFilterCount memberi tahu apa yang sebenarnya disimpulkan aturan adaptif tentang korpus Anda. Hasil semua-box berarti pengecilannya curam atau kontennya diklasifikasikan line art; hasil semua-Lanczos pada dokumen yang Anda yakini line art adalah sinyal bahwa ContentClass sebaiknya disetel eksplisit. AverageDetailScore adalah angka untuk dibandingkan dengan ambang 0.08 milik Lanczos saat menyetel PreferredQuality, dan PeakWorkingBytes menunjukkan seberapa besar dari MaxWorkingBytes yang benar-benar dibutuhkan eksekusi. Opsi invalid gagal dengan keras, bukan diam-diam: TargetDpi non-positif, MinDpiRatio di bawah 1, PreferredQuality di luar 0 sampai 1, atau MaxWorkingBytes non-positif melempar EPdfError sebelum halaman mana pun disentuh. Dan OptimizeImages hanya mengedit dokumen di memori; setiap halaman yang diubah dikomit dengan FPDFPage_GenerateContent, setelah itu Anda tetap memanggil SaveAs sendiri. Untuk melihat apa yang berubah, render dokumen sebelum dan sesudah ke bitmap seperti dijelaskan di mengonversi halaman PDF ke gambar JPEG dan bandingkan pada zoom penuh
Resampling adaptif adalah salah satu fitur yang tak terlihat saat bekerja dan menghasilkan tiket dukungan saat tidak, itulah sebabnya pengukuran, penanganan alpha, dan anggaran memori harus mendarat bersamaan alih-alih sebagai tiga penyempurnaan terpisah. Jika Anda mengevaluasi ini untuk produk Delphi, C++Builder, atau Lazarus, permukaan API lengkap dan detail lisensinya ada di halaman PDFiumPas Delphi PDFium component