D
P
0
← All articles Baca dalam Bahasa Indonesia

CSS Layout, Overflow & Cascade

`·` and `•` in Your Rendered PDF? `Get-Content` Read Your UTF-8 as Windows-1252

· · 5 min read
`·` and `•` in Your Rendered PDF? `Get-Content` Read Your UTF-8 as Windows-1252

When a document needs to look polished, I tend to build it as HTML plus CSS and render it to PDF with headless Chrome. Every typographic decision stays under my control, and revisions can be driven from a script.

Then one day I opened a freshly rendered PDF and found garbage sitting in the middle of the text.

The symptom

The middle dot separator that used to read · now read ·. Bullets that used to be had become •. The same pattern repeated across every non-ASCII character: one character replaced by two or three strange ones, always led by an accented letter.

What made it hard to spot: I never retyped any of those characters. The file had been correct all along. All I had done was run a small PowerShell script to swap a few strings inside the HTML.

My first suspect was Chrome. Maybe a font issue, maybe a charset problem at render time. But opening the HTML directly in a browser showed the same garbage. So the damage was not happening during rendering. It was already baked into the file.

The fastest way to confirm that is to look at bytes instead of glyphs:

Format-Hex -Path .\cv.html | Select-Object -First 20

A · should be stored as the two UTF-8 bytes C2 B7. What I found was four bytes, C3 82 C2 B7. That is not a wrong character. That is the right character, encoded twice.

The root cause: Get-Content and its default encoding

My script was this simple:

(Get-Content $path -Raw) -replace 'Placeholder', 'New Value' |
  Set-Content $path -Encoding utf8

It looks harmless. I even felt safe about it, because the write side names utf8. The trouble is in the first line, the only one where I never named an encoding at all.

In Windows PowerShell, Get-Content without an -Encoding parameter does not read the file as UTF-8. It reads it using the system ANSI code page, which on most machines means Windows-1252. And Windows-1252 is a single byte per character encoding, so it has no concept of multibyte sequences at all.

Here is the chain. The bytes C2 B7 in the file get read as two separate characters: C2 becomes Â, B7 becomes ·. PowerShell now holds the string "·" and sees nothing wrong with it. When that string is written back to disk as real UTF-8, which is exactly what the write side was told to do, both characters get encoded again, and you end up with C3 82 C2 B7.

Higher codepoints break worse because they take three bytes. A bullet is E2 80 A2 in UTF-8, read one byte at a time as â, , and ¢, then written back out as seven bytes, C3 A2 E2 82 AC C2 A2. That is where all the • in my document came from.

So nothing was lost. Every character was still there, just wrapped in an extra layer.

The fix: read and write with an explicit encoding

Stop relying on defaults. Go through the .NET APIs directly and name the encoding on both sides:

$utf8 = [System.Text.UTF8Encoding]::new($false)
 
$text = [System.IO.File]::ReadAllText($path, $utf8)
$text = $text -replace 'Placeholder', 'New Value'
[System.IO.File]::WriteAllText($path, $text, $utf8)

The $false argument to UTF8Encoding matters. It means UTF-8 without a BOM. Let the BOM through and those three marker bytes sit at the very front of the HTML file, ahead of <!DOCTYPE html>, where they can surface as a stray character or upset a parser further down the pipeline. That is another reason not to fall back on Set-Content -Encoding utf8 in Windows PowerShell: it writes the BOM for you, and does not offer a way out.

One detail people skip: fixing only the read side is not enough. If ReadAllText is correct but you still write through the default Set-Content or Out-File, you have simply moved the problem to the other end. Both ends need the same encoding object.

Reversing files that are already corrupted

Because no information was lost, this mojibake can be undone. You replay the broken path in reverse: take the wrong string, encode it as Windows-1252 bytes, then decode those bytes as 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)

The GetBytes line is doing the real work. It forces  back down to the byte C2 and · back down to B7, restoring the original byte sequence. GetString then reads that sequence as proper UTF-8 and hands you a clean · again.

Run this exactly once, and copy the file before you start. The operation is not safe to repeat: applying it to an already clean file will corrupt that file in the opposite direction.

What I took away