kobofix 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
kobofix-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dmang-dev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
kobofix-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,295 @@
1
+ Metadata-Version: 2.4
2
+ Name: kobofix
3
+ Version: 1.0.0
4
+ Summary: Make EPUBs work on Kobo e-readers (Adobe RMSDK): detect and fix the modern CSS that breaks them.
5
+ Author: dmang-dev
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/dmang-dev/kobofix
8
+ Project-URL: Repository, https://github.com/dmang-dev/kobofix
9
+ Project-URL: Issues, https://github.com/dmang-dev/kobofix/issues
10
+ Keywords: epub,kobo,rmsdk,css,ebook,epubcheck,adobe-digital-editions,kepub
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: End Users/Desktop
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Topic :: Text Processing :: Markup
18
+ Classifier: Topic :: Utilities
19
+ Requires-Python: >=3.7
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Dynamic: license-file
23
+
24
+ # kobofix
25
+
26
+ Make an EPUB that **passes `epubcheck` but won't open (or renders broken) on a Kobo**
27
+ work on a Kobo. Pure Python, standard library only — no `pip install`.
28
+
29
+ This is the problem described in
30
+ [*"Your EPUB is fine, Kobo disagrees — blame Adobe"*](https://andreklein.net/your-epub-is-fine-kobo-disagrees-blame-adobe/),
31
+ generalized to every known cause, not just the one line in that post.
32
+
33
+ ---
34
+
35
+ ## Three ways to use it
36
+
37
+ - **Web app (no install):** [`web/`](web/) is a 100%-client-side version for
38
+ GitHub Pages — drag in an `.epub`, see what breaks on Kobo, download the fixed
39
+ file. The book never leaves the browser. See [web/README.md](web/README.md).
40
+ - **CLI fixer:** `python kobofix.py book.epub` → repaired EPUB.
41
+ - **CLI linter:** `python kobofix.py --check book.epub` → report, optionally
42
+ merged with EPUBCheck.
43
+
44
+ A worked demo on a *real* book: [`make_modern_alice.py`](make_modern_alice.py)
45
+ takes real Project Gutenberg *Alice* content + the modern stylesheet a
46
+ contemporary authoring tool emits → EPUBCheck passes it clean, kobofix flags 11
47
+ book-breaking errors and repairs them.
48
+
49
+ ## Why your "valid" EPUB fails on Kobo
50
+
51
+ Kobo renders sideloaded `.epub` files through Adobe's **legacy RMSDK** engine,
52
+ whose CSS parser is frozen around 2013. Two independent things bite you:
53
+
54
+ ### 1. RMSDK has no CSS fault tolerance
55
+
56
+ A normal browser, on hitting one CSS declaration it doesn't understand, drops
57
+ *that one declaration* and keeps the rest. RMSDK does the opposite: a single
58
+ **value-function token it can't parse — `calc()`, `min()`, `max()`, `clamp()`,
59
+ `var()`, `env()` — makes it throw away the *entire stylesheet***, and on some
60
+ firmware **refuse to open the book** ("this book is corrupted").
61
+
62
+ The critical consequence: **a fallback declaration placed *before* the modern
63
+ one does not help** — the whole sheet (fallback included) is discarded. The
64
+ modern construct has to be *removed*, not shadowed. That's the core thing
65
+ `kobofix` does.
66
+
67
+ ```css
68
+ /* This whole stylesheet is discarded by RMSDK because of one token: */
69
+ .copyright img { max-width: min(150px, 30vw); }
70
+
71
+ /* kobofix rewrites it to: */
72
+ .copyright img { max-width: 150px; }
73
+ ```
74
+
75
+ ### 2. "Corrupted" is often a packaging bug, not CSS
76
+
77
+ The most common literal-"corrupted" cause is the EPUB's ZIP packaging: the
78
+ `mimetype` entry must be **first**, **uncompressed (STORED)**, with **no extra
79
+ field** and the **exact bytes** `application/epub+zip`. Many tools (some
80
+ calibre save paths, Windows "Send to → compressed folder", naive zip libraries)
81
+ violate this and Kobo is the strictest mainstream reader about it. `kobofix`
82
+ **always re-emits a spec-correct OCF ZIP**, so it fixes this even when no CSS
83
+ needed changing.
84
+
85
+ `epubcheck` catches neither problem class reliably: the CSS is valid CSS4 so it
86
+ passes, and older `epubcheck` didn't flag a compressed `mimetype`. Passing
87
+ `epubcheck` is **not** a signal of Kobo/RMSDK compatibility.
88
+
89
+ ---
90
+
91
+ ## Install
92
+
93
+ Nothing to install. You need Python 3.7+ (tested on 3.12).
94
+
95
+ ```
96
+ python kobofix.py --version
97
+ ```
98
+
99
+ ## Usage
100
+
101
+ ```
102
+ python kobofix.py INPUT.epub [-o OUTPUT.epub] [options]
103
+ ```
104
+
105
+ Default output is `INPUT.kobofixed.epub` next to the input. The original is
106
+ never modified.
107
+
108
+ ```bash
109
+ # Fix a book
110
+ python kobofix.py mybook.epub
111
+
112
+ # Choose the output name
113
+ python kobofix.py mybook.epub -o mybook-kobo.epub
114
+
115
+ # Just analyze — write nothing, print the report
116
+ python kobofix.py mybook.epub --dry-run
117
+
118
+ # LINT mode: report Kobo/RMSDK issues (KOBO-* ids + line numbers) WITHOUT changing
119
+ # anything, and merge in EPUBCheck's spec report for one combined readout.
120
+ python kobofix.py --check mybook.epub --epubcheck path\to\epubcheck.jar
121
+
122
+ # Machine-readable report
123
+ python kobofix.py mybook.epub --report json --report-file report.json
124
+
125
+ # Also run EPUBCheck on the result (you supply the jar; Java must be on PATH)
126
+ python kobofix.py mybook.epub --epubcheck path\to\epubcheck.jar
127
+
128
+ # Prove the engine works on your machine
129
+ python kobofix.py --selftest
130
+ ```
131
+
132
+ You can also pass an **already-extracted EPUB folder** as the input; the tool
133
+ will read it and produce a correctly-packaged `.epub`.
134
+
135
+ ### Options
136
+
137
+ | Option | Default | Meaning |
138
+ |---|---|---|
139
+ | `-o, --output PATH` | `<input>.kobofixed.epub` | Output file |
140
+ | `--check` | off | Lint only: report `KOBO-*` issues + line numbers, merge EPUBCheck; write nothing |
141
+ | `--dry-run` | off | Analyze and report only; write nothing |
142
+ | `--report {text,json}` | `text` | Report format |
143
+ | `--report-file PATH` | — | Also write the report to a file |
144
+ | `--no-rem` | rem→px on | Don't convert `rem` to `px` |
145
+ | `--root-font-size PX` | `16` | Pixel value of `1rem` for the conversion |
146
+ | `--no-viewport` | vw/vh→px on | Don't convert standalone `vw/vh/vmin/vmax` |
147
+ | `--vw-base PX` | `600` | Assumed reader viewport **width** for `vw` |
148
+ | `--vh-base PX` | `800` | Assumed reader viewport **height** for `vh` |
149
+ | `--clamp-pick {min,pref,max}` | `pref` | Which `clamp()` term to keep when none is an absolute length |
150
+ | `--epubcheck [PATH]` | off | Run EPUBCheck after building (`epubcheck`/`*.jar`, or no value to auto-detect) |
151
+ | `--strict` | off | Exit non-zero if any manual-review warnings were found |
152
+
153
+ Exit codes: `0` clean · `1` `--strict` with warnings · `2` output failed its
154
+ own packaging self-check.
155
+
156
+ ---
157
+
158
+ ## What it fixes automatically
159
+
160
+ These are the changes that make Kobo **open and style** the book. Every one
161
+ leaves **zero** residual modern-value tokens in RMSDK-visible CSS.
162
+
163
+ | Construct | Action |
164
+ |---|---|
165
+ | `var(--x)` / `--x:` | Resolved to the literal value and inlined; the `--x` declarations are deleted |
166
+ | `calc(...)` | Evaluated when units allow (`calc(2em + 1em)`→`3em`); mixed-unit (`calc(100% - 20px)`) keeps the first term and is flagged |
167
+ | `min(a,b)` / `max(a,b)` | Reduced to the absolute-length term (`min(150px,30vw)`→`150px`) |
168
+ | `clamp(a,b,c)` | Reduced to one static size (prefers an absolute term; else `--clamp-pick`) |
169
+ | `env(x, fallback)` | Replaced with the fallback (or removed) |
170
+ | `rem` | Converted to `px` (RMSDK mis-treats `rem` as `em`, compounding sizes) |
171
+ | `vw/vh/vmin/vmax` | Converted to `px` against the assumed viewport (verified crash: `margin:50vh`) |
172
+ | empty `@media {}` / `@supports {}` | Removed (crashes old RMSDK) |
173
+ | `mimetype` packaging | Rewritten first / STORED / no-extra / exact bytes |
174
+
175
+ All of the above are applied inside standalone `.css` files **and** inside
176
+ `<style>` blocks and `style="..."` attributes in XHTML.
177
+
178
+ ## What it flags but does NOT change
179
+
180
+ These have no safe automatic equivalent — converting them risks silently
181
+ reflowing your book, so `kobofix` reports them (with file and line) and leaves
182
+ the decision to you:
183
+
184
+ - **flexbox** and **grid** (and grid sub-properties)
185
+ - **`position: absolute/fixed/sticky`** in reflowable content
186
+ - **`transform`**, **`transition`/`animation`/`@keyframes`**
187
+ - **`object-fit`**, **`aspect-ratio`**
188
+ - **`:has()`**, **`:is()`/`:where()`**
189
+ - **`writing-mode`** — *kept on purpose*; Kobo supports it for vertical CJK text
190
+
191
+ The typical fix for flex/grid is to wrap the modern rules in
192
+ `@supports (display:flex) { … }` (legacy RMSDK skips the whole block) with a
193
+ plain block/float fallback outside it.
194
+
195
+ ---
196
+
197
+ ## How to verify the result
198
+
199
+ 1. `kobofix` self-checks its own output (mimetype first/STORED/no-extra/exact).
200
+ 2. Open the fixed file in **Adobe Digital Editions** — it uses the same RMSDK
201
+ engine as Kobo, so if it opens and styles correctly there, Kobo will too.
202
+ This is the single highest-value manual test.
203
+ 3. Optionally run EPUBCheck (`--epubcheck`) for container/structural validity.
204
+ 4. Sideload to an actual Kobo for final confirmation.
205
+
206
+ ---
207
+
208
+ ## Checking mode (`--check`) — the Kobo readiness linter
209
+
210
+ `kobofix --check book.epub` makes **no changes**. It reports every RMSDK landmine
211
+ with a stable rule id and a `file:line` location (correct even inside `<style>`
212
+ blocks and `style=""` attributes), and — if you point it at an EPUBCheck jar —
213
+ runs EPUBCheck too and prints **one combined report**: spec conformance from the
214
+ validator you already trust, plus the Kobo/RMSDK layer EPUBCheck can't see.
215
+
216
+ ```
217
+ python kobofix.py --check book.epub --epubcheck path\to\epubcheck.jar
218
+ ```
219
+
220
+ If `--epubcheck` is given with no value, kobofix auto-discovers an `epubcheck`
221
+ on `PATH` or a bundled `tools/**/epubcheck.jar`. Exit codes: **2** if any
222
+ book-breaking issue (a `KOBO-001` function or `KOBO-000` packaging error, or an
223
+ EPUBCheck error/fatal), **1** with `--strict` if only warnings, else **0** —
224
+ so it drops straight into CI. Use `--report json` for machine output.
225
+
226
+ ### Rule ids
227
+
228
+ | Rule | Sev. | What |
229
+ |---|---|---|
230
+ | `KOBO-000` | error | `mimetype` packaging wrong (the literal "corrupted" cause) |
231
+ | `KOBO-001` | error | `calc/min/max/clamp/var/env` — drops the whole stylesheet / fails to open |
232
+ | `KOBO-002` | warning | viewport unit `vw/vh/vmin/vmax` (in a margin: blank-screen crash) |
233
+ | `KOBO-003` | warning | empty `@media`/`@supports` block (crashes old RMSDK) |
234
+ | `KOBO-004` | warning | `rem` (rendered as `em` → wrong sizes) |
235
+ | `KOBO-010..019` | warn/info | flexbox, grid, position, transform, animation, object-fit, aspect-ratio, `:has()`, `:is()/:where()`, writing-mode |
236
+
237
+ ## Why not just add this to EPUBCheck?
238
+
239
+ Short answer: **you can't, and you shouldn't have to.** This was researched
240
+ against EPUBCheck's actual source and issue history:
241
+
242
+ - **Upstreaming is out of scope.** EPUBCheck is the *spec-conformance* checker.
243
+ Valid EPUB3 CSS like `calc()` is **not** a conformance violation, and the
244
+ maintainers have repeatedly closed CSS-lint requests as out of scope
245
+ ([publ-cg#69](https://github.com/w3c/publ-cg/issues/69),
246
+ [#935](https://github.com/w3c/epubcheck/issues/935),
247
+ [#149](https://github.com/w3c/epubcheck/issues/149)). In publ-cg#69 — which
248
+ raises this *exact* "old SDK drops the stylesheet on `calc()`/`var()`" problem —
249
+ the lead maintainer's recommendation was to use **user-side severity overrides
250
+ or a separate tool**. The only CSS warning ever accepted (`position:fixed`)
251
+ landed solely because the EPUB CSS Profile itself flags it.
252
+ - **`--customMessages` can't add a rule.** It only re-maps the severity/text of
253
+ *existing* message ids; there is no `calc()`-used id to re-map.
254
+ - **A fork is possible but costly.** EPUBCheck has no plugin API, so new checks
255
+ mean editing the Java (`CSSHandler.declaration()`), adding `KOBO-*` message
256
+ ids, and rebuilding the jar — which needs a JDK + Maven, perpetual rebasing on
257
+ upstream, and a rename (BSD-3-Clause forbids shipping it as "EPUBCheck").
258
+ EPUBCheck 5.x also requires Java 11+ to run, so a runnable-on-older-readers
259
+ build means forking the **4.2.6** tag (which, conveniently, is the version
260
+ Kobo itself runs).
261
+
262
+ So `kobofix --check` *is* the integration: it wraps your real, unmodified
263
+ EPUBCheck and adds the Kobo layer beside it — exactly the workflow EPUBCheck's
264
+ own maintainer recommended. (A source fork that emits native `KOBO-001/002/003`
265
+ messages inside EPUBCheck's stream is fully specced in `epubcheck-fork-notes.md`
266
+ if a pipeline ever needs that instead.)
267
+
268
+ ## Known limitations
269
+
270
+ - **Heuristic value substitution.** Reducing `clamp()`/mixed `calc()`/viewport
271
+ units to one static value is inherently lossy. Defaults are sensible
272
+ (`16px`/rem, `600×800` viewport) and tunable, but check any
273
+ layout-critical sizes. All such reductions are listed in the report.
274
+ - **Custom-property scope.** Variables are resolved with a global "last
275
+ definition wins" map. Books that redefine the same `--name` differently in
276
+ different selectors (rare in practice; almost all ebook CSS defines them once
277
+ in `:root`) may resolve to the wrong value — review the report.
278
+ - **Layout modules are reported, not converted** (see above).
279
+ - **DRM / font obfuscation.** The tool does not decrypt DRM'd books and does not
280
+ touch `encryption.xml`; run it on un-DRM'd EPUBs you have the right to edit.
281
+ - **It targets the RMSDK reflowable path** (sideloaded `.epub` on Kobo / ADE),
282
+ which is the one that breaks. It does not generate Kobo's `.kepub.epub`.
283
+
284
+ ---
285
+
286
+ ## Sources
287
+
288
+ The behavior rules are drawn from (and were re-verified against) these:
289
+
290
+ - André Klein — *Your EPUB is fine, Kobo disagrees — blame Adobe*
291
+ - dvschultz/99problems #53 — *Legacy RMSDK will ignore the entire stylesheet if you use `calc()`*
292
+ - Jiminy Panoz — *Five interesting facts about Adobe legacy eBook RMSDK* (`calc()` whole-sheet drop, `rem`-as-`em`, empty `@media` crash)
293
+ - Readium CSS docs — `CSS21-epub_compat`, `CSS07-variables`
294
+ - J-Novel Club forum — *Blank epub on Kobo?* (`margin:50vh` crash)
295
+ - EPUB OCF 3.x / W3C EPUB 3.3 — `mimetype` ZIP packaging rules
@@ -0,0 +1,272 @@
1
+ # kobofix
2
+
3
+ Make an EPUB that **passes `epubcheck` but won't open (or renders broken) on a Kobo**
4
+ work on a Kobo. Pure Python, standard library only — no `pip install`.
5
+
6
+ This is the problem described in
7
+ [*"Your EPUB is fine, Kobo disagrees — blame Adobe"*](https://andreklein.net/your-epub-is-fine-kobo-disagrees-blame-adobe/),
8
+ generalized to every known cause, not just the one line in that post.
9
+
10
+ ---
11
+
12
+ ## Three ways to use it
13
+
14
+ - **Web app (no install):** [`web/`](web/) is a 100%-client-side version for
15
+ GitHub Pages — drag in an `.epub`, see what breaks on Kobo, download the fixed
16
+ file. The book never leaves the browser. See [web/README.md](web/README.md).
17
+ - **CLI fixer:** `python kobofix.py book.epub` → repaired EPUB.
18
+ - **CLI linter:** `python kobofix.py --check book.epub` → report, optionally
19
+ merged with EPUBCheck.
20
+
21
+ A worked demo on a *real* book: [`make_modern_alice.py`](make_modern_alice.py)
22
+ takes real Project Gutenberg *Alice* content + the modern stylesheet a
23
+ contemporary authoring tool emits → EPUBCheck passes it clean, kobofix flags 11
24
+ book-breaking errors and repairs them.
25
+
26
+ ## Why your "valid" EPUB fails on Kobo
27
+
28
+ Kobo renders sideloaded `.epub` files through Adobe's **legacy RMSDK** engine,
29
+ whose CSS parser is frozen around 2013. Two independent things bite you:
30
+
31
+ ### 1. RMSDK has no CSS fault tolerance
32
+
33
+ A normal browser, on hitting one CSS declaration it doesn't understand, drops
34
+ *that one declaration* and keeps the rest. RMSDK does the opposite: a single
35
+ **value-function token it can't parse — `calc()`, `min()`, `max()`, `clamp()`,
36
+ `var()`, `env()` — makes it throw away the *entire stylesheet***, and on some
37
+ firmware **refuse to open the book** ("this book is corrupted").
38
+
39
+ The critical consequence: **a fallback declaration placed *before* the modern
40
+ one does not help** — the whole sheet (fallback included) is discarded. The
41
+ modern construct has to be *removed*, not shadowed. That's the core thing
42
+ `kobofix` does.
43
+
44
+ ```css
45
+ /* This whole stylesheet is discarded by RMSDK because of one token: */
46
+ .copyright img { max-width: min(150px, 30vw); }
47
+
48
+ /* kobofix rewrites it to: */
49
+ .copyright img { max-width: 150px; }
50
+ ```
51
+
52
+ ### 2. "Corrupted" is often a packaging bug, not CSS
53
+
54
+ The most common literal-"corrupted" cause is the EPUB's ZIP packaging: the
55
+ `mimetype` entry must be **first**, **uncompressed (STORED)**, with **no extra
56
+ field** and the **exact bytes** `application/epub+zip`. Many tools (some
57
+ calibre save paths, Windows "Send to → compressed folder", naive zip libraries)
58
+ violate this and Kobo is the strictest mainstream reader about it. `kobofix`
59
+ **always re-emits a spec-correct OCF ZIP**, so it fixes this even when no CSS
60
+ needed changing.
61
+
62
+ `epubcheck` catches neither problem class reliably: the CSS is valid CSS4 so it
63
+ passes, and older `epubcheck` didn't flag a compressed `mimetype`. Passing
64
+ `epubcheck` is **not** a signal of Kobo/RMSDK compatibility.
65
+
66
+ ---
67
+
68
+ ## Install
69
+
70
+ Nothing to install. You need Python 3.7+ (tested on 3.12).
71
+
72
+ ```
73
+ python kobofix.py --version
74
+ ```
75
+
76
+ ## Usage
77
+
78
+ ```
79
+ python kobofix.py INPUT.epub [-o OUTPUT.epub] [options]
80
+ ```
81
+
82
+ Default output is `INPUT.kobofixed.epub` next to the input. The original is
83
+ never modified.
84
+
85
+ ```bash
86
+ # Fix a book
87
+ python kobofix.py mybook.epub
88
+
89
+ # Choose the output name
90
+ python kobofix.py mybook.epub -o mybook-kobo.epub
91
+
92
+ # Just analyze — write nothing, print the report
93
+ python kobofix.py mybook.epub --dry-run
94
+
95
+ # LINT mode: report Kobo/RMSDK issues (KOBO-* ids + line numbers) WITHOUT changing
96
+ # anything, and merge in EPUBCheck's spec report for one combined readout.
97
+ python kobofix.py --check mybook.epub --epubcheck path\to\epubcheck.jar
98
+
99
+ # Machine-readable report
100
+ python kobofix.py mybook.epub --report json --report-file report.json
101
+
102
+ # Also run EPUBCheck on the result (you supply the jar; Java must be on PATH)
103
+ python kobofix.py mybook.epub --epubcheck path\to\epubcheck.jar
104
+
105
+ # Prove the engine works on your machine
106
+ python kobofix.py --selftest
107
+ ```
108
+
109
+ You can also pass an **already-extracted EPUB folder** as the input; the tool
110
+ will read it and produce a correctly-packaged `.epub`.
111
+
112
+ ### Options
113
+
114
+ | Option | Default | Meaning |
115
+ |---|---|---|
116
+ | `-o, --output PATH` | `<input>.kobofixed.epub` | Output file |
117
+ | `--check` | off | Lint only: report `KOBO-*` issues + line numbers, merge EPUBCheck; write nothing |
118
+ | `--dry-run` | off | Analyze and report only; write nothing |
119
+ | `--report {text,json}` | `text` | Report format |
120
+ | `--report-file PATH` | — | Also write the report to a file |
121
+ | `--no-rem` | rem→px on | Don't convert `rem` to `px` |
122
+ | `--root-font-size PX` | `16` | Pixel value of `1rem` for the conversion |
123
+ | `--no-viewport` | vw/vh→px on | Don't convert standalone `vw/vh/vmin/vmax` |
124
+ | `--vw-base PX` | `600` | Assumed reader viewport **width** for `vw` |
125
+ | `--vh-base PX` | `800` | Assumed reader viewport **height** for `vh` |
126
+ | `--clamp-pick {min,pref,max}` | `pref` | Which `clamp()` term to keep when none is an absolute length |
127
+ | `--epubcheck [PATH]` | off | Run EPUBCheck after building (`epubcheck`/`*.jar`, or no value to auto-detect) |
128
+ | `--strict` | off | Exit non-zero if any manual-review warnings were found |
129
+
130
+ Exit codes: `0` clean · `1` `--strict` with warnings · `2` output failed its
131
+ own packaging self-check.
132
+
133
+ ---
134
+
135
+ ## What it fixes automatically
136
+
137
+ These are the changes that make Kobo **open and style** the book. Every one
138
+ leaves **zero** residual modern-value tokens in RMSDK-visible CSS.
139
+
140
+ | Construct | Action |
141
+ |---|---|
142
+ | `var(--x)` / `--x:` | Resolved to the literal value and inlined; the `--x` declarations are deleted |
143
+ | `calc(...)` | Evaluated when units allow (`calc(2em + 1em)`→`3em`); mixed-unit (`calc(100% - 20px)`) keeps the first term and is flagged |
144
+ | `min(a,b)` / `max(a,b)` | Reduced to the absolute-length term (`min(150px,30vw)`→`150px`) |
145
+ | `clamp(a,b,c)` | Reduced to one static size (prefers an absolute term; else `--clamp-pick`) |
146
+ | `env(x, fallback)` | Replaced with the fallback (or removed) |
147
+ | `rem` | Converted to `px` (RMSDK mis-treats `rem` as `em`, compounding sizes) |
148
+ | `vw/vh/vmin/vmax` | Converted to `px` against the assumed viewport (verified crash: `margin:50vh`) |
149
+ | empty `@media {}` / `@supports {}` | Removed (crashes old RMSDK) |
150
+ | `mimetype` packaging | Rewritten first / STORED / no-extra / exact bytes |
151
+
152
+ All of the above are applied inside standalone `.css` files **and** inside
153
+ `<style>` blocks and `style="..."` attributes in XHTML.
154
+
155
+ ## What it flags but does NOT change
156
+
157
+ These have no safe automatic equivalent — converting them risks silently
158
+ reflowing your book, so `kobofix` reports them (with file and line) and leaves
159
+ the decision to you:
160
+
161
+ - **flexbox** and **grid** (and grid sub-properties)
162
+ - **`position: absolute/fixed/sticky`** in reflowable content
163
+ - **`transform`**, **`transition`/`animation`/`@keyframes`**
164
+ - **`object-fit`**, **`aspect-ratio`**
165
+ - **`:has()`**, **`:is()`/`:where()`**
166
+ - **`writing-mode`** — *kept on purpose*; Kobo supports it for vertical CJK text
167
+
168
+ The typical fix for flex/grid is to wrap the modern rules in
169
+ `@supports (display:flex) { … }` (legacy RMSDK skips the whole block) with a
170
+ plain block/float fallback outside it.
171
+
172
+ ---
173
+
174
+ ## How to verify the result
175
+
176
+ 1. `kobofix` self-checks its own output (mimetype first/STORED/no-extra/exact).
177
+ 2. Open the fixed file in **Adobe Digital Editions** — it uses the same RMSDK
178
+ engine as Kobo, so if it opens and styles correctly there, Kobo will too.
179
+ This is the single highest-value manual test.
180
+ 3. Optionally run EPUBCheck (`--epubcheck`) for container/structural validity.
181
+ 4. Sideload to an actual Kobo for final confirmation.
182
+
183
+ ---
184
+
185
+ ## Checking mode (`--check`) — the Kobo readiness linter
186
+
187
+ `kobofix --check book.epub` makes **no changes**. It reports every RMSDK landmine
188
+ with a stable rule id and a `file:line` location (correct even inside `<style>`
189
+ blocks and `style=""` attributes), and — if you point it at an EPUBCheck jar —
190
+ runs EPUBCheck too and prints **one combined report**: spec conformance from the
191
+ validator you already trust, plus the Kobo/RMSDK layer EPUBCheck can't see.
192
+
193
+ ```
194
+ python kobofix.py --check book.epub --epubcheck path\to\epubcheck.jar
195
+ ```
196
+
197
+ If `--epubcheck` is given with no value, kobofix auto-discovers an `epubcheck`
198
+ on `PATH` or a bundled `tools/**/epubcheck.jar`. Exit codes: **2** if any
199
+ book-breaking issue (a `KOBO-001` function or `KOBO-000` packaging error, or an
200
+ EPUBCheck error/fatal), **1** with `--strict` if only warnings, else **0** —
201
+ so it drops straight into CI. Use `--report json` for machine output.
202
+
203
+ ### Rule ids
204
+
205
+ | Rule | Sev. | What |
206
+ |---|---|---|
207
+ | `KOBO-000` | error | `mimetype` packaging wrong (the literal "corrupted" cause) |
208
+ | `KOBO-001` | error | `calc/min/max/clamp/var/env` — drops the whole stylesheet / fails to open |
209
+ | `KOBO-002` | warning | viewport unit `vw/vh/vmin/vmax` (in a margin: blank-screen crash) |
210
+ | `KOBO-003` | warning | empty `@media`/`@supports` block (crashes old RMSDK) |
211
+ | `KOBO-004` | warning | `rem` (rendered as `em` → wrong sizes) |
212
+ | `KOBO-010..019` | warn/info | flexbox, grid, position, transform, animation, object-fit, aspect-ratio, `:has()`, `:is()/:where()`, writing-mode |
213
+
214
+ ## Why not just add this to EPUBCheck?
215
+
216
+ Short answer: **you can't, and you shouldn't have to.** This was researched
217
+ against EPUBCheck's actual source and issue history:
218
+
219
+ - **Upstreaming is out of scope.** EPUBCheck is the *spec-conformance* checker.
220
+ Valid EPUB3 CSS like `calc()` is **not** a conformance violation, and the
221
+ maintainers have repeatedly closed CSS-lint requests as out of scope
222
+ ([publ-cg#69](https://github.com/w3c/publ-cg/issues/69),
223
+ [#935](https://github.com/w3c/epubcheck/issues/935),
224
+ [#149](https://github.com/w3c/epubcheck/issues/149)). In publ-cg#69 — which
225
+ raises this *exact* "old SDK drops the stylesheet on `calc()`/`var()`" problem —
226
+ the lead maintainer's recommendation was to use **user-side severity overrides
227
+ or a separate tool**. The only CSS warning ever accepted (`position:fixed`)
228
+ landed solely because the EPUB CSS Profile itself flags it.
229
+ - **`--customMessages` can't add a rule.** It only re-maps the severity/text of
230
+ *existing* message ids; there is no `calc()`-used id to re-map.
231
+ - **A fork is possible but costly.** EPUBCheck has no plugin API, so new checks
232
+ mean editing the Java (`CSSHandler.declaration()`), adding `KOBO-*` message
233
+ ids, and rebuilding the jar — which needs a JDK + Maven, perpetual rebasing on
234
+ upstream, and a rename (BSD-3-Clause forbids shipping it as "EPUBCheck").
235
+ EPUBCheck 5.x also requires Java 11+ to run, so a runnable-on-older-readers
236
+ build means forking the **4.2.6** tag (which, conveniently, is the version
237
+ Kobo itself runs).
238
+
239
+ So `kobofix --check` *is* the integration: it wraps your real, unmodified
240
+ EPUBCheck and adds the Kobo layer beside it — exactly the workflow EPUBCheck's
241
+ own maintainer recommended. (A source fork that emits native `KOBO-001/002/003`
242
+ messages inside EPUBCheck's stream is fully specced in `epubcheck-fork-notes.md`
243
+ if a pipeline ever needs that instead.)
244
+
245
+ ## Known limitations
246
+
247
+ - **Heuristic value substitution.** Reducing `clamp()`/mixed `calc()`/viewport
248
+ units to one static value is inherently lossy. Defaults are sensible
249
+ (`16px`/rem, `600×800` viewport) and tunable, but check any
250
+ layout-critical sizes. All such reductions are listed in the report.
251
+ - **Custom-property scope.** Variables are resolved with a global "last
252
+ definition wins" map. Books that redefine the same `--name` differently in
253
+ different selectors (rare in practice; almost all ebook CSS defines them once
254
+ in `:root`) may resolve to the wrong value — review the report.
255
+ - **Layout modules are reported, not converted** (see above).
256
+ - **DRM / font obfuscation.** The tool does not decrypt DRM'd books and does not
257
+ touch `encryption.xml`; run it on un-DRM'd EPUBs you have the right to edit.
258
+ - **It targets the RMSDK reflowable path** (sideloaded `.epub` on Kobo / ADE),
259
+ which is the one that breaks. It does not generate Kobo's `.kepub.epub`.
260
+
261
+ ---
262
+
263
+ ## Sources
264
+
265
+ The behavior rules are drawn from (and were re-verified against) these:
266
+
267
+ - André Klein — *Your EPUB is fine, Kobo disagrees — blame Adobe*
268
+ - dvschultz/99problems #53 — *Legacy RMSDK will ignore the entire stylesheet if you use `calc()`*
269
+ - Jiminy Panoz — *Five interesting facts about Adobe legacy eBook RMSDK* (`calc()` whole-sheet drop, `rem`-as-`em`, empty `@media` crash)
270
+ - Readium CSS docs — `CSS21-epub_compat`, `CSS07-variables`
271
+ - J-Novel Club forum — *Blank epub on Kobo?* (`margin:50vh` crash)
272
+ - EPUB OCF 3.x / W3C EPUB 3.3 — `mimetype` ZIP packaging rules