PDFPipe

East Asian scripts / Korean

Korean text in a PDF

Hangul syllables exist in two forms in Unicode, precomposed and assembled from parts, and a font can support one and not the other.

A font with the characters is not enough

A Hangul syllable such as the one for han can be a single precomposed code point, of which there are 11,172, or it can be a sequence of jamo, the individual consonant and vowel letters, which the renderer composes into a syllable block. Normalisation decides which you have: NFC produces precomposed syllables and NFD decomposes them into jamo. Fonts overwhelmingly support the precomposed forms and support jamo composition unevenly, so text that arrives in NFD from a system that normalised it, which macOS filesystems historically did, renders as a row of separated letter shapes instead of syllable blocks. Korean also differs from Chinese and Japanese in having word spaces, so its line breaking is closer to Latin than to its neighbours.

What the failure looks like

Syllables that have fallen apart into their component letters, sitting side by side at full width instead of stacked into a block. It is unmistakable once seen and it is invisible in a database, because the string is correct and only its normalisation form differs.

Line breaking

Korean uses spaces between words, so breaking works like Latin rather than like Chinese. The engine may still break between syllables where a word is too long for the line, which is acceptable in Korean typography and surprises people who expect Latin word-breaking behaviour.

The markup and the CSS

The language attribute is doing real typographic work in every one of these examples. It is what lets the engine choose a regional glyph form, a case mapping or a hyphenation dictionary, and leaving it off is the most common single omission in a multilingual document.

html
<p lang="ko" class="korean">청구서가 준비되었습니다.</p>

<style>
@font-face {
  font-family: "Noto Sans KR";
  src: url("https://assets.example.com/fonts/noto-sans-kr.woff2")
       format("woff2");
}

.korean {
  font-family: "Noto Sans KR", sans-serif;
  line-height: 1.7;
  /* Korean has word spaces, so ordinary word breaking applies, unlike
     the Chinese and Japanese cases. */
  word-break: keep-all;
  overflow-wrap: break-word;
}</style>

<!-- Normalise to NFC on the way in. Text that arrives decomposed will
     render as separated jamo in most fonts:
       const text = raw.normalize("NFC"); -->

The mistake people make

Setting word-break: break-all on a container to stop long strings overflowing, and applying it to Korean. Korean has word spaces and break-all ignores them, so words break mid-syllable at arbitrary points. Use keep-all with overflow-wrap for Korean, which respects word boundaries and still prevents overflow.

How to test this without reading the language

Render a string you know is correct, and compare it against a reference rendering from a system that definitely handles the script, such as a browser on a machine with the language installed. Comparing against nothing tells you only that characters appeared, and for most of the scripts on this site characters appearing is not the same as text being right. Where you can, get one native reader to look at one page once; it is faster than any amount of specification reading.

Frequently asked

Why does Korean look wrong when every character is there?

Syllables that have fallen apart into their component letters, sitting side by side at full width instead of stacked into a block. It is unmistakable once seen and it is invisible in a database, because the string is correct and only its normalisation form differs. The underlying reason is that a font is two things: a set of glyphs and a set of layout tables that say how those glyphs combine and position. Character coverage is the first half. Most of the failures on this page are the second half.

Does the lang attribute actually change the rendering?

Yes, and it is not an accessibility nicety here. It selects regional glyph forms where a code point has more than one correct shape, it picks the case mapping for a text transform, it chooses a hyphenation dictionary, and it triggers the locl OpenType feature. A document with no lang attribute gets whatever the font defaults to, which is usually right for one language and wrong for the others.

Can I subset the font to keep the file small?

Carefully, and not with a tool configured for Latin. Subsetters routinely drop the GSUB and GPOS tables that shape and position glyphs, because for Latin those are largely optional. The subset renders without error and every complex feature in it is broken. Test a subset by rendering real text and looking at it, never by checking that the file got smaller.

Related writing systems

Scripts with a related mechanism, and one from each of the other families.

Paste a real string in this script into the playground and compare the output against a reference. It is the only test that means anything if you do not read the language.