Repository navigation
feat(fonts): read bitmap .fon and convert each size to TTF, OTF, WOFF, WOFF2 and EOT - #561
Conversation
|
@KnifeLemon 님, 리더를 다시 점검하다가 비슷한 문제를 하나 찾아서 지금 고치고 있어요. 글자들이 같은 비트맵을 가리키는 작은 파일의 메모리·시간: 글자 256개가 모두 같은 1024×1024 비트맵을 가리키게 만든 395 KB 파일을 TTF로 바꾸면 메모리를 약 1 GB 쓰고 9분 가까이 걸려요. 글자마다 비트맵을 새로 펼치고, 높이 상한은 그 뒤에야 확인하기 때문이에요. 비트맵을 펼치기 전에 글자 픽셀 총량에 상한을 두고, 높이도 먼저 확인하고, 글자마다 취소를 확인하게 바꾸는 중이에요. 곧 커밋 하나로 올릴게요. 그때까지 머지는 잠시만 기다려 주세요. |
KnifeLemon
left a comment
There was a problem hiding this comment.
@SeoJCalive 님, 큰 기능을 꼼꼼하게 만들어 주셔서 감사해요. NE 리소스 테이블과 FNT v2/v3 헤더를 오프셋 하나하나 범위 검사하며 읽고 있고, 외곽선도 같은 가로 구간을 직사각형으로 묶은 뒤 겹치는 변을 지워서 바깥은 시계 방향, 구멍은 반시계 방향으로 깔끔하게 나와요. 크기가 한 개면 접미사 없이, 여러 개면 Name-13px.ttf로 나누는 것도 이야기한 대로이고, 쓰기 전에 모든 출력을 메모리에서 먼저 만드는 것도 좋아요.
먼저 질문에 답할게요.
- 같은 높이의
-2: 같은 높이에서 굵기나 기울임만 다르면Name-13px-Bold.ttf처럼 스타일 이름을 붙이고, 패밀리는Name 13px그대로 두고 스타일만Bold로 해 주세요. 그래야 글꼴 메뉴에서 한 패밀리로 묶여요.-2는 높이와 스타일이 모두 같을 때만 쓰면 돼요. - 단일 바이트 문자 집합만: 지금은 그 범위로 충분해요. 2바이트 글꼴은 요청이 오면 따로 볼게요.
- 엔진 설명: "NE, monochrome, single-byte"는 사용자가 알기 어려운 말이라 짧게 해 주세요. 예를 들면 en은 "Fonts: TTF, OTF, WOFF, WOFF2 and EOT into each other; Windows bitmap fonts (FON) into them."이에요. README 글꼴 줄도 3D 모델의 "FBX read"처럼
TTF, OTF, WOFF, WOFF2, EOT;<br>FON (Windows bitmap fonts) read | each other;<br>FON → one font per size정도면 충분해요.FontConverter.GetStatus의 문자열도 같이 줄여 주세요. 자세한 조건은docs/ENGINES.md에 남아 있으니 괜찮아요.
고칠 점이에요.
FonReader.ReadFnt가 glyph마다bool[height, width]를 만들어서 작은 파일이 큰 메모리를 써요. 높이와 너비에 상한이 없고, 여러 glyph가 같은 비트맵 오프셋을 가리켜도 막지 않아요. 그래서 비트맵 1바이트가 glyph 하나에 8바이트로, 그것도 glyph 수만큼 불어나요. glyph 224개가 4096×1024 비트맵 하나를 같이 가리키는 514 KB 파일을 CLI로 TTF로 바꿔 보니 최대 메모리가 959 MB, 19.7초였어요.FonOutlineBuilder가 어차피 거절하는 높이(upem 16384 초과)와 너비(좌표 32767 초과)를 할당 전에 리더에서 먼저 거르고, 파일 전체의 픽셀 수(모든 크기의 width×height 합)에도 상한을 걸어 주세요. 아니면bool[,]대신 리소스의 바이트를 그대로 들고Outline에서 비트를 읽어도 돼요. #530의Huge_declared_data_size…처럼GC.GetAllocatedBytesForCurrentThread로 할당량을 확인하는 테스트도 하나 넣어 주세요.- 지원하지 않는 파일이 영어 오류로 나와요. 벡터 .fon(Modern, Roman, Script)이 어느 언어에서나 "엔진에서 오류가 났어요 (Vector FNT fonts are not supported.)"처럼 보여요. FIT의
error.fit_*처럼ConversionRefusedException과 문구 키로 바꿔 주세요. 벡터 글꼴용, 지원하지 않는 종류용(PE, 색상, 2바이트 문자 집합), 손상된 파일용 정도면 충분해요. 새 문구는 16개 언어 파일에 모두 넣어 주세요. - 테스트 샘플 파일을 저장소에 넣지 말아 주세요.
tests/Filee.Engines.Tests/Fonts/의Fon-*.fon12개와make-test-fon.py,FON-FIXTURES.md가 커밋돼 있는데, 이 저장소는 테스트 입력을 실행할 때 만들어요. #530의FitBuilder처럼 C#FonBuilder로 테스트 안에서 .fon을 만들어 주세요. 이 파일들과 두 csproj의.fon복사 항목,docs/ENGINES.md의 샘플 설명 문장도 같이 빼 주세요.
스키마 47은 #530과 함께 쓰기로 했으니, #530이 먼저 들어가면 같은 단계로 합치는 건 저희가 할게요.
이 세 가지가 정리되면 머지할 수 있어요.
a81c311 to
7742a94
Compare
|
@KnifeLemon 님, 꼼꼼하게 봐 주셔서 감사해요. 말씀하신 내용을 커밋 다섯 개로 나눠 올렸고, 전에 말씀드린 비트맵 상한 수정도 같이 들어가 있어요. 먼저 질문 답에 맞춘 부분이에요.
요청하신 수정 사항은 이렇게 반영했어요.
정상 폰트의 출력은 수정 전과 바이트 단위로 같아요. 다시 봐 주세요. |
KnifeLemon
left a comment
There was a problem hiding this comment.
@SeoJCalive 님, 다섯 커밋 확인했어요. 스타일 이름, 짧아진 엔진 설명과 README·GetStatus, 리더의 픽셀 상한과 할당 테스트, error.fon_* 네 키, 샘플 파일을 FonBuilder로 바꾼 것까지 모두 말씀드린 대로예요. 하나만 더 봐 주세요.
-
비트맵을 같이 쓰는 작은 파일이 외곽선 단계에서 오래 걸려요. 리더 상한으로 할당은 막혔는데,
FonOutlineBuilder.Outline이 같은 비트맵을 가리키는 glyph마다 외곽선을 처음부터 다시 만들어요. 또 윤곽선을 하나 시작할 때마다edges.First()가 지금까지 지운 칸을 처음부터 다시 훑어서, 켜진 픽셀 수의 제곱에 가깝게 느려져요. 128×128 체크무늬 비트맵 하나를 glyph 256개가 가리키는 4.6 KB 파일(픽셀 합이 딱 4,194,304)은 외곽선만 63초 걸렸고, 할당 합계가 4.5 GB, TTF가 23 MB였어요. 2048×2048 체크무늬 glyph 하나(526 KB)는 결국 65535점 한도로 거부되지만, 그 전에 간선을 다 만들어서 최대 메모리가 1 GB였어요.- 같은 (비트맵 오프셋, 너비)는 외곽선을 한 번만 만들어서 같이 써 주세요.
- 윤곽선 시작점은 간선을 넣은 순서대로 둔 목록에서 앞으로만 가며 찾으면 돼요. 이렇게만 바꿔 보니 180×180 체크무늬 glyph 하나가 1.3초에서 0.3초가 됐어요.
- 점 수는 간선을 만들기 전에 픽셀에서 바로 셀 수 있어요. 각 격자 꼭짓점의 2×2 이웃에서 켜진 칸이 1개나 3개면 점 1개, 대각선으로 2개면 점 2개예요. 이 합이 65535를 넘으면 간선을 만들기 전에
error.fon_too_large로 거부해 주세요.
리더처럼 할당량을 확인하는 테스트도 하나 넣어 주세요(예: 2048×2048 체크무늬가 할당 전에 거부되는지).
스키마 47은 #530과 같이 쓰기로 했으니, #530이 먼저 들어가면 47 단계를 ["fit", "fon"]으로 합치는 건 저희가 할게요. 이것만 정리되면 머지할 수 있어요.
… EOT Read monochrome FNT 2.0/3.0 resources from NE .fon files and build a pixel-exact TrueType font for each size, then reuse the existing font writers for the other formats. A single resource keeps the plain name; several resources get one file per size. Characters map through the code page named by the font charset. Vector, PE, colour, double-byte and corrupt files are refused before anything is written. Closes #177
A small crafted .fon could alias one large bitmap from every glyph and force minutes of work and about 1 GB of memory. The reader now charges max(width, 1) * height per glyph across the whole file against a 4,194,304-unit budget and rejects heights above 16384 before allocating any pixels. Resource overlap is checked after sorting, same-height ordinals are counted in one pass, and cancellation is observed in the reader, the converter and every outline and serialization stage. Output for valid fonts is unchanged.
Cache outlines by bitmap offset and width, and serialize shared glyphs once. Trace contours from a forward-only edge insertion cursor and reject excess points before allocating edges. Cover early refusal, shared allocation, width-specific outlines, and counted points with tests.
7742a94 to
a508d73
Compare
Keep both schema steps: v47 learns .fit and .fon, v48 learns .m4s (#603). Keep the error.fon_* and error.m4s_needs_init keys in every language file. README format count 265 -> 266 (the registry now has .fon too). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApPC2ToT2xB1UGEZCUBD4q
|
@SeoJCalive 님, 마지막 커밋 확인했어요. 같은 (비트맵 오프셋, 너비)는 외곽선을 한 번만 만들고, 시작점은 넣은 순서대로 앞으로만 찾고, 점 수는 간선을 만들기 전에 2×2 이웃으로 세서 거르는 것까지 말씀드린 대로예요. 그 사이 #603(.m4s, 스키마 48)이 main에 들어가서 충돌이 났기에, main을 이 브랜치에 머지해 두었어요( CI가 초록이면 머지할게요. |
|
@KnifeLemon 님, 다시 봐 주셔서 감사해요. 말씀하신 세 가지를 커밋 하나로 올렸고, #530이 먼저 들어가서 main으로 리베이스하면서 스키마 47 단계도
시험은 네 개 넣었어요. 2048×2048 체크무늬가 빌더 할당 1 MB 미만으로 거부되는지, 같은 비트맵을 가리키는 64글자의 할당이 한 글자의 4배 미만인지, 오프셋이 같아도 너비가 다르면 외곽선을 따로 만드는지, 미리 센 점 수가 실제 외곽선의 점 수와 같은지 봐요. 정상 폰트 네 개 중 세 개는 출력이 바이트 단위로 수정 전과 같고, 하나는 윤곽선 시작점이 바뀌어 바이트는 달라졌지만 glyf·loca 밖의 표는 모두 같고 글자마다 외곽선 도형도 같아요. 하나 여쭤볼게요. 외곽선은 한 번만 만들지만 glyf에는 글자마다 그대로 들어가서, 위 256글자 파일의 TTF는 여전히 23 MB예요. 같은 외곽선을 composite glyph로 가리키게 해서 출력도 줄일까요, 아니면 지금처럼 둘까요? |
There was a problem hiding this comment.
@SeoJCalive 님, 꼼꼼하게 재고 고쳐 주셔서 감사해요. 54초가 0.8초로, 2048×2048 체크무늬가 1 GB에서 83 MB로 줄어든 것, 그리고 미리 센 점 수가 실제 외곽선과 같은지까지 테스트로 묶어 둔 것 모두 좋아요.
질문에 답할게요. composite glyph는 이 PR에서는 하지 않고 지금처럼 둘게요. 리더의 픽셀 상한 덕분에 출력 크기도 그 안에서 묶여 있고, 정상적인 .fon에서 여러 글자가 같은 비트맵을 가리키는 경우는 드물어요. 23 MB는 일부러 만든 파일에서만 나와요. 실제 폰트에서 크기 문제가 보이면 그때 따로 이슈로 볼게요.
macOS CI가 끝나면 머지할게요. 큰 기능 감사해요!
What does this change?
Closes #177. As planned in the issue, Filee now reads Windows bitmap fonts (
.fon) and converts them to TTF, OTF, WOFF, WOFF2 and EOT, keeping the pixel look..fonfile, fixed-width and proportional..fonwith one font resource givesName.ttfwith no suffix, as you suggested. Several resources give one file per size, such asName-13px.ttfandName-14px.ttf; a repeated height gets-2. The family names inside the fonts also differ per size, so they can be installed side by side. Skip, Rename and Overwrite apply to each output file..fon(Modern, Roman, Script), PE-format font files, colour or externally stored bitmaps, standalone.fnt, double-byte or unknown charsets, and truncated or corrupt files. All outputs are built in memory first, so a bad resource fails the whole file before anything is written..fonis input only; Filee does not write it.Implementation notes
1bf94e71, after 2.1.0); the new European Portuguese language file and README get the Fonts description too.FonReader.cs(NE resource table and FNT parsing with bounds checks),FonOutlineBuilder.cs(outlines and the TrueType tables: head, hhea, maxp, OS/2, name, cmap, post, glyf, loca, hmtx). The result goes through the existingFontConverterwriters for OTF, WOFF, WOFF2 and EOT.FontConverter.cs(fon → ttf/otf/woff/woff2/eot edges only, existing font routes unchanged),FormatRegistry.cs(fon, not writable),AppSettings.csandUserDataStore.cs(schema 47), the font engine description in every language file,docs/ENGINES.mdand the font row in every README. No shared conversion contracts (IConverter,JobQueue,OutputPathResolver) changed..fonin its Fonts donut once, the same way.fits/.usmdid; a profile of the user's own that already claims.fonkeeps it. Read Garmin FIT activities into the geospatial formats (#451) #530 also takes 47 for.fit. Since 47 is not released yet, both can share the one step: whichever PR merges second puts bothLearnExtensionscalls underif (from < 47). If Read Garmin FIT activities into the geospatial formats (#451) #530 ships first, I'll move this to 48.Fon-*.fonfiles are synthetic, drawn from pixel art bymake-test-fon.py(hashes inFON-FIXTURES.md), so there are no licence questions.Questions for you
-2(Name-13px-2.ttf, familyName 13px-2). Is that naming OK?Type of change
How was it tested?
dotnet format --verify-no-changesdotnet testTried it in the app (what you converted or clicked, and on which OS):
.fon→ TTF/OTF/WOFF/WOFF2/EOT from the donut and the CLI on Linux (X11)New tests (39), each checked to fail when its fix is reverted:
FonTests(Engines: parsing and rejections, FNT 2.0/3.0 × fixed/proportional, every glyph rendered in Skia and compared pixel by pixel with its bitmap, metrics, names, cmap per code page, build-everything-before-writing on failure and cancellation),FonFormatTests(Core, including the schema 47 step and a custom.fonclaim), CLI tests (single resource has no suffix in all five formats, one file per size, conflicts,--to fonrefused),EnginesPageTestsand the newPresetEditorTargetTests(App).Existing tests: all
FontTestspass. Core and Cli all pass on top of 2.1.0. Engines and App fail only the tests that also fail on main here (LibreOffice and Ghostscript installed on the machine).Build and format: the same warnings as 2.1.0 main,
dotnet format --verify-no-changesclean.Real CLI runs: two real bitmap fonts (
fixed.fon,tektite.fon) and the samples to TTF, OTF, WOFF, WOFF2 and EOT; every mapped glyph of every output rendered in SkiaSharp matches the FNT bitmap (0 differing pixels), andfc-queryshows a distinct family per size. 30 broken inputs (truncated, corrupt headers, vector FNT, PE and non-font NE files, unsupported v3 flags) each fail with a clear message, leave no output and keep existing files unchanged.App window (Linux): the donut offers TTF/OTF/WOFF/WOFF2/EOT for a
.fon(also mixed with a.ttf, and.FON), the donut and the CLI produce identical files, and the Fonts card reads well in English, Russian, Spanish and the dark theme.Size and speed:
fixed.fon(8.8 KB, one 13 px size) → TTF 21.6 KB, WOFF2 3.7 KB; one CLI run takes about 0.66 s, close to an existing TTF → WOFF2 run (0.62 s).Screenshots (UI changes)
Fonts engine card (new description):
Donut for a multi-size
.fon(TTF / OTF / WOFF / WOFF2 / EOT):Checklist
src/Filee.App/Assets/i18n/*.jsonfilesTHIRD-PARTY-NOTICES.md(none added)