The Need: a book you can read on a phone

I had translated six old commentary volumes into Korean with an API. The first thing I made from one of them was a print-style PDF, and then a page-image viewer: 301 JPG files, 48 MB, one image per page.

It looked fine on a tablet. On a phone held upright, the letters were tiny. Pinch-zooming a page image is not reading.

So the real need was simple: text that reflows to the screen, with a font size I can change.

The Proof: what I have now

One command per book. Each result is one index.html plus two font files.

BookChapters in the menuFootnotesFolder size
Book 1 (Genesis volume)30210.99 MB
Book 227151.1 MB
Book 320231.1 MB
Book 428241.3 MB
Book 5 (part 1)6221.2 MB
Book 6 (part 2)25261.3 MB

The image version of one book was 48 MB. This is about 1 MB.

What the viewer does:

  • A cover page, a nested table of contents, and a “back to cover” button in the menu.
  • Font size, line height, serif or sans, three color themes, and justified or left-aligned text.
  • Tap a footnote number and the note slides up from the bottom.
  • It remembers where I stopped, and shows a ✓ on chapters I finished.
  • A setting called View mode: Scroll / Page turn. Page turn shows one page at a time. Tap the right side of the screen, swipe, or press the arrow keys.

I tested it on my own phone over home Wi-Fi. My words after the page-turn test were “it works well.” That is a sample of one phone, one person.

The Story: why not just make an EPUB?

These books are for my own Bible study, and I wanted to read them in bed, on the phone. An EPUB needs a reader app, and I did not want another app. A single HTML file opens in any browser, and I can later put it on the NAS like my other apps.

I also did not want to depend on a web font service. The viewer ships its own font files, so it works offline.

Honest status: the translations are machine output and I have not reviewed them. The Bible quotes inside them are the model’s wording, not a standard Korean Bible text. The viewer says so on the cover. I have not copied any of these books to the family media server, because that server is used by my daughter too.

The How

1. The viewer reads the same text as the PDF

The translation script already saves one JSON line per paragraph. One function, classify_rows(), labels each paragraph as chapter heading, separator, footnote, or body. The PDF builder and the new viewer share it. After I moved the code into that shared function, I checked that the PDF still had the same 294 pages with the same text.

py make_ebook.py 37915 --title "Genesis Notes" --en-title "..." --viewer text

The default is still --viewer image, so the old output did not change. The new option writes to a separate folder. Chapter titles come from the English headings in the source, not from the model. In one chapter the model had translated a heading as a normal sentence.

2. A free font, cut down to what the book uses

I checked the license of the fonts first. Noto Serif KR and Noto Sans KR are under the SIL Open Font License 1.1. That allows free use, including commercial use.

A full Korean font is several MB. So I used Python’s fontTools to make two static weights (400 and 700) from the variable font that Windows already had, then subset them to only the characters that appear in the book, as woff2:

pip install fonttools brotli

Each file came out near 150 KB. The folder also gets a LICENSE.txt for the font.

3. Lost footnote markers

This one surprised me. The English source had 21 footnotes, written as [1], [2] and so on. After translation, 15 of the 21 markers were gone from the body text, and the numbers in front of the footnotes were gone too. The model had dropped them.

I did not edit the translation file. I wrote a small file, footnote_fixes.json. Each entry says: footnote number, paragraph number, and a Korean phrase after which the marker goes.

{"fn": 5, "para": 52, "after": "그를 주 하나님과", "reason": "English: 'the Lord God[5]'"}

The build script applies it while typesetting. It prints a warning if the paragraph is missing, if the phrase is found zero times or more than once, or if the marker already exists. So if I re-translate a paragraph later, the build tells me.

I chose each position by reading the English sentence, and I looked at all 15 in a table before applying them.

🗂 Claude.md Rule When a translation drops structure (footnote markers, numbering), fix it in a separate file applied at build time. Keep the raw translation output unchanged, and make the build warn when a fix no longer matches.

For the other five books I did not write fix files. I only checked that each footnote has a marker in the body. Five books pass. In book 4, footnote 1 has no marker, and I have not fixed it yet.

4. Page-turn mode with CSS columns

Scroll mode is easy. Page-turn mode is where most of the work went.

The idea: lay out one whole chapter as CSS columns. Make each column exactly as wide as the screen’s text area, and make the column height equal to the screen height. Then “page 3” is just the chapter shifted left by three column widths.

#text { column-width: var(--w); column-gap: 40px; column-fill: auto; height: var(--h); }

A few details that took real time:

  • Height. Mobile browsers change the viewport height when the address bar hides. If the page height changes, the page breaks move and you lose your place. I used 100svh (the smallest viewport height), and recompute only when the page geometry really changes.
  • Position. Saving “page 7” is wrong, because page 7 changes when I change the font size. I save a text anchor: which paragraph, and which character in it. After any change, I find the page that contains that character.
  • Chapter edges. “Next” on the last page goes to page 1 of the next chapter. “Previous” on page 1 goes to the last page of the previous chapter. At the very start and end of the book, nothing happens.
  • Reduced motion. The slide takes 0.18 seconds, and it is switched off if the phone asks for reduced motion.

💬 Prompt that worked “I want to add a ‘View mode: Scroll / Page turn’ option to settings. The default stays as it is now (scroll). Don’t change any code yet — make a plan first. Include a running head and page number, keep the reading position when I change the font size, and split the work into steps I can check on my phone.”

Asking for a plan with steps I could check on the phone mattered. I got five small commits instead of one big one.

5. What broke on a real phone

After step 2 I looked at it on my phone, and found three problems that my PC browser tests had missed:

  1. Tapping to go to the next page did nothing on the first page. My tap zones sat inside the text box only, and on a phone most of the screen is margin. Fix: listen on the whole document. Left 30% is previous, right 30% is next, the middle is the menu. I also added swipe and keyboard keys.
  2. The page number was 11px and hard to read. Now 14px.
  3. The menu bar seemed to have only the font-size button. The bar starts hidden in page mode, by my own design, and I had not made that clear. Tapping the middle of the screen shows it.

Later I got a “resume reading is gone” report. It turned out I had given the phone a test link with nosave=1 and ch=3 in it, which turns saving off and skips the cover. Not a bug, but I had forgotten to say so. I gave the plain link and the report ended.

6. Testing from a PC browser automation

My automation tab runs in the background, so CSS transitions and resize events only fire for about one second after a screenshot. At first this looked like “the slide animation is broken.” It was not. Run the test right after a screenshot.

For the phone, I served the folder with Python’s built-in server on the home Wi-Fi, opened the PC’s IP on the phone, and stopped the server afterward:

python -m http.server 8802 --bind 0.0.0.0

Cost

$0. The fonts are free (OFL). The viewer needs no API calls. The translation cost was in an earlier post. The only extra tool I installed was the brotli Python package, to write woff2.

What I have not checked

  • Real phone rotation and the in-app browser of a chat app. I tested rotation by resizing a desktop window only.
  • iPhone Safari. It does not support text-justify, so justified text may have wider gaps there.
  • Switching from page mode back to scroll mode on the last page of a chapter can move you one or two pages earlier. A short chapter that fits on one screen has the same problem.
  • Book 2 has two chapters both named “Chapter 2” in the menu. That is how the source headings are, and I left them.
  • The other five books were built and checked by script only (chapter list and footnote markers). I have not read them on the phone.