Browser-based TTF/OTF → EPDFont converter for CrossPoint Reader. Runs entirely client-side with no file-size or font-size limits.
The app is a Next.js 16 static export. FreeType is shipped as a patched WASM module and all rasterization work happens in a Web Worker, so the UI stays responsive even when converting large fonts (Pretendard, Noto Sans CJK, etc.).
- TTF / OTF / WOFF / WOFF2 input
- 1-bit or 2-bit grayscale EPDFont output
- Full Unicode range picker with 14 quick presets, "select all", and custom intervals (hex)
- Rendering controls: char / line spacing, boldness, italic angle, horizontal scale, baseline shift, antialiasing
- Live canvas preview with adjustable BG/FG and zoom
- Korean / English UI (language toggle in the header)
- Static export compatible with GitHub Pages and any plain-file host
This project uses Bun as its package manager and runtime.
bun install
bun run devOpen http://localhost:3000.
| Script | Description |
|---|---|
bun run dev |
Next dev server (Turbopack) |
bun run build |
Production static export to out/ |
bun start |
Serve the built out/ directory locally |
bun run lint |
ESLint |
The predev / prebuild hooks run scripts/sync-freetype-wasm.mjs, which
copies node_modules/freetype-wasm/dist/* into public/wasm/ and patches the
binary / JS to enable heap growth (see "How it works").
bun run build
# out/ is ready to deploy as-is (GitHub Pages, S3, Netlify, etc.)Set NEXT_PUBLIC_BASE_PATH at build time if the site lives under a sub-path:
NEXT_PUBLIC_BASE_PATH=/epdfont-converter bun run buildLittle-endian binary. See src/lib/epdfont-converter.ts for the encoder and
src/types/epdfont.ts for the types.
Header (32 bytes):
magic uint32 0x46445045 ("EPDF")
version uint16 1
is2Bit uint8 0 = 1-bit B/W, 1 = 2-bit grayscale
reserved uint8
advanceY uint8 line height (px)
ascender int8 max above baseline
descender int8 max below baseline
reserved uint8
intervalCount uint32
glyphCount uint32
intervalsOffset uint32
glyphsOffset uint32
bitmapOffset uint32
Intervals (12 bytes each):
start uint32 first codepoint (inclusive)
end uint32 last codepoint (inclusive)
glyphOffset uint32 index into glyph array
Glyphs (16 bytes each):
width, height uint8
advanceX uint8
reserved uint8
left, top int16
dataLength uint32
dataOffset uint32
Bitmap data:
1-bit → 8 px/byte, MSB first
2-bit → 4 px/byte, 2 MSBs are the first pixel
freetype-wasm@0.0.4 on npm is compiled with ALLOW_MEMORY_GROWTH=0 and the
WASM module itself hard-codes min = max = 16 MB. That ceiling blows up on
anything larger than a trivial Latin font.
scripts/sync-freetype-wasm.mjs runs on every install / dev / build and does
two targeted patches:
- WASM memory section — rewrites the memory limits so
max = 32768 pages (2 GB). Initial stays at 256 pages (16 MB); Emscripten grows on demand. - JS wrapper — replaces the stub
emscripten_resize_heap(which just callsabort("OOM")) with a realwasmMemory.grow()+ view-refresh loop, using the module's existingupdateGlobalBufferAndViewshelper.
The patched assets land in public/wasm/ and are loaded from
${basePath}/wasm/freetype.js. The main thread loads FreeType for font
validation and canvas preview; conversion itself happens in
src/lib/freetype-worker.ts, which owns its own FreeType instance and streams
progress + glyph data back via postMessage with transferable buffers.
src/
app/ Next.js app router, root layout, icons
components/
Header.tsx, Footer.tsx
font-converter/ Main UI and converter controls
unicode-range.ts Range IDs, intervals, categories
i18n/
index.tsx React context + useT hook
locales/{ko,en}.ts All translated strings, incl. range names
lib/
epdfont-converter.ts Binary encoder, worker client wrapper
freetype-loader.ts Main-thread FreeType loader
freetype-worker.ts Off-thread rasterizer
freetype-worker-client.ts Worker message protocol
types/ FreeType + EPDFont type definitions
public/wasm/ Patched freetype.js + freetype.wasm
scripts/sync-freetype-wasm.mjs Install-time WASM patcher
- FreeType — glyph rasterization
- freetype-wasm — Emscripten wrapper, patched at install time for memory growth
MIT. See LICENSE.