Skip to content
Merged
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,8 @@ One-time supporters can ask to be listed too.
MIT, © 2026 Myeonggu Kim (KnifeLemon) and Filee contributors.
Bundled third-party components keep their own licenses, listed in [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md).

본 제품은 한글과컴퓨터의 한/글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. (Filee's HWP and HWPX code was developed with reference to Hancom's published HWP file format document.)

<p align="center">
<a href="https://star-history.com/#KnifeLemon/Filee&Date"><img src="https://api.star-history.com/svg?repos=KnifeLemon/Filee&type=Date" alt="Star history" width="600"></a>
</p>
9 changes: 9 additions & 0 deletions THIRD-PARTY-NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,15 @@ Windows headers, without a C runtime. Pieces of these projects that can end up i
the x86 branch filter of the LZMA SDK (public domain, Igor Pavlov, https://www.7-zip.org/sdk.html), and the published
app as its payload. Filee 1.x's installer was built with Inno Setup, which is no longer used.

## Hancom HWP document format

Filee's own HWP and HWPX code (the layout engine and the readers it uses) was developed with reference to Hancom's
published document on the HWP file format, 「한글 문서 파일 형식 5.0」, which asks for this notice:

본 제품은 한글과컴퓨터의 한/글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다.

(This product was developed with reference to Hancom's published document on the Hangul (.hwp) file format.)

## Unicode data

`src/Filee.Engines/Typesetting/UnicodeData.Generated.cs` holds Line_Break, Bidi_Class, Script and East_Asian_Width
Expand Down
2 changes: 1 addition & 1 deletion build/render-compare.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ $project = Join-Path $repository 'tests/Filee.Engines.Tests/Filee.Engines.Tests.
$env:FILEE_RENDER_CORPUS = (Resolve-Path $Corpus).Path
$env:FILEE_RENDER_REPORT = [IO.Path]::GetFullPath($Report)
try {
dotnet test $project -c $Configuration --filter 'FullyQualifiedName~RenderQuality.RenderCorpusTests' --logger 'console;verbosity=normal'
dotnet test $project -c $Configuration --filter 'FullyQualifiedName~RenderQuality.RenderCorpusTests|FullyQualifiedName~RenderQuality.HangulLineBreakTests' --logger 'console;verbosity=normal'
if ($LASTEXITCODE -ne 0) { throw "The compare failed (exit $LASTEXITCODE)." }
}
finally {
Expand Down
60 changes: 60 additions & 0 deletions docs/ENGINES.md
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,66 @@ properties) with every installed font of the script, against recorded HarfBuzz o
font (joining forms and lam-alef, vowel signs drawn before their consonant, conjuncts, the SARA AM split, jamo composed
into precomposed syllables, a dotted circle only for a broken cluster).

## Filee renderer: line breaking and paragraph layout

본 제품은 한글과컴퓨터의 한/글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. (The layout engine's HWP and HWPX handling was checked against Hancom's published HWP 5.0 file format
document, whose license asks for this notice in the product's interface, manuals, help and source; it is also in
THIRD-PARTY-NOTICES.md, which the About page shows.)

Paragraph layout of Filee's own renderer (GitHub Project 4, item *Layout engine: paragraph layout and line breaking*),
in `src/Filee.Engines/Typesetting`. No converter uses it yet; the render quality harness measures it against rhwp.

- **Line breaking** (`LineBreaker.cs`): UAX #14 at Unicode 16.0, rules LB1–LB31; it passes all 16,672 cases of
Unicode's `LineBreakTest.txt` (checked when written; the file isn't kept, `LineBreakerTests` covers each rule
family). Tailorings: 한글 어절 keeps Hangul words whole, 글자 breaks between syllables as UAX #14 does (한글 writes
them the other way round from the names: `breakNonLatinWord` BREAK_WORD is 어절, KEEP_WORD 글자; the binary HWP
of the same document and 한글's own line layout say so); Latin words broken anywhere (`breakLatinWord` BREAK_WORD,
Word without `w:wordWrap`); a document's own characters forbidden at a line's start or end (Word
`w:noLineBreaksBefore` / `After`). Hyphenation keeps words whole (no dictionaries), and Thai, Lao, Khmer and Myanmar
break only at spaces until there is dictionary breaking.
- **Paragraph text** (`ParagraphText.cs`): the inlines as one string (tabs, line breaks as U+2028, links and bookmarks
kept, a list item's number or bullet in front, followed by a tab when the paragraph hangs) and each character's style:
the font, 장평, 자간, relative size and 글자 위치 of its 한글 language slot, or Word's run font, scale, absolute
spacing and raise. 한글's 자간 is a share of each character's own width (장평 applied), not of the font size, so a
space or a narrowed character closes up less (한글's stored line layout breaks where this puts the line end). 한글's
"글꼴에 어울리는 빈칸" off (`useFontSpace="0"`, as in Filee's template) makes a space half the size
wide, and `useKerning="0"` turns the font's kerning off; both are read into `HCharLayout`.
- **Layout** (`ParagraphLayout.cs`, output in `LineBox.cs`): items of one style, font, script and direction are shaped
(`Shaper`), lines fill greedily to the column width and break at the last opportunity (a word wider than the line
between clusters), spaces hang at the line end. 최소 공백 (`condense`) keeps a word that fits once the spaces
shrink; `lineWrap` KEEP and SQUEEZE hold the paragraph on one line. Each line's parts are shaped again with their
context, so a break inside an Arabic word or after an Indic cluster shapes as it ends.
- Alignment: left, right, centre; justify widens the spaces (the clusters where a line has none) on all but the last
line; distribute (배분) widens between all clusters, the last line too. Indents and hanging indents as the model
has them; the start indent of a right-to-left paragraph is on the right.
- Tabs: left, right, centre and decimal stops with dot, dash and solid leaders, measured from the paragraph's left
indent as 한글 does; default stops every 40 pt; a hanging indent is a stop on the first line.
- Line height by the paragraph's rule: 한글's percent, fixed, at least and between lines measure from the font size
with the baseline at 85% of the text height (as 한글's stored line segments put it); Word's multiple and 한글's
"글꼴에 어울리는 줄 높이" from the fonts' OS/2 win ascent and descent.
- Characters: size, ratio, letter spacing, baseline shift, super- and subscript, colour, shade, underline,
strike-through, links and bookmarks; synthetic bold and italic (`GlyphRun.Embolden` / `SkewX`) for faces a family
lacks.
- Direction: a simplified bidirectional algorithm (strong letters and numbers set the levels, neutrals take the
direction around them, rule L2 reorders). The runs stay in logical order in the page, so the PDF's text reads as
it was written. Explicit embeddings, isolates and brackets come with the bidi milestone.
- **Interim page flow** (`FileeRenderer.cs`): each section's pages with its paper and margins, paragraphs stacked down
the body with a new page where the next line doesn't fit, headings as the PDF outline. It stands in for the page
composer; table cells are stacked as paragraphs and objects left out until then.

The PDF writer's subsets now keep only the characters the text has in their cmap: CJK fonts map 文 and the Kangxi
radical ⽂ to one glyph, and the PDF's ToUnicode took the lower code point, so Japanese and Chinese text copied out as
radicals.

Tests: `LineBreakerTests`, `ParagraphLayoutTests` (with the pinned Noto fonts of `build/fetch-fonts.ps1`), and the
render quality harness, where the renderer is `DocumentRenderers.Candidate`. Against rhwp, Japanese, Chinese, lists
and tabs break into the same lines on the same baselines; rhwp splits some Korean words even in 어절 paragraphs. The
rest waits for the page composer. Against 한글 itself, `HangulLineBreakTests` compares the line starts 한글 stored in
its HWPX files (`hp:linesegarray`) with Filee's, for the plain-text paragraphs of a private corpus
(`FILEE_RENDER_CORPUS`): on the HWPX files attached to Filee's issues, 150 of 152 paragraphs break where 한글 broke
them. Of the other two, one uses a font that isn't installed, and the other is a run of 75 hyphens wider than the
line, which 한글 keeps on one line.

## DOCX writer

The counterpart of the HWPX writer: `Hwp/Hwpx/Docx/DocxWriter*.cs` writes WordprocessingML by hand from the same
Expand Down
9 changes: 6 additions & 3 deletions docs/RENDER-QUALITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ internal sealed record DocumentRenderer(string Name, IReadOnlySet<string> Format
(`RhwpInput`'s fixes, then `rhwp export-pdf` with Filee's fallback fonts). rhwp is only the yardstick here; nothing in
the harness adds a conversion path through it.

`DocumentRenderers.Candidate` is Filee's layout engine. It is null until the first layout milestone sets it, and until
then the compare test is skipped with that reason. Once it is set, its PDFs get the same read-back checks and are
compared with rhwp's, with no other change.
`DocumentRenderers.Candidate` is Filee's layout engine: paragraph layout on an interim page flow
(`Typesetting/FileeRenderer.cs`) until the page composer replaces it. Its PDFs get the same read-back checks and are
compared with rhwp's; set to null, the compare test is skipped.

## The feature matrix

Expand Down Expand Up @@ -111,6 +111,9 @@ shaping.
holds documents next to the PDFs 한글 printed of them (`doc.hwpx` + `doc.pdf`, `doc.hwp` + `doc.pdf`). Each renderer
is compared with 한글's PDF and read back. An HTML report goes to the temp folder, with the page that differs most of
each document side by side. The corpus run only reports; it never fails on the numbers.
`HangulLineBreakTests` runs on the same folder: for every HWPX 한글 saved (no PDF needed), the plain-text
paragraphs are laid out by Filee at the width 한글 used, and their line starts are compared with the ones 한글
stored (`hp:linesegarray`).

## rhwp today

Expand Down
1 change: 1 addition & 0 deletions src/Filee.App/Assets/i18n/id.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@
"error.fon_unsupported": "Jenis font .fon ini tidak didukung",
"error.fon_too_large": "Font .fon terlalu besar untuk dikonversi",
"error.fon_damaged": "File .fon rusak atau terpotong",
"error.document_too_complex": "Dokumen terlalu besar atau terlalu rumit untuk dibaca dengan aman",
"error.m4s_needs_init": "File .m4s ini adalah salah satu pecahan video streaming: tidak dapat dibaca tanpa segmen awal (init) terpisahnya",
"notice.pdf_not_smaller": "Ukuran tidak dapat diperkecil lagi, sehingga disalin apa adanya",
"notice.under_min_size": "Disimpan, tetapi lebih kecil dari ukuran file minimum",
Expand Down
1 change: 1 addition & 0 deletions src/Filee.App/Assets/i18n/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@
"error.fon_unsupported": "Questo tipo di font .fon non è supportato",
"error.fon_too_large": "Il font .fon è troppo grande per essere convertito",
"error.fon_damaged": "Il file .fon è danneggiato o troncato",
"error.document_too_complex": "Il documento è troppo grande o troppo complesso per essere letto in sicurezza",
"error.m4s_needs_init": "Questo .m4s è un frammento di un video in streaming: non può essere letto senza il suo segmento iniziale (init) separato",
"notice.pdf_not_smaller": "Impossibile ridurlo, quindi è stato copiato così com'è",
"notice.under_min_size": "Salvato, ma più piccolo della dimensione minima del file",
Expand Down
10 changes: 8 additions & 2 deletions src/Filee.Engines/Fonts/FontSubsetter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,14 @@ public static bool CanSubset(SfntFont font, out string? reason)
/// A TrueType font with the outlines of <paramref name="glyphs"/> (plus .notdef and the components they use) at
/// their original glyph IDs and every other glyph empty.
/// </summary>
/// <param name="codePoints">
/// The characters drawn with the font, or null: the cmap then keeps only these, so a glyph several characters share
/// (文 and the Kangxi radical ⽂ in CJK fonts) maps back to the character the text has, not the lowest one.
/// </param>
/// <exception cref="NotSupportedException">The font is one <see cref="CanSubset"/> refuses.</exception>
/// <exception cref="ArgumentOutOfRangeException">A glyph ID is outside the font.</exception>
public static SfntFont Subset(SfntFont font, IEnumerable<int> glyphs, CancellationToken cancellationToken = default)
public static SfntFont Subset(SfntFont font, IEnumerable<int> glyphs, CancellationToken cancellationToken = default,
IReadOnlySet<int>? codePoints = null)
{
if (!CanSubset(font, out var reason))
throw new NotSupportedException(reason);
Expand Down Expand Up @@ -95,7 +100,8 @@ public static SfntFont Subset(SfntFont font, IEnumerable<int> glyphs, Cancellati
if (source["cmap"] is { } cmap)
{
var map = CmapTable.Read(cmap, out var symbol);
subset.Tables["cmap"] = CmapTable.Build(map.Where(p => used.Contains(p.Value)).ToDictionary(), symbol);
subset.Tables["cmap"] = CmapTable.Build(
map.Where(p => used.Contains(p.Value) && (codePoints is null || codePoints.Contains(p.Key))).ToDictionary(), symbol);
}
else
{
Expand Down
18 changes: 15 additions & 3 deletions src/Filee.Engines/Hwp/Hwpx/HwpxModel.Layout.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,29 @@ namespace Filee.Engines.Hwp.Hwpx;
/// <param name="Scale">Word character scale (w:w) in percent: the width of every glyph, like 한글's 장평.</param>
/// <param name="LetterSpacing">Word letter spacing (w:spacing) as an absolute distance added after each character.</param>
/// <param name="Position">Word baseline shift (w:position): positive raises the text.</param>
/// <param name="FontSpace">
/// 한글 "글꼴에 어울리는 빈칸" (useFontSpace): a space is as wide as the font's own; off, it is half the font size.
/// Null where the source has no such setting (the font's own width).
/// </param>
/// <param name="Kerning">한글 "커닝" (useKerning): the font's kerning applies; null = it does (as in Word).</param>
internal sealed record HCharLayout(
HLanguageSlots? Slots = null,
HWordFonts? WordFonts = null,
int? Scale = null,
int? LetterSpacing = null,
int? Position = null)
int? Position = null,
bool? FontSpace = null,
bool? Kerning = null)
{
/// <summary>This layout with the members it leaves open taken from <paramref name="baseline"/>.</summary>
public HCharLayout Over(HCharLayout? baseline) => baseline is null ? this : new HCharLayout(
Slots is null ? baseline.Slots : Slots.Over(baseline.Slots),
WordFonts ?? baseline.WordFonts,
Scale ?? baseline.Scale,
LetterSpacing ?? baseline.LetterSpacing,
Position ?? baseline.Position);
Position ?? baseline.Position,
FontSpace ?? baseline.FontSpace,
Kerning ?? baseline.Kerning);
}

/// <summary>한글's seven language slots (글꼴 언어): each character takes the properties of the slot its script falls into.</summary>
Expand Down Expand Up @@ -176,7 +185,10 @@ internal sealed record HParaBorder(HBorders Borders, string? Fill, HInsets Offse

/// <summary>Where lines may break and how they are fitted. Null members are left to the style (or the default rules).</summary>
/// <param name="Latin">Latin words: kept whole (어절), hyphenated or broken anywhere (한글 breakLatinWord, Word w:wordWrap).</param>
/// <param name="NonLatin">Korean words: kept whole (어절) or broken at any syllable (글자) (한글 breakNonLatinWord).</param>
/// <param name="NonLatin">
/// Korean words: kept whole (어절) or broken at any syllable (글자) (한글 breakNonLatinWord, whose BREAK_WORD is 어절 and
/// KEEP_WORD 글자).
/// </param>
/// <param name="LineWrap">한글 lineWrap: wrap, squeeze the line, or keep it on one line.</param>
/// <param name="Kinsoku">East Asian line start / end rules apply (Word w:kinsoku; 한글 applies its 금칙 characters).</param>
/// <param name="OverflowPunctuation">Punctuation may hang past the line end (Word w:overflowPunct).</param>
Expand Down
9 changes: 7 additions & 2 deletions src/Filee.Engines/Hwp/Hwpx/HwpxReader.Header.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ private HCharFormat CharFormat(string? id)
Color: Color(Attr(charPr, "textColor")) ?? "#000000",
Shade: Color(Attr(charPr, "shadeColor")),
Spacing: Int(Child(charPr, "spacing"), "hangul"),
Layout: new HCharLayout(Slots(charPr, Face)));
Layout: new HCharLayout(Slots(charPr, Face), FontSpace: Flag(charPr, "useFontSpace"), Kerning: Flag(charPr, "useKerning")));
return _charFormats[id] = format;
}

Expand Down Expand Up @@ -175,7 +175,12 @@ private HParaLayout ParaLayout(XElement paraPr, XElement? breaks, HLineHeight li
"BREAK_WORD" => HLatinBreak.BreakWord,
_ => HLatinBreak.KeepWord,
},
NonLatin: Attr(breaks, "breakNonLatinWord") == "BREAK_WORD" ? HNonLatinBreak.BreakWord : HNonLatinBreak.KeepWord,
// 한글 writes the 한글 단위 setting the other way round from its names: KEEP_WORD for 글자 (a break at any
// syllable) and BREAK_WORD for 어절. The binary HWP of the same document has bit 7 of the paragraph shape set
// (1 = 글자 in Hancom's HWP 5.0 format document) where the HWPX says KEEP_WORD, and 한글's own line layout of
// KEEP_WORD paragraphs breaks inside words.
// 본 제품은 한글과컴퓨터의 한/글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다.
NonLatin: Attr(breaks, "breakNonLatinWord") == "BREAK_WORD" ? HNonLatinBreak.KeepWord : HNonLatinBreak.BreakWord,
LineWrap: Attr(breaks, "lineWrap") switch
{
"SQUEEZE" => HLineWrap.Squeeze,
Expand Down
11 changes: 11 additions & 0 deletions src/Filee.Engines/Rendering/PageContent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ internal sealed record GlyphRun(PageFont Font, float Size, ushort[] Glyphs, SKPo
/// <summary>Horizontal scale of the glyphs (1 = as designed; 한글's 장평).</summary>
public float ScaleX { get; init; } = 1;

/// <summary>Bold the font lacks, drawn by thickening the outlines.</summary>
public bool Embolden { get; init; }

/// <summary>Horizontal skew of the glyphs (italic the font lacks: <see cref="SyntheticSlant"/>), 0 = upright.</summary>
public float SkewX { get; init; }

/// <summary>The skew of synthetic italic, as Skia and browsers slant (about 12°).</summary>
public const float SyntheticSlant = -0.2f;

/// <summary>Throws when the arrays don't describe the same glyphs or a cluster points outside the text.</summary>
public void Validate()
{
Expand Down Expand Up @@ -119,6 +128,8 @@ private static void DrawRun(SKCanvas canvas, GlyphRun run, SKTypeface typeface)
using var font = new SKFont(typeface, run.Size)
{
ScaleX = run.ScaleX,
SkewX = run.SkewX,
Embolden = run.Embolden,
Subpixel = true,
Edging = SKFontEdging.Antialias,
Hinting = SKFontHinting.None,
Expand Down
Loading
Loading