comicforge 0.2.2__tar.gz → 0.2.4__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.
Files changed (142) hide show
  1. {comicforge-0.2.2 → comicforge-0.2.4}/.claude/skills/comicforge/SKILL.md +83 -14
  2. {comicforge-0.2.2 → comicforge-0.2.4}/.claude/skills/comicforge/reference.md +62 -16
  3. {comicforge-0.2.2 → comicforge-0.2.4}/.github/workflows/ci.yml +2 -2
  4. {comicforge-0.2.2 → comicforge-0.2.4}/.github/workflows/docs.yml +3 -3
  5. {comicforge-0.2.2 → comicforge-0.2.4}/.github/workflows/release.yml +1 -1
  6. {comicforge-0.2.2 → comicforge-0.2.4}/PKG-INFO +1 -1
  7. {comicforge-0.2.2 → comicforge-0.2.4}/comicforge/__init__.py +8 -1
  8. comicforge-0.2.4/comicforge/bubbles.py +485 -0
  9. {comicforge-0.2.2 → comicforge-0.2.4}/comicforge/caption.py +42 -14
  10. {comicforge-0.2.2 → comicforge-0.2.4}/comicforge/cli.py +42 -10
  11. comicforge-0.2.4/comicforge/layout.py +343 -0
  12. {comicforge-0.2.2 → comicforge-0.2.4}/comicforge/pixelart.py +3 -3
  13. {comicforge-0.2.2 → comicforge-0.2.4}/comicforge/raster.py +94 -12
  14. {comicforge-0.2.2 → comicforge-0.2.4}/comicforge/render.py +342 -176
  15. {comicforge-0.2.2 → comicforge-0.2.4}/comicforge/validate.py +146 -9
  16. {comicforge-0.2.2 → comicforge-0.2.4}/docs/claude.md +8 -3
  17. comicforge-0.2.4/docs/demos/auto-rows.yaml +22 -0
  18. comicforge-0.2.4/docs/demos/speakers.yaml +47 -0
  19. comicforge-0.2.4/docs/demos/tails.yaml +44 -0
  20. {comicforge-0.2.2 → comicforge-0.2.4}/docs/guide/bubbles.md +145 -3
  21. {comicforge-0.2.2 → comicforge-0.2.4}/docs/guide/captions-frames.md +45 -1
  22. {comicforge-0.2.2 → comicforge-0.2.4}/docs/guide/images.md +23 -0
  23. {comicforge-0.2.2 → comicforge-0.2.4}/docs/guide/pages.md +51 -0
  24. {comicforge-0.2.2 → comicforge-0.2.4}/docs/reference/cli.md +36 -4
  25. {comicforge-0.2.2 → comicforge-0.2.4}/docs/reference/python-api.md +61 -0
  26. {comicforge-0.2.2 → comicforge-0.2.4}/docs/reference/spec.md +17 -1
  27. {comicforge-0.2.2 → comicforge-0.2.4}/pyproject.toml +2 -2
  28. {comicforge-0.2.2 → comicforge-0.2.4}/skills/comicforge/SKILL.md +83 -14
  29. {comicforge-0.2.2 → comicforge-0.2.4}/skills/comicforge/reference.md +62 -16
  30. {comicforge-0.2.2 → comicforge-0.2.4}/tests/test_bubbles.py +35 -1
  31. comicforge-0.2.4/tests/test_caption.py +128 -0
  32. comicforge-0.2.4/tests/test_layout.py +311 -0
  33. comicforge-0.2.4/tests/test_raster.py +447 -0
  34. {comicforge-0.2.2 → comicforge-0.2.4}/uv.lock +7 -7
  35. comicforge-0.2.2/comicforge/bubbles.py +0 -232
  36. comicforge-0.2.2/tests/test_caption.py +0 -68
  37. comicforge-0.2.2/tests/test_raster.py +0 -241
  38. {comicforge-0.2.2 → comicforge-0.2.4}/.github/dependabot.yml +0 -0
  39. {comicforge-0.2.2 → comicforge-0.2.4}/.gitignore +0 -0
  40. {comicforge-0.2.2 → comicforge-0.2.4}/.python-version +0 -0
  41. {comicforge-0.2.2 → comicforge-0.2.4}/CLAUDE.md +0 -0
  42. {comicforge-0.2.2 → comicforge-0.2.4}/README.md +0 -0
  43. {comicforge-0.2.2 → comicforge-0.2.4}/comicforge/__main__.py +0 -0
  44. {comicforge-0.2.2 → comicforge-0.2.4}/comicforge/inspire.py +0 -0
  45. {comicforge-0.2.2 → comicforge-0.2.4}/comicforge/library.py +0 -0
  46. {comicforge-0.2.2 → comicforge-0.2.4}/comicforge/scaffold.py +0 -0
  47. {comicforge-0.2.2 → comicforge-0.2.4}/comicforge/scene.py +0 -0
  48. {comicforge-0.2.2 → comicforge-0.2.4}/docs/art/characters.md +0 -0
  49. {comicforge-0.2.2 → comicforge-0.2.4}/docs/art/inspire.md +0 -0
  50. {comicforge-0.2.2 → comicforge-0.2.4}/docs/art/pixel.md +0 -0
  51. {comicforge-0.2.2 → comicforge-0.2.4}/docs/art/scenes.md +0 -0
  52. {comicforge-0.2.2 → comicforge-0.2.4}/docs/assets/favicon.svg +0 -0
  53. {comicforge-0.2.2 → comicforge-0.2.4}/docs/assets/logo.svg +0 -0
  54. {comicforge-0.2.2 → comicforge-0.2.4}/docs/concepts.md +0 -0
  55. {comicforge-0.2.2 → comicforge-0.2.4}/docs/contributing.md +0 -0
  56. {comicforge-0.2.2 → comicforge-0.2.4}/docs/demos/arms.yaml +0 -0
  57. {comicforge-0.2.2 → comicforge-0.2.4}/docs/demos/bubble-at.yaml +0 -0
  58. {comicforge-0.2.2 → comicforge-0.2.4}/docs/demos/bubble-stack.yaml +0 -0
  59. {comicforge-0.2.2 → comicforge-0.2.4}/docs/demos/bubbles.yaml +0 -0
  60. {comicforge-0.2.2 → comicforge-0.2.4}/docs/demos/captions.yaml +0 -0
  61. {comicforge-0.2.2 → comicforge-0.2.4}/docs/demos/coords.yaml +0 -0
  62. {comicforge-0.2.2 → comicforge-0.2.4}/docs/demos/faces.yaml +0 -0
  63. {comicforge-0.2.2 → comicforge-0.2.4}/docs/demos/frames.yaml +0 -0
  64. {comicforge-0.2.2 → comicforge-0.2.4}/docs/demos/grid.yaml +0 -0
  65. {comicforge-0.2.2 → comicforge-0.2.4}/docs/demos/illustration.yaml +0 -0
  66. {comicforge-0.2.2 → comicforge-0.2.4}/docs/demos/images.yaml +0 -0
  67. {comicforge-0.2.2 → comicforge-0.2.4}/docs/demos/lettering.yaml +0 -0
  68. {comicforge-0.2.2 → comicforge-0.2.4}/docs/demos/pixel.yaml +0 -0
  69. {comicforge-0.2.2 → comicforge-0.2.4}/docs/demos/poses.yaml +0 -0
  70. {comicforge-0.2.2 → comicforge-0.2.4}/docs/demos/raster-art/backdrop.png +0 -0
  71. {comicforge-0.2.2 → comicforge-0.2.4}/docs/demos/scenes.yaml +0 -0
  72. {comicforge-0.2.2 → comicforge-0.2.4}/docs/demos/strip.yaml +0 -0
  73. {comicforge-0.2.2 → comicforge-0.2.4}/docs/demos/tutorial-art/characters/pip/base.svg +0 -0
  74. {comicforge-0.2.2 → comicforge-0.2.4}/docs/demos/tutorial-art/characters/pip/character.yaml +0 -0
  75. {comicforge-0.2.2 → comicforge-0.2.4}/docs/demos/tutorial-art/characters/pip/face-happy.svg +0 -0
  76. {comicforge-0.2.2 → comicforge-0.2.4}/docs/demos/tutorial-art/characters/pip/face-neutral.svg +0 -0
  77. {comicforge-0.2.2 → comicforge-0.2.4}/docs/demos/tutorial-art/pages/first.yaml +0 -0
  78. {comicforge-0.2.2 → comicforge-0.2.4}/docs/demos/tutorial-art/pixel/heart.yaml +0 -0
  79. {comicforge-0.2.2 → comicforge-0.2.4}/docs/gallery.md +0 -0
  80. {comicforge-0.2.2 → comicforge-0.2.4}/docs/guide/actors.md +0 -0
  81. {comicforge-0.2.2 → comicforge-0.2.4}/docs/guide/illustrations.md +0 -0
  82. {comicforge-0.2.2 → comicforge-0.2.4}/docs/guide/pixel-art.md +0 -0
  83. {comicforge-0.2.2 → comicforge-0.2.4}/docs/guide/scenes.md +0 -0
  84. {comicforge-0.2.2 → comicforge-0.2.4}/docs/hooks/render_demos.py +0 -0
  85. {comicforge-0.2.2 → comicforge-0.2.4}/docs/index.md +0 -0
  86. {comicforge-0.2.2 → comicforge-0.2.4}/docs/install.md +0 -0
  87. {comicforge-0.2.2 → comicforge-0.2.4}/docs/quickstart.md +0 -0
  88. {comicforge-0.2.2 → comicforge-0.2.4}/docs/reference/troubleshooting.md +0 -0
  89. {comicforge-0.2.2 → comicforge-0.2.4}/docs/starting-a-project.md +0 -0
  90. {comicforge-0.2.2 → comicforge-0.2.4}/docs/stylesheets/extra.css +0 -0
  91. {comicforge-0.2.2 → comicforge-0.2.4}/examples/README.md +0 -0
  92. {comicforge-0.2.2 → comicforge-0.2.4}/examples/README.md.old +0 -0
  93. {comicforge-0.2.2 → comicforge-0.2.4}/examples/pes/characters/bara/character.yaml +0 -0
  94. {comicforge-0.2.2 → comicforge-0.2.4}/examples/pes/characters/bara/face-happy.svg +0 -0
  95. {comicforge-0.2.2 → comicforge-0.2.4}/examples/pes/characters/bara/face-neutral.svg +0 -0
  96. {comicforge-0.2.2 → comicforge-0.2.4}/examples/pes/characters/bara/poses/sit/base.svg +0 -0
  97. {comicforge-0.2.2 → comicforge-0.2.4}/examples/pes/characters/bara/poses/sit/pose.yaml +0 -0
  98. {comicforge-0.2.2 → comicforge-0.2.4}/examples/pes/characters/bara/poses/walk/base.svg +0 -0
  99. {comicforge-0.2.2 → comicforge-0.2.4}/examples/pes/characters/bara/poses/walk/pose.yaml +0 -0
  100. {comicforge-0.2.2 → comicforge-0.2.4}/examples/pes/characters/tom/arms-crossed.svg +0 -0
  101. {comicforge-0.2.2 → comicforge-0.2.4}/examples/pes/characters/tom/arms-down.svg +0 -0
  102. {comicforge-0.2.2 → comicforge-0.2.4}/examples/pes/characters/tom/arms-hips.svg +0 -0
  103. {comicforge-0.2.2 → comicforge-0.2.4}/examples/pes/characters/tom/arms-point.svg +0 -0
  104. {comicforge-0.2.2 → comicforge-0.2.4}/examples/pes/characters/tom/arms-thumbsup.svg +0 -0
  105. {comicforge-0.2.2 → comicforge-0.2.4}/examples/pes/characters/tom/arms-wave.svg +0 -0
  106. {comicforge-0.2.2 → comicforge-0.2.4}/examples/pes/characters/tom/base.svg +0 -0
  107. {comicforge-0.2.2 → comicforge-0.2.4}/examples/pes/characters/tom/character.yaml +0 -0
  108. {comicforge-0.2.2 → comicforge-0.2.4}/examples/pes/characters/tom/face-angry.svg +0 -0
  109. {comicforge-0.2.2 → comicforge-0.2.4}/examples/pes/characters/tom/face-happy.svg +0 -0
  110. {comicforge-0.2.2 → comicforge-0.2.4}/examples/pes/characters/tom/face-laugh.svg +0 -0
  111. {comicforge-0.2.2 → comicforge-0.2.4}/examples/pes/characters/tom/face-neutral.svg +0 -0
  112. {comicforge-0.2.2 → comicforge-0.2.4}/examples/pes/characters/tom/face-sad.svg +0 -0
  113. {comicforge-0.2.2 → comicforge-0.2.4}/examples/pes/characters/tom/face-surprised.svg +0 -0
  114. {comicforge-0.2.2 → comicforge-0.2.4}/examples/pes/characters/tom/face-wink.svg +0 -0
  115. {comicforge-0.2.2 → comicforge-0.2.4}/examples/pes/pages/dvur-scene.yaml +0 -0
  116. {comicforge-0.2.2 → comicforge-0.2.4}/examples/pes/pages/kosticka.yaml +0 -0
  117. {comicforge-0.2.2 → comicforge-0.2.4}/examples/pes/pages/slepice.yaml +0 -0
  118. {comicforge-0.2.2 → comicforge-0.2.4}/examples/pes/pixel/bone.yaml +0 -0
  119. {comicforge-0.2.2 → comicforge-0.2.4}/examples/pes/pixel/heart.yaml +0 -0
  120. {comicforge-0.2.2 → comicforge-0.2.4}/examples/pes/pixel/star.yaml +0 -0
  121. {comicforge-0.2.2 → comicforge-0.2.4}/examples/pes/pixel/sun.yaml +0 -0
  122. {comicforge-0.2.2 → comicforge-0.2.4}/examples/pes/references.yaml +0 -0
  123. {comicforge-0.2.2 → comicforge-0.2.4}/examples/pes/scenes/dvur/base.svg +0 -0
  124. {comicforge-0.2.2 → comicforge-0.2.4}/examples/pes/scenes/dvur/scene.yaml +0 -0
  125. {comicforge-0.2.2 → comicforge-0.2.4}/examples/pes/scenes/dvur/weather-clear.svg +0 -0
  126. {comicforge-0.2.2 → comicforge-0.2.4}/examples/pes/scenes/dvur/weather-rain.svg +0 -0
  127. {comicforge-0.2.2 → comicforge-0.2.4}/examples/pes/scenes/pokoj/base.svg +0 -0
  128. {comicforge-0.2.2 → comicforge-0.2.4}/examples/pes/scenes/pokoj/scene.yaml +0 -0
  129. {comicforge-0.2.2 → comicforge-0.2.4}/examples/pes/theme.yaml +0 -0
  130. {comicforge-0.2.2 → comicforge-0.2.4}/hooks/pre-commit +0 -0
  131. {comicforge-0.2.2 → comicforge-0.2.4}/mkdocs.yml +0 -0
  132. {comicforge-0.2.2 → comicforge-0.2.4}/poe_tasks.toml +0 -0
  133. {comicforge-0.2.2 → comicforge-0.2.4}/tests/__init__.py +0 -0
  134. {comicforge-0.2.2 → comicforge-0.2.4}/tests/conftest.py +0 -0
  135. {comicforge-0.2.2 → comicforge-0.2.4}/tests/test_cli.py +0 -0
  136. {comicforge-0.2.2 → comicforge-0.2.4}/tests/test_inspire.py +0 -0
  137. {comicforge-0.2.2 → comicforge-0.2.4}/tests/test_library.py +0 -0
  138. {comicforge-0.2.2 → comicforge-0.2.4}/tests/test_pixelart.py +0 -0
  139. {comicforge-0.2.2 → comicforge-0.2.4}/tests/test_render.py +0 -0
  140. {comicforge-0.2.2 → comicforge-0.2.4}/tests/test_scaffold.py +0 -0
  141. {comicforge-0.2.2 → comicforge-0.2.4}/tests/test_scene.py +0 -0
  142. {comicforge-0.2.2 → comicforge-0.2.4}/tests/test_validate.py +0 -0
@@ -47,15 +47,21 @@ needs no Python (see [`reference.md`](reference.md)).
47
47
  against the libraries without drawing anything, and lists *all* problems at
48
48
  once. Unlike `render`, it flags mis-spelled keys (e.g. `fcae:`, `imge:`) that
49
49
  render silently ignores. Exits non-zero when anything is wrong. Works on page
50
- and `scene` specs.
50
+ and `scene` specs. It also prints bubble layout **warnings** (out of reading
51
+ order, crossing tails, a bubble covering a speaker point) — they don't fail
52
+ the check unless `--strict`, but fix them before calling a page done.
51
53
  4. Render:
52
54
  - comic page: `cmf render mystrip.yaml -o out.pdf`
53
55
  - single illustration: `cmf scene myscene.yaml -o out.png`
54
56
  - one panel of a page (to iterate on a single panel without re-reading the
55
57
  whole page): `cmf panel mystrip.yaml --row 0 --col 1 -o panel.png`
56
58
  (0-indexed; defaults to row 0 col 0), or `--all` to write every panel into a
57
- directory. This is the right way to inspect one panel — **do not** render the
58
- full page and crop it with ImageMagick or other tooling.
59
+ directory. Add `--on-page` to get the panel exactly as the page draws it
60
+ (full frame stroke, page background at the corners); `--layout` prints
61
+ each panel's position on the page as JSON (`--all` writes it as
62
+ `layout.json`) to lay the renders out again. This is the right way
63
+ to inspect one panel — **do not** render the full page and crop it with
64
+ ImageMagick or other tooling.
59
65
  - one character on its own (to eyeball a pose/expression):
60
66
  `cmf character bara sit happy --library examples/pes/characters`
61
67
  — extra args are bare pose/variant names or `key=value` (`pose=walk`,
@@ -93,7 +99,9 @@ pixel_dir: "../pixel" # path to pixel-art dir (omit if inline only)
93
99
 
94
100
  rows: # page is a stack of rows…
95
101
  - height: 1.0 # relative row height (default 1), or
96
- # height_mm: 60 for a fixed height —
102
+ # height_mm: 60 for a fixed height, or
103
+ # height: auto to size it from its images
104
+ # + captions (squeezed to fit the page) —
97
105
  # weighted rows share what is left
98
106
  panels: # …each row is a left→right list of panels
99
107
  - width: 1.0 # relative panel width (default 1)
@@ -159,6 +167,22 @@ image: {src: "01.png", fit: contain} # or the long form, to pick the fit
159
167
  distorts, exactly like a `scene:` background.
160
168
  - `fit: contain` scales it to fit *inside* the panel; the leftover strips show
161
169
  the panel's `bg:` colour.
170
+ - `crop: {top, bottom, left, right}` trims that many *image pixels* off each
171
+ edge before fitting — the trimmed image is what `fit`, a scene canvas and a
172
+ `height: auto` row see.
173
+ - `at: t|b|l|r|tl|tr|bl|br|c` pins the image to that edge, so `fit: cover`
174
+ crops from the other side (`at: t` keeps heads, loses feet). Default centred.
175
+ - **Making a page of images fit:** give the rows `height: auto`. Each row is as
176
+ tall as its images want at their panel width plus the caption band; if the
177
+ rows overflow the page their art (never the captions) is squeezed by one
178
+ common factor and `cover` crops the excess. Steer what gets lost with `at:`
179
+ per image, and `crop:` away empty sky/floor first so the squeeze is smaller.
180
+ Don't compute row `height_mm` from image sizes and caption wraps yourself.
181
+ To paginate, call `comicforge.page_squeeze(spec)` (1.0 = fits unsqueezed) on
182
+ a candidate page and break when it drops below what you tolerate.
183
+ - Captions in panels of varying width: `caption_style: {max_chars: auto}` wraps
184
+ to the band's width; `em` (default 1.0) scales the measure for a narrower
185
+ (<1) or wider (>1) font than DejaVu Sans.
162
186
  - `.png`, `.jpg`/`.jpeg`, `.gif` and `.webp` are supported.
163
187
  - The image is **embedded** as a base64 data URI, so an `.svg` or `.pdf` render
164
188
  is one self-contained file that still works when moved. That also means the
@@ -171,13 +195,27 @@ A standalone illustration can be image-backed too — set `type: scene` with an
171
195
  dimensions times `scale:` (default `1`, i.e. one output px per image px).
172
196
 
173
197
  **Where bubbles land on a raster panel.** There are no actors to anchor to, so
174
- `speaker:` is meaningless — use `x`/`y`/`to` fractions, or leave them out. A
175
- bubble with no `y` is placed below the measured bottom of the bubble before it,
176
- starting near the panel top, so any number of bubbles of any length stack
177
- without overlapping. A bubble with no `x` is horizontally centred. Every
178
- bubble — explicit or automatic — is then nudged so it stays inside the panel
179
- (one wider or taller than the panel is centred instead). A generated spec can
180
- therefore emit dialogue in order and omit the coordinates entirely.
198
+ mark each speaking head with a panel-level `speakers: {name: [x, y]}` (panel
199
+ fractions) and give bubbles `speaker: name`. The tail then aims at the point
200
+ (unless `to:` is given), and every bubble with a speaker and no `x`/`y`/`at` is
201
+ placed **in reading order**: on its speaker's side, its top clearly below the
202
+ previous bubble's top, clear of speaker points and earlier tails. So a
203
+ right-hand first speaker gets the top right and the reply goes lower left. Emit
204
+ the lines in the order they are spoken and omit coordinates.
205
+
206
+ Without `speakers:`, a bubble with no `y` is placed below the measured bottom of
207
+ the bubble before it, starting near the panel top, and a bubble with no `x` is
208
+ horizontally centred. Every bubble — explicit or automatic — is then nudged so
209
+ it stays inside the panel (one wider or taller than the panel is centred
210
+ instead).
211
+
212
+ ```yaml
213
+ image: "art/03.png"
214
+ speakers: {ema: [0.72, 0.3], jan: [0.25, 0.42]}
215
+ bubbles:
216
+ - {text: "Kde jsi byl?", speaker: ema}
217
+ - {text: "Venku.", speaker: jan}
218
+ ```
181
219
 
182
220
  ## Standalone illustration (no comic grid)
183
221
 
@@ -212,6 +250,7 @@ bubbles: [ ... ]
212
250
  kind: speech # speech | thought | shout
213
251
  speaker: tom # OPTIONAL: auto-place above this actor + aim the tail at
214
252
  # their head. Prefer this over manual x/y/to.
253
+ # Also names a panel `speakers:` point (actors win).
215
254
  at: tr # OPTIONAL corner/edge to hug: tl t tr l c r bl b br.
216
255
  # Each column (l/c/r) stacks its own top and
217
256
  # bottom, so tl + tr sit side by side.
@@ -221,9 +260,21 @@ bubbles: [ ... ]
221
260
  # kept inside the panel.
222
261
  to: [0.4, 0.5] # OPTIONAL tail target (panel fraction); else speaker's head
223
262
  max_chars: 22 # wrap width (optional)
263
+ glue_singles: false # OPTIONAL let a one-letter word end a line (default: it
264
+ # is tied to the next word; see Line breaks below)
224
265
  fs: 16 # font size px (optional; overrides bubble_style.font_size)
225
266
  uppercase: true # OPTIONAL: force this bubble's text to CAPS
226
267
  # (overrides bubble_style.uppercase)
268
+ tail_from: r # OPTIONAL where the tail leaves: edge t/b/l/r, a position
269
+ # 0..1 along the auto edge, or {edge: r, pos: 0.8}
270
+ tail: curve # OPTIONAL wedge (default) | curve: tapered comic tail with
271
+ # curved sides | line: a thin line running to just
272
+ # short of the speaker | none. Any kind; a thought
273
+ # keeps its circles (dotted along a line)
274
+ tail_bend: 0.4 # OPTIONAL curve/line bow, -1..1, 0 straight, + = right of
275
+ # travel; omit for a gentle bend away from the
276
+ # nearest bubble (or panel centre)
277
+ tail_gap: 12 # OPTIONAL px a line tail stops short of its target
227
278
  ```
228
279
 
229
280
  **Page-wide bubble defaults** — set `bubble_style` at the *top level* of a
@@ -239,6 +290,8 @@ bubble_style:
239
290
  ink: "#21304a" # text colour; font: "DejaVu Sans, sans-serif"
240
291
  em: 1.0 # width scale of the text measure — 0.8 for a narrow
241
292
  # handwriting font, so bubbles hug the words
293
+ tail: wedge # wedge | curve | line | none; tail_bend: (auto)
294
+ # tail_gap: 12; tail_from: (auto)
242
295
  rows: [ ... ]
243
296
  ```
244
297
 
@@ -247,12 +300,28 @@ along the bottom of the panel, inside the frame, separated from the art by a
247
300
  hairline in the frame colour. The art box shrinks to make room, so bubble and
248
301
  actor coordinates stay relative to the picture. Page-wide look via
249
302
  `caption_style` (`font`, `font_size`, `ink`, `bg`, `pad`, `max_chars`,
250
- `align: left|center`, `rule`, `uppercase`); `comicforge.caption.height(text, style)` tells
303
+ `align: left|center`, `rule`, `uppercase`, `glue_singles`);
304
+ `comicforge.caption.height(text, style)` tells
251
305
  you how tall a band will be, for sizing rows.
252
306
 
307
+ **Line breaks.** Wrapping breaks between words, with two exceptions, in bubbles
308
+ and captions alike. A one-letter word is tied to the word after it, because
309
+ Czech typography does not leave a one-letter preposition or conjunction
310
+ (`k s v z o u a i`) at the end of a line — set `glue_singles: false` on the
311
+ style or the line to allow it. A no-break space (`U+00A0`, or `U+202F`) is
312
+ never broken at, so type one to tie words the automatic rule cannot know about
313
+ (`v roce 1223`, a name and its title). Ordinary runs of spaces and newlines
314
+ still collapse to one space.
315
+
253
316
  `thought` draws an ellipse with a trail of dots; `shout` draws a spiky burst.
254
- The tail is a slim line dropping from the bubble underside; its tip is capped
255
- short so it never overlaps the figure.
317
+ The tail is a slim wedge dropping from the bubble underside; its tip is capped
318
+ short so it never overlaps the figure (`tail: curve` draws the classic curved
319
+ comic tail, `tail: line` a thin line to just short of the speaker, `tail: none`
320
+ nothing).
321
+
322
+ `cmf validate` also prints layout **warnings** (exit 0 unless `--strict`): a
323
+ bubble out of reading order, crossing tails, a bubble covering a speaker point.
324
+ When one shows up, nudge that bubble with `at:` or `x`/`y`.
256
325
 
257
326
  ### pixel art
258
327
 
@@ -440,15 +440,21 @@ renders a default-faced, default-posed actor with no error. `validate` flags:
440
440
  - actor / scene keys that aren't reserved and aren't a real slot (likely typos)
441
441
  - panel keys the renderer doesn't know — `imge:` for `image:`, say
442
442
  - raster `image:` files that are missing, unreadable, or of an unsupported type;
443
- an `image:` with no `src`, an unknown image key, an unknown `fit`
444
- - bubble `speaker` naming no actor in the panel; unknown bubble `kind`
443
+ an `image:` with no `src`, an unknown image key, an unknown `fit` or `at`,
444
+ a bad `crop` (unknown side, negative, or trimming the whole image); a row
445
+ `height` that is neither a weight nor `auto`, an `auto` row with no image
446
+ - bubble `speaker` naming neither an actor nor a `speakers:` point in the panel;
447
+ a malformed `speakers:` point; unknown bubble `kind`, `tail`, `tail_from`,
448
+ or a `tail_bend` outside -1..1
445
449
  - structural holes (a page with no `rows`, a row with no `panels`, a bubble with
446
450
  no `text`, a `scene` spec with neither `scene:` nor `image:`); an unknown
447
451
  `type:`, or a `type:` that contradicts the structure (a `scene` spec carrying
448
452
  `rows`)
449
453
 
450
454
  Exits `0` when the spec is sound, `1` (with a bulleted problem list) otherwise —
451
- so it works in a pre-render check or CI.
455
+ so it works in a pre-render check or CI. It also lays out the bubbles and
456
+ prints **warnings** — a bubble out of reading order, tails that cross, a bubble
457
+ covering a speaker point — which only fail the check with `--strict`.
452
458
 
453
459
  ```bash
454
460
  cmf validate examples/pes/pages/slepice.yaml # -> "...: ok"
@@ -479,6 +485,13 @@ cmf panel <spec.yaml> [-o <output>] # default: output/<spec>-r<R>c<C>-<ts>.png
479
485
  [--all] # render every panel into a directory
480
486
  # (default: output/<spec>-panels-<ts>/)
481
487
  [--scale 0.5] # size vs full-page (default 0.5 = low res)
488
+ [--on-page] # crop panel + half a gutter out of the page render,
489
+ # so frame/corners look exactly as on the page
490
+ [--layout] # print each panel's page position as JSON, no render
491
+ # ({width, height, panels: [{row, col, file,
492
+ # box: {x,y,w,h}, image: {x,y,w,h}}]}, page px;
493
+ # image = what the render covers, honours --on-page);
494
+ # --all also writes it as layout.json
482
495
  [--library <dir>]
483
496
  [--scenes <dir>]
484
497
  [--pixel-dir <dir>]
@@ -602,34 +615,63 @@ For the full spec grammar, see [SKILL.md](SKILL.md). Key points:
602
615
  corner radius also clips the art),
603
616
  `bubble_style` (page-wide bubble look: `font`, `font_size`, `pad`, `radius`,
604
617
  `stroke`, `stroke_width`, `fill`, `ink`, `uppercase`, `em` — width scale of
605
- the text measure for narrower fonts)
618
+ the text measure for narrower fonts; `tail` `wedge`/`curve`/`line`/`none`,
619
+ `tail_bend`, `tail_gap`, `tail_from`)
606
620
  - **Rows and panels**: `rows[].height` (relative weight) or `rows[].height_mm`
607
- (fixed; weighted rows share the rest, all-fixed leaves the bottom blank),
621
+ (fixed; weighted rows share the rest, all-fixed leaves the bottom blank) or
622
+ `rows[].height: auto` (tallest of each panel's image aspect at its width,
623
+ after `crop:`, plus its caption band; auto rows that overflow the page have
624
+ their art — not their captions — squeezed by one common factor, the excess
625
+ cropped by `cover`; needs an `image:` panel; fixed rows are measured first,
626
+ weighted rows get what is left; `comicforge.page_squeeze(spec)` returns that
627
+ art factor, 1.0 when nothing is squeezed — use it to paginate),
608
628
  `rows[].panels[].width` (relative weight), panel keys: `bg`, `frame`
609
629
  (per-panel override), `caption` (narration band under the art, inside the
610
630
  frame: a string or `{text, max_chars}`; page-wide `caption_style` with
611
- `font`, `font_size`, `ink`, `bg`, `pad`, `max_chars`, `align`, `rule`, `uppercase`),
612
- `scene`, `image`, `actors`, `pixel`, `bubbles` — `validate` flags any other
613
- panel key as a typo
614
- - **Image keys**: `image: path.png` or `image: {src:, fit:}` with
631
+ `font`, `font_size`, `ink`, `bg`, `pad`, `max_chars` (a count, or `auto` to
632
+ wrap to the band's width), `em` (width scale of the `auto` measure),
633
+ `align`, `rule`, `uppercase`, `glue_singles`),
634
+ `scene`, `image`, `actors`, `pixel`, `bubbles`, `speakers` (`{name: [x, y]}`
635
+ head points in panel fractions that bubbles' `speaker:` resolves to when no
636
+ actor has that char) — `validate` flags any other panel key as a typo
637
+ - **Line breaking** (bubbles and captions): a one-letter word is tied to the
638
+ word after it with a no-break space, so a Czech one-letter preposition or
639
+ conjunction (`k s v z o u a i`) never ends a line; `glue_singles: false` on
640
+ the style or the individual line turns that off. A no-break space in the
641
+ text (`U+00A0`, `U+202F`) is never a break point, which is how an author
642
+ ties words the rule cannot know about. Other whitespace runs collapse to one
643
+ space.
644
+ - **Image keys**: `image: path.png` or `image: {src:, fit:, at:, crop:}` with
615
645
  `fit: cover` (default; scale-to-fill + centre-crop) or `contain`
616
- (fit inside + letterbox). The path resolves against the spec file's dir and
646
+ (fit inside + letterbox); `at:` (`t`, `b`, `l`, `r`, `tl`, `tr`, `bl`, `br`,
647
+ `c` — default centre) pins the image to that edge so `cover` crops the
648
+ opposite side; `crop: {top, bottom, left, right}` trims image pixels off
649
+ each edge before fitting (and before a `height: auto` row or a scene canvas
650
+ measures it). The path resolves against the spec file's dir and
617
651
  the image is embedded as a base64 data URI, so SVG/PDF output stays
618
652
  self-contained. A `type: scene` spec can use `image:` in place of `scene:`,
619
653
  and takes its canvas size from the image
620
654
  - **Actor keys**: `char`, `pose` (optional; defaults to the character's default
621
655
  pose), per-slot variant keys (`face`, `arms`, etc.), `x`, `y`, `scale`, `flip`
622
656
  - **Bubble keys**: `text`, `kind` (speech/thought/shout), `speaker`, optional
623
- `at`/`x`/`y`/`to`/`max_chars`/`fs`/`uppercase` (`fs` and `uppercase` override
624
- the page-level `bubble_style`). Omit `y` and bubbles stack down by their
657
+ `at`/`x`/`y`/`to`/`max_chars`/`fs`/`uppercase`/`glue_singles` (`fs`,
658
+ `uppercase` and `glue_singles` override the page-level `bubble_style`).
659
+ Omit `y` and bubbles stack down by their
625
660
  measured height without overlapping; omit `x` and they centre (or sit above
626
661
  their `speaker`). `at` hugs a corner or edge instead — `tl`, `t`, `tr`, `l`,
627
662
  `c`, `r`, `bl`, `b`, `br` — and each column keeps its own top and bottom
628
663
  stack, so `tl` + `tr` sit side by side and `bl` climbs up from the bottom.
629
- `to: [x, y]` (panel fractions) aims a tail without a speaker. Every bubble is
630
- kept inside the panel — so a generated spec over raster panels, which has no
631
- `speaker` to anchor to, can omit coordinates entirely, or place each with `at`
632
- to keep it off the faces
664
+ `to: [x, y]` (panel fractions) aims a tail without a speaker. `tail_from`
665
+ (`t`/`b`/`l`/`r`, a position 0..1, or `{edge, pos}`) picks where the tail
666
+ leaves; `tail` (`wedge`/`curve`/`line`/`none`, any kind) picks the look
667
+ and `tail_bend` (-1..1, auto: away from the nearest bubble) bows a curve
668
+ or line. On a panel with
669
+ `speakers:`, bubbles with a speaker and no `x`/`y`/`at` are placed in
670
+ reading order (speaker's side, each top clearly below the previous one's,
671
+ clear of heads and earlier tails). Every bubble is
672
+ kept inside the panel — so a generated spec over raster panels marks the heads
673
+ in `speakers:`, emits the lines in spoken order with a `speaker` each, and
674
+ omits coordinates; reach for `at` or `x`/`y` only when `validate` warns
633
675
  - **Pixel keys**: `art` (library name) or `grid`+`palette`, plus `x`/`y`/`scale`
634
676
 
635
677
  ---
@@ -645,6 +687,10 @@ render_spec("examples/pes/pages/slepice.yaml", "slepice.png")
645
687
  # Render a standalone scene
646
688
  render_scene("examples/pes/pages/dvur-scene.yaml", "dvur.png")
647
689
 
690
+ # Bubble boxes + tail endpoints of one panel, in panel fractions, no render
691
+ from comicforge import bubble_layout
692
+ bubble_layout("examples/pes/pages/slepice.yaml", row=0, col=1)["bubbles"]
693
+
648
694
  # Load a spec dict for inspection
649
695
  spec = load_spec("examples/pes/pages/slepice.yaml")
650
696
 
@@ -21,7 +21,7 @@ jobs:
21
21
  run: sudo apt-get update && sudo apt-get install -y libcairo2
22
22
 
23
23
  - name: Install uv
24
- uses: astral-sh/setup-uv@v10.0.1
24
+ uses: astral-sh/setup-uv@v10.1.0
25
25
  with:
26
26
  enable-cache: true
27
27
 
@@ -70,7 +70,7 @@ jobs:
70
70
  run: sudo apt-get update && sudo apt-get install -y libcairo2
71
71
 
72
72
  - name: Install uv
73
- uses: astral-sh/setup-uv@v10.0.1
73
+ uses: astral-sh/setup-uv@v10.1.0
74
74
  with:
75
75
  enable-cache: true
76
76
 
@@ -37,7 +37,7 @@ jobs:
37
37
  run: sudo apt-get update && sudo apt-get install -y libcairo2
38
38
 
39
39
  - name: Install uv
40
- uses: astral-sh/setup-uv@v10.0.1
40
+ uses: astral-sh/setup-uv@v10.1.0
41
41
  with:
42
42
  enable-cache: true
43
43
 
@@ -49,7 +49,7 @@ jobs:
49
49
  - name: Build site
50
50
  run: uv run mkdocs build --strict
51
51
 
52
- - uses: actions/upload-pages-artifact@v4
52
+ - uses: actions/upload-pages-artifact@v5
53
53
  with:
54
54
  path: site/
55
55
 
@@ -66,4 +66,4 @@ jobs:
66
66
  id-token: write
67
67
  steps:
68
68
  - id: deployment
69
- uses: actions/deploy-pages@v4
69
+ uses: actions/deploy-pages@v5
@@ -14,7 +14,7 @@ jobs:
14
14
  - uses: actions/checkout@v7
15
15
 
16
16
  - name: Install uv
17
- uses: astral-sh/setup-uv@v10.0.1
17
+ uses: astral-sh/setup-uv@v10.1.0
18
18
 
19
19
  - name: Build sdist + wheel
20
20
  run: uv build
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: comicforge
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: A tiny, scriptable comic-page engine — author comics as YAML, render to SVG / PNG / PDF
5
5
  Requires-Python: >=3.13
6
6
  Requires-Dist: cairosvg>=2.7
@@ -5,6 +5,13 @@ Designed so an LLM (or you) can author pages as plain declarative text.
5
5
  """
6
6
 
7
7
  from .pixelart import PixelLibrary # noqa: F401
8
- from .render import load_spec, render_scene, render_spec # noqa: F401
8
+ from .render import ( # noqa: F401
9
+ bubble_layout,
10
+ load_spec,
11
+ page_squeeze,
12
+ panel_layout,
13
+ render_scene,
14
+ render_spec,
15
+ )
9
16
 
10
17
  __version__ = "0.2.0"