Saya punya kebiasaan menyusun dokumen yang butuh tampilan rapi sebagai HTML plus CSS, lalu me-render-nya jadi PDF pakai Chrome headless. Enak karena semua kontrol tipografi ada di tangan saya, dan revisinya bisa dijalankan dari skrip.
Sampai suatu hari saya buka PDF hasil render dan menemukan sampah di tengah teks.
Gejalanya
Di beberapa baris, separator titik tengah yang tadinya · berubah jadi ·. Bullet • berubah jadi •. Pola yang sama muncul di semua karakter non-ASCII: satu karakter berubah jadi dua atau tiga karakter aneh, dan selalu diawali huruf berkait aksen.
Yang bikin ini tidak langsung ketahuan: saya tidak pernah mengetik karakter itu ulang. Isi berkasnya sudah benar sejak awal. Yang saya lakukan cuma menjalankan satu skrip PowerShell untuk mengganti beberapa string di dalam HTML-nya.
Tersangka pertama saya jelas Chrome. Mungkin ada masalah font, mungkin ada masalah charset saat render. Tapi begitu saya buka HTML-nya langsung di browser, sampahnya sudah ada di sana juga. Berarti kerusakannya bukan di tahap render, tapi sudah terjadi di berkasnya sendiri.
Cara paling cepat memastikan itu adalah melihat byte-nya, bukan tampilannya:
Format-Hex -Path .\cv.html | Select-Object -First 20Karakter · seharusnya tersimpan sebagai dua byte UTF-8, C2 B7. Yang saya temukan malah empat byte, C3 82 C2 B7. Ini bukan karakter salah. Ini karakter yang benar, tapi di-encode dua kali.
Akar masalahnya: default encoding Get-Content
Skrip saya sesederhana ini:
(Get-Content $path -Raw) -replace 'Placeholder', 'Nilai Baru' |
Set-Content $path -Encoding utf8Kelihatan tidak berbahaya. Saya malah merasa sudah aman karena sudah menyebut utf8 di sisi tulis. Masalahnya justru ada di baris pertama, satu-satunya baris yang tidak saya sebutkan encoding-nya.
Di Windows PowerShell, Get-Content tanpa parameter -Encoding tidak membaca berkas sebagai UTF-8. Dia membacanya memakai code page ANSI sistem, yang di kebanyakan mesin berarti Windows-1252. Dan Windows-1252 itu encoding satu byte per karakter, jadi dia tidak punya konsep urutan multibyte sama sekali.
Alurnya jadi begini. Byte C2 B7 di berkas dibaca sebagai dua karakter terpisah: C2 jadi Â, B7 jadi ·. PowerShell sekarang memegang string "·" dan tidak merasa ada yang salah. Waktu string itu ditulis balik ke disk sebagai UTF-8 sungguhan, karena sisi tulis memang saya suruh pakai utf8, kedua karakter tadi masing-masing di-encode ulang, dan hasilnya C3 82 C2 B7.
Karakter yang lebih tinggi rusak lebih parah karena butuh tiga byte. Bullet • adalah E2 80 A2 di UTF-8, dibaca satu per satu jadi â, €, dan ¢, lalu ditulis ulang jadi tujuh byte, C3 A2 E2 82 AC C2 A2. Itu asal usul • yang bertebaran di dokumen saya.
Jadi tidak ada karakter yang hilang. Semuanya masih ada, cuma dibungkus dua lapis.
Perbaikannya: baca dan tulis dengan encoding eksplisit
Berhenti mengandalkan default. Pakai API .NET langsung dan sebutkan encoding-nya di kedua sisi:
$utf8 = [System.Text.UTF8Encoding]::new($false)
$text = [System.IO.File]::ReadAllText($path, $utf8)
$text = $text -replace 'Placeholder', 'Nilai Baru'
[System.IO.File]::WriteAllText($path, $text, $utf8)Argumen $false pada UTF8Encoding itu penting. Artinya UTF-8 tanpa BOM. Kalau kamu biarkan BOM ikut ditulis, tiga byte penanda itu nangkring di paling depan berkas HTML, sebelum <!DOCTYPE html>, dan bisa muncul sebagai karakter liar atau mengganggu parser di tempat lain dalam pipeline. Ini juga alasan lain untuk tidak kembali ke Set-Content -Encoding utf8 di Windows PowerShell: perintah itu ikut menulis BOM tanpa menawarkan pilihan.
Satu hal yang sering dilupakan: mengganti hanya sisi baca tidak cukup. Kalau ReadAllText sudah benar tapi kamu masih menulis lewat Set-Content atau Out-File bawaan, kamu tinggal pindah masalah ke ujung yang lain. Kedua ujung harus memakai objek encoding yang sama.
Membalik berkas yang sudah telanjur rusak
Karena tidak ada informasi yang hilang, mojibake ini bisa dibatalkan. Caranya adalah mengulang jalur rusaknya dengan arah terbalik: ambil string yang salah, encode jadi byte Windows-1252, lalu decode byte itu sebagai UTF-8.
$utf8 = [System.Text.UTF8Encoding]::new($false)
$win1252 = [System.Text.Encoding]::GetEncoding(1252)
$broken = [System.IO.File]::ReadAllText($path, $utf8)
$bytes = $win1252.GetBytes($broken)
$fixed = $utf8.GetString($bytes)
[System.IO.File]::WriteAllText($path, $fixed, $utf8)Baris GetBytes itu yang melakukan pekerjaan sesungguhnya. Dia memaksa  kembali jadi byte C2 dan · kembali jadi byte B7, mengembalikan urutan byte aslinya. Setelah itu GetString membacanya sebagai UTF-8 yang benar dan kamu dapat · utuh lagi.
Jalankan ini sekali saja, dan salin dulu berkasnya sebelum mulai. Operasi ini bukan operasi yang aman diulang: menjalankannya pada berkas yang sudah bersih justru akan merusak berkas itu ke arah sebaliknya.
Yang saya bawa pulang
- Kalau karakter Unicode berubah jadi dua atau tiga karakter aneh, itu bukan masalah font dan bukan masalah render. Itu double encoding, dan bukti terkuatnya ada di level byte.
- Di Windows PowerShell,
Get-Contenttanpa-Encodingmengikuti code page ANSI sistem, bukan UTF-8. Yang merusak adalah campurannya: sisi baca ANSI ketemu sisi tulis UTF-8. Pipeline sesederhana baca lalu replace lalu tulis sudah cukup untuk menghancurkan berkas yang tadinya sehat. - Sebutkan encoding di kedua ujung, baca dan tulis, dan pilih UTF-8 tanpa BOM untuk berkas web.
- Mojibake jenis ini bisa dibalik selama tidak ada karakter yang jatuh ke tanda tanya. Selama byte-nya masih lengkap, kamu tinggal memutar ulang konversinya ke arah sebaliknya.