edof 4.2.4__tar.gz → 4.3.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.
Files changed (192) hide show
  1. edof-4.3.0/CHANGELOG.md +3539 -0
  2. edof-4.3.0/MANIFEST.in +6 -0
  3. {edof-4.2.4 → edof-4.3.0}/PKG-INFO +22 -5
  4. {edof-4.2.4 → edof-4.3.0}/README.md +19 -4
  5. edof-4.3.0/docs/INSTALL.md +141 -0
  6. edof-4.3.0/docs/QUICKSTART.md +208 -0
  7. edof-4.3.0/docs/README.md +65 -0
  8. edof-4.3.0/docs/advanced/extending.md +335 -0
  9. edof-4.3.0/docs/advanced/file-format.md +499 -0
  10. edof-4.3.0/docs/advanced/troubleshooting.md +353 -0
  11. edof-4.3.0/docs/cookbook/batch-pdf.md +314 -0
  12. edof-4.3.0/docs/cookbook/certificate.md +219 -0
  13. edof-4.3.0/docs/cookbook/effects-poster.md +84 -0
  14. edof-4.3.0/docs/cookbook/encrypted-template.md +289 -0
  15. edof-4.3.0/docs/cookbook/invoice.md +345 -0
  16. edof-4.3.0/docs/cookbook/pdf-import-edit.md +280 -0
  17. edof-4.3.0/docs/reference/01-document.md +431 -0
  18. edof-4.3.0/docs/reference/02-objects.md +443 -0
  19. edof-4.3.0/docs/reference/03-styles.md +308 -0
  20. edof-4.3.0/docs/reference/04-variables.md +309 -0
  21. edof-4.3.0/docs/reference/05-export.md +278 -0
  22. edof-4.3.0/docs/reference/06-import.md +160 -0
  23. edof-4.3.0/docs/reference/07-encryption.md +411 -0
  24. edof-4.3.0/docs/reference/08-editor.md +465 -0
  25. edof-4.3.0/docs/reference/09-cli.md +329 -0
  26. edof-4.3.0/docs/reference/10-helpers.md +267 -0
  27. edof-4.3.0/docs/reference/11-docx.md +132 -0
  28. edof-4.3.0/docs/reference/12-effects.md +133 -0
  29. edof-4.3.0/docs/reference/API.md +1874 -0
  30. {edof-4.2.4 → edof-4.3.0}/edof/_apps/assets/__init__.py +25 -0
  31. edof-4.3.0/edof/_apps/assets/cursors/cur-add_point.png +0 -0
  32. edof-4.3.0/edof/_apps/assets/cursors/cur-crosshair.png +0 -0
  33. edof-4.3.0/edof/_apps/assets/cursors/cur-eyedropper.png +0 -0
  34. edof-4.3.0/edof/_apps/assets/cursors/cur-hand.png +0 -0
  35. edof-4.3.0/edof/_apps/assets/cursors/cur-hand_grab.png +0 -0
  36. edof-4.3.0/edof/_apps/assets/cursors/cur-move.png +0 -0
  37. edof-4.3.0/edof/_apps/assets/cursors/cur-not_allowed.png +0 -0
  38. edof-4.3.0/edof/_apps/assets/cursors/cur-pen.png +0 -0
  39. edof-4.3.0/edof/_apps/assets/cursors/cur-pen_close.png +0 -0
  40. edof-4.3.0/edof/_apps/assets/cursors/cur-remove_point.png +0 -0
  41. edof-4.3.0/edof/_apps/assets/cursors/cur-resize_ew.png +0 -0
  42. edof-4.3.0/edof/_apps/assets/cursors/cur-resize_nesw.png +0 -0
  43. edof-4.3.0/edof/_apps/assets/cursors/cur-resize_ns.png +0 -0
  44. edof-4.3.0/edof/_apps/assets/cursors/cur-resize_nwse.png +0 -0
  45. edof-4.3.0/edof/_apps/assets/cursors/cur-rotate.png +0 -0
  46. edof-4.3.0/edof/_apps/assets/cursors/cur-tangent.png +0 -0
  47. edof-4.3.0/edof/_apps/assets/cursors/cur-text.png +0 -0
  48. edof-4.3.0/edof/_apps/assets/cursors/cur-zoom_in.png +0 -0
  49. edof-4.3.0/edof/_apps/assets/cursors/cur-zoom_out.png +0 -0
  50. edof-4.3.0/edof/_apps/assets/cursors/cursors.json +232 -0
  51. edof-4.3.0/edof/_apps/assets/ui_icons/ic-align-center.png +0 -0
  52. edof-4.3.0/edof/_apps/assets/ui_icons/ic-align-left.png +0 -0
  53. edof-4.3.0/edof/_apps/assets/ui_icons/ic-align-right.png +0 -0
  54. edof-4.3.0/edof/_apps/assets/ui_icons/ic-blend.png +0 -0
  55. edof-4.3.0/edof/_apps/assets/ui_icons/ic-bold.png +0 -0
  56. edof-4.3.0/edof/_apps/assets/ui_icons/ic-delete.png +0 -0
  57. edof-4.3.0/edof/_apps/assets/ui_icons/ic-duplicate.png +0 -0
  58. edof-4.3.0/edof/_apps/assets/ui_icons/ic-ellipse.png +0 -0
  59. edof-4.3.0/edof/_apps/assets/ui_icons/ic-export.png +0 -0
  60. edof-4.3.0/edof/_apps/assets/ui_icons/ic-eyedropper.png +0 -0
  61. edof-4.3.0/edof/_apps/assets/ui_icons/ic-fit.png +0 -0
  62. edof-4.3.0/edof/_apps/assets/ui_icons/ic-fx.png +0 -0
  63. edof-4.3.0/edof/_apps/assets/ui_icons/ic-grid.png +0 -0
  64. edof-4.3.0/edof/_apps/assets/ui_icons/ic-group.png +0 -0
  65. edof-4.3.0/edof/_apps/assets/ui_icons/ic-guides.png +0 -0
  66. edof-4.3.0/edof/_apps/assets/ui_icons/ic-hand.png +0 -0
  67. edof-4.3.0/edof/_apps/assets/ui_icons/ic-hidden.png +0 -0
  68. edof-4.3.0/edof/_apps/assets/ui_icons/ic-image.png +0 -0
  69. edof-4.3.0/edof/_apps/assets/ui_icons/ic-italic.png +0 -0
  70. edof-4.3.0/edof/_apps/assets/ui_icons/ic-layer-down.png +0 -0
  71. edof-4.3.0/edof/_apps/assets/ui_icons/ic-layer-up.png +0 -0
  72. edof-4.3.0/edof/_apps/assets/ui_icons/ic-line.png +0 -0
  73. edof-4.3.0/edof/_apps/assets/ui_icons/ic-lock.png +0 -0
  74. edof-4.3.0/edof/_apps/assets/ui_icons/ic-new.png +0 -0
  75. edof-4.3.0/edof/_apps/assets/ui_icons/ic-open.png +0 -0
  76. edof-4.3.0/edof/_apps/assets/ui_icons/ic-pen.png +0 -0
  77. edof-4.3.0/edof/_apps/assets/ui_icons/ic-print.png +0 -0
  78. edof-4.3.0/edof/_apps/assets/ui_icons/ic-qr.png +0 -0
  79. edof-4.3.0/edof/_apps/assets/ui_icons/ic-rect.png +0 -0
  80. edof-4.3.0/edof/_apps/assets/ui_icons/ic-redo.png +0 -0
  81. edof-4.3.0/edof/_apps/assets/ui_icons/ic-save.png +0 -0
  82. edof-4.3.0/edof/_apps/assets/ui_icons/ic-select.png +0 -0
  83. edof-4.3.0/edof/_apps/assets/ui_icons/ic-snap.png +0 -0
  84. edof-4.3.0/edof/_apps/assets/ui_icons/ic-strike.png +0 -0
  85. edof-4.3.0/edof/_apps/assets/ui_icons/ic-table.png +0 -0
  86. edof-4.3.0/edof/_apps/assets/ui_icons/ic-text.png +0 -0
  87. edof-4.3.0/edof/_apps/assets/ui_icons/ic-underline.png +0 -0
  88. edof-4.3.0/edof/_apps/assets/ui_icons/ic-undo.png +0 -0
  89. edof-4.3.0/edof/_apps/assets/ui_icons/ic-ungroup.png +0 -0
  90. edof-4.3.0/edof/_apps/assets/ui_icons/ic-unlock.png +0 -0
  91. edof-4.3.0/edof/_apps/assets/ui_icons/ic-visible.png +0 -0
  92. edof-4.3.0/edof/_apps/assets/ui_icons/ic-zoom-in.png +0 -0
  93. edof-4.3.0/edof/_apps/assets/ui_icons/ic-zoom-out.png +0 -0
  94. edof-4.3.0/edof/_apps/assets/ui_icons/ic-zoom.png +0 -0
  95. edof-4.3.0/edof/_apps/assets/ui_icons/icons.json +356 -0
  96. {edof-4.2.4 → edof-4.3.0}/edof/_apps/editor.py +3804 -521
  97. {edof-4.2.4 → edof-4.3.0}/edof/_apps/edof_text_editor.py +61 -17
  98. {edof-4.2.4 → edof-4.3.0}/edof/_apps/viewer.py +1 -1
  99. {edof-4.2.4 → edof-4.3.0}/edof/api/commands.py +1 -1
  100. {edof-4.2.4 → edof-4.3.0}/edof/crypto/document_protection.py +1 -1
  101. {edof-4.2.4 → edof-4.3.0}/edof/engine/color.py +0 -1
  102. {edof-4.2.4 → edof-4.3.0}/edof/engine/debug_log.py +9 -2
  103. {edof-4.2.4 → edof-4.3.0}/edof/engine/document_flow.py +1 -1
  104. {edof-4.2.4 → edof-4.3.0}/edof/engine/document_paginate.py +44 -6
  105. edof-4.3.0/edof/engine/gpu.py +1054 -0
  106. edof-4.3.0/edof/engine/renderer.py +2723 -0
  107. {edof-4.2.4 → edof-4.3.0}/edof/engine/text_engine.py +43 -7
  108. {edof-4.2.4 → edof-4.3.0}/edof/engine/text_layout.py +112 -12
  109. {edof-4.2.4 → edof-4.3.0}/edof/export/pdf.py +0 -47
  110. {edof-4.2.4 → edof-4.3.0}/edof/export/pdf_writer.py +2 -7
  111. {edof-4.2.4 → edof-4.3.0}/edof/format/document.py +2 -1
  112. {edof-4.2.4 → edof-4.3.0}/edof/format/document_body.py +41 -1
  113. {edof-4.2.4 → edof-4.3.0}/edof/format/document_boxes.py +1 -1
  114. {edof-4.2.4 → edof-4.3.0}/edof/format/objects.py +5 -1
  115. {edof-4.2.4 → edof-4.3.0}/edof/format/serializer.py +1 -0
  116. {edof-4.2.4 → edof-4.3.0}/edof/format/styles.py +132 -3
  117. {edof-4.2.4 → edof-4.3.0}/edof/format/svg_io.py +0 -1
  118. {edof-4.2.4 → edof-4.3.0}/edof/gui/pyqt6_widget.py +2 -1
  119. {edof-4.2.4 → edof-4.3.0}/edof/gui/tkinter_canvas.py +0 -1
  120. {edof-4.2.4 → edof-4.3.0}/edof/utils/legacy_v2.py +1 -1
  121. {edof-4.2.4 → edof-4.3.0}/edof/utils/pdf_import.py +1 -1
  122. {edof-4.2.4 → edof-4.3.0}/edof/utils/qr.py +1 -1
  123. {edof-4.2.4 → edof-4.3.0}/edof/utils/rtf.py +1 -1
  124. {edof-4.2.4 → edof-4.3.0}/edof/utils/safe_eval.py +1 -1
  125. {edof-4.2.4 → edof-4.3.0}/edof/version.py +2 -2
  126. {edof-4.2.4 → edof-4.3.0}/edof.egg-info/PKG-INFO +22 -5
  127. edof-4.3.0/edof.egg-info/SOURCES.txt +188 -0
  128. {edof-4.2.4 → edof-4.3.0}/edof.egg-info/entry_points.txt +1 -0
  129. {edof-4.2.4 → edof-4.3.0}/edof.egg-info/requires.txt +3 -0
  130. edof-4.3.0/examples/01_hello_document.py +20 -0
  131. edof-4.3.0/examples/02_table_and_rich_text.py +22 -0
  132. edof-4.3.0/examples/03_vector_gradients.py +24 -0
  133. edof-4.3.0/examples/04_layer_effects.py +40 -0
  134. edof-4.3.0/examples/05_variables_template.py +16 -0
  135. edof-4.3.0/examples/06_save_load_roundtrip.py +15 -0
  136. edof-4.3.0/examples/README.md +19 -0
  137. {edof-4.2.4 → edof-4.3.0}/pyproject.toml +8 -1
  138. {edof-4.2.4 → edof-4.3.0}/tests/test_v403.py +11 -5
  139. edof-4.3.0/version.txt +1 -0
  140. edof-4.2.4/edof/engine/renderer.py +0 -1393
  141. edof-4.2.4/edof.egg-info/SOURCES.txt +0 -87
  142. {edof-4.2.4 → edof-4.3.0}/LICENSE +0 -0
  143. {edof-4.2.4 → edof-4.3.0}/edof/__init__.py +0 -0
  144. {edof-4.2.4 → edof-4.3.0}/edof/_apps/__init__.py +0 -0
  145. {edof-4.2.4 → edof-4.3.0}/edof/_apps/_assoc_dialog.py +0 -0
  146. {edof-4.2.4 → edof-4.3.0}/edof/_apps/assets/icons/__init__.py +0 -0
  147. {edof-4.2.4 → edof-4.3.0}/edof/_apps/assets/icons/edof-document-512.png +0 -0
  148. {edof-4.2.4 → edof-4.3.0}/edof/_apps/assets/icons/edof-document.icns +0 -0
  149. {edof-4.2.4 → edof-4.3.0}/edof/_apps/assets/icons/edof-document.ico +0 -0
  150. {edof-4.2.4 → edof-4.3.0}/edof/_apps/assets/icons/edof-document.png +0 -0
  151. {edof-4.2.4 → edof-4.3.0}/edof/_apps/assets/icons/edof-editor-512.png +0 -0
  152. {edof-4.2.4 → edof-4.3.0}/edof/_apps/assets/icons/edof-editor.icns +0 -0
  153. {edof-4.2.4 → edof-4.3.0}/edof/_apps/assets/icons/edof-editor.ico +0 -0
  154. {edof-4.2.4 → edof-4.3.0}/edof/_apps/assets/icons/edof-editor.png +0 -0
  155. {edof-4.2.4 → edof-4.3.0}/edof/_apps/assets/icons/edof-viewer-512.png +0 -0
  156. {edof-4.2.4 → edof-4.3.0}/edof/_apps/assets/icons/edof-viewer.icns +0 -0
  157. {edof-4.2.4 → edof-4.3.0}/edof/_apps/assets/icons/edof-viewer.ico +0 -0
  158. {edof-4.2.4 → edof-4.3.0}/edof/_apps/assets/icons/edof-viewer.png +0 -0
  159. {edof-4.2.4 → edof-4.3.0}/edof/_apps/cli.py +0 -0
  160. {edof-4.2.4 → edof-4.3.0}/edof/_apps/file_assoc.py +0 -0
  161. {edof-4.2.4 → edof-4.3.0}/edof/_apps/shortcuts.py +0 -0
  162. {edof-4.2.4 → edof-4.3.0}/edof/api/__init__.py +0 -0
  163. {edof-4.2.4 → edof-4.3.0}/edof/crypto/__init__.py +0 -0
  164. {edof-4.2.4 → edof-4.3.0}/edof/crypto/encryption.py +0 -0
  165. {edof-4.2.4 → edof-4.3.0}/edof/crypto/permissions.py +0 -0
  166. {edof-4.2.4 → edof-4.3.0}/edof/editor_lang/en.json +0 -0
  167. {edof-4.2.4 → edof-4.3.0}/edof/engine/__init__.py +0 -0
  168. {edof-4.2.4 → edof-4.3.0}/edof/engine/textbox_flow.py +0 -0
  169. {edof-4.2.4 → edof-4.3.0}/edof/engine/transform.py +0 -0
  170. {edof-4.2.4 → edof-4.3.0}/edof/exceptions.py +0 -0
  171. {edof-4.2.4 → edof-4.3.0}/edof/export/__init__.py +0 -0
  172. {edof-4.2.4 → edof-4.3.0}/edof/export/bitmap.py +0 -0
  173. {edof-4.2.4 → edof-4.3.0}/edof/export/legacy_v3.py +0 -0
  174. {edof-4.2.4 → edof-4.3.0}/edof/export/printer.py +0 -0
  175. {edof-4.2.4 → edof-4.3.0}/edof/export/svg.py +0 -0
  176. {edof-4.2.4 → edof-4.3.0}/edof/format/__init__.py +0 -0
  177. {edof-4.2.4 → edof-4.3.0}/edof/format/variables.py +0 -0
  178. {edof-4.2.4 → edof-4.3.0}/edof/gui/__init__.py +0 -0
  179. {edof-4.2.4 → edof-4.3.0}/edof/interop/__init__.py +0 -0
  180. {edof-4.2.4 → edof-4.3.0}/edof/interop/docx_io.py +0 -0
  181. {edof-4.2.4 → edof-4.3.0}/edof/py.typed +0 -0
  182. {edof-4.2.4 → edof-4.3.0}/edof/units.py +0 -0
  183. {edof-4.2.4 → edof-4.3.0}/edof/utils/__init__.py +0 -0
  184. {edof-4.2.4 → edof-4.3.0}/edof/utils/compat.py +0 -0
  185. {edof-4.2.4 → edof-4.3.0}/edof.egg-info/dependency_links.txt +0 -0
  186. {edof-4.2.4 → edof-4.3.0}/edof.egg-info/top_level.txt +0 -0
  187. {edof-4.2.4 → edof-4.3.0}/setup.cfg +0 -0
  188. {edof-4.2.4 → edof-4.3.0}/tests/test_v310.py +0 -0
  189. {edof-4.2.4 → edof-4.3.0}/tests/test_v400.py +0 -0
  190. {edof-4.2.4 → edof-4.3.0}/tests/test_v401.py +0 -0
  191. {edof-4.2.4 → edof-4.3.0}/tests/test_v402.py +0 -0
  192. {edof-4.2.4 → edof-4.3.0}/tests/test_v410.py +0 -0
@@ -0,0 +1,3539 @@
1
+ ## [4.3.0] - 2026-06-11
2
+
3
+ PyPI release. Highlights of the 4.2.11.x series rolled into this version:
4
+ the long-shadow effect was rebuilt around a per-point ray model with three
5
+ independent mode selectors (blur: Solid / Constant / Linear / Custom; colour:
6
+ Solid / Custom; alpha: Solid / Fade / Custom), Photoshop-style multi-stop
7
+ gradient editors, fractional-radius variable blur, an exact GPU kernel for
8
+ the ray field, and a 2.5-5x faster CPU path. File format 4.2.19.
9
+
10
+ This release also ships a complete documentation refresh: a new layer-effects
11
+ reference (docs/reference/12-effects.md) covering all 13 effect types with the
12
+ full long-shadow mode and gradient-stop documentation, a new cookbook recipe
13
+ (effects-poster.md), a new runnable `examples/` directory (six end-to-end
14
+ scripts, all verified), layer-effects sections in README and QUICKSTART, the
15
+ effects serialization schema in the file-format guide, and updated doc
16
+ indexes. All new documentation code blocks are executed as part of validation.
17
+
18
+ The cleanup pass itself has no behaviour changes:
19
+
20
+ ### Changed
21
+ - **Dead code removed:** four unreferenced internal functions
22
+ (`_render_object_raw`, `_pdf_hex_string`, `_emit_rasterized`, `_from_qc`)
23
+ and a shadowed duplicate definition of `_composite_with_blend` in the
24
+ renderer (~6 KB; the later definition always won at import time, so
25
+ behaviour is identical). One dead local in the GPU selftest.
26
+ - **28 unused imports removed** (conservative pass: top-level from-imports
27
+ only; bare imports, package re-exports in `__init__.py` and local imports
28
+ untouched). `List` added to the typing imports in `format/styles.py` (the
29
+ gradient stop annotations referenced it lazily).
30
+ - **Library prints converted to `logging`:** the sub-document load failure in
31
+ the renderer and the render-error path in the PyQt6 widget now go through
32
+ `logging.getLogger(__name__).warning` instead of stdout.
33
+ - **Documentation paths neutralised:** installation examples no longer
34
+ reference a machine-specific drive layout; internal first-person notes in
35
+ comments and changelog entries reworded to neutral technical language.
36
+
37
+ ### Validation
38
+ - Full suite 179 passed / 3 skipped; 120-combo long-shadow mode smoke clean;
39
+ backward-halo, perpendicular-profile and CPU-vs-exact parity checks
40
+ unchanged from .61. Headless import smoke of editor / viewer / CLI passes.
41
+ `python -m build` produces a clean sdist + wheel (no caches, tests-only or
42
+ launcher files inside the wheel); the wheel installs into a fresh venv and
43
+ the core API works. FORMAT_PATCH 19 unchanged; GPU paths untouched, 3090
44
+ selftest expectations unchanged.
45
+ ## [4.2.11.61] - 2026-06-11
46
+
47
+ Long shadow: the side (perpendicular) blur is back at full width -- the .60
48
+ backward-halo fix had tightened it.
49
+
50
+ ### Fixed
51
+ - **Side halo width.** The .60 radius driver composed the X and Y min passes
52
+ into a 2D window minimum, so a perpendicular halo pixel inherited the
53
+ SMALLEST t within the whole reach window -- typically from body pixels up
54
+ to `reach` px upstream -- instead of the local body t right beside it. A
55
+ smaller t means a smaller radius, so the side halo visibly tightened
56
+ ("nebluruje do boku"). The propagation is now a SEQUENTIAL FILL that
57
+ approximates "the t of the NEAREST shadow": the Y pass assigns the local
58
+ body t to the perpendicular halo (exactly the .59 behaviour), and the X
59
+ pass fills ONLY the cells the Y pass left empty -- the backward halo past
60
+ the leading edge (kept from .60: constant / nonzero-start custom soften
61
+ backward, linear stays root-sharp) and the corners, which read the
62
+ already-filled perpendicular strips and therefore wrap roundly instead of
63
+ notching.
64
+
65
+ ### Validation
66
+ - Perpendicular profiles bit-match .59 again (x=200 max 66, x=350 max 102,
67
+ x=500 max 111); the backward-halo block repro bit-matches .60 (constant
68
+ ramps 18..153 across the leading edge, custom ramps, linear exactly sharp);
69
+ circle: linear contact clean (crescent 0), constant smooth all around (max
70
+ alpha step 8). Two-block outward halo 103; 30-config matrix OK (linear
71
+ solid 2466 px); 120-combo smoke clean; CPU-vs-exact parity unchanged (mean
72
+ 0.3120, max 10/255, solid-alpha fast path bit-identical); suite 179 passed
73
+ / 3 skipped. GPU paths untouched, 3090 selftest expectations unchanged
74
+ incl. lsf_*. FORMAT_PATCH 19 unchanged.
75
+ ## [4.2.11.60] - 2026-06-11
76
+
77
+ Long shadow: with a nonzero START blur (Constant mode, Custom stops with
78
+ blur(0) > 0) the shadow now softens BACKWARD over the silhouette's leading
79
+ boundary too -- the razor edge against the throw direction is gone.
80
+
81
+ ### Fixed
82
+ - **Backward halo at the leading edge.** The local radius driver propagated
83
+ the shadow's t only PERPENDICULAR to the throw, so pixels straight upstream
84
+ of the leading edge always kept the no-shadow sentinel (radius 0) and the
85
+ leading boundary stayed razor-sharp even in Constant mode, where the blur
86
+ must spread in every direction. The propagation is now a separable 2D min
87
+ over the blur reach (X then Y, symmetric doubling shifts): upstream pixels
88
+ inherit the adjacent shadow's t ~ 0, which maps to radius = blur(0) --
89
+ Constant / nonzero-start Custom soften backward, while Linear (blur(0) = 0)
90
+ keeps the root razor-sharp by design. The rotated-frame crop box gets an
91
+ upstream margin for the backward halo (it would have cut it into a hard
92
+ diagonal otherwise).
93
+
94
+ ### Validation
95
+ - Block repro: Constant 30 px now ramps smoothly across the leading edge
96
+ (18..153, was a 0 -> 227 jump); Custom with start 20 px ramps; Linear stays
97
+ exactly sharp. Circle: Constant grows a clean halo all around the leading
98
+ rim (max alpha step 8 on the leading side), Linear keeps the .59 clean
99
+ contact (crescent check still 0). Two-block outward halo, 30-config matrix,
100
+ 120-combo smoke, CPU-vs-exact parity (mean 0.31, max 10/255) and the suite
101
+ (179 passed / 3 skipped) all pass. GPU paths untouched, 3090 selftest
102
+ expectations unchanged incl. lsf_*.
103
+ ## [4.2.11.59] - 2026-06-11
104
+
105
+ Long shadow: the hard edge / dark crescent at the start is gone.
106
+
107
+ ### Fixed
108
+ - **Hard cut and dark crescent at the shadow start.** Two stacked causes,
109
+ both rooted in the silhouette's faint leading AA fringe (alpha 2..40, just
110
+ upstream of the source threshold):
111
+ 1. those pixels SELF-emit (their own ray at t = 0) but carried the
112
+ no-upstream sentinel t = 1, so the leading rim got the MAXIMUM blur
113
+ radius; the radius smooth spread it to the neighbourhood, whose gather
114
+ then pulled the nearby body at near-full strength;
115
+ 2. pixels with no shadow in reach clipped the same sentinel to t = 1 as
116
+ well, growing a spurious halo BACKWARD past the leading edge that the
117
+ rotated-frame crop box cut into a hard straight diagonal (the visible
118
+ razor edge in the reported screenshot).
119
+ Fix: the leading AA fringe gets its true t = 0 at the source of everything
120
+ (the youngest-distance field itself, so the blur driver, the colour lookup
121
+ and the constant-alpha path are all consistent), and the no-shadow sentinel
122
+ maps to radius 0 instead of max. Verified on the circle reproduction: the
123
+ crescent (max alpha 243 outside the rim) and both tangent spikes are gone;
124
+ the shadow emerges cleanly, sharp at the contact, softening along the throw.
125
+
126
+ ### Validation
127
+ - Circle repro clean at the contact; two-block outward-halo repro and the
128
+ single-block profiles bit-match .58; 30-config halo matrix OK; 120-combo
129
+ mode smoke clean; CPU-vs-exact parity unchanged (mean 0.31, max 10/255);
130
+ suite 179 passed / 3 skipped. GPU paths untouched (the fix is in the shared
131
+ CPU-side t field), 3090 selftest expectations unchanged incl. lsf_*.
132
+ ## [4.2.11.58] - 2026-06-11
133
+
134
+ Long shadow: the outward blur fixed. The blur ate the shadow inward but the
135
+ outer contour stayed razor-sharp for any shadow cast before the object's
136
+ global trailing edge -- which with text or any multi-part silhouette is most
137
+ of the shadow.
138
+
139
+ ### Fixed
140
+ - **Outward halo everywhere.** The radius driver for pixels OUTSIDE the shadow
141
+ (where the soft halo must grow) was anchored to the GLOBAL trailing object
142
+ column: anything left of it got a zero outside radius. One glyph's shadow
143
+ passing beside another therefore blurred inward only, with hard outer edges
144
+ ("blur jen dovnitr, ven ostre okraje"). The driver is now LOCAL: each halo
145
+ pixel takes the t of the nearby shadow it belongs to, propagated vertically
146
+ out of the field over the blur reach (symmetric sliding min, doubling
147
+ shifts), plus the row's own distance for the downstream end cap. The root
148
+ flank stays sharp (propagated t ~ 0 near the contact); pixels with no shadow
149
+ within reach gather empty windows at O(1) SAT cost, so nothing is wasted.
150
+ Reproduced with a two-block silhouette (shadow of block 1 passing block 2:
151
+ outward halo max was 0, now a smooth 0 -> 103 ramp) and verified the
152
+ single-block profiles are bit-identical to .57.
153
+
154
+ ### Validation
155
+ - 30-config halo matrix (5 directions x 3 blur modes x 2 alpha modes): outward
156
+ halo present in every case; text-like multi-glyph silhouette blurs outward
157
+ along the whole shadow with sharp glyph contacts. 120-combo mode smoke
158
+ clean; suite 179 passed / 3 skipped. GPU paths untouched (the driver is
159
+ CPU-side; the field kernel and the variable box blur are unchanged), 3090
160
+ selftest expectations unchanged (incl. lsf_mean_diff / lsf_max_diff from
161
+ .57).
162
+ ## [4.2.11.57] - 2026-06-11
163
+
164
+ Long shadow: taper and cast removed; three clean mode selectors; the blur
165
+ fixed (no more radius rings, no value/distance mispairing); 2.5-5x faster on
166
+ CPU and a new exact GPU kernel for the ray field.
167
+
168
+ ### Changed
169
+ - **Taper and cast are removed.** Legacy documents map: ls_mode
170
+ 'cast' renders as a linear-blur soft shadow, ls_taper is ignored (straight
171
+ rays). With taper gone the per-point ray union is EXACTLY per-row in the
172
+ rotated frame, which unlocked the speed and the GPU kernel below.
173
+ - **Three independent mode selectors** (UI + format):
174
+ - BLUR mode: Solid (sharp, hides the rest) | Constant (amount in UI) |
175
+ Linear (END amount in UI) | Custom (blur gradient stops);
176
+ - COLOR mode: Solid (colour button) | Custom (colour gradient stops);
177
+ - ALPHA mode: Solid | Fade | Custom (alpha gradient stops); the ls_fade
178
+ flag is gone from the UI (still written for legacy readers).
179
+ All gradient bars come PRE-FILLED with sensible defaults (never empty); the
180
+ x button resets to the default stops. Only the controls relevant to the
181
+ selected mode are visible. New serialized fields ls_alpha_mode /
182
+ ls_color_mode (FORMAT_PATCH 18 -> 19); empty = derive from legacy fields, so
183
+ old documents render unchanged.
184
+
185
+ ### Fixed
186
+ - **Blur quality.** Two real defects fixed:
187
+ - the variable blur radius map was quantised to integers, which drew faint
188
+ RINGS (radius steps) across the soft skirt; the radius is now FRACTIONAL --
189
+ two integer-radius variable box blurs lerped per pixel -- which is
190
+ mathematically continuous across radius transitions, identically on the
191
+ CPU and GPU paths;
192
+ - the radius driver went through an 8-bit roundtrip (uint8 de-stair smooth),
193
+ quantising t to 256 levels; it now uses a small float32 separable Gaussian.
194
+ - **Field correctness.** The CPU youngest-ray term paired the value of the
195
+ STRONGEST upstream emitter with the distance of the NEAREST one, so the
196
+ shadow behind a semi-transparent emitter jumped to the strength of a distant
197
+ opaque one. It now carries the actual nearest emitter's value (a true
198
+ single-ray contribution, never an overestimate). Measured against the exact
199
+ max-convolution: max error dropped 84 -> 10 (of 255), mean 0.62 -> 0.31, and
200
+ the dominant regime is exact.
201
+
202
+ ### Performance / GPU
203
+ - CPU: constant-alpha fast path (the exact field is ONE doubling smear, no
204
+ segment loop), allocation-free segment hot loop, monotone-aware segment
205
+ count, tight crop before the heavy ops. 1600x1200 page, L = 500 px: 2.0-2.5 s
206
+ -> 0.45-0.97 s depending on mode (2.5-5x).
207
+ - GPU: new `gpu_long_shadow_field` kernel computes the EXACT per-row
208
+ max-convolution (every emitter, LINEAR-filtered alpha-gradient LUT, no
209
+ segment quantisation) in a single pass; the variable box blur already runs
210
+ on the GPU, so with GPU enabled the whole heavy pipeline is on the card and
211
+ the CPU only rotates. CPU remains the fallback (approximate, documented).
212
+ New selftest entries lsf_mean_diff / lsf_max_diff compare the kernel against
213
+ a brute-force numpy reference (expected: mean well under 0.05, max under
214
+ 1.0 -- the LUT linear filtering).
215
+
216
+ ### Validation
217
+ - 120-combo smoke (5 directions x 4 blur modes x 2 colour modes x 3 alpha
218
+ modes) clean; suite 179 passed / 3 skipped; gradient round-trip serialization
219
+ OK; solid blur renders with zero soft pixels beyond edge AA; the sharp
220
+ contact and growing blur verified visually. GUI smoke-tested headless only.
221
+ ## [4.2.11.56] - 2026-06-10
222
+
223
+ Long shadow: the PER-POINT RAY model + Photoshop-style
224
+ multi-stop COLOUR / ALPHA / BLUR gradients with a stops editor in the UI.
225
+
226
+ ### Changed
227
+ - **Per-point rays.** Every silhouette point (any source: path, PNG with
228
+ alpha, text) emits a ray of length `ls_length` along the throw; the shadow is
229
+ the max-union of all rays. The source point's alpha scales its whole ray.
230
+ Along each ray, everything is a multi-stop gradient over the ray's own
231
+ progression t: colour c(t), alpha a(t), blur radius r(t); TAPER makes each
232
+ ray straight, widening or narrowing -- so with taper the widening starts at
233
+ each EMITTING point (the leading edge included), which is what the previous
234
+ trailing-boundary model got wrong at the front. Narrowing erodes rays along
235
+ their travel, so thin strokes naturally die off toward the tip.
236
+ Implementation: rotated frame (throw = +X), a single trailing-window sliding
237
+ max + N cheap shifts realise the max-convolution over distance segments,
238
+ vertical morphological dilation / erosion per segment realises taper, and an
239
+ exact continuous youngest-ray term keeps the dominant regime perfectly
240
+ smooth (skipped when narrowing, where erosion must be able to remove
241
+ coverage). All float until the final compose.
242
+ - **Multi-stop gradients (format + UI).** New serialized fields on the effect:
243
+ `ls_grad_colors` [[t, r, g, b], ...], `ls_grad_alphas` [[t, a01], ...],
244
+ `ls_grad_blurs` [[t, mm], ...]. Empty lists = legacy behaviour (fade flag,
245
+ color -> color2, blur mode/gamma), so old documents render as before;
246
+ non-empty stops override the legacy controls. FORMAT_PATCH 17 -> 18 (new
247
+ serialized fields). The Long Shadow effect page gets three Photoshop-style
248
+ gradient bars (drag a stop to move, double-click to add / edit -- colour
249
+ dialog or numeric value, right-click to delete, x to clear back to legacy).
250
+ Cast stays physical (light-driven) and ignores the stylistic gradients.
251
+
252
+ ### Validation
253
+ - Front behaviour: with taper > 1 the widening cone starts at the leading
254
+ corner and runs along the object's flank; taper < 1 narrows to the tip;
255
+ taper 1 is a straight column. Multi-stop colour, non-monotone alpha (the
256
+ union over a thick body correctly lets older interior rays dominate where
257
+ they should) and mid-peaking blur stops all verified end-to-end through the
258
+ dispatch. Effect serialization round-trips; legacy dicts load with empty
259
+ stops. Cast bit-identity checks still hold. 180-combo smoke clean; suite
260
+ 179 passed / 3 skipped; 1600x1200 page with L = 500 px renders in ~2-2.5 s.
261
+ - GPU pipeline untouched (the model is CPU numpy; halftone / blur / CA GPU
262
+ paths unchanged). 3090 selftest expectations unchanged.
263
+ ## [4.2.11.55] - 2026-06-10
264
+
265
+ Long shadow: ONE model. The scaled-copy loft is deleted; everything (including
266
+ taper) renders through the rotated-frame column model where every property is a
267
+ function of the progression t along the throw.
268
+
269
+ ### Changed
270
+ - **The loft path is gone.** Its design could never deliver a sharp start: the
271
+ sweep merged its discrete scaled copies by pre-blurring the silhouette, so the
272
+ coverage was soft from the contact by construction, and every patch on top of
273
+ that (distance clamps, contact bands, protection gates) traded one artifact
274
+ for another. The column model needs none of it.
275
+ - **The unified column model.** The silhouette is rotated so the throw is +X
276
+ and extruded by a float max-smear of the ANTI-ALIASED alpha (the flank carries
277
+ the silhouette's own AA). Every pixel has an exact progression t, and all
278
+ properties are functions of it:
279
+ - alpha a(t): fade is a 2-stop alpha gradient 1 -> 0; the colour-gradient
280
+ alphas are a 2-stop a0 -> a1;
281
+ - colour c(t): the 2-stop colour gradient, applied in page frame;
282
+ - blur r(t): 0 at the contact, growing linearly (or by the gamma curve) --
283
+ sharp at the root where the alpha originates, progressively blurring;
284
+ - cross-section s(t): TAPER widens/narrows THE END. The scale driver is the
285
+ per-column progression measured from the last object column, constant
286
+ across the perpendicular: s = 1 over the whole object (the silhouette is
287
+ never stretched, the contact never moves) and the cross-section changes
288
+ only past the object, reaching `taper` at the tip. The height field t is
289
+ resampled together with the coverage so the widened/narrowed flank keeps
290
+ its true t for fade, colour and blur.
291
+ The far end terminates in the (scaled) silhouette shape itself -- no stroke,
292
+ no outline; with fade it dissolves.
293
+ - The whole pipeline stays in float until the final compose (no 8-bit
294
+ quantisation stair-steps / checkerboarding), and the back-rotation bleed is
295
+ trimmed by a validity mask.
296
+ - Cast remains standalone (light elevation + disk + height only) per .54.
297
+
298
+ ### Validation
299
+ - Sharp contact verified at the corner for taper 0.5 / 1.0 / 1.6 (full-opacity
300
+ step at the casting edge, blur growing downstream). Detailed text silhouettes
301
+ render clean at all tapers with no tearing or detached patches. 180-combo
302
+ smoke clean. Suite 179 passed / 3 skipped.
303
+ - The GPU loft-sweep primitive in gpu.py is now unused by the renderer but kept
304
+ (the GL selftest still exercises it; 3090 selftest expectations unchanged).
305
+ Document format unchanged (FORMAT_PATCH 17).
306
+
307
+ ### Next (agreed direction)
308
+ - Photoshop-style gradient controls replacing fade: multi-stop COLOUR, ALPHA
309
+ and BLUR gradients over t. The model already consumes c(t), a(t), r(t), so
310
+ this is format fields (stop lists) + the effects-dialog gradient editor.
311
+ ## [4.2.11.54] - 2026-06-10
312
+
313
+ Long shadow rework: tapered shadows no longer blur their whole start, and cast
314
+ is now a fully standalone physical mode that soft-mode settings cannot touch.
315
+
316
+ ### Fixed
317
+ - **Tapered long shadows (taper != 1) no longer blur everything.** The loft
318
+ sweep merges its discrete steps by pre-blurring the silhouette (~the step
319
+ spacing, easily several px in soft mode), so the coverage itself was soft from
320
+ the very contact; on top of that the blur-radius field was driven by a
321
+ diffusion average that is nonzero right next to the object. Together the whole
322
+ start (and with it effectively the whole shadow) read as blurred. Now:
323
+ - the blur radius is clamped by the true geometric distance from the object
324
+ silhouette (EDT), in the body and in the outside halo alike, so blur grows
325
+ physically with distance and is exactly 0 at the contact;
326
+ - a sharp CONTACT BAND (the unblurred silhouette extruded along the throw in
327
+ page frame) replaces the pre-blurred coverage near the object, crossfaded
328
+ out by distance, so the start emerges crisp at any taper;
329
+ - tmap (nearest covering throw distance) is now honest: it is assigned at the
330
+ 50% contour of each swept copy instead of the >5% blur skirt, on both the
331
+ CPU and GPU sweep (same threshold passed to both), so shadow passing close
332
+ over the silhouette from far upstream is correctly recognised and protected
333
+ from the band replace -- previously the skirt-contaminated tmap caused a
334
+ light seam above the silhouette. Skirt pixels never sharply covered get a
335
+ geometric distance estimate for fade / colour-gradient / blur instead of a
336
+ bogus one.
337
+ - **Cast is standalone: soft settings no longer affect it.** Cast was
338
+ implemented as a parameterisation of the soft path and leaked two soft inputs:
339
+ the soft `size` acted as the penumbra whenever the light disk size was 0, and
340
+ a non-1 `taper` silently rerouted cast through the tapered loft. Cast is now
341
+ driven ONLY by the light elevation, the light disk size and the object height:
342
+ a zero light disk is a point light (perfectly sharp shadow, only edge AA),
343
+ taper is ignored, and fade / blur-curve / gamma do not apply.
344
+ - **Straight (taper 1) start polish.** The shadow column is now extruded from
345
+ the anti-aliased silhouette itself instead of a binarised mask, so the flank
346
+ carries the silhouette's own AA and meets the object without a light seam or
347
+ dithered stubs; the sub-pixel bleed of the back-rotation is trimmed with a
348
+ validity mask.
349
+
350
+ ### Validation
351
+ - Verified per mode: cast with light size 0 renders with zero soft pixels beyond
352
+ edge AA and is bit-identical for any soft `size`; cast with taper 1.6 is
353
+ bit-identical to taper 1.0; penumbra area grows with the light disk. Tapered
354
+ starts (0.5 / 1.4 / 1.5, solid and soft) emerge sharp with blur growing
355
+ downstream; no seam above the silhouette; detailed text-like silhouettes keep
356
+ sharp starts without tearing. 180-combo smoke (5 directions x 3 tapers x
357
+ solid/soft/cast x fade x colour-gradient) all clean. Suite 179 passed /
358
+ 3 skipped.
359
+ - The GPU loft sweep primitive is unchanged (the honest-tmap threshold is a
360
+ parameter passed identically to CPU and GPU); the GL selftest is
361
+ self-contained and its 3090 expectations are unchanged. Document format
362
+ unchanged (FORMAT_PATCH 17).
363
+ ## [4.2.11.53] - 2026-06-10
364
+
365
+ Long shadow: the start (the casting contact with the object) is now sharp.
366
+
367
+ ### Fixed
368
+ - **Soft long shadows no longer blur their start.** In soft / cast / tapered
369
+ modes the shadow was already feathered right where it leaves the object, when
370
+ the contact should be the sharpest part and the blur should grow with distance.
371
+ Three causes, all fixed:
372
+ - The silhouette got an unconditional 0.6 px pre-blur before the projection
373
+ rotate. For an axis-aligned throw (0/90/180/270) the rotate is lossless, so
374
+ that pre-blur is now skipped and the contact stays crisp; it is kept only for
375
+ arbitrary angles, where it actually helps the rotate.
376
+ - The straight (matte) path drove the side / leading halo blur from a
377
+ per-column *average* throw distance, which was nonzero at the contact and
378
+ bloomed the start. The blur radius is now the true per-pixel throw distance
379
+ (in-shadow) anchored to the casting-edge column (halo), which is exactly 0 at
380
+ the contact, and the exact casting line is re-zeroed after a gentle de-stair
381
+ smooth, so the start is sharp and the blur ramps up with distance.
382
+ - The tapered (loft) path pre-blurred the silhouette to merge its sweep steps,
383
+ softening the contact. The swept body is now unioned with the original sharp
384
+ silhouette so the emerging edge is crisp, and inside the covered shadow the
385
+ blur radius is clamped to the true throw distance so the diffusion can't
386
+ bloom the contact.
387
+
388
+ ### Validation
389
+ - Measured: the soft side edge at the contact went from ~3 px of feather to 0 px
390
+ (sharp), while the distance-growing blur is preserved (≈7 px at 1/8 length,
391
+ ≈17 px at 1/2). 180-combo smoke (5 directions x 3 tapers x solid/soft/cast x
392
+ fade x colour-gradient) all render cleanly; detailed silhouettes (text-like
393
+ strokes) keep sharp starts with no tearing. Suite 179 passed / 3 skipped.
394
+ - The variable-blur core and the GPU loft sweep are unchanged, so GPU parity is
395
+ untouched. Document format unchanged (FORMAT_PATCH 17).
396
+ ## [4.2.11.52] - 2026-06-10
397
+
398
+ WYSIWYG halftone: the dot pattern no longer changes with canvas zoom. Plus a
399
+ one-click local-deploy-and-run launcher.
400
+
401
+ ### Fixed
402
+ - **Halftone dots are now stable across zoom (WYSIWYG).** The dot lattice
403
+ (which cells are on, their size bucket) is decided by sampling the source over
404
+ a small per-cell window. The canvas renders at an adaptive DPI that drops when
405
+ you zoom out, which shrank that window to a few pixels and made near-threshold
406
+ dots flicker in and out from one zoom level to the next, so the dot count kept
407
+ changing. The editor now pins a minimum render DPI whenever the page carries a
408
+ halftone effect: the lattice is computed at a resolution where the cell
409
+ spacing is at least 24 px (where the count has converged) and the resulting
410
+ pixmap is simply scaled onto the scene. Measured: per-channel dot counts are
411
+ now identical at every zoom from 72 to 576 DPI, where before they drifted by
412
+ several percent. The densest dot on the page (smallest ht_dot) sets the floor,
413
+ which is capped at 600 DPI and re-clamped to the page memory cap.
414
+
415
+ ### Added
416
+ - **edof-deploy-run.bat** next to the other launchers: installs edof locally
417
+ from the package files (no PyPI) and launches the editor in one step, for
418
+ quickly testing a freshly unzipped build.
419
+
420
+ ### Notes
421
+ - The halftone renderer itself is unchanged, so CPU/GPU parity is untouched
422
+ (re-confirmed: real-pattern 18-case matrix all 0.00000/0.00). Suite 179 passed
423
+ / 3 skipped. Document format unchanged (FORMAT_PATCH 17).
424
+ ## [4.2.11.51] - 2026-06-10
425
+
426
+ Two real-world fixes surfaced while validating real-world halftone patterns:
427
+ the pattern library submenu crashed on open, and opaque (white-on-black)
428
+ pattern PNGs stamped as solid squares instead of their shape.
429
+
430
+ ### Fixed
431
+ - **Pattern library submenu no longer crashes.** Opening a halftone thumbnail
432
+ menu with a non-empty pattern library raised
433
+ `'QMenu' object has no attribute 'setIconSize'` (QMenu has no such method in
434
+ Qt6). The 28px library icon size is now set via a QProxyStyle overriding
435
+ PM_SmallIconSize, parented to the submenu. The "Load image" and "Clear"
436
+ paths were unaffected; only the populated "From library" submenu hit it.
437
+ - **Opaque white-on-black patterns now keep their shape.** A custom pattern
438
+ PNG exported as a white silhouette on an opaque black background (the most
439
+ natural export) has a flat, fully-opaque alpha channel. The stencil decoder
440
+ only fell back to luminance when alpha was fully *transparent*, so a flat
441
+ *opaque* alpha was taken as the stencil and every dot became a solid square,
442
+ losing the heart / dragon / peace shape entirely. The decoder now falls back
443
+ to luminance whenever the alpha carries no shape (negligible spread), which
444
+ covers both the transparent and the opaque-flat cases. Genuine alpha-shaped
445
+ patterns (transparent background, shape in alpha) are unchanged.
446
+
447
+ ### Validation
448
+ - Emulated-stamp CPU-vs-GPU parity re-confirmed on three real-world patterns
449
+ (heart, dragon, peace), 18-case matrix, all 0.00000 mean / 0.00 max: cmyk and
450
+ rgb, single and per-channel, random rotation, decentralization 40 + hex, and
451
+ transparency render mode. The stencil fix lives in the shared decode path, so
452
+ both CPU and GPU consume the identical stencil and parity is preserved.
453
+ - Suite 179 passed / 3 skipped. Document format unchanged (FORMAT_PATCH 17).
454
+
455
+ ### Notes
456
+ - 3090 GL selftest still expected halftone 0/0; the GL stamp shader is unchanged.
457
+ ## [4.2.11.50] - 2026-06-10
458
+
459
+ GPU halftone now covers ALL cases: custom patterns, random rotation,
460
+ decentralization. This completes the GPU effects roadmap.
461
+
462
+ ### Changed
463
+ - **The GPU halftone fast path no longer excludes anything.** Previously it
464
+ handled only the common case (built-in shapes, no random dot rotation, no
465
+ decentralization); the remaining cases ran the CPU loop. Now:
466
+ - **Custom pattern images** flow through the same size-bucket atlas as the
467
+ built-in shapes -- they were only excluded by the eligibility gate, the
468
+ stamping path is identical.
469
+ - **Random dot rotation** extends the atlas to K x 8 rotation-variant tiles;
470
+ the per-cell variant is picked by the exact same _ht_rot_idx hash the CPU
471
+ loop uses, so dot-for-dot identical output. Rotation variants are
472
+ expand=True, so the tile pitch is the largest variant, not the base size.
473
+ - **Decentralization** is a constant per-channel offset, applied to the
474
+ vectorised grid exactly as in the CPU loop.
475
+ - A 256MB atlas memory guard falls back to CPU for absurd dot sizes;
476
+ the per-call CPU fallback stays for any GPU failure, as always.
477
+
478
+ ### Validation
479
+ - Emulated-stamp parity matrix, all 0.00000 mean / 0.00 max vs the CPU loop:
480
+ random rotation (circle; diamond+hex grid), decentralization 40,
481
+ custom pattern, pattern+randrot, pattern+randrot+decentralization+hex,
482
+ and randrot in transparency render mode. Suite 179 passed / 3 skipped.
483
+ - The GL stamp shader itself is unchanged (it just receives more tiles), and
484
+ was previously confirmed exact on RTX 3090 (selftest halftone 0/0).
485
+
486
+ ### Notes
487
+ - Document format unchanged (FORMAT_PATCH still 17).
488
+ ## [4.2.11.49] - 2026-06-10
489
+
490
+ The text ribbon is present from startup (disabled until editing).
491
+
492
+ ### Changed
493
+ - **The ribbon no longer pops in on the first text edit** -- in basic mode it
494
+ used to appear only once a textbox edit started, shifting the page at that
495
+ moment. A disabled skeleton ribbon is now shown as soon as a document opens
496
+ (any mode), reserving the top band up front; starting a text edit merely
497
+ enables the controls in place, so the page never moves. Opening another
498
+ document rebuilds the skeleton.
499
+ - Internals: ribbon construction was extracted into _build_text_ribbon(); the
500
+ skeleton binds to a hidden dummy editor whose controls can never fire
501
+ (everything is disabled until a real session rebuilds the contents).
502
+
503
+ ### Notes
504
+ - Document format unchanged (FORMAT_PATCH still 17).
505
+ ## [4.2.11.48] - 2026-06-10
506
+
507
+ Copy/paste keeps source formatting; header/footer auto-shrink & auto-fill.
508
+
509
+ ### Fixed
510
+ - **Ctrl+A / Ctrl+C in a header or footer, Ctrl+V elsewhere: the text now
511
+ really keeps its attributes.** Rich copy/paste existed, but attributes the
512
+ runs INHERIT from the box's base style (typical for bands, which carry their
513
+ own font/size in the band style) round-tripped as "inherit" -- pasting into a
514
+ box with a different base style visibly changed the text. The clipboard now
515
+ resolves the identity attributes (font family, size, colour) from the source
516
+ box style at copy time, Word-style "keep source formatting", so the pasted
517
+ text looks like what was copied, in any target box.
518
+
519
+ ### Added
520
+ - **Auto-shrink and auto-fill for headers and footers**, in the page-setup
521
+ panel (one pair per band). The flags live in the persisted band style, so
522
+ pagination applies them on every page; when "different odd & even pages" is
523
+ on they apply to both template sets. Seeding preserves the band's existing
524
+ style attributes (font size etc.) when the flags are first toggled.
525
+
526
+ ### Notes
527
+ - Document format unchanged (FORMAT_PATCH still 17).
528
+ ## [4.2.11.47] - 2026-06-10
529
+
530
+ The text ribbon is persistent -- no more page jumping, in any mode.
531
+
532
+ ### Fixed
533
+ - **The page jumped because the text toolbar was destroyed and recreated on
534
+ every editing cycle** -- every sticky commit in document mode, every switch
535
+ between body and header/footer, every edit in basic mode. Each teardown
536
+ released the reserved top band and each rebuild claimed it again, shifting
537
+ the page. The ribbon is now a PERSISTENT widget: created once per document,
538
+ its contents are rebuilt in place for each session (so buttons bind to the
539
+ current editor), and ending a session merely disables the controls -- the
540
+ ribbon stays visible and the reserved band never moves. It is hidden only
541
+ when the document is replaced. Applies to document mode AND basic mode.
542
+ - The one-row/two-row decision now includes a fixed allowance for the optional
543
+ per-session buttons (page-number menu, Apply/Cancel), so the ribbon height is
544
+ identical for every session and cannot flip between edits.
545
+ - Verified across the full lifecycle (open body edit, commit, sticky re-enter,
546
+ double-click switch to header, header commit; and basic-mode box-to-box):
547
+ reserved band constant, ribbon always visible, scroll position unchanged.
548
+
549
+ ### Notes
550
+ - Document format unchanged (FORMAT_PATCH still 17).
551
+ ## [4.2.11.46] - 2026-06-10
552
+
553
+ Header/footer: persistent band style, first page number, odd/even pages.
554
+ FORMAT change: 1.0.17 (new DocumentBody fields; older files load fine).
555
+
556
+ ### Added
557
+ - **Band style persists across pages.** Setting e.g. vertical align = middle
558
+ while editing a header/footer now survives repagination and applies on every
559
+ page: the band's box-level style is stored on the document
560
+ (header_style/footer_style) and re-applied by pagination, the same way the
561
+ text template is.
562
+ - **First page number.** Page setup gained "First page number"; {page_number}
563
+ starts there and {page_count} shows the last page's number (start 5 over 8
564
+ pages renders "Strana 5 z 12" ... "Strana 12 z 12").
565
+ - **Different odd & even pages (optional).** When enabled in page setup, pages
566
+ with an EVEN page number use their own header/footer template + style.
567
+ Double-clicking a band edits the template of that page's parity, so you edit
568
+ even pages' chrome on an even page. Odd template stays untouched.
569
+
570
+ ### Fixed
571
+ - **The page no longer jumps around while editing headers/footers.** Two causes:
572
+ switching editors (body <-> band) flipped the reserved toolbar margin off and
573
+ back on, shifting the whole page; and the post-commit repagination reset the
574
+ scroll position. The margin now stays put during a direct switch and the
575
+ scroll position is preserved across the commit.
576
+
577
+ ### Format
578
+ - FORMAT_PATCH 16 -> 17: DocumentBody gains header_style, footer_style,
579
+ page_number_start, hf_odd_even, header_runs_even, footer_runs_even,
580
+ header_style_even, footer_style_even. All have backward-compatible defaults;
581
+ files saved by older versions load unchanged. Verified save/load roundtrip.
582
+ ## [4.2.11.45] - 2026-06-10
583
+
584
+ Double-click into a header/footer now works while the body is being edited.
585
+
586
+ ### Fixed
587
+ - **The real reason header/footer editing "did not work": in document mode the
588
+ body sits in near-permanent sticky inline edit, and the double-click handler
589
+ ignored ALL double-clicks while any inline editor was open.** So with the
590
+ body editor active (i.e. almost always), double-clicking a header/footer band
591
+ did nothing -- which matched exactly what was reported, and is why the
592
+ band-position fix alone (4.2.11.44, also real) was not enough. A double-click
593
+ that lands on a header/footer while an inline session is active now commits
594
+ that session (nothing typed is lost), suppresses the sticky body re-entry,
595
+ and switches the editor into the band. Verified with real event dispatch in
596
+ the sticky state: body editing -> double-click header -> editor switches
597
+ (role header), typed template persists ("HLAVICKA {page_number}" -> page
598
+ shows "HLAVICKA 1"), and the body keeps the text typed before the switch.
599
+ - The header/footer write-back path also resets the sticky-suppression flag it
600
+ previously could leak on its early return.
601
+
602
+ ### Notes
603
+ - Document format unchanged (FORMAT_PATCH still 16).
604
+ ## [4.2.11.44] - 2026-06-10
605
+
606
+ Header/footer guide band now matches the real box (double-click works).
607
+
608
+ ### Fixed
609
+ - **The header band drew at the page edge but the box lived inside the margin**,
610
+ so the dashed band you saw at the very top did not line up with the box the
611
+ double-click targets -- clicking the visible band hit nothing and editing
612
+ seemed dead. The guide band is now drawn at the box's actual geometry from
613
+ pagination (header sits just below the top margin, footer just above the
614
+ bottom margin), so the band you see IS the click target. Verified: a real
615
+ double-click on the band opens inline editing with the page-number menu.
616
+
617
+ ### Notes
618
+ - When a header/footer is enabled the body text area shifts to make room (the
619
+ body starts below the header band), so they do not overlap.
620
+ - Document format unchanged (FORMAT_PATCH still 16).
621
+ ## [4.2.11.43] - 2026-06-10
622
+
623
+ Header/footer are now visible and discoverable (editing works again).
624
+
625
+ ### Fixed
626
+ - **You could not find where to edit a header/footer.** After 4.2.11.42 made
627
+ them non-selectable as objects (correct), an enabled but empty band had no
628
+ text, no fill and no outline -- nothing on screen to aim a double-click at, so
629
+ it looked like editing was broken. Enabled header/footer bands now draw a
630
+ faint tinted rectangle with a dashed outline, and while empty show a
631
+ "Header / Footer (double-click to edit)" hint. The drawn band is exactly the
632
+ region the double-click hit-test targets, so double-clicking it opens inline
633
+ editing (where the #▾ page-number menu lives). The editing path itself was
634
+ intact; this restores the visual affordance. Single-click / hover still ignore
635
+ them (no handles), per 4.2.11.42.
636
+
637
+ ### Notes
638
+ - Document format unchanged (FORMAT_PATCH still 16).
639
+ ## [4.2.11.42] - 2026-06-10
640
+
641
+ Header/footer are no longer free objects; Fit page fixed properly.
642
+
643
+ ### Fixed
644
+ - **Header and footer boxes no longer behave like draggable objects.** They are
645
+ geometry-locked document furniture (size/position driven by page setup), so
646
+ showing a selection box with resize handles on them was wrong and ugly.
647
+ Single-click and hover now skip them entirely (no selection, no handles, no
648
+ edge dragging); they are still editable by DOUBLE-CLICK, which is the only
649
+ path that picks them. Even if one is selected via the Objects list, no
650
+ transform overlay is drawn. The document body is treated the same way.
651
+ - **Fit Page now actually places the page below the inline toolbar.** The
652
+ previous attempt reserved height in the zoom math, but the scene rect is
653
+ exactly the page so centerOn had no scroll room and the page stayed centered
654
+ under the ribbon. The toolbar band is now reserved with setViewportMargins
655
+ (and the toolbar reparented to the view frame so it sits in that band), so the
656
+ scrollable area genuinely starts below it. Verified: page top lands well below
657
+ the toolbar bottom; the margin is released when the editor closes.
658
+
659
+ ### Notes
660
+ - The page-number insert menu (#▾) appears in the inline toolbar while editing a
661
+ header or footer; double-click the band to start. Verified all six variants
662
+ present.
663
+ - Document format unchanged (FORMAT_PATCH still 16).
664
+ ## [4.2.11.41] - 2026-06-10
665
+
666
+ Document mode: page setup panel, editable header/footer, page numbering UI.
667
+
668
+ ### Added
669
+ - **Page setup properties for the document body.** Selecting the body (which
670
+ has no meaningful per-object properties) now shows a page-setup panel instead:
671
+ page size (presets A4/A5/A3/Letter portrait+landscape, or custom W/H),
672
+ margins (top/right/bottom/left), and header/footer enable + height. Changes
673
+ apply to the whole document and repaginate live.
674
+ - **Editable header & footer, directly on the page.** Double-click the
675
+ header/footer band to edit it like a normal text box. The text is shared
676
+ across all pages: edits are written back to the document's header/footer
677
+ template and every page updates. (The engine already placed and resolved
678
+ these boxes; this wires up authoring.) When editing a header/footer the
679
+ editor shows the raw template, so you see and edit the actual {page_number}
680
+ token rather than a resolved value.
681
+ - **Page-number insert menu** in the inline toolbar (header/footer only): a #▾
682
+ button offering Page number, "Page X of Y", Total pages, and left / center /
683
+ right-aligned number variants. Inserts the matching template token at the
684
+ caret; the paginator resolves it per page.
685
+
686
+ ### Notes
687
+ - All built on existing engine support (DocumentBody header/footer runs,
688
+ DocumentHeaderBox/FooterBox pagination, {page_number}/{page_count} template
689
+ resolution) -- no format change (FORMAT_PATCH still 16). Verified headless:
690
+ body shows page setup, enabling header/footer creates the boxes, multi-page
691
+ numbering resolves ("Strana 3 z 8"), and on-page header edits persist to the
692
+ template and re-resolve across pages. Suite 179/3.
693
+ ## [4.2.11.40] - 2026-06-10
694
+
695
+ Fit-page accounts for the inline text toolbar (regression from 4.2.11.39).
696
+
697
+ ### Fixed
698
+ - After the toolbar became a 2-row ribbon in 4.2.11.39, Fit Page (Ctrl+0)
699
+ still measured the full viewport height, so the page was tucked partly under
700
+ the toolbar in both modes. Fit now reserves the visible inline toolbar's
701
+ height and centres the page in the area below it. The toolbar reposition on
702
+ window resize also keeps its current height (1 or 2 rows) instead of forcing
703
+ 40px.
704
+
705
+ ### Notes
706
+ - Document format unchanged (FORMAT_PATCH still 16).
707
+ ## [4.2.11.39] - 2026-06-10
708
+
709
+ Inline text toolbar: spinboxes no longer fall off the edge in design mode.
710
+
711
+ ### Fixed
712
+ - **The size / line-spacing / letter-spacing spinboxes were missing from the
713
+ inline text toolbar in normal (design) mode.** The toolbar is a fixed-width
714
+ single row pinned to the top of the viewport; its full content needs more
715
+ width than was available, so trailing controls were silently pushed past the
716
+ right edge. Document mode has two fewer buttons (no Apply/Cancel on the
717
+ permanent body), which is why the spinboxes still squeezed in there but
718
+ disappeared in design mode.
719
+ - The toolbar now measures its content against the viewport width and packs
720
+ everything into ONE row when it fits, or wraps into TWO rows (format + font
721
+ controls on top; alignment, lists, vertical align, Apply/Cancel below) when
722
+ it does not. Verified in both modes at wide and narrow window widths: all
723
+ three spinboxes visible and inside the toolbar bounds.
724
+
725
+ ### Notes
726
+ - Document format unchanged (FORMAT_PATCH still 16).
727
+ ## [4.2.11.38] - 2026-06-10
728
+
729
+ Glyph mask cache: ~7x faster text drawing, bit-identical, self-verifying.
730
+
731
+ ### Performance
732
+ - **Text drawing now reuses cached FreeType glyph masks.** PIL's draw.text for
733
+ our plain case boils down to: rasterise the glyph (getmask2 with the
734
+ fractional position) and blend it (draw_bitmap). Only the rasterisation is
735
+ expensive and it is a pure function of (font, char, fontmode, ink, fractional
736
+ offset) -- so it is now cached, and the final blend still goes through PIL's
737
+ own draw_bitmap. The output is therefore bit-identical BY CONSTRUCTION, not
738
+ just visually similar.
739
+ - **Self-verifying:** on first use a probe string is rendered char by char both
740
+ ways (integer + fractional positions; transparent, opaque and
741
+ semi-transparent backgrounds) and the fast path enables itself ONLY if every
742
+ byte matches. On a Pillow version with different text internals it silently
743
+ falls back to plain draw.text -- the rendering can never change.
744
+ - Numbers: a full A4 text page 677 -> 396 ms cold (chars repeat within the
745
+ page) and ~104 ms with a warm cache; a document-mode keystroke re-render
746
+ drops from ~0.7 s to ~0.1 s. Verified bit-identical against 4.2.11.37 on
747
+ plain text, auto-shrink + center, glyph-scale deform, and rich multi-run
748
+ text (colours, bold/italic, underline, strikethrough). The glyph cache is
749
+ cleared together with the font cache.
750
+
751
+ ### Notes
752
+ - This closes the "editor speed vs pixel-perfect WYSIWYG" question from the
753
+ ROADMAP: no trade-off needed, the middle path delivers both.
754
+ - Document format unchanged (FORMAT_PATCH still 16).
755
+ ## [4.2.11.37] - 2026-06-10
756
+
757
+ Ctrl+Enter (hard page break) crash in document mode fixed.
758
+
759
+ ### Fixed
760
+ - **The whole app could crash on Ctrl+Enter in document mode.** Reproduced: in
761
+ PyQt6 any exception escaping a slot or key handler aborts the entire process
762
+ (qFatal), and the editor-rebuild dance around a hard page break (cancel old
763
+ inline widget, repaginate, hop, start new inline widget) had several escape
764
+ routes -- most notably a stale reference calling into the old editor widget
765
+ after its C++ side was destroyed (RuntimeError on a deleted QTimer followed by
766
+ a segfault in the reproduction).
767
+ Fixes, layered: (1) the whole hard-page-break slot now runs inside a guard
768
+ that logs the traceback (debug log + console) and leaves the editor alive
769
+ instead of killing the app; (2) the text editor's keyPressEvent has the same
770
+ top-level guard; (3) the editor's _invalidate / cursor-blink slots tolerate a
771
+ destroyed underlying widget (no more RuntimeError -> abort); (4)
772
+ _cancel_inline stops the widget's render-debounce and blink timers before the
773
+ deferred deletion so a queued timeout cannot land on a half-destroyed widget.
774
+ The previously-crashing scenario (type until overflow, Ctrl+Enter mid-text,
775
+ hop, second Ctrl+Enter, plus deliberate stale-reference abuse) now completes
776
+ cleanly: 4 pages, editor alive.
777
+
778
+ ### Notes
779
+ - If a page break ever fails now, it logs instead of crashing -- check the
780
+ debug log via Help if Ctrl+Enter seems to do nothing.
781
+ - Document format unchanged (FORMAT_PATCH still 16).
782
+ ## [4.2.11.36] - 2026-06-10
783
+
784
+ Text drawing: per-run context hoisted out of the per-character loop.
785
+
786
+ ### Performance
787
+ - render_layout_onto resolved the run style, looked up the font and unpacked
788
+ the colour for EVERY character (thousands of times on a document page). All
789
+ of that only depends on the run, so it is now computed once per run and
790
+ reused; the draw calls themselves are unchanged. Output verified
791
+ bit-identical; a full A4 text page is ~7% faster (~725 -> ~677 ms). The
792
+ remaining cost is FreeType glyph rasterisation itself.
793
+
794
+ ### Notes
795
+ - An exact glyph-raster cache was investigated and PROVEN infeasible: PIL
796
+ renders glyphs subpixel-positioned (float vs floored coordinates differ) and
797
+ mask-paste compositing is bit-exact on opaque backgrounds but NOT on
798
+ transparent ones. So a glyph cache can be visually identical but not
799
+ bit-identical; it is NOT enabled anywhere. See ROADMAP for the open decision
800
+ (editor-only glyph cache vs strict WYSIWYG parity).
801
+ - GPU parity fully confirmed on hardware (RTX 3090) as of 4.2.11.35: blur
802
+ 0.35/4 (expected), CA 0/0, loft 0.001/1.7 + tmap rim pixels, halftone 0/0,
803
+ variable box blur 0/0.
804
+ - Document format unchanged (FORMAT_PATCH still 16).
805
+ ## [4.2.11.35] - 2026-06-10
806
+
807
+ Halftone GPU parity: the max-255 outliers are gone (exact bucket parity).
808
+
809
+ ### Fixed
810
+ - **Halftone GPU self-test showed mean ~0.02 but max 255**: a few dots came out
811
+ one size bucket different. Root cause was NOT the GPU: the instance builder
812
+ sampled cell values via a cumsum (sequential float32 summation) while the CPU
813
+ loop uses window .sum() (pairwise float32). The different rounding order
814
+ shifted val by ~1e-6, which at an exact .5 boundary flipped
815
+ round(val * 19) into the neighbouring size bucket - a whole dot one step
816
+ bigger/smaller (hence max 255). The GPU instance builder now samples cell
817
+ values with the CPU's exact expressions per cell (identical summation order),
818
+ so buckets match bit for bit. Verified: all shape/mode/grid/render-mode
819
+ combinations now diff 0.00000 / 0.00, including transparency mode.
820
+ - The stamping shader also lost its last numeric transforms: the stencil atlas
821
+ uploads as float32 (no more u8 quantisation) and sampling is texelFetch with
822
+ integer coordinates derived from gl_FragCoord (no interpolated UVs), so the
823
+ fragment path is fetch * ad + MAX blend, nothing else.
824
+
825
+ ### Notes
826
+ - Expected on-hardware self-test after this: halftone 0 / 0 (like the variable
827
+ box blur). Loft tmap max ~0.2 on the 0..1 scale remains expected: a handful of
828
+ rim pixels at the coverage threshold pick a neighbouring sweep step due to
829
+ bilinear rounding; the radius smoothing absorbs it (mean 0).
830
+ - Document format unchanged (FORMAT_PATCH still 16).
831
+ ## [4.2.11.34] - 2026-06-10
832
+
833
+ GPU self-test dialog now reports ALL parity sections.
834
+
835
+ ### Fixed
836
+ - **The GPU self-test dialog only displayed the Gaussian blur and chromatic
837
+ aberration results**, silently discarding the loft-sweep, halftone and
838
+ variable-blur parity metrics added in 4.2.11.28-.32 (the engine computed
839
+ them; the dialog hardcoded two sections). The report is now built from
840
+ everything the engine returns: blur, CA, loft coverage, loft distance map
841
+ (0..1 scale), halftone stamping, variable box blur, any per-section errors
842
+ (e.g. loft section FAILED: ...), and any future *_diff metric is listed
843
+ generically so the dialog cannot go stale again. Expected-noise notes are
844
+ shown where a tiny diff is normal (weight-summation order; sequential cumsum
845
+ vs parallel scan).
846
+
847
+ ### Notes
848
+ - Engine self-test itself unchanged. Document format unchanged (FORMAT_PATCH 16).
849
+ ## [4.2.11.33] - 2026-06-10
850
+
851
+ Shortcuts fixed (Ctrl+S et al.) + document mode / textboxes much faster.
852
+
853
+ ### Fixed
854
+ - **Keyboard shortcuts work again.** Ctrl+S, Ctrl+N, Ctrl+O, Ctrl+Z, Ctrl+Y,
855
+ Ctrl+D, Ctrl+=, Ctrl+-, Ctrl+0 were registered on BOTH the toolbar action and
856
+ the menu action. Qt treats a key bound to two actions as ambiguous and fires
857
+ NEITHER, so those shortcuts silently did nothing. There is now a single
858
+ window-wide shortcut registry: menu actions register first (menus display the
859
+ key natively), toolbar actions show the key in the tooltip and register only
860
+ keys the menu did not claim. Verified: every shortcut registered exactly once.
861
+
862
+ ### Performance
863
+ - **Text layout measurement cache.** Word width (getbbox) and font ascender
864
+ measurements are pure functions of (font, text) but were recomputed for every
865
+ word on every layout pass: every keystroke in the inline editor, every
866
+ fitting-scale probe (auto-shrink runs a binary search of full layouts), every
867
+ repagination in document mode, every canvas textbox render. They are now
868
+ cached centrally (strong font refs pin ids; caches cleared with the font
869
+ cache). A full A4 page of text renders ~1476ms -> ~830ms, and the layout part
870
+ of a keystroke in document mode drops to near zero. Output verified
871
+ bit-identical (plain text, auto-shrink + center, glyph-scale deform).
872
+
873
+ ### Notes
874
+ - Vector rasterization profiled: ~1.8ms/shape, ~117ms for a full-page ellipse at
875
+ 300 DPI. PIL is fast enough; GPU rasterization is deprioritized (same verdict
876
+ as image resize). Remaining typing cost in document mode is the PIL glyph
877
+ DRAW of the full page (~0.8s); a draw-side cache is the next candidate but
878
+ needs care to keep pixel-perfect parity, so it ships separately.
879
+ - Document format unchanged (FORMAT_PATCH still 16).
880
+ ## [4.2.11.32] - 2026-06-10
881
+
882
+ Long-shadow variable blur: big CPU speedup (bit-identical) + full GPU path.
883
+
884
+ ### Performance
885
+ - **CPU: the variable box blur gather now uses flat take() indices** instead of
886
+ 2D fancy indexing. Bit-identical output (verified render-vs-render against
887
+ 4.2.11.31), about 1.9x faster on the gather; a large straight shadow went
888
+ ~4.9s -> ~1.8s end to end on CPU. Both the straight and the loft path share one
889
+ helper now.
890
+ - **GPU: the whole variable blur runs on the GPU when enabled.** Each of the 4
891
+ box passes builds an inclusive 2D prefix sum by Hillis-Steele scans (log2 W
892
+ horizontal + log2 H vertical ping-pong draws) and one gather pass mirrors the
893
+ CPU clamped-window box average exactly (same y1/y2/x1/x2 clamping and area).
894
+ This was the last big CPU stage of the long shadow; with the loft sweep
895
+ (4.2.11.28) the heavy parts of the effect are now GPU-resident.
896
+ - Parity self-test added (vb_mean_diff / vb_max_diff): float32 addition order
897
+ differs between a sequential cumsum and a parallel scan, so a tiny diff (mean
898
+ well under 0.1 grey level) is expected and invisible; the structure was
899
+ validated exactly (1e-9 in float64) against the CPU SAT.
900
+
901
+ ### Notes
902
+ - Opt-in with per-call CPU fallback as always; CPU output is bit-identical to
903
+ 4.2.11.31. Document format unchanged (FORMAT_PATCH still 16).
904
+ ## [4.2.11.31] - 2026-06-09
905
+
906
+ Drag preview no longer shifts the layout; only the dragged object pixelates.
907
+
908
+ ### Fixed
909
+ - **During a drag/resize the static elements stay put.** The low-res interaction
910
+ preview used to drop the WHOLE page (background included) to 0.6x DPI, so every
911
+ element re-rounded to the coarse grid and the layout visibly shifted, then
912
+ snapped back on release. Now the page and all static objects render at full DPI
913
+ (identical positions, no jump) and only the dragged object renders at reduced
914
+ resolution and is upscaled NEAREST, so just that one object pixelates for
915
+ responsiveness while everything else is rock-steady.
916
+ - render_page_active gained an active_scale parameter; the editor keeps full DPI
917
+ during interaction and passes active_scale=0.6 for the active object. The old
918
+ whole-page low-DPI preview remains only as a fallback when there is no single
919
+ active object (e.g. dirty-region disabled).
920
+
921
+ ### Notes
922
+ - The dragged object's position stays full-DPI exact (within ~1px of the final
923
+ raster); it snaps to full quality on release as before.
924
+ - Document format unchanged (FORMAT_PATCH still 16).
925
+ ## [4.2.11.30] - 2026-06-09
926
+
927
+ Long shadow: ~2x faster by right-sizing the work buffer (no visual change).
928
+
929
+ ### Performance
930
+ - **The straight (taper 100%) long shadow renders about twice as fast.** Its soft
931
+ blur is a box that reaches ~size, but the work buffer was padded by the generic
932
+ size*3 used for 3-sigma Gaussian effects (glow, drop shadow). Long shadow now
933
+ pads by size*1.3 + length (+ taper widening), shrinking the buffer area a lot.
934
+ Output is identical (verified across throw angles, no clipping). A 28mm-blur
935
+ shadow on a large shape went ~4.5s -> ~2.4s; 14mm ~2.7s -> ~1.2s.
936
+
937
+ ### Notes
938
+ - The remaining cost is the per-pixel variable box blur itself. Making that
939
+ GPU-fast is the next step (either an exact summed-area-table scan shader, or a
940
+ GPU Gaussian-pyramid variable blur).
941
+ - Document format unchanged (FORMAT_PATCH still 16).
942
+ ## [4.2.11.29] - 2026-06-09
943
+
944
+ GPU acceleration for the halftone screen (common path), faithful to the CPU.
945
+
946
+ ### Added
947
+ - **The halftone dot stamping now runs on the GPU when enabled**, for the common
948
+ path (built-in shapes, RGB/CMYK, hex or square grid, screen rotation, size and
949
+ transparency modes). The per-cell grid loop is vectorised into instance data
950
+ (position, size bucket, value) computed exactly as the CPU does, then the dots
951
+ are stamped by GPU instanced quads sampling a bucket-stencil atlas with MAX
952
+ blend, at the same integer positions as the CPU _stamp_max. Channel compositing
953
+ (RGB add / CMYK multiply / key over) stays on CPU, unchanged.
954
+ - Halftone parity self-test (CPU loop vs GPU): reports mean/max diff, alongside the
955
+ blur, CA and loft checks.
956
+
957
+ ### Notes
958
+ - Opt-in and safe: custom pattern dots, random dot rotation and decentralization
959
+ still use the tested CPU loop, as does any case when the GPU is unavailable or a
960
+ step fails. The CPU path is byte-identical to before.
961
+ - The full GPU pipeline except the GL shader itself was validated bit-exact here
962
+ (instances, atlas, integer placement, MAX stamping == CPU, diff 0); the GL stamp
963
+ is validated by the on-hardware self-test, same conventions as the loft sweep.
964
+ - Document format unchanged (FORMAT_PATCH still 16).
965
+ ## [4.2.11.28] - 2026-06-09
966
+
967
+ GPU acceleration for the long-shadow taper loft sweep (the slow part).
968
+
969
+ ### Added
970
+ - **The tapered long-shadow loft sweep now runs on the GPU when enabled** (the
971
+ per-step scale+translate sweep that dominated CPU cost, ~0.3s soft / ~1.5s solid
972
+ for a large shape). It is one GPU job: N steps composite a uniformly scaled,
973
+ throw-translated copy of the silhouette into a coverage target (MAX blend) and a
974
+ nearest-distance tmap target (MIN blend), mirroring the CPU sweep step for step.
975
+ The same uv->image convention as the chromatic-aberration pass keeps the throw
976
+ direction correct. The post step (variable blur, fade, colour) stays on CPU.
977
+ - Parity self-test for the loft sweep (asymmetric silhouette + diagonal throw):
978
+ reports cov/tmap mean and max diff vs the CPU sweep, alongside the blur and CA
979
+ parity checks.
980
+
981
+ ### Notes
982
+ - Opt-in and safe: if the GPU is unavailable or any step fails, the renderer falls
983
+ back to the identical CPU sweep, so output is always produced. taper == 100% is
984
+ unchanged. The GPU path is validated by the on-hardware self-test (no GL context
985
+ in CI); the CPU path and the sweep math were validated here.
986
+ - Document format unchanged (FORMAT_PATCH still 16).
987
+ ## [4.2.11.27] - 2026-06-09
988
+
989
+ Long shadow taper rebuilt as a true LOFT (morph), per the per-point model.
990
+
991
+ ### Changed
992
+ - **Taper (!= 100%) is now a loft / morph, not a perpendicular squash.** The
993
+ cross-section goes gradually from the object silhouette at the contact (scale 1)
994
+ to a UNIFORMLY scaled copy of the silhouette at the far end (scale = taper),
995
+ swept along the throw. So the far end is a shrunk or enlarged *image* of the
996
+ object, with the right aspect ratio, instead of a perpendicular squash.
997
+ - At length 0 the taper is just the scaled copy in the object's shape (plus rays
998
+ + blur), concentric; no more square clipping or splatting.
999
+ - Widening fans out gradually; narrowing converges gradually; the contact stays
1000
+ glued to the object at any size.
1001
+ - Taper == 100% is unchanged (the fast straight-extrusion path).
1002
+
1003
+ ### Notes
1004
+ - The loft is a forward sweep, so it costs more than the straight path: roughly
1005
+ 0.3s soft / ~1.5s solid for a large shape on CPU (the editor drag preview uses
1006
+ reduced DPI). A later GPU pass can do the sweep in parallel.
1007
+ - Document format unchanged (FORMAT_PATCH still 16).
1008
+ ## [4.2.11.26] - 2026-06-09
1009
+
1010
+ Long shadow taper: the casting contact no longer shifts sideways (the real taper bug).
1011
+
1012
+ ### Fixed
1013
+ - **Taper shifted the start of the shadow perpendicular to the throw, worse on
1014
+ big objects.** The taper scale was per-column, but a column of a large object
1015
+ mixes object rows (distance 0) and shadow rows (distance > 0), so the column
1016
+ average was non-zero right at the contact: the scale came out != 1 there and
1017
+ pushed the contact sideways, proportionally to the distance from the object
1018
+ centre (hence very visible on big objects). The taper scale is now taken
1019
+ per-pixel from the real distance, so it is exactly 1 at the object (the contact
1020
+ stays glued to the edge at any object size) and only grows into the shadow.
1021
+ Per-pixel distance is also smooth, so detailed shapes / text still do not tear.
1022
+
1023
+ ### Notes
1024
+ - Document format unchanged (FORMAT_PATCH still 16).
1025
+ ## [4.2.11.25] - 2026-06-09
1026
+
1027
+ Long shadow taper: widening taper no longer clips, and the taper warp no longer
1028
+ smears edge content.
1029
+
1030
+ ### Fixed
1031
+ - **Widening taper (>100%) was cut off by a straight edge.** The work buffer was
1032
+ cropped to the object extent plus the blur reach, but a widening taper makes the
1033
+ shadow wider than the object, so the fanned part was clipped. The perpendicular
1034
+ crop now also accounts for the taper widening.
1035
+ - **Taper resample no longer smears edge rows.** Where the taper sampling fell
1036
+ outside the buffer it was clamped to the edge row (smearing whatever was there);
1037
+ it now reads as empty, which is correct.
1038
+
1039
+ ### Notes
1040
+ - A single text shadow tapers cleanly; the busy pattern in a dense grid of text is
1041
+ the expected overlap of many faded shadows, not a per-shadow artifact.
1042
+ - Document format unchanged (FORMAT_PATCH still 16).
1043
+ ## [4.2.11.24] - 2026-06-09
1044
+
1045
+ Long shadow: taper no longer tears detailed shapes, and "cast" mode is now in the UI.
1046
+
1047
+ ### Fixed
1048
+ - **Taper no longer tears text / detailed silhouettes.** The per-column taper
1049
+ scale was read from a distance that jumped column-to-column wherever the set of
1050
+ covered rows changed (e.g. between glyphs), so neighbouring columns were scaled
1051
+ by different amounts and sheared the shadow into streaks. The per-column
1052
+ distance is now smoothed along the axis, so the taper scale varies gradually and
1053
+ the shadow tapers as one coherent shape (the convergence toward the centre line
1054
+ is kept; the tearing is gone). Solid shapes are unchanged.
1055
+
1056
+ ### Added
1057
+ - **"cast" mode and its light controls are now selectable in the effect panel**
1058
+ (Mode: solid / soft / cast; plus Light elevation and Light disk, enabled in
1059
+ cast mode). Previously the cast mode existed only in the engine.
1060
+
1061
+ ### Notes
1062
+ - Document format unchanged (FORMAT_PATCH still 16).
1063
+ ## [4.2.11.23] - 2026-06-09
1064
+
1065
+ Long shadow: sharp casting edge at any angle, plus a new physical "cast" mode.
1066
+
1067
+ ### Fixed
1068
+ - **The casting edge now stays sharp along its whole length, at any edge angle.**
1069
+ The blur radius was per-column (one value across the whole perpendicular), so
1070
+ on an edge slanted to the throw the column average was non-zero right at the
1071
+ edge and softened it more the further along / the more slanted it was. The
1072
+ radius is now taken per-pixel from the real distance to the casting edge, so it
1073
+ is zero exactly at the edge everywhere; the side bleed still uses the column
1074
+ value so there is no border ridge.
1075
+
1076
+ ### Added
1077
+ - **"cast" mode: a physically-modelled shadow** (ls_mode = 'cast'), as an
1078
+ alternative to the stylistic 'soft' long shadow. A light at elevation
1079
+ `ls_light_angle` (1..90 deg, lower = longer shadow, length = height * cot(angle)
1080
+ using ls_length as the object height) shines on the object; the light disk
1081
+ `ls_light_size` is the penumbra, so edges soften with distance while the umbra
1082
+ stays solid (no length fade) and the contact stays sharp. Switchable so the two
1083
+ looks can be compared.
1084
+
1085
+ ### Notes
1086
+ - New serialised fields `ls_light_angle`, `ls_light_size`; document format bumped
1087
+ to .16. Backward/forward compatible (defaults, unknown keys ignored).
1088
+ ## [4.2.11.22] - 2026-06-09
1089
+
1090
+ Long shadow soft mode: one coherent field instead of two effects fighting. Fixes
1091
+ the halo and the chopped-off tip.
1092
+
1093
+ ### Fixed
1094
+ - **Halo / "two effects that don't cooperate" is gone.** The shadow was built by
1095
+ blurring the silhouette and then SEPARATELY multiplying a fade, with the fade
1096
+ extrapolated into the blur halo, the mismatch read as a faint offset echo. Now
1097
+ the fade (and the gradient alpha) are baked into a SINGLE sharp alpha first, and
1098
+ that one real, already-faded shadow is blurred once. No extrapolation, no
1099
+ post-multiply, no halo. The blur softens the actual shadow as one thing.
1100
+ - **Chopped-off far end is gone.** The work buffer was cropped at the shadow
1101
+ length, so the blurred, rounded tip (which reaches ~`size` beyond the end) was
1102
+ clipped into a straight cut. The crop now extends past the end by the blur reach,
1103
+ so the tip dissolves smoothly.
1104
+
1105
+ ### Notes
1106
+ - Document format unchanged (FORMAT_PATCH still 15). CPU render of a full scene
1107
+ stays ~1s; the SAT blur keeps cost independent of the blur size.
1108
+ ## [4.2.11.21] - 2026-06-09
1109
+
1110
+ Long shadow: fixed the border around the shadow and made taper use the good blur.
1111
+
1112
+ ### Fixed
1113
+ - **Border / ridge around the soft shadow at taper = 1.** Two causes: the blur
1114
+ radius was taken per-pixel from the extended distance field, so it varied
1115
+ across the side edges and the box average left a faint ridge; and the 4-pass
1116
+ box reached well past the cropped work buffer, so its tail was clipped into a
1117
+ faint outline. The radius is now per-COLUMN (constant across the perpendicular,
1118
+ no ridge), the soft edge reaches exactly `size` (4 passes of size/4) and the
1119
+ crop margin contains it (no clipped tail).
1120
+ - **Changing taper no longer smears the blur across the whole shadow.** Taper was
1121
+ still going through the old per-step path, which applied one uniform blur and
1122
+ lost the sharp start. Taper is now folded into the main pipeline as a
1123
+ per-column vertical resample of the column, so any taper keeps the sharp
1124
+ casting edge and the same isotropic distance-driven blur (sharp start, soft
1125
+ growing tail). The old taper path is gone.
1126
+
1127
+ ### Notes
1128
+ - Document format unchanged (FORMAT_PATCH still 15).
1129
+ ## [4.2.11.20] - 2026-06-09
1130
+
1131
+ Long shadow soft blur rebuilt for quality: a true isotropic variable Gaussian.
1132
+
1133
+ ### Changed
1134
+ - The blur is now a genuine isotropic, spatially-varying Gaussian: every pixel is
1135
+ blurred by a radius taken from its distance along the shadow, in all directions.
1136
+ This replaces the perpendicular-only, per-column blur, which produced a hard
1137
+ core against a softer edge and could band column-wise. The shadow now softens
1138
+ smoothly in every direction, the far tip rounds off naturally, and there is no
1139
+ core/halo seam, no banding, and no detached double-shadow.
1140
+ - Implemented with a 2D Summed-Area Table and a per-pixel radius (4 box passes ~
1141
+ a high-quality Gaussian); SAT keeps each pass independent of the blur radius.
1142
+
1143
+ ### Notes
1144
+ - Quality favoured over speed here: a full multi-object scene renders in ~1.3s at
1145
+ 150 dpi. The drag preview renders at reduced DPI and only the moved object, so
1146
+ it stays responsive; a faster preview path can come with the GPU port.
1147
+ - Document format unchanged (FORMAT_PATCH still 15).
1148
+ ## [4.2.11.19] - 2026-06-09
1149
+
1150
+ Long shadow perpendicular blur rewritten as a Summed-Area-Table (prefix-sum) box,
1151
+ so its cost no longer depends on the blur radius.
1152
+
1153
+ ### Performance
1154
+ - The per-column variable blur now builds a 1D prefix sum along the column once
1155
+ per pass, then reads each pixel's window as two lookups (SAT[y+r+1]-SAT[y-r]),
1156
+ dividing by the true clamped window count (edge-correct, no dark rim). Three
1157
+ passes approximate a Gaussian. Cost is independent of the blur size: a 4mm and
1158
+ an 80mm blur now cost about the same, so the drag preview stays smooth even
1159
+ with a very large soft shadow. Visual output is unchanged.
1160
+
1161
+ ### Notes
1162
+ - Document format unchanged (FORMAT_PATCH still 15).
1163
+ - Next (GPU): the same pipeline as moderngl passes (log-step silhouette + SAT
1164
+ blur + gradient pass) with a parity self-test against this CPU path.
1165
+ ## [4.2.11.18] - 2026-06-09
1166
+
1167
+ Long shadow refactored to a 3D-column model and gained a colour + alpha gradient
1168
+ along its length.
1169
+
1170
+ ### Changed
1171
+ - The soft long shadow is now built as an explicit pipeline that treats the
1172
+ silhouette as a prism extruded along the throw: (1) project (rotate the throw
1173
+ to one axis), (2) extrude into the column with a height field t (0 at the
1174
+ casting edge, 1 at the tip), (3) per-height perpendicular blur that grows with
1175
+ t, (4) per-height colour and alpha, (5) project back. This is the same maths as
1176
+ before, but the structure now matches how the effect actually behaves and makes
1177
+ per-length properties first-class.
1178
+
1179
+ ### Added
1180
+ - **Colour + alpha gradient along the shadow** (`ls_color_grad`). When enabled,
1181
+ the shadow runs from `color` at the object to `color2` at the tip, with the
1182
+ alpha interpolating between the two colours' alphas. Output is RGBA. When
1183
+ disabled, behaviour is unchanged (flat tint with the usual fade).
1184
+
1185
+ ### Notes
1186
+ - New serialised field `ls_color_grad`; document format bumped to .15. Old files
1187
+ load unchanged (gradient defaults off); files written now still load in older
1188
+ builds (the unknown field is ignored).
1189
+ ## [4.2.11.17] - 2026-06-09
1190
+
1191
+ (Numbering: resuming the 4.2.11.x build sequence after 4.2.11.16; the 4.2.12 and
1192
+ 4.2.13 tags were a mis-numbered detour. Their content is folded in here.)
1193
+
1194
+ Long shadow soft mode: removed the detached "double shadow" halo, the blur now
1195
+ widens the shadow smoothly as it travels.
1196
+
1197
+ ### Fixed
1198
+ - **Detached halo / double-shadow look is gone.** It came from blending discrete
1199
+ pre-blurred levels (a sharp copy plus a wide copy superimposed). Each column is
1200
+ now blurred by its own continuous radius (a true variable blur), so the shadow
1201
+ goes from sharp at the object to a smooth widening soft tail with no seam.
1202
+ - Carried over from the 4.2.12/4.2.13 work: anisotropic perpendicular blur (the
1203
+ shadow fans out with distance instead of only softening in place), the earlier
1204
+ edge outline removed, and the rotated work buffer cropped to the shadow region
1205
+ so a full-scene render is well under a second and the drag preview is smooth.
1206
+
1207
+ ### Notes
1208
+ - Document format unchanged (FORMAT_PATCH still 14).
1209
+ ## [4.2.13] - 2026-06-09
1210
+
1211
+ Long shadow soft mode: the blur now widens the shadow as it travels, the hard
1212
+ outline around the shadow is gone, and it renders several times faster.
1213
+
1214
+ ### Fixed
1215
+ - **The shadow now fans out / widens with distance.** The blur is applied
1216
+ perpendicular to the throw with a radius that grows along it (anisotropic),
1217
+ instead of an isotropic blur that only softened the edge in place without ever
1218
+ making the shadow wider. Linear and curve modes now visibly spread.
1219
+ - **Removed the outline / contour around the shadow.** It was edge ringing from
1220
+ blending isotropically pre-blurred copies of the hard silhouette. The
1221
+ perpendicular blur keeps a constant radius within each cross-section, so there
1222
+ is no ridge along the edges.
1223
+
1224
+ ### Performance
1225
+ - The rotated work buffer is cropped to the shadow region (it was mostly empty),
1226
+ cutting a full-scene render from a few seconds to well under one. At the reduced
1227
+ DPI used during a drag, and re-rendering only the dragged object, the live
1228
+ preview is now smooth.
1229
+
1230
+ ### Notes
1231
+ - Document format unchanged (FORMAT_PATCH still 14).
1232
+ ## [4.2.12] - 2026-06-09
1233
+
1234
+ Long shadow soft mode rewritten from the ground up (shear-to-axis architecture),
1235
+ synthesising five independent design reviews. Sharp casting edge at every angle,
1236
+ fade to nothing, and a progressive blur that is finally visible across the body,
1237
+ all at once, plus a clean thin diagonal line for text.
1238
+
1239
+ ### Changed
1240
+ - The shadow is computed by rotating the object so the throw points +X, then
1241
+ doing everything as 1D operations along that axis:
1242
+ - **One unified distance = upstream distance** (how far a pixel is from the
1243
+ object's trailing/casting edge, against the throw), via np.maximum.accumulate.
1244
+ It is zero over the whole object (sharp casting edge at any orientation) yet
1245
+ spans the full range beyond it (blur and fade develop for any shape, even a
1246
+ line drawn along the throw). This resolves the old distance-vs-projection
1247
+ conflict with a single correct measure.
1248
+ - **Blur** = weighted blend of pre-blurred levels keyed to that distance
1249
+ (constant / linear / curve). **Fade** is a SEPARATE step applied AFTER the
1250
+ blur, so the blur shows across the dark body instead of only in the faded
1251
+ tail (that coupling was why the blur looked absent).
1252
+ - The smear is built from binary coverage, so the shadow body is fully opaque
1253
+ and fades cleanly, regardless of the source antialiasing.
1254
+ - **Thin diagonal lines no longer need the morphological close** (which rounded
1255
+ text corners): the 1D extrusion is continuous, with a tiny pre-AA so a hard 1px
1256
+ line survives the rotation. No checker, no corner rounding.
1257
+ - O(log length) instead of O(length): a long shadow now renders in about a second.
1258
+
1259
+ ### Notes
1260
+ - Taper (narrow / widen) keeps the previous per-step path for now.
1261
+ - Document format unchanged (FORMAT_PATCH still 14).
1262
+ ## [4.2.11.16] - 2026-06-09
1263
+
1264
+ Long shadow soft mode: the blur is now actually visible (it was being applied
1265
+ after the fade, so it only affected the already-faded far end).
1266
+
1267
+ ### Fixed
1268
+ - **Progressive blur was invisible.** The blur was applied AFTER the fade, so the
1269
+ dark, visible part of the shadow (near the object) stayed sharp and the blur
1270
+ only touched the far end that the fade had already dimmed to almost nothing.
1271
+ The order is now: apply the directional, distance-driven blur to the full solid
1272
+ silhouette FIRST, then fade. The whole shadow (not just its faint tail) shows
1273
+ the blur, the casting edge stays sharp at any angle, and it still fades to zero.
1274
+ ## [4.2.11.15] - 2026-06-09
1275
+
1276
+ Long shadow soft mode: sharp casting edge at every angle, fade to nothing, and
1277
+ blur that grows with distance, now all at once for any object orientation.
1278
+
1279
+ ### Changed
1280
+ - The blur and the fade are now driven by two different distance measures, which
1281
+ removes the conflict between them:
1282
+ - **Blur** is keyed to the distance a shadow pixel has travelled FROM the object
1283
+ along the throw. That distance is zero over the whole object, so the casting
1284
+ edge stays sharp at any orientation, and the blur grows smoothly outward.
1285
+ - **Fade** is keyed to the pixel's projection along the throw across the shadow.
1286
+ That spans a full range even for a line drawn along the throw direction, so
1287
+ the shadow always fades to nothing at the far end (object slant adds only a
1288
+ mild darkness gradient, never blur at the source).
1289
+ Result: sharp-at-origin (all angles) + fade-to-zero + increasing blur together.
1290
+ ## [4.2.11.14] - 2026-06-09
1291
+
1292
+ Long shadow soft mode: blur and fade now scale correctly for a line perpendicular
1293
+ to the throw (line2.edof), while keeping the whole casting edge sharp (line3.edof).
1294
+
1295
+ ### Fixed
1296
+ - **Sharp edges and almost no fade for a line perpendicular to the throw.** The
1297
+ distance field was built by a separate coarse shift loop; for a thin feature
1298
+ the coarse steps left uncovered slivers (a holey field), and smoothing then
1299
+ crushed the field toward zero, so the blur radius (size*g) and the fade (1-g)
1300
+ stayed tiny. The field is now computed inside the dense extrusion loop itself,
1301
+ so it is gap-free for any feature; only light smoothing is applied. Blur and
1302
+ fade scale smoothly with distance again, the casting edge stays sharp for any
1303
+ object orientation, and the shadow fades to nothing at the far end.
1304
+ ## [4.2.11.13] - 2026-06-09
1305
+
1306
+ Long shadow soft mode: fixed one side of a slanted object being blurred at the
1307
+ source. Verified on line3.edof (wide near-horizontal line).
1308
+
1309
+ ### Fixed
1310
+ - **A slanted/wide object was sharp on one side of the casting edge but already
1311
+ blurred at maximum on the other, right at the source.** The previous field was
1312
+ the projection along the throw, so the object's own far-projection parts (one
1313
+ end of a near-horizontal line) got a non-zero distance and were blurred at the
1314
+ object. The field is back to the distance a shadow pixel has travelled FROM the
1315
+ object along the throw (nearest generating offset), which is zero over the whole
1316
+ object -> the entire casting edge is sharp. It is normalised by its own maximum
1317
+ so the fade still reaches zero and the blur still reaches full radius even for an
1318
+ object with extent along the throw (the 4.2.11.12 fix is preserved).
1319
+ ## [4.2.11.12] - 2026-06-09
1320
+
1321
+ Long shadow soft mode: fade now reaches zero and the blur is no longer hard-edged.
1322
+ Verified on line2.edof (long throw, large blur).
1323
+
1324
+ ### Fixed
1325
+ - **Fade did not reach transparency at the far end, and the soft blur had a hard
1326
+ outer boundary.** Both came from the distance field. It used the "nearest
1327
+ generating shift" which, for an object with extent ALONG the throw (a line
1328
+ drawn in the shadow direction), stays near 0 across almost the whole shadow:
1329
+ fade = (1 - g) never approached 0, and the blur radius (size * g) stayed tiny,
1330
+ so edges were sharp. The field is now the pixel's projection onto the throw
1331
+ axis, normalised across the smear (0 at the object, 1 at the far tip). It spans
1332
+ a full 0..1 for any object orientation, so the shadow fades fully to nothing and
1333
+ the blur grows to the full radius (soft silhouette that softens with distance).
1334
+ It is also analytic and per-pixel, so it is faster than the previous loop.
1335
+ ## [4.2.11.11] - 2026-06-08
1336
+
1337
+ Long shadow finally smooth on the real test cases (thin diagonal line, circle,
1338
+ text). Verified against the actual line.edof.
1339
+
1340
+ ### Fixed
1341
+ - **Perpendicular wavy bands (ripples) in soft mode.** They came from tiling the
1342
+ shadow into discrete distance bands. Soft blur is now CONTINUOUS: a weighted
1343
+ sum of pre-blurred copies, weighted by a triangular partition of unity over the
1344
+ distance, so the blur radius varies smoothly with no banding.
1345
+ - **White halo + ghost second shadow in soft mode.** The distance field is now
1346
+ extended into the blur halo by normalized convolution and blended as a full
1347
+ weighted sum (not a 2-level pick), so the outward falloff is soft with no ring.
1348
+ - **Checker / stripes on a thin diagonal line, in solid AND soft.** The solid
1349
+ extrusion is a dense per-step smear (every 1px offset covered -> uniform, no
1350
+ sparse-coverage stripes) followed by a morphological close that fills the 1px
1351
+ diagonal gaps a 45-deg thin feature leaves, so a 1px line extrudes to a SOLID
1352
+ plane. Residual quantisation in the distance field is smoothed proportionally
1353
+ to its contour spacing so the soft blur shows no faint stripes either.
1354
+
1355
+ ### Known
1356
+ - The dense smear is O(length): ~1s at screen DPI, ~6s for a very long shadow at
1357
+ 300 dpi (idle full-quality / export render; interaction uses the low-res path).
1358
+ A faster smear (GPU / van-Herk line dilation) is a planned follow-up.
1359
+ ## [4.2.11.10] - 2026-06-08
1360
+
1361
+ Long shadow rewrite addressing the reported checker / hard start / halo issues.
1362
+
1363
+ ### Fixed
1364
+ - **Thin diagonal features checkered and solid edges were jagged** (visible even
1365
+ in solid mode, not just soft). The smear was an integer-step paste, so a 1px
1366
+ feature swept across the throw landed on a checkerboard of pixels. The smear is
1367
+ now built SUPERSAMPLED (2x) and downsampled with antialiasing, so a thin line
1368
+ extrudes to a solid plane and edges are smooth. Benefits solid and soft modes.
1369
+ - **Soft mode showed a white ring and a ghost second shadow.** The previous
1370
+ distance-field extension + 2-level interpolation produced those artifacts.
1371
+ Soft progressive blur is now done by tiling the antialiased shadow into
1372
+ overlapping distance bands (triangular partition of unity) and blurring each by
1373
+ its distance's radius, max-composited. The blur spreads outward on its own, so
1374
+ no halo; bands overlap, so no gaps; blur grows from sharp at the object to soft
1375
+ at the far end.
1376
+
1377
+ ### Known
1378
+ - The supersampled smear costs ~1.5-2s on CPU for medium/large shadows (idle
1379
+ full-quality render; interaction uses the low-res path). Enabling GPU effects
1380
+ offloads the band blurs. A faster smear (log-step / GPU) is a planned follow-up.
1381
+ ## [4.2.11.9] - 2026-06-08
1382
+
1383
+ ### Fixed
1384
+ - **Soft + linear long shadow: gaps/steps (4.2.11.8 bands) and hard edges
1385
+ (earlier).** Rewritten so the gap-free solid extrusion is kept whole and per
1386
+ pixel interpolated between full pre-blurred copies, using a distance field that
1387
+ is EXTENDED into the blur halo outside the shadow (the missing piece: outside
1388
+ pixels previously took the unblurred level, which clipped the outward falloff
1389
+ and kept edges hard). Result: gap-free, real progressive blur (sharp at the
1390
+ object, soft at the far end), no checker. Far-edge softness and low
1391
+ high-frequency content verified numerically.
1392
+
1393
+ ### Known / next
1394
+ - Very thin diagonal features can still show slight rasterisation jaggedness in
1395
+ the smear (integer-step sweep). The clean fix is a supersampled smear (render
1396
+ at 2x, downsample); planned next if needed.
1397
+ - Heavy cases (very long shadow + large blur) take ~2s on CPU for the soft path;
1398
+ enabling GPU effects offloads the blurs.
1399
+
1400
+ ## [4.2.11.8] - 2026-06-08
1401
+
1402
+ ### Fixed
1403
+ - **Soft + linear long shadow was not actually blurring** (sharp edges, and a
1404
+ checker on thin lines). It blurred the whole shadow at a few levels and blended
1405
+ per pixel by distance, so a pixel near the object picked the unblurred level and
1406
+ its far-projected parts stayed sharp. Rewritten as a true spatially-varying
1407
+ blur: the shadow is tiled into contiguous distance bands by nearest distance
1408
+ from the object (the solid extrusion is gap-free) and each band is blurred by
1409
+ the radius for its distance, then composited. Blur now grows smoothly from sharp
1410
+ at the object to soft at the far end; thin features no longer checker. Fade still
1411
+ works (per-band alpha).
1412
+
1413
+ ### Added
1414
+ - **'curve' blur mode** (engine groundwork): variable blur intensity along the
1415
+ throw via an adjustable exponent (ls_blur_gamma). A full curve editor with
1416
+ save/load is planned as a follow-up UI step. Constant and linear modes unchanged
1417
+ in meaning, now both rendered through the clean path.
1418
+
1419
+ ## [4.2.11.7] - 2026-06-08
1420
+
1421
+ ### Fixed
1422
+ - **GPU effect blur produced a sheared "woven grid" for images whose width was
1423
+ not a multiple of 4** (very visible on long shadows with GPU enabled). Single-
1424
+ channel GL texture rows can be padded to a 4-byte boundary; at non-multiple-of-4
1425
+ widths the read-back was misaligned row to row. The CPU path was always correct.
1426
+ The GPU blur now pads the buffer to a multiple of 4 (edge-replicated) and crops
1427
+ back, and sets texture/read alignment explicitly. The blur self-test used a
1428
+ 256px image so it never hit this. Workaround on older builds: turn GPU effects
1429
+ off (the CPU blur is unaffected).
1430
+
1431
+ ## [4.2.11.6] - 2026-06-08
1432
+
1433
+ Long shadow fixes (reported on text and on thin-lined artwork).
1434
+
1435
+ ### Fixed
1436
+ - **Soft-linear blur followed horizontal position, not the shadow.** The
1437
+ graduated blur gradient projected each pixel's absolute position onto the
1438
+ throw direction from a single object-centre anchor, so for a wide object (a
1439
+ line of text) the blur depended on left/right position (left sharp, right
1440
+ blurred) instead of on distance from each letter. The gradient is now a
1441
+ distance-along-throw map measured from the casting silhouette, so every part
1442
+ of the shadow ramps from its own base: sharp at the object, blurred at the
1443
+ far end, everywhere.
1444
+ - **Long shadows were truncated at 600px and broke into a comb on thin
1445
+ features.** The per-step offset used the (capped) step index directly, so any
1446
+ shadow longer than 600px stopped at 600px, and long shadows took >1px steps
1447
+ that left gaps across thin features perpendicular to the throw. The offset now
1448
+ spans the full length and the step count gives <=1px increments (continuous
1449
+ smear) up to a sanity cap.
1450
+
1451
+ ## [4.2.11.5] - 2026-06-08
1452
+
1453
+ GPU acceleration extended to chromatic aberration (the second effect on GPU).
1454
+
1455
+ ### Added
1456
+ - **GPU chromatic aberration**: per-channel tint + linear shift / radial scale in
1457
+ a single fragment shader (gpu_chromatic_aberration). Wired into the CA effect
1458
+ behind the "Use GPU for effect blur" toggle, with per-effect CPU fallback —
1459
+ verified identical to CPU (both linear and radial) when GPU is unavailable.
1460
+ - **CA added to the parity self-test**: the GPU self-test now reports chromatic
1461
+ aberration mean/max difference (vs a numpy reference) alongside the blur, and
1462
+ saves ca_ref/ca_gpu/ca_diff images.
1463
+
1464
+ ### Note
1465
+ - Export and the idle full-quality render stay on CPU. Next GPU candidate:
1466
+ halftone.
1467
+
1468
+ ## [4.2.11.4] - 2026-06-08
1469
+
1470
+ ### Fixed
1471
+ - **Text box was missing the Copy / Paste / Clear layer-effect buttons**. It
1472
+ added the Layer Effects button bare instead of via the shared FX helper; now
1473
+ it uses the same helper as every other object type.
1474
+
1475
+ ### Changed
1476
+ - **Layer-effects controls are now two separate rows for every object type**:
1477
+ the "Layer Effects" button on its own row, and Copy / Paste / Clear on a
1478
+ separate row below it (was a single combined row).
1479
+
1480
+ ## [4.2.11.3] - 2026-06-08
1481
+
1482
+ ### Changed
1483
+ - **All effect blur now routes through the GPU path** when enabled. The long
1484
+ shadow soft-LINEAR branch (its graduated multi-level blur) was still on CPU;
1485
+ it now uses the same GPU-or-CPU helper as the soft-CONSTANT branch and every
1486
+ other effect. The only remaining direct CPU Gaussian call is the fallback
1487
+ inside the helper itself. Verified: both long-shadow soft modes render and are
1488
+ identical to CPU when GPU is unavailable.
1489
+
1490
+ ## [4.2.11.2] - 2026-06-08
1491
+
1492
+ GPU blur wired into the live renderer (opt-in), after the parity self-test
1493
+ passed on real hardware (mean diff 0.35 / max 4 of 255).
1494
+
1495
+ ### Added
1496
+ - **GPU-accelerated effect blur**: drop/inner shadow, outer/inner glow and the
1497
+ bevel soften now run their Gaussian blur on the GPU when "Use GPU for effect
1498
+ blur" is on (View -> Performance / optimizations). A single long-lived GPU
1499
+ worker thread owns the GL context and processes blur jobs from a queue, so the
1500
+ context is used safely from the render worker threads.
1501
+ - **Per-effect CPU fallback**: any GPU miss (disabled, unavailable, radius over
1502
+ budget, or error) transparently uses the CPU, so output is always produced and
1503
+ matches the CPU path. Verified identical when GPU is unavailable.
1504
+ - **Status badge**: the resolution status shows "· GPU" when GPU effects are on
1505
+ and available.
1506
+ - **GPU effects toggle** in the Performance dialog (default OFF), persisted; the
1507
+ self-test button remains for validation.
1508
+
1509
+ ### Note
1510
+ - Export and the idle full-quality render are unaffected by the live wiring;
1511
+ this accelerates interactive/effect rendering. Other effects (chromatic
1512
+ aberration, halftone) remain CPU for now and will be moved over incrementally,
1513
+ each gated by its own parity check.
1514
+
1515
+ ## [4.2.11.1] - 2026-06-08
1516
+
1517
+ ### Fixed
1518
+ - **GPU blur output was vertically mirrored** vs the CPU reference. The parity
1519
+ self-test made this obvious (the diff image was a clean vertical-mirror
1520
+ pattern). An extra np.flipud in gpu_gaussian_blur_L was the culprit: the
1521
+ texture upload and framebuffer read-back orientations already cancel, so no
1522
+ flip is needed. Removed it; GPU and CPU blur should now align.
1523
+
1524
+ ## [4.2.11.0] - 2026-06-08
1525
+
1526
+ Build 5 groundwork: optional GPU acceleration scaffolding + a CPU-vs-GPU parity
1527
+ self-test. No live-render behaviour changes yet.
1528
+
1529
+ ### Added
1530
+ - **edof/engine/gpu.py**: optional moderngl-based GPU module. A lazily-created
1531
+ standalone GL context gates everything via gpu_available(); when moderngl or a
1532
+ GPU is absent, every entry point falls back (returns None) and the CPU path is
1533
+ used. Includes a two-pass separable Gaussian blur shader (gpu_gaussian_blur_L)
1534
+ for the shadow/glow blur, capped to a fixed tap budget (large radii fall back).
1535
+ - **Parity self-test**: View -> Performance / optimizations -> "Run GPU
1536
+ self-test…" (and the `edof-gpu-selftest` console script). Blurs a test mask on
1537
+ CPU (PIL) and GPU, saves cpu/gpu/diff images, and reports mean/max pixel
1538
+ difference so the shader can be validated on real hardware.
1539
+ - **Optional `gpu` extra**: `pip install moderngl` (pyproject [gpu]).
1540
+
1541
+ ### Note
1542
+ - The GPU path is NOT wired into the live renderer or export yet — that comes
1543
+ after the parity self-test confirms acceptable CPU/GPU agreement on real
1544
+ hardware. Export and the idle full-quality render will always stay on CPU.
1545
+
1546
+ ## [4.2.10.14] - 2026-06-08
1547
+
1548
+ Follow-up to the BASIC-mode undo fix.
1549
+
1550
+ ### Verified
1551
+ - Layer-effect changes (effects dialog OK, Copy / Paste / Clear effects) are
1552
+ undoable, along with every property-panel edit (fill, stroke, corners, font,
1553
+ blend, opacity, ...): they all route through the changed / objectChanged
1554
+ handler that now records a coalesced history step.
1555
+
1556
+ ### Fixed
1557
+ - **Cancelling the Layer Effects dialog left a dead no-op undo step**. The cancel
1558
+ path restores the pre-dialog state (identical to the current history top), so
1559
+ it now suppresses history recording instead of pushing a redundant snapshot.
1560
+
1561
+ ## [4.2.10.13] - 2026-06-08
1562
+
1563
+ Undo/redo now works for object edits in BASIC (free-design) mode.
1564
+
1565
+ ### Fixed
1566
+ - **Move / resize / rotate and property-panel edits were not undoable** in BASIC
1567
+ mode. These all route through the objectChanged / panel-changed handler, which
1568
+ only set the modified flag and never recorded history; only explicit ops (add,
1569
+ delete, duplicate, ...) pushed snapshots. Object edits are now coalesced into
1570
+ one history step per burst (mirroring the body-text model) and pushed, so
1571
+ Ctrl+Z / Ctrl+Y step through moves, resizes, rotations and style changes.
1572
+
1573
+ ### Details
1574
+ - A short debounce (450 ms) groups a burst of edits (e.g. a slider drag or a
1575
+ move) into a single undo step. Explicit ops flush/cancel the pending burst so
1576
+ no duplicate snapshot is recorded; undo/redo flush it first so the latest edit
1577
+ is captured. Programmatic doc swaps (New / Open / Import / history restore)
1578
+ suppress recording so they never add spurious steps.
1579
+
1580
+ ## [4.2.10.12] - 2026-06-08
1581
+
1582
+ Halftone fixes (reported on paths): dot sizing and the default screen angle.
1583
+
1584
+ ### Fixed
1585
+ - **Dot sizes were inconsistent / edge dots dropped or detached**. Each dot's
1586
+ channel value was sampled from a SINGLE pixel at the cell centre, so a centre
1587
+ landing on a partial-alpha edge made the dot jump in size or vanish. Now the
1588
+ value is AVERAGED over the cell (radius ~ cell/4), alpha-weighted, matching the
1589
+ reference mosaic generator. Solid fills now reach near-full coverage instead of
1590
+ looking washed out, and edge dots are sized consistently.
1591
+ - **Default screen-angle step was 45 deg, not 72**. The halftone effect created
1592
+ from the dialog forced ht_angle = 45 (the model default was already 72); it now
1593
+ uses 72, so the per-channel screens are spaced as intended.
1594
+
1595
+ ### Note
1596
+ - For additive RGB, dark areas legitimately produce no ink (black = no light);
1597
+ enable the extra (black) key channel to render blacks in RGB mode.
1598
+
1599
+ ## [4.2.10.11] - 2026-06-08
1600
+
1601
+ In-app control over the render optimizations, including the GPU viewport (which
1602
+ was previously forced on with no way to disable it).
1603
+
1604
+ ### Added
1605
+ - **View -> Performance / optimizations…**: a dialog to toggle each render
1606
+ optimization on/off, applied live and persisted via QSettings:
1607
+ per-object raster cache, dirty-region (active object only), adaptive render
1608
+ DPI (zoom + HiDPI), supersample-when-zoomed-out, low-res preview while
1609
+ dragging, and the GPU (OpenGL) viewport. All default ON. Turning everything
1610
+ off gives the simplest, most predictable render path for comparison.
1611
+
1612
+ ### Changed
1613
+ - The QOpenGLWidget viewport (on by default since 4.1.0) is now behind the
1614
+ gl_viewport setting and can be swapped to the plain raster viewport at runtime
1615
+ (useful if a GPU/driver shows a black viewport or artifacts).
1616
+
1617
+ ### Note
1618
+ - These flags affect the interactive render path only; the idle full-quality
1619
+ render and all exports always use the exact path. GUI smoke-tested headless;
1620
+ the GPU viewport toggle needs verifying on real hardware.
1621
+
1622
+ ## [4.2.10.10] - 2026-06-08
1623
+
1624
+ Dragging a heavy object is now cheap: the active object's raster is reused
1625
+ across pure moves instead of being re-rendered every frame.
1626
+
1627
+ ### Added
1628
+ - **Active-object translation cache**: render_page_active() now keys the active
1629
+ object's raster on a signature that EXCLUDES translation, so a pure move (only
1630
+ transform.x/y changes) re-composites the cached crop at the new pixel offset
1631
+ instead of re-rendering the object and its expensive effects (blur / glow /
1632
+ bevel / halftone / drop shadow). Rotation, flip, size and content still force a
1633
+ re-render. Normal-blend only; non-normal blends render directly.
1634
+
1635
+ ### Performance
1636
+ - ~9.5x faster while dragging an object that carries a drop shadow + halftone on
1637
+ a cluttered page, vs the per-object cache. Output matches a full render within
1638
+ 1 LSB (interactive preview only).
1639
+
1640
+ ## [4.2.10.9] - 2026-06-08
1641
+
1642
+ Build 4 (render & performance) dirty-region pass: editing one object no longer
1643
+ re-renders the whole page each frame.
1644
+
1645
+ ### Added
1646
+ - **render_page_active()**: while a single object is being edited, the static
1647
+ rest of the page is cached as two flattened layers (below / above the active
1648
+ object) keyed on a signature of every non-active object; only the active
1649
+ object is re-rendered and composited between them each frame. Falls back to a
1650
+ full render when the active object is not on the page or an above-object uses a
1651
+ non-normal blend. The editor uses this on the interactive (drag / resize /
1652
+ path-edit) render path; the idle full-quality render and export are unchanged.
1653
+
1654
+ ### Performance
1655
+ - ~2x faster interactive re-render on a 40-object page vs the existing
1656
+ per-object cache, scaling further with object count (one flatten instead of
1657
+ re-compositing every static object every frame). Pixel output matches a full
1658
+ render within 1 LSB (interactive preview only; full-quality / export use the
1659
+ exact path).
1660
+
1661
+ ## [4.2.10.8] - 2026-06-08
1662
+
1663
+ Pen / path tool fixes found via the debug log: stroke rendering, draw-mode
1664
+ handle sizing, and closed-path geometry.
1665
+
1666
+ ### Fixed
1667
+ - **Path stroke rendered as disjoint rectangles** with cracks at every vertex
1668
+ when widened. Stroke polylines are now drawn with rounded joints (and round
1669
+ end caps for wide strokes), so the stroke is continuous.
1670
+ - **Wide path stroke clipped by the bounding box**. The path buffer is padded by
1671
+ half the stroke width on every side (the bbox only covers the centreline), so
1672
+ a thick stroke is no longer cut off at the object edge.
1673
+ - **Draw-mode control points scaled with zoom**. Preview anchor/handle dots are
1674
+ now sized in screen pixels (divided by zoom, cosmetic pens), matching the
1675
+ constant on-screen size used in edit mode.
1676
+ - **Closed path did not connect with a curve, and an extra phantom anchor
1677
+ appeared**. The wrap segment's endpoint was hardcoded to (0,0) instead of the
1678
+ start point M; whenever M was not at the bbox origin the closure was mistaken
1679
+ for an extra anchor and did not curve back. The wrap now ends exactly at M and
1680
+ is emitted whenever either end is curved, with the correct outgoing/incoming
1681
+ tangents.
1682
+ - **Dragging a point on a closed path could make the last point vanish**. When M
1683
+ was dragged, the wrap-sync overwrote the last user anchor's endpoint on paths
1684
+ without a real wrap-cmd. The sync now runs only for a genuine wrap-cmd
1685
+ (endpoint matching M's previous position).
1686
+
1687
+ ## [4.2.10.7] - 2026-06-08
1688
+
1689
+ The reliable way to turn on debug logging: do it from inside the editor. No more
1690
+ dependence on how the app was launched (file association, shortcut, entry point,
1691
+ or batch quoting).
1692
+
1693
+ ### Added
1694
+ - **Help -> Debug log (curves/keys)**: checkable menu toggle that enables/disables
1695
+ detailed pen-tool + keypress logging at runtime. On enable it creates the log
1696
+ file immediately and shows the exact path, with an "Open folder" button.
1697
+ - **Help -> Open debug log location…**: opens the folder containing the log and
1698
+ shows the resolved path (works whether or not the file exists yet).
1699
+
1700
+ ### Note
1701
+ - Path: uses EDOF_DEBUG_PATH if set, otherwise edof_debug.log in the user home.
1702
+ Launching via edof-editor-debug.bat still works and pins the log next to the
1703
+ launcher; the menu is the launch-independent fallback.
1704
+
1705
+ ## [4.2.10.6] - 2026-06-08
1706
+
1707
+ Make the debug log impossible to miss: it now appears the instant the editor
1708
+ launches with EDOF_DEBUG enabled, instead of only after the first curve/key
1709
+ event. Pairs with the 4.2.10.5 launcher fix.
1710
+
1711
+ ### Added
1712
+ - **Guaranteed startup log line**: on launch with EDOF_DEBUG enabled, the editor
1713
+ writes an `editor.startup` event (with version and the resolved log path)
1714
+ immediately, creating the log file up front. Instant confirmation that
1715
+ logging is on and where the file lives.
1716
+
1717
+ ### Note
1718
+ - The .bat launchers run the INSTALLED edof package, so the 4.2.10.5/4.2.10.6
1719
+ fixes only take effect after re-installing (deploy-edof.bat -> [1]) AND using
1720
+ the new edof-editor-debug.bat.
1721
+
1722
+ ## [4.2.10.5] - 2026-06-08
1723
+
1724
+ Fix: the debug log was never written. The debug launcher set
1725
+ EDOF_DEBUG with a trailing "REM ..." comment on the same line, so on Windows
1726
+ the variable held "1 REM ..." instead of "1" and the truthy check failed,
1727
+ silently disabling logging.
1728
+
1729
+ ### Fixed
1730
+ - **edof-editor-debug.bat**: EDOF_DEBUG and EDOF_DEBUG_PATH are now each on
1731
+ their own line with no inline comment. The log is written next to the
1732
+ launcher (%~dp0edof_debug.log) as intended.
1733
+ - **debug_log._env_enabled()**: hardened so only the first whitespace token of
1734
+ EDOF_DEBUG decides truthiness (strips a stray trailing comment / quotes).
1735
+ A polluted launcher value can no longer silently disable logging.
1736
+
1737
+ # Changelog
1738
+
1739
+ All notable changes to **edof** are documented here.
1740
+ Format: Keep a Changelog (https://keepachangelog.com/en/1.0.0/)
1741
+ Versioning: SemVer (https://semver.org/)
1742
+
1743
+ ================================================================================
1744
+
1745
+ ## [4.2.10.4] - 2026-06-05
1746
+
1747
+ ### Added
1748
+ - **Detailed opt-in debug logging for curve creation and editing**, plus every
1749
+ key press/release. Each entry records the current mode and path state (point
1750
+ count, edit target, drag state) for context. Logged events include: placing
1751
+ pen points, drag-to-curve, closing a path, finishing/cancelling a drawing
1752
+ (with the created object id, command count and bbox), entering/exiting path
1753
+ edit, grabbing/dragging/releasing anchor and control-point handles (old→new
1754
+ positions + active modifiers), anchor selection, point insertion, and
1755
+ connect/disconnect. Use this to reproduce and pin down curve-tool bugs.
1756
+ - **The debug launcher (edof-editor-debug.bat) now enables the debug log**
1757
+ (EDOF_DEBUG=1), writing edof_debug.log next to the launcher. Run that launcher,
1758
+ reproduce the issue, then read/send the log.
1759
+
1760
+ ================================================================================
1761
+
1762
+ ## [4.2.10.3] - 2026-06-05
1763
+
1764
+ ### Fixed
1765
+ - **The grid, page margins and alignment guides now keep a constant on-screen
1766
+ width at any zoom** (and stay crisp on HiDPI), matching the selection-handle
1767
+ fix. They were drawn with scene-unit pens, so lines and grid dots got thick
1768
+ when zoomed in and faint when zoomed out. They now use cosmetic pens (constant
1769
+ screen width and dash); the grid also hides when the dots would be too dense
1770
+ on screen rather than judging density in scene units.
1771
+
1772
+ ================================================================================
1773
+
1774
+ ## [4.2.10.2] - 2026-06-05
1775
+
1776
+ ### Fixed
1777
+ - **Selection handles, the transform bounding box and the rotate handle now stay
1778
+ a constant on-screen size at any zoom** (and are crisp on HiDPI displays).
1779
+ They were drawn in scene units, so they grew when zooming in and shrank when
1780
+ zooming out. Outlines now use cosmetic pens (constant screen width + dash) and
1781
+ the handle squares/circles, rotate-handle distance and hit-test radius are
1782
+ divided by the view zoom, so grabbing a handle feels the same at every zoom.
1783
+
1784
+ ================================================================================
1785
+
1786
+ ## [4.2.10.1] - 2026-06-05
1787
+
1788
+ ### Changed
1789
+ - **The draw preview now matches the shape you are drawing.** Dragging out an
1790
+ ellipse shows an ellipse preview, and a line/arrow shows a live segment from
1791
+ the start point to the cursor, instead of always a dashed rectangle.
1792
+
1793
+ ### Fixed
1794
+ - **Lines now keep the direction you draw them in.** Dragging from bottom-right
1795
+ to top-left previously snapped the committed line to a top-left → bottom-right
1796
+ diagonal. The two endpoints are now taken from the actual drag.
1797
+ - **Horizontal and vertical lines can be drawn again.** The draw was cancelled
1798
+ unless the bounding box was at least 5 mm in *both* dimensions, so a perfectly
1799
+ axis-aligned line (zero height or width) was rejected. Lines now use a minimum
1800
+ *length* of 2 mm instead.
1801
+
1802
+ ================================================================================
1803
+
1804
+ ## [4.2.10.0] - 2026-06-05
1805
+
1806
+ ### Changed
1807
+ - **Maximum zoom is now 10x** (1 document pixel : 10 screen pixels, shown as
1808
+ 1000% in the status bar), up from 5x, for fine pixel-level inspection. Pixel
1809
+ edges stay crisp (nearest-neighbour) in this range.
1810
+ - **Live previews while tuning layer effects now render at reduced DPI** for
1811
+ responsiveness, then snap to full quality ~0.35s after you stop adjusting.
1812
+ This makes dragging sliders on heavy effects (halftone, blur) smooth instead
1813
+ of lagging on every tick. Reuses the same low-res-during-interaction path as
1814
+ object dragging.
1815
+
1816
+ ================================================================================
1817
+
1818
+ ## [4.2.9.9] - 2026-06-05
1819
+
1820
+ Build 4 (Render & performance) slice: anti-aliased minification.
1821
+
1822
+ ### Changed
1823
+ - **Zoomed-out pages are now supersampled for smooth minification.** Fine detail
1824
+ (halftone dots, thin strokes, small text) was aliasing / moiring when the page
1825
+ was shrunk, because it rendered at the bare on-screen resolution. The page now
1826
+ renders above that, up to the base DPI, and Qt minifies it (= anti-aliasing).
1827
+ Extreme zoom-out still scales below base so large pages stay cheap. On a
1828
+ halftone-filled ellipse this cut the error vs a high-DPI reference by ~55%.
1829
+
1830
+ ================================================================================
1831
+
1832
+ ## [4.2.9.8] - 2026-06-05
1833
+
1834
+ Build 4 (Render & performance) slice: low-resolution preview during interaction.
1835
+
1836
+ ### Changed
1837
+ - **Dragging or resizing an object now renders at a reduced DPI for
1838
+ responsiveness**, then snaps back to full quality the moment you release.
1839
+ Previously the whole page was re-rendered at full DPI on every mouse-move,
1840
+ which lagged for heavy objects (large images, halftone, blur). Geometry and
1841
+ hit-testing are unaffected (the low-res pixmap is scaled onto the base-DPI
1842
+ scene exactly like the adaptive-DPI zoom rendering).
1843
+
1844
+ ================================================================================
1845
+
1846
+ ## [4.2.9.7] - 2026-06-05
1847
+
1848
+ ### Changed
1849
+ - **Zoom limits are now content-relative instead of a fixed percentage.** Max
1850
+ zoom = 5 screen pixels per document pixel (shows as 500% in the status bar),
1851
+ and at that range the page is drawn with **crisp pixel edges** (nearest-
1852
+ neighbour once we're upscaling beyond the render-DPI cap) instead of a blur.
1853
+ Min zoom = half the fit-to-window zoom (the page fills about half the
1854
+ viewport), so you can't lose the page off-screen.
1855
+ - **Holding Alt over the page shows a magnifier cursor** (a plain loupe, no
1856
+ +/- sign) to signal that the wheel will zoom. It clears on release / when the
1857
+ pointer leaves the page.
1858
+
1859
+ ================================================================================
1860
+
1861
+ ## [4.2.9.6] - 2026-06-05
1862
+
1863
+ ### Fixed
1864
+ - **The zoom % in the status bar now updates live and is meaningful.** It was
1865
+ stuck at 100% because wheel/pinch zoom changed the canvas without notifying
1866
+ the status bar; the canvas now emits a zoom-changed signal. The displayed
1867
+ percentage is also rescaled so **100% means 1 document pixel : 1 screen
1868
+ pixel** (previously it showed the internal canvas zoom, which is relative to
1869
+ the dynamic render DPI, so a 300-DPI page looked "zoomed in" at "100%").
1870
+ - **New documents now fit the page to the window** (like Open in 4.2.9.5), so a
1871
+ new document is fully visible instead of opening zoomed in.
1872
+
1873
+ ================================================================================
1874
+
1875
+ ## [4.2.9.5] - 2026-06-05
1876
+
1877
+ ### Added
1878
+ - **Opening a document now fits the page to the window automatically** (the same
1879
+ as Fit / Ctrl+0), so you start at a sensible zoom instead of an arbitrary one.
1880
+ Deferred until the viewport is sized so the fit is correct. Only on open —
1881
+ editing, adding/removing pages and undo/redo no longer change your zoom.
1882
+
1883
+ ================================================================================
1884
+
1885
+ ## [4.2.9.4] - 2026-06-05
1886
+
1887
+ ### Changed
1888
+ - **Status bar now shows document info instead of the internal canvas DPI.** It
1889
+ reads e.g. ``300 DPI · 210×297 mm · 2480×3508 px`` (page export DPI, size in
1890
+ millimetres, and size in pixels at that DPI). The old "DPI 300 (canvas 150)"
1891
+ exposed the internal render DPI, which is now dynamic (zoom/display-aware) and
1892
+ not meaningful to the user. Updates on page switch; a tooltip notes on-screen
1893
+ sharpness is automatic and independent of this.
1894
+
1895
+ ================================================================================
1896
+
1897
+ ## [4.2.9.3] - 2026-06-05
1898
+
1899
+ Build 4 (Render & performance) slice 3: HiDPI-aware rendering.
1900
+
1901
+ ### Changed
1902
+ - **The canvas now renders at the display's device pixel ratio.** On HiDPI /
1903
+ retina screens and Windows display scaling (125-150%), the page was rendered
1904
+ at logical resolution and the OS upscaled it, looking soft. The render DPI now
1905
+ includes the device pixel ratio (clamped to 4x, with the same 24-DPI
1906
+ quantisation, 48-DPI floor and 3x / ~28 MP caps), so on-screen pixels are ~1:1
1907
+ with physical pixels: crisp text and edges on scaled displays. On a 1x display
1908
+ at ~100% behaviour is unchanged.
1909
+
1910
+ ================================================================================
1911
+
1912
+ ## [4.2.9.2] - 2026-06-05
1913
+
1914
+ ### Changed
1915
+ - **Adaptive render DPI now also applies when zooming out.** The page is
1916
+ rendered to match the effective on-screen resolution (base DPI x zoom): zoomed
1917
+ out it renders fewer pixels (faster, identical sharpness, since the screen
1918
+ can't show more than its pixels), zoomed in it renders more (crisp). DPI is
1919
+ quantised to 24-DPI steps to limit cache churn while zooming, with a 48-DPI
1920
+ floor and the 3x / ~28 MP cap; at ~100% the exact base DPI is used. As before,
1921
+ the pixmap is scaled onto the base-DPI scene, so positions never shift.
1922
+ (Rendering at the object's full DPI when the page is shown small would only
1923
+ waste pixels the display can't show, so we target the screen resolution.)
1924
+
1925
+ ================================================================================
1926
+
1927
+ ## [4.2.9.1] - 2026-06-05
1928
+
1929
+ Build 4 (Render & performance) slice 2: adaptive render DPI by zoom.
1930
+
1931
+ ### Changed
1932
+ - **Zooming in now renders crisp instead of upscaling a blurry base-DPI image.**
1933
+ When zoom > 1.25x the page is rendered at a proportionally higher DPI (capped
1934
+ at 3x and at ~28 MP) and the resulting pixmap is scaled back onto the base-DPI
1935
+ scene, so on-screen pixels are ~1:1 with rendered pixels (sharp text and
1936
+ edges). At zoom <= 1.25x behaviour is unchanged (base DPI, no oversampling).
1937
+ Crucially the scene coordinate system stays at base DPI, so object/overlay
1938
+ positions and hit-testing do not shift with zoom (the old oversampling bug
1939
+ that caused text-position jumps does not return).
1940
+
1941
+ ================================================================================
1942
+
1943
+ ## [4.2.9.0] - 2026-06-05
1944
+
1945
+ Build 4 (Render & performance, phase 1) start: per-object render cache.
1946
+
1947
+ ### Added
1948
+ - **Per-object raster cache** in the renderer. Each object's rendered result is
1949
+ cached and reused while it is unchanged, so editing one object on a busy page
1950
+ no longer re-renders every other object. Pixel-identical to the uncached path
1951
+ (verified): the cache key is a content signature of the object (geometry,
1952
+ content, effects, blend, opacity) plus DPI, page size, variables and a
1953
+ resource fingerprint; only normal-blend objects are cached (a non-normal
1954
+ blend blends against the layers beneath it, so it falls back to a direct
1955
+ render). Opt-in via ``render_page(..., use_cache=True)``; the editor canvas
1956
+ uses it, exports stay uncached for safety. New ``clear_object_cache()``.
1957
+
1958
+ ### Performance
1959
+ - Re-render of a 40-object page after changing one object: ~514 ms -> ~36 ms in
1960
+ testing; an all-unchanged re-render ~30x faster. (Editor canvas re-render.)
1961
+
1962
+ ================================================================================
1963
+
1964
+ ## [4.2.8.7] - 2026-06-05
1965
+
1966
+ ### Changed
1967
+ - **Live colour preview is now everywhere a colour dialog is used**, not just
1968
+ object fill/stroke/text. Dragging in the picker live-updates: all layer-effect
1969
+ colours (Colour Overlay, Gradient Overlay, outer/inner Glow, Satin, Stroke
1970
+ effect, Bevel highlight/shadow, Long Shadow, etc.) and the **page background**
1971
+ in Page Settings. Cancel restores the previous colour (page background is also
1972
+ restored if Page Settings itself is cancelled). The only colour control
1973
+ without live preview is the new-document custom-background swatch, which has
1974
+ no object to preview on.
1975
+
1976
+ ================================================================================
1977
+
1978
+ ## [4.2.8.6] - 2026-06-05
1979
+
1980
+ Build 3 (Colors) complete: live colour preview.
1981
+
1982
+ ### Added
1983
+ - **Live preview on the object while picking a colour.** Dragging in the SV
1984
+ square / hue / alpha strip (or editing the fields) updates the object on the
1985
+ canvas in real time, alpha included. Cancel restores the original colour; OK
1986
+ keeps the new one. Wired for text colour, fill, stroke, line stroke and QR
1987
+ colours.
1988
+
1989
+ ================================================================================
1990
+
1991
+ ## [4.2.8.5] - 2026-06-05
1992
+
1993
+ Halftone pattern UX + sensible background default.
1994
+
1995
+ ### Fixed
1996
+ - **Background no longer forces a solid fill by default.** The default is now
1997
+ *Transparent*, so the document background shows through the gaps between dots
1998
+ (previously the *Native* default painted a solid black base for RGB / white
1999
+ for CMYK, e.g. when applied to a square — not wanted as a default). *Native*
2000
+ and *Layer* remain selectable.
2001
+ - **Per-channel patterns are settable again.** After loading one image you can
2002
+ now load the others.
2003
+
2004
+ ### Changed
2005
+ - **Explicit Pattern mode selector** is back: *Built-in shape* / *One image
2006
+ (all channels)* / *Per channel (individual)*. The thumbnail slots show one
2007
+ slot for "one image", or one per channel for "per channel".
2008
+ - The **extra key channel is now optional (off by default)** as intended — turn
2009
+ it on for full-range black(RGB)/white(CMYK) dots in Transparent mode.
2010
+
2011
+ ================================================================================
2012
+
2013
+ ## [4.2.8.4] - 2026-06-05
2014
+
2015
+ ### Added / Changed
2016
+ - Eyedropper now uses the bundled **eyedropper cursor**, shows a live **loupe**
2017
+ (zoomed pixels) with a **colour swatch + hex** next to the cursor as you move,
2018
+ and **right-click (or Esc) cancels** the pick.
2019
+
2020
+ ================================================================================
2021
+
2022
+ ## [4.2.8.3] - 2026-06-05
2023
+
2024
+ ### Fixed
2025
+ - **Eyedropper now works.** The "Pick from screen" overlay is now a modal
2026
+ dialog, so it takes input precedence over the (modal) colour dialog — the
2027
+ cross cursor shows and clicking anywhere samples the pixel. It also grabs the
2028
+ screen under the cursor (multi-monitor) and maps the click through that
2029
+ screen's offset and device-pixel-ratio.
2030
+
2031
+ ================================================================================
2032
+
2033
+ ## [4.2.8.2] - 2026-06-05
2034
+
2035
+ Colour eyedropper + halftone pattern thumbnails & library.
2036
+
2037
+ ### Added
2038
+ - **Eyedropper in the colour picker.** A "Pick from screen" button grabs a
2039
+ full-screen snapshot; click anywhere (canvas, another window, anywhere) to
2040
+ sample that pixel's colour. Esc cancels.
2041
+ - **Halftone pattern thumbnails.** Each channel has its own thumbnail slot
2042
+ showing its loaded pattern. Click a slot to: Load image…, pick one From the
2043
+ library, Use a library item / this item for ALL channels, or Clear. Empty
2044
+ slots fall back to the built-in shape.
2045
+ - **Halftone pattern library.** Patterns you load are remembered across the app
2046
+ (persisted) and reusable from any slot's menu. Patterns load per channel
2047
+ separately.
2048
+
2049
+ ================================================================================
2050
+
2051
+ ## [4.2.8.1] - 2026-06-05
2052
+
2053
+ Halftone: full-range reproduction on any background.
2054
+
2055
+ ### Added
2056
+ - **Background mode** for the halftone screen: *Native* (default — the screen
2057
+ paints its own base, black for RGB / white for CMYK, so blacks and whites are
2058
+ reproduced faithfully regardless of the document background, self-contained),
2059
+ *Transparent* (dots only, gaps see-through), *Layer content* (dots over the
2060
+ original layer).
2061
+ - **Extra key channel**: adds an achromatic screen — black dots for dark areas
2062
+ in RGB, white dots for bright areas in CMYK — so the Transparent mode can also
2063
+ cover the full tonal range. Ink is Auto / White / Black. Default on.
2064
+ - **Per-channel enable**: turn individual screens (R/G/B(+key) or C/M/Y/K(+key))
2065
+ on or off.
2066
+
2067
+ ### Fixed
2068
+ - RGB omitted blacks and CMYK omitted whites unless the document background
2069
+ happened to be black / white (the original design baked them into the
2070
+ background). Native background and the extra channel now handle them.
2071
+
2072
+ ### Changed
2073
+ - File schema 4.2.13 -> 4.2.14.
2074
+ - (Still to come: pattern thumbnails + a reusable pattern library, and an
2075
+ eyedropper inside the colour dialog.)
2076
+
2077
+ ================================================================================
2078
+
2079
+ ## [4.2.8.0] - 2026-06-05
2080
+
2081
+ Build 3 (Colors) start: Photoshop-style colour picker.
2082
+
2083
+ ### Changed
2084
+ - **The colour picker is now a Photoshop-style dialog.** A saturation/value
2085
+ square plus a hue strip (and an alpha strip when alpha is enabled), with
2086
+ HSB / RGB / hex (#RRGGBB or #RRGGBBAA) fields that all stay in sync, a live
2087
+ "new vs current" swatch, and click-drag on the square/strips. Replaces the
2088
+ previous RGBA-slider dialog. All existing colour buttons use it (same
2089
+ get_color API), so it applies everywhere colours are chosen.
2090
+ - Picker number fields widened so 3-digit values (e.g. 255 / 360) and the
2091
+ 8-digit hex are no longer clipped.
2092
+
2093
+ ================================================================================
2094
+
2095
+ ## [4.2.7.20] - 2026-06-05
2096
+
2097
+ Build 2 phase 2 (slice 20): halftone background + edge-clip modes.
2098
+
2099
+ ### Fixed
2100
+ - **The halftone no longer leaves the original image showing underneath** (it
2101
+ did even with fill_opacity 0). The dot coverage is now the layer alpha, so the
2102
+ gaps between dots are transparent and, in the default "just patterns" mode,
2103
+ the layer body is not composited at all.
2104
+
2105
+ ### Added
2106
+ - **"Keep background under dots" toggle.** Default OFF = only the screened dots
2107
+ are shown, gaps transparent (body dropped). ON = dots are drawn over the
2108
+ original layer content.
2109
+ - **"Edge clip" mode** for the dots: Whole dots (no clip, default — dots stay
2110
+ whole and may extend past the source edge), Hard (clip to the source
2111
+ silhouette), Soft (feather to the source alpha).
2112
+
2113
+ ### Changed
2114
+ - File schema 4.2.12 -> 4.2.13 (new halftone fields). Older files load with
2115
+ defaults.
2116
+
2117
+ ================================================================================
2118
+
2119
+ ## [4.2.7.19] - 2026-06-05
2120
+
2121
+ Build 2 phase 2 (slice 19): halftone custom patterns, random rotation, opacity fix.
2122
+
2123
+ ### Added
2124
+ - **Custom halftone pattern images.** Pattern source can be the built-in shape,
2125
+ 1 image used for every channel, or per-channel images (3 for RGB / 4 for
2126
+ CMYK). A channel with no supplied pattern falls back to the built-in shape.
2127
+ The pattern's own alpha (transparency) is used as the dot mask.
2128
+ - **Random dot rotation** for halftone (each dot rotated by a stable
2129
+ pseudo-random angle).
2130
+
2131
+ ### Changed
2132
+ - Default halftone screen-angle step is now 72 degrees.
2133
+ - **Halftone now respects the effect Opacity** (mixes the screen with the layer
2134
+ content); previously the opacity slider was ignored on this effect.
2135
+
2136
+ ### Notes
2137
+ - Blend: the halftone effect's Blend mode blends the screen against the layer
2138
+ content; the object-level blend (Blending Options) blends the whole layer
2139
+ against what is beneath it. Both work; note that "multiply" over a white page
2140
+ is identity by definition, so it can look like nothing changed.
2141
+ - File schema 4.2.11 -> 4.2.12 (new halftone fields). Older files load with
2142
+ defaults.
2143
+
2144
+ ================================================================================
2145
+
2146
+ ## [4.2.7.18] - 2026-06-05
2147
+
2148
+ Build 2 phase 2 (slice 18): real halftone (per-channel mosaic screen) + more icons.
2149
+
2150
+ ### Changed
2151
+ - **Halftone effect fully reworked into a per-channel "mosaic" screen.** The old
2152
+ effect only did a single luminance dot/line screen ("dots and lines"). It now
2153
+ reconstructs the image from shaped dots, each colour channel screened on its
2154
+ own rotated grid: RGB channels composite additively on black, CMYK channels
2155
+ composite multiplicatively on white. New controls: colour mode (RGB/CMYK), dot
2156
+ driven by Size or Transparency, dot shape (circle / diamond / square / ring /
2157
+ cross / line / triangle / hex), cell size, per-channel screen-angle step, dot
2158
+ scale, max-dot-vs-cell overlap, decentralization, and hex vs square grid.
2159
+ Pure numpy + Pillow, no new dependencies.
2160
+ - File schema 4.2.10 -> 4.2.11 (new halftone fields). Older files still load
2161
+ (missing fields default).
2162
+
2163
+ ### Added
2164
+ - More buttons attempt bundled icons with graceful glyph fallback: sub-document
2165
+ / SVG / PDF / CSV toolbar buttons, the layer-order buttons (up/down light up
2166
+ now), and polygon / arrow object types. Icons still to draw are listed in
2167
+ edof-icons-todo.json (drop the PNG in and it appears; otherwise the current
2168
+ default shows).
2169
+
2170
+ ================================================================================
2171
+
2172
+ ## [4.2.7.17] - 2026-06-05
2173
+
2174
+ Build 2 phase 2 (slice 17): Copy/Paste/Clear layer effects + more real icons.
2175
+
2176
+ ### Added
2177
+ - **Copy / Paste / Clear Layer Effects.** Small buttons sit next to the
2178
+ "Layer Effects…" button in the property panel, and a "Layer Effects" submenu
2179
+ is in the right-click menu both on the canvas and in the Objects panel. Copy
2180
+ grabs the whole layer style (effects + blend mode + opacity + fill opacity);
2181
+ Paste applies it to another object; Clear removes all effects. Paste is
2182
+ disabled until something is copied; Clear is disabled when the object has no
2183
+ effects.
2184
+
2185
+ ### Changed
2186
+ - **Objects panel uses real bundled icons.** Each row shows a per-type icon
2187
+ (text / image / rect / ellipse / line / pen for paths / qr / table / group),
2188
+ and the visibility and lock toggles now use proper eye / lock icons instead of
2189
+ emoji. A glyph fallback is kept where no matching asset exists.
2190
+
2191
+ ================================================================================
2192
+
2193
+ ## [4.2.7.16] - 2026-06-05
2194
+
2195
+ Build 2 phase 2 (slice 16): fix bevel ring/contour banding.
2196
+
2197
+ ### Fixed
2198
+ - **Bevel & Emboss no longer shows concentric-ring / contour banding** across
2199
+ the face. Previously the height map came from a blurred 8-bit alpha, which (a)
2200
+ domed over the whole shape for larger bevels and (b) quantized into contour
2201
+ rings. It is now built from a **distance transform** (ramps over the bevel
2202
+ width, then a flat plateau) computed in float, so the interior face has zero
2203
+ slope and receives no shading. The bevel stays confined to the edge band.
2204
+ - scipy is used for the distance transform when available, with a pure-numpy
2205
+ fallback (downsampled for large objects to stay fast) so EDOF keeps no hard
2206
+ scipy dependency.
2207
+
2208
+ ================================================================================
2209
+
2210
+ ## [4.2.7.15] - 2026-06-05
2211
+
2212
+ Build 2 phase 2 (slice 15): proper Photoshop-style bevel shading + more sliders.
2213
+
2214
+ ### Changed
2215
+ - **Bevel & Emboss rewritten to normal-based shading.** Instead of the old
2216
+ "blur the alpha and subtract" edge trick, it now builds a height ramp from the
2217
+ edge, derives surface normals, and lights them with the light azimuth
2218
+ (angle) and elevation (altitude). The shading is confined to the sloped bevel
2219
+ band, giving the smooth rounded highlight/shadow gradient you get in
2220
+ Photoshop. Technique, Depth, Direction, Soften, Altitude and the separate
2221
+ Highlight/Shadow opacities all feed into it; inner/emboss/smooth and outer
2222
+ share the renderer.
2223
+ - **More sliders:** in the Bevel panel, Depth, Light angle and Altitude are now
2224
+ sliders (with live value labels).
2225
+
2226
+ ================================================================================
2227
+
2228
+ ## [4.2.7.14] - 2026-06-05
2229
+
2230
+ Build 2 phase 2 (slice 14): richer Bevel & Emboss render.
2231
+
2232
+ ### Added / Changed
2233
+ - **Bevel & Emboss now uses its full set of controls** (previously the render
2234
+ ignored most of them): Technique (smooth / chisel hard / chisel soft), Depth
2235
+ (%), Direction (up / down), Soften (mm), Altitude (light height °), and
2236
+ separate Highlight opacity / Shadow opacity. The inner/emboss/smooth kinds
2237
+ share one richer renderer and the outer bevel honours the same controls.
2238
+ - The Bevel dialog panel exposes all of these.
2239
+
2240
+ ================================================================================
2241
+
2242
+ ## [4.2.7.13] - 2026-06-05
2243
+
2244
+ Build 2 phase 2 (slice 13): blending / opacity correctness.
2245
+
2246
+ ### Fixed
2247
+ - **An object's Blending mode was ignored whenever the object had any layer
2248
+ effect.** The body was blended against an empty internal buffer instead of the
2249
+ real canvas, so multiply / screen / etc. did nothing on shapes, curves or text
2250
+ that also had effects. The object is now composited onto the actual background
2251
+ with its blend mode (and shadows/below-effects underneath it).
2252
+
2253
+ ### Notes
2254
+ - Verified the Photoshop semantics: **Opacity** fades the whole layer (object +
2255
+ effects); **Fill opacity** fades only the object pixels while effects (drop
2256
+ shadow, glow, ...) stay at full strength.
2257
+
2258
+ ================================================================================
2259
+
2260
+ ## [4.2.7.12] - 2026-06-05
2261
+
2262
+ Build 2 phase 2 (slice 12): rich Chromatic Aberration.
2263
+
2264
+ ### Added
2265
+ - **Chromatic Aberration is now fully per-channel.** Each of the R / G / B
2266
+ channels has its own **colour**, and either its own **offset + angle**
2267
+ (linear mode) or its own **radial distortion %** (radial / lens mode). A
2268
+ Mode switch chooses linear vs radial. Defaults reproduce the classic split.
2269
+
2270
+ ### Changed
2271
+ - File format schema -> 4.2.10 (added the per-channel CA fields; older files load).
2272
+
2273
+ ================================================================================
2274
+
2275
+ ## [4.2.7.11] - 2026-06-05
2276
+
2277
+ Build 2 phase 2 (slice 11): smooth Long Shadow soft-linear blur.
2278
+
2279
+ ### Changed
2280
+ - **Long Shadow soft "linear" ramp is now smooth.** It previously blurred in 6
2281
+ discrete distance bands, leaving visible steps; it now uses a continuous
2282
+ graduated blur (interpolating between a few pre-blurred copies by a distance
2283
+ gradient), so the blur grows seamlessly along the throw.
2284
+
2285
+ ================================================================================
2286
+
2287
+ ## [4.2.7.10] - 2026-06-05
2288
+
2289
+ Build 2 phase 2 (slice 10): mm fields become sliders, Long Shadow taper + soft mode.
2290
+
2291
+ ### Added
2292
+ - **mm fields in the effects dialog are now slider + number + adjustable max**
2293
+ (shadow distance/size, glow size, bevel size, stroke size, long-shadow length,
2294
+ halftone spacing). Drag the slider, type an exact value, or raise the "≤ max"
2295
+ box to go past the default cap.
2296
+ - **Long Shadow — Taper (0-200%, 100% = uniform):** progressively narrows
2297
+ (towards 0%) or widens (towards 200%) the shadow along its throw.
2298
+ - **Long Shadow — Soft mode:** the shadow blurs along the throw, with a ramp of
2299
+ `linear` (blur grows with distance) or `constant` (uniform soft blur), plus a
2300
+ "Soft blur size" control.
2301
+
2302
+ ### Changed
2303
+ - File format schema -> 4.2.9 (added ls_taper, ls_mode, ls_blur_mode; older files
2304
+ load fine).
2305
+
2306
+ ================================================================================
2307
+
2308
+ ## [4.2.7.9] - 2026-06-05
2309
+
2310
+ Build 2 phase 2 (slice 9): effects dialog reworked to an instance list
2311
+ (multiple effects of one type, duplication, drag-to-reorder).
2312
+
2313
+ ### Added
2314
+ - **Multiple instances per effect type.** The Layer Style list is now a list of
2315
+ effect *instances* (e.g. two Drop Shadows), shown as "Drop Shadow 1",
2316
+ "Drop Shadow 2", ...
2317
+ - **+ Add** (menu of every effect type, can add the same type repeatedly),
2318
+ **⧉ Duplicate** (copies the selected effect right below it), and
2319
+ **- Remove** buttons.
2320
+ - **Drag to reorder** effects in the list — this is the render/stacking order.
2321
+ Blending Options stays pinned at the top and is not movable.
2322
+
2323
+ ### Changed
2324
+ - Effects are now collected and rendered in the list's order, allowing fine
2325
+ control over which effect sits on top.
2326
+
2327
+ ================================================================================
2328
+
2329
+ ## [4.2.7.8] - 2026-06-05
2330
+
2331
+ Build 2 phase 2 (slice 8): Photoshop-style Spread / Choke for shadows and glows.
2332
+
2333
+ ### Added
2334
+ - **Spread** (Drop Shadow, Outer Glow) / **Choke** (Inner Shadow, Inner Glow),
2335
+ exactly like Photoshop: it expands (or, for inner effects, chokes) the solid
2336
+ matte before the blur. At 0% you get the usual soft edge; at 100% the soft
2337
+ edge collapses into a hard, stroke-like outline. Controlled by a new `spread`
2338
+ field on layer effects.
2339
+
2340
+ ### Changed
2341
+ - File format schema -> 4.2.8 (added the `spread` field; older files load fine,
2342
+ new files carry it).
2343
+
2344
+ ================================================================================
2345
+
2346
+ ## [4.2.7.7] - 2026-06-05
2347
+
2348
+ Build 2 phase 2 (slice 7): the four newer effects are now in the Layer Style dialog.
2349
+
2350
+ ### Added
2351
+ - **Long Shadow**, **Chromatic Aberration**, **Halftone** and **Light Sweep**
2352
+ are now selectable in the Layer Style (effects) dialog with full parameter
2353
+ panels (they already rendered; now they can be added and tuned from the UI):
2354
+ - Long Shadow: blend, opacity, color, angle, length, fade-out toggle.
2355
+ - Chromatic Aberration: opacity, offset, angle.
2356
+ - Halftone: blend, opacity, color, dot/line spacing, angle, shape (dot/line).
2357
+ - Light Sweep: blend, opacity, light colour, position, width, angle.
2358
+
2359
+ ================================================================================
2360
+
2361
+ ## [4.2.7.6] - 2026-06-05
2362
+
2363
+ Build 2 phase 2 (slice 6): effects dialog modernization (visual pass).
2364
+
2365
+ ### Changed
2366
+ - **Layer Style (effects) dialog restyled** for a cleaner, more modern look:
2367
+ card-style panels and effect list with rounded corners, an accent-highlighted
2368
+ selection, hover states, restyled sliders, and more breathing room (larger
2369
+ default window). Structure and functionality unchanged. Deeper changes
2370
+ (per-effect descriptions, right-panel de-duplication, drag-to-reorder /
2371
+ duplicate / remove, and the blending/opacity logic) come next.
2372
+
2373
+ ================================================================================
2374
+
2375
+ ## [4.2.7.5] - 2026-06-05
2376
+
2377
+ Build 2 phase 2 (slice 5): more UI icons.
2378
+
2379
+ ### Added
2380
+ - UI icons on the text formatting toolbar: bold, italic, underline,
2381
+ strikethrough, and align left / center / right.
2382
+
2383
+ ================================================================================
2384
+
2385
+ ## [4.2.7.4] - 2026-06-05
2386
+
2387
+ Build 2 phase 2 (slice 4): coloured icons + text toolbar at the top.
2388
+
2389
+ ### Changed
2390
+ - **UI toolbar icons keep their original colours** (the previous build wrongly
2391
+ flattened them to a grey tint).
2392
+ - **The text formatting toolbar is now always pinned to the top** of the canvas
2393
+ (Word-style ribbon, like document mode), instead of floating squished above
2394
+ the textbox.
2395
+
2396
+ ================================================================================
2397
+
2398
+ ## [4.2.7.3] - 2026-06-05
2399
+
2400
+ Build 2 phase 2 (slice 3): UI icons on the main toolbar + cursor refinements.
2401
+
2402
+ ### Added
2403
+ - **Custom UI icons on the main toolbar** (new, open, save, undo, redo, select,
2404
+ hand, text, image, rect, ellipse, line, qr, table, pen, zoom-in, zoom-out,
2405
+ fit, duplicate, delete, export), from the bundled icon set. Dark artwork is
2406
+ recoloured to a light tint so it is visible on the dark toolbar; buttons with
2407
+ no matching icon keep their text/emoji label.
2408
+
2409
+ ### Changed
2410
+ - Per-corner rectangle radii: the label is now English ("Corners (mm)") and the
2411
+ four fields are laid out in two rows (TL / TR on top, BL / BR below) so they
2412
+ no longer get squeezed.
2413
+ - Cursor refinements: resize cursors now follow the object's rotation; path
2414
+ point-editing shows tangent / move / add-point / remove-point cursors; the
2415
+ pen tool shows a close cursor near the first point; custom cursors scaled down
2416
+ so they are not oversized.
2417
+
2418
+ ================================================================================
2419
+
2420
+ ## [4.2.7.2] - 2026-06-05
2421
+
2422
+ Build 2 phase 2 (slice 2): custom tool cursors. Also carries the 4.2.7.1 work
2423
+ (per-corner rectangle radii + the rounded-rect stroke-clipping fix).
2424
+
2425
+ ### Added
2426
+ - **Custom tool cursors.** The canvas cursor now changes by tool and by what is
2427
+ under the pointer, using a bundled cursor set (move, pen, tangent, resize
2428
+ NWSE/NESW/NS/EW, rotate, hand / hand-grab, crosshair, not-allowed, ...) loaded
2429
+ from `cursors.json` with per-cursor hotspots. Falls back to the native Qt
2430
+ cursor if an asset is missing.
2431
+ - Bundled the cursor and UI-icon asset sets into the package
2432
+ (`edof/_apps/assets/cursors`, `edof/_apps/assets/ui_icons`). Wiring the UI
2433
+ icons into the toolbars/panels comes next.
2434
+
2435
+ ================================================================================
2436
+
2437
+ ## [4.2.7.1] - 2026-06-05
2438
+
2439
+ Build 2 phase 2 (slice 1): effects coverage. More of phase 2 (effects dialog
2440
+ modernization + right-panel cleanup, blending/opacity logic, drag-drop reorder,
2441
+ custom cursors, live draw preview) follows in 4.2.7.2+.
2442
+
2443
+ ### Added
2444
+ - **Layer Effects are now available for QR codes and for lines** (and confirmed
2445
+ working for shapes / curves). Effects already rendered for any object; the
2446
+ entry point was simply missing from the QR and line property panels. Effects
2447
+ follow the object's real alpha, so a QR whose background colour is made
2448
+ transparent casts its shadow / glow / bevel from the modules only.
2449
+
2450
+ ================================================================================
2451
+
2452
+ ## [4.2.7.0] - 2026-06-05
2453
+
2454
+ Build 2 (phase 1) of the editor roadmap: layer-effect model + file versioning.
2455
+ The new effects render and are fully controllable programmatically; the editor
2456
+ UI to add / reorder / duplicate them (and the richer bevel controls) lands in
2457
+ 4.2.7.1.
2458
+
2459
+ ### Added
2460
+ - **Four new layer effects** (Photoshop does not have these as one-click,
2461
+ non-destructive effects):
2462
+ - **Long Shadow** — flat-design shadow thrown along a direction for a set
2463
+ length, optional fade.
2464
+ - **Chromatic Aberration** — RGB channel split for a glitch / retro look.
2465
+ - **Halftone** — luminance dot (or line) screen clipped to the object.
2466
+ - **Light Sweep** — glossy diagonal specular streak.
2467
+ - **Extended bevel model** towards Photoshop parity: technique
2468
+ (smooth / chisel hard / chisel soft), depth, direction (up / down), soften,
2469
+ altitude, and separate highlight / shadow opacity. (These ship in the schema
2470
+ now; the richer bevel *rendering* and UI controls come in 4.2.7.1.)
2471
+ - **File versioning**: every saved `.edof` now records `writer_version` (the
2472
+ exact library version that wrote it) alongside `edof_version` (the format /
2473
+ schema version). The schema version is bumped to **4.2.7** because the effect
2474
+ schema grew.
2475
+
2476
+ ### Changed
2477
+ - File format (schema) version: 4.2.0 -> 4.2.7. The change is additive
2478
+ (new optional effect fields), so older 4.2.x readers still open new files and
2479
+ new readers open old files.
2480
+
2481
+ ================================================================================
2482
+
2483
+ ## [4.2.6.0] - 2026-06-05
2484
+
2485
+ Build 1 of the editor roadmap: curve (path) editing.
2486
+
2487
+ ### Fixed
2488
+ - **Path handles keep a constant on-screen size at any zoom** and no longer
2489
+ grow or look pixelated when you zoom in; their outlines stay crisp (1–2 px).
2490
+ - **Dragging multiple selected anchors now moves their tangent handles too**, so
2491
+ a group move keeps the curve shape instead of leaving tangents behind.
2492
+
2493
+ ### Added
2494
+ - **Grid snapping and Photoshop-style modifiers while dragging path points**
2495
+ (anchors and tangents):
2496
+ - no modifier: snap to the grid (when grid snap is on),
2497
+ - Ctrl: no snapping,
2498
+ - Shift: constrain to 0 / 45 / 90 degrees from the drag start (with snap),
2499
+ - Ctrl+Shift: constrain without snapping.
2500
+
2501
+ ================================================================================
2502
+
2503
+ ## [4.2.5] - 2026-06-05
2504
+
2505
+ ### Fixed
2506
+ - **New Document now respects the size and DPI you type.** The width / height /
2507
+ DPI fields are the single source of truth: picking a preset (A4, Full HD, ...)
2508
+ just fills those fields, and the document is always created from the fields.
2509
+ Previously, if a preset row stayed selected, the typed width / height / DPI
2510
+ were ignored and you got the preset size (usually A4).
2511
+ - **Custom DPI is respected.** Once you set a DPI in New Document, picking a
2512
+ preset no longer overwrites it, and the DPI range is widened (1–9600).
2513
+ - **Millimetre fields now keep 0.01 mm precision and no longer jump by 0.5 mm.**
2514
+ The geometry fields step by 0.1 mm on the arrows (was 0.5 mm) and accept two
2515
+ decimals; fields that rounded to 0.1 mm (corner radius, table border, layer
2516
+ effect sizes/distances) now keep hundredths too. Canvas snapping is unchanged.
2517
+
2518
+ ### Changed
2519
+ - The documentation version (docs landing page) is now generated from the
2520
+ package version automatically, so it never goes stale.
2521
+
2522
+ ================================================================================
2523
+
2524
+ ## [4.2.4] - 2026-06-05
2525
+
2526
+ ### Fixed
2527
+ - **The Editor and Viewer now show the EDOF icon in the Windows taskbar**
2528
+ (instead of the Python interpreter's icon). Each app sets its own
2529
+ AppUserModelID at startup and an application-level window icon.
2530
+ - **File association now always records an absolute path to the real launcher**
2531
+ and never a `.bat`/`.cmd`. Previously the resolver could pick up a launcher
2532
+ `edof-viewer.bat` from the current directory and register it with a relative
2533
+ path (`.\edof-viewer.BAT`), which has no icon and fails on double-click. It
2534
+ now prefers `edof-viewer.exe` / `edof-editor.exe` next to the interpreter,
2535
+ rejects batch files, and falls back to `pythonw -m ...` with absolute paths.
2536
+
2537
+ ### Changed
2538
+ - `edof-editor` and `edof-viewer` are now GUI entry points (no console window
2539
+ flashes on launch); `edof-cli` stays a console command.
2540
+
2541
+ ================================================================================
2542
+
2543
+ ## [4.2.3] - 2026-06-05
2544
+
2545
+ ### Changed
2546
+ - **You now choose the default opener inside EDOF, not in the OS dialog.** The
2547
+ "File association (.edof)" dialog (in both the Editor and the Viewer) lets you
2548
+ pick whether double-clicking a `.edof` file opens the **Viewer** or the
2549
+ **Editor**, and registers that choice as the default. The other app stays
2550
+ available via right-click → Open With, and files keep the EDOF icon. The same
2551
+ choice is available on the command line: `edof-cli associate-files --app
2552
+ editor` (or `--app viewer`, the default). In 4.2.2 the OS prompted you to pick
2553
+ on first open; now the choice is made in the app.
2554
+
2555
+ ================================================================================
2556
+
2557
+ ## [4.2.2] - 2026-06-05
2558
+
2559
+ Bug-fix release with application/document icons.
2560
+
2561
+ ### Fixed
2562
+ - **Centre (and other) paragraph alignment is no longer lost on save or on
2563
+ Word export.** `TextRun` now serializes its `alignment`, and the body sync
2564
+ promotes run-carried alignment onto `Paragraph.alignment`, so the chosen
2565
+ alignment survives the `.edof` round-trip and is picked up by `export_docx`.
2566
+ - **Ctrl+S / Ctrl+Shift+S now work while typing in the body editor.** They are
2567
+ routed to the window's Save / Save As; previously Ctrl+S did nothing inside
2568
+ the inline editor and Ctrl+Shift+S toggled strikethrough instead of Save As.
2569
+ - **File association no longer errors with "cannot unpack NoneType".** The
2570
+ associate / unassociate functions now return `(ok, message)`.
2571
+ - Removed an unsupported `outline` property from the stylesheet that caused
2572
+ repeated "Could not parse stylesheet of object ..." console warnings.
2573
+
2574
+ ### Added
2575
+ - **Icons.** The Editor and Viewer windows now use their own icons, `.edof`
2576
+ files show a document icon in Explorer, and the Viewer / Editor appear with
2577
+ their own icons under "Open with". Icons ship inside the package
2578
+ (`edof/_apps/assets/icons/`, `.ico` + `.png` + `.icns` for Windows / Linux /
2579
+ macOS).
2580
+ - **File association** that registers the Viewer and the Editor as open-with
2581
+ choices and gives `.edof` files the EDOF icon. The Viewer gained a
2582
+ register / remove toggle and **File → Open in Editor (Ctrl+E)**.
2583
+
2584
+ ================================================================================
2585
+
2586
+ ## [4.2.1] - 2026-06-04
2587
+
2588
+ Follow-up to 4.2.0 (which was already published to PyPI, where versions are
2589
+ immutable, so these ship in a new version).
2590
+
2591
+ ### Fixed
2592
+ - `edof._apps.viewer` now imports without PyQt6 installed (it falls back to a
2593
+ harmless base class and `main()` prints an install hint), so the CI test
2594
+ suite passes on the lean `[dev,qr]` install instead of erroring on import.
2595
+
2596
+ ### Added
2597
+ - Ko-fi funding links (`https://ko-fi.com/davidschobl`): `.github/FUNDING.yml`,
2598
+ the `Funding` project URL, the README, and the in-app "Support the
2599
+ developer" action and About dialogs (alongside GitHub Sponsors).
2600
+
2601
+ ### Packaging
2602
+ - `deploy-edof.bat` / `deploy-edof.sh` default the deploy clone to a directory
2603
+ next to the script (the unzipped package) instead of a hard-coded developer
2604
+ path.
2605
+
2606
+ ================================================================================
2607
+
2608
+ ## [4.2.0] - 2026-06-04
2609
+
2610
+ First public release since 4.0.3. It folds in a long line of editor and
2611
+ format work developed in between, so the highlights below cover everything
2612
+ new since 4.0.3.
2613
+
2614
+ ### Added
2615
+ - **Word (.docx) import and export** (optional, needs `python-docx`:
2616
+ `pip install edof[docx]`). New top-level API `edof.export_docx(doc, path)`
2617
+ and `edof.import_docx(path, return_report=False)`, plus **File → Import /
2618
+ Export Word (.docx)…** in the desktop editor.
2619
+ - Export writes the document body flow: runs with bold / italic / underline
2620
+ / strikethrough, font family and size, run colour, paragraph alignment,
2621
+ page size and margins, page-break-before, single-level lists, and a line
2622
+ height matched **exactly** to EDOF's so Word paginates the same way the
2623
+ editor does.
2624
+ - Import builds a document-mode file and produces a compatibility report.
2625
+ It never silently drops content: tables, images, drawings, text boxes,
2626
+ equations and embedded objects are detected and the user is advised
2627
+ against importing when such content is significant; headers/footers,
2628
+ footnotes and comments are reported as dropped. See
2629
+ `docs/reference/11-docx.md`.
2630
+ - **Unified document-wide undo/redo.** A single timeline now covers both
2631
+ body-text editing and object operations (move / add / delete / style).
2632
+ Body edits coalesce into one step per typing burst and are flushed before
2633
+ any undo/redo or object change, so Ctrl+Z/Ctrl+Y behave consistently
2634
+ everywhere and never split between two histories.
2635
+ - **Document mode** maturity: continuous multi-page text flow with automatic
2636
+ pagination, hard page breaks (Ctrl+Enter), per-paragraph keep/break
2637
+ controls, and per-run/paragraph line spacing.
2638
+ - Complete generated **API reference** (`docs/reference/API.md`) and a
2639
+ MkDocs-Material documentation site published to GitHub Pages.
2640
+ - **Read-only Viewer** (`edof-viewer`) and OS-level **file associations** for
2641
+ `.edof` (open by double-click; register from the editor, the viewer, or
2642
+ `edof-cli associate-files`).
2643
+ - **Document mode**: a continuous, multi-page text flow you can edit like a
2644
+ word processor, alongside the free-form object canvas.
2645
+ - **Layer effects** (Photoshop-style), **per-side padding** on text boxes, and
2646
+ a **bezier path tool**.
2647
+ - **Table editor UI** (tables remain experimental / a work in progress).
2648
+
2649
+ ### Changed
2650
+ - **Debug logging is now opt-in.** It is disabled by default and only writes a
2651
+ log when `EDOF_DEBUG=1` is set or it is enabled programmatically;
2652
+ `EDOF_DEBUG_PATH` overrides the location. Releases no longer create
2653
+ `edof_debug.log` in the home directory during normal use.
2654
+ - Line-height model on export uses an exact point value (not Word's "multiple"
2655
+ rule), eliminating the ~15 % taller spacing that pushed extra lines onto
2656
+ later pages.
2657
+
2658
+ ### Fixed
2659
+ - Pagination: hard page breaks and empty break-pages survive re-pagination;
2660
+ the caret no longer drops below the bottom margin while typing at the end of
2661
+ a full page; the empty-document caret respects paragraph alignment;
2662
+ continuing pages no longer render a spurious trailing cursor line.
2663
+ - Close / New / Open now prompt to save unsaved body edits, including edits
2664
+ made on a single line that previously failed to mark the document modified.
2665
+ - New documents no longer capture the editor's placeholder hint into undo
2666
+ history, so undoing to the very start leaves a clean empty document.
2667
+
2668
+ ### Packaging
2669
+ - Clean release tree (no build/debug helpers or caches); restored the
2670
+ `.github/workflows/publish.yml` Trusted-Publishing (OIDC) workflow; added a
2671
+ `Funding` project URL and a `.gitignore`.
2672
+
2673
+ ================================================================================
2674
+
2675
+ ## [4.0.3] - 2026-05-04
2676
+
2677
+ The "editor catches up with the API" release. Substantial editor improvements,
2678
+ PDF import bug fixes, RTF import/export, and a long list of polish.
2679
+ No format changes — files saved by 4.0.3 are wire-compatible with 4.0.2.
2680
+
2681
+ ================================================================================
2682
+ FIXED — PDF import: vector paths had wrong bounding box
2683
+ ================================================================================
2684
+
2685
+ Previously, vector paths (lines, curves, rectangles) imported from PDF were
2686
+ created with a transform spanning the entire page, while the path coordinates
2687
+ themselves were absolute. The renderer worked, but the editor couldn't select
2688
+ or move them — the bounding box covered the whole page.
2689
+
2690
+ In 4.0.3:
2691
+ - `_extract_paths()` now computes the actual bbox of each path
2692
+ - Path coordinates are stored as local (relative to transform.x/y)
2693
+ - The renderer auto-detects local vs absolute coords for backward compatibility
2694
+ with documents created before this fix
2695
+
2696
+ Also added new flags to `import_pdf()`:
2697
+ - `extract_paths` (default True): convert PDF vector paths to Shape objects
2698
+ - `extract_images` (default True): extract embedded raster images
2699
+
2700
+ These flags were promised in the 4.0.2 docs but were not actually wired up.
2701
+
2702
+ ================================================================================
2703
+ FIXED — Image scale X breaks position
2704
+ ================================================================================
2705
+
2706
+ When resizing an ImageBox by dragging an edge handle (only one axis), the
2707
+ opposite anchor was not held fixed, causing the image to "jump" sideways. The
2708
+ resize now correctly keeps the opposite corner / edge as the anchor regardless
2709
+ of which handle you drag.
2710
+
2711
+ ================================================================================
2712
+ FIXED — Subpixel text rendering disappears at low zoom
2713
+ ================================================================================
2714
+
2715
+ When zoomed below 100%, thin text strokes on the canvas would render at
2716
+ sub-pixel widths and disappear into anti-aliasing — the text became unreadable.
2717
+
2718
+ In 4.0.3, the editor canvas renders at higher DPI when zoomed out (up to 2×)
2719
+ and downscales with LANCZOS, so thin strokes survive. This affects only the
2720
+ canvas preview; export quality is unchanged.
2721
+
2722
+ ================================================================================
2723
+ CHANGED — Modifier semantics for resize/rotation/move
2724
+ ================================================================================
2725
+
2726
+ The editor's modifier behaviors were inconsistent and didn't match user
2727
+ expectations. Revised in 4.0.3:
2728
+
2729
+ - **Ctrl** while dragging — bypass ALL snapping (grid, alignment guides, margins).
2730
+ This is the primary "give me precise control" modifier.
2731
+ - **Alt** while dragging — bypass snapping (legacy alias for Ctrl, kept for
2732
+ compatibility).
2733
+ - **Shift on resize** — toggles uniform/non-uniform scale:
2734
+ - For ImageBox: default is uniform (preserve aspect ratio); Shift toggles
2735
+ to non-uniform.
2736
+ - For other objects: default is non-uniform; Shift forces uniform.
2737
+ - **Shift on rotation** — snap to 15° increments.
2738
+
2739
+ This means typical workflows do the right thing automatically:
2740
+ - Drag image corner → preserves aspect ratio
2741
+ - Drag image corner with Shift → free distortion if you really want it
2742
+ - Drag rectangle corner → free resize
2743
+ - Drag rectangle corner with Shift → preserve aspect ratio
2744
+
2745
+ ================================================================================
2746
+ ADDED — Page margins (per-document) with snap support
2747
+ ================================================================================
2748
+
2749
+ Documents now have an optional `doc.margins` field — a 4-tuple of
2750
+ (top, right, bottom, left) in mm. Margins are saved/loaded with the document.
2751
+
2752
+ In the editor:
2753
+ - View menu → "Use Page Margins (snap)" toggle
2754
+ - View menu → "Set Margins…" dialog
2755
+ - When enabled, dragged objects snap their edges to the margin lines
2756
+
2757
+ Margins are editor-only — they're not enforced at render or export.
2758
+
2759
+ ================================================================================
2760
+ ADDED — Editor: Insert Table dialog
2761
+ ================================================================================
2762
+
2763
+ Tables existed in the API since 4.0.0 but had no UI to create them. Insert
2764
+ Table dialog now offers:
2765
+ - Rows × columns
2766
+ - Width × height in mm
2767
+ - Optional header (first row in bold + accent color)
2768
+ - Optional alternating row colors
2769
+
2770
+ ================================================================================
2771
+ ADDED — Editor: Path drawing tool
2772
+ ================================================================================
2773
+
2774
+ A new toolbar button (✎) puts the canvas in path-drawing mode:
2775
+ - Click adds a point
2776
+ - Double-click or Enter finishes the path
2777
+ - Esc cancels
2778
+ - Snap-to-grid is honored if enabled
2779
+
2780
+ The result is a `Shape(shape_type="path")` with proper local coordinates and
2781
+ correct bounding box.
2782
+
2783
+ ================================================================================
2784
+ ADDED — Editor: Object panel rename, drag-and-drop, context menu
2785
+ ================================================================================
2786
+
2787
+ The left-side object list panel was bare-bones. Now:
2788
+
2789
+ - **F2** or **double-click** an item to rename the object inline
2790
+ - **Drag** items up/down to reorder layers (front-to-back ordering)
2791
+ - **Right-click** an item for: Rename, Bring to Front, Bring Forward,
2792
+ Send Backward, Send to Back, Show/Hide, Lock/Unlock, Duplicate, Delete
2793
+
2794
+ Also fixed a dark-theme rendering bug where alternating row colors were too
2795
+ bright; the panel now uses a quieter selected-state highlight.
2796
+
2797
+ ================================================================================
2798
+ ADDED — Editor: Properties panel — Advanced section
2799
+ ================================================================================
2800
+
2801
+ A new "Advanced" group on the right-side properties panel exposes API-level
2802
+ features that previously had no UI:
2803
+
2804
+ - **Show if** (`visible_if` expression) — conditional visibility
2805
+ - **Lock level** dropdown (none / fill / edit / design / admin) — for
2806
+ permission-aware editing of encrypted templates
2807
+ - **Lock text** checkbox — prevents changes to text content even when the
2808
+ object itself is editable
2809
+ - **Blend mode** dropdown (normal / multiply / screen / darken / lighten / overlay)
2810
+ - **Shape type** changer — convert rect ↔ ellipse ↔ polygon ↔ path on the fly
2811
+ - **Drop shadow** — toggle + offset X/Y + blur
2812
+
2813
+ ================================================================================
2814
+ ADDED — Editor: Help → Keyboard Shortcuts dialog
2815
+ ================================================================================
2816
+
2817
+ F1 now opens a comprehensive reference dialog covering File, Edit, View,
2818
+ Insert, Document, Selection, Modifier keys (with the new v4.0.3 semantics),
2819
+ Object panel actions, and the Path tool.
2820
+
2821
+ Help → About also added.
2822
+
2823
+ ================================================================================
2824
+ ADDED — Editor: PDF Export dialog with Vector / Raster choice
2825
+ ================================================================================
2826
+
2827
+ The PDF export menu item now opens a dialog explaining the trade-off:
2828
+ - **Vector PDF** (default): pure-Python writer, smaller files, selectable text,
2829
+ limited to Standard 14 PDF fonts
2830
+ - **Raster PDF**: rendered as bitmap, larger files, no text selection,
2831
+ supports any TTF font, requires reportlab
2832
+
2833
+ Plus a DPI control for raster mode.
2834
+
2835
+ ================================================================================
2836
+ ADDED — Editor: Resizable docks + persistence
2837
+ ================================================================================
2838
+
2839
+ The left and right panels were previously fixed-width. Now:
2840
+ - Both docks are user-resizable
2841
+ - Both are dockable (movable, can detach to floating)
2842
+ - Geometry, snap-to-grid state, alignment guides state, margin state, and
2843
+ full window/dock layout persist across sessions (via `QSettings`)
2844
+ - View menu → "Reset Panel Layout" to restore defaults
2845
+
2846
+ ================================================================================
2847
+ ADDED — Editor: Toolbar tooltips
2848
+ ================================================================================
2849
+
2850
+ Every toolbar button now has a descriptive tooltip + status-bar message +
2851
+ keyboard shortcut hint. Previously hovering "💾" gave no explanation; now it
2852
+ shows "Save (Ctrl+S)".
2853
+
2854
+ ================================================================================
2855
+ ADDED — RTF import / export
2856
+ ================================================================================
2857
+
2858
+ A new utility module (`edof.utils.rtf`) provides best-effort interop with
2859
+ Rich Text Format documents:
2860
+
2861
+ - `edof.import_rtf(path)` reads an RTF file into an EDOF Document. Each
2862
+ non-empty paragraph becomes a TextBox; runs preserve bold/italic/underline/
2863
+ size/color. Tables, images, lists, fields are not supported.
2864
+ - `doc.export_rtf(path)` writes an EDOF document as flat RTF — paragraphs in
2865
+ vertical order, runs with formatting. Other object types (shapes, images,
2866
+ tables) are not exported.
2867
+
2868
+ In the editor:
2869
+ - File → Import RTF…
2870
+ - File → Export RTF…
2871
+
2872
+ ================================================================================
2873
+ DOCUMENTATION
2874
+ ================================================================================
2875
+
2876
+ - Documentation site at https://davidschobl.github.io/edof/ updated for 4.0.3
2877
+ - Editor reference page (`docs/reference/08-editor.md`) updated with
2878
+ v4.0.3 modifier semantics, margins, panel persistence
2879
+ - New "Path tool" section in editor docs
2880
+
2881
+ ================================================================================
2882
+ TESTS
2883
+ ================================================================================
2884
+
2885
+ 138/138 tests passing (vs 112 in 4.0.2):
2886
+ - 36 v3.1 (legacy)
2887
+ - 36 v4.0
2888
+ - 21 v4.0.1
2889
+ - 19 v4.0.2
2890
+ - 26 v4.0.3 (new)
2891
+
2892
+ ================================================================================
2893
+
2894
+ ## [4.0.2] - 2026-05-04
2895
+
2896
+ Polish, bug fixes, and CLI completeness release. No format changes — files saved by 4.0.2 are bit-identical to 4.0.1 when no 4.0.2-only behaviors are exercised.
2897
+
2898
+ ================================================================================
2899
+ FIXED — Variable `{name}` placeholder substitution at render time
2900
+ ================================================================================
2901
+
2902
+ Previously, `{name}` placeholders inside `obj.text` were only substituted by `repeat_objects()`. Direct rendering (`doc.export_pdf()`, `doc.export_bitmap()`, `doc.export_svg()`) left placeholders as literal text. Documentation and examples in 4.0.1 promised this worked — now it actually does.
2903
+
2904
+ Behaviour:
2905
+ - `obj.text = "Hello {name}!"` with `doc.set_variable("name", "Alice")` now renders as "Hello Alice!".
2906
+ - Multiple placeholders supported: `"{greeting} {name}"`.
2907
+ - Undefined variable names stay as literal `{name}` (graceful fallback, no exception).
2908
+ - Table cell substitution (which already worked) is unchanged.
2909
+ - The previous mechanism of binding a textbox to a single variable via `obj.variable = "name"` continues to work and takes priority.
2910
+
2911
+ ================================================================================
2912
+ FIXED — Editor: snap-to-grid during resize and rotation
2913
+ ================================================================================
2914
+
2915
+ When **Snap to Grid** (Ctrl+G) was enabled in 4.0.1, snapping only applied while moving objects. Resize handles (non-uniform scale corner / edge dragging) and rotation handles ignored the setting.
2916
+
2917
+ In 4.0.2:
2918
+ - **Resize**: when grid snap is active and the object is non-rotated, the mouse position is snapped to the 5mm grid before computing the new size, so resize ends on grid increments. Rotated objects skip this (snapping along rotated axes is unintuitive). Hold **Alt** to bypass.
2919
+ - **Rotation**: when grid snap is active, rotation now snaps to 15° increments by default (the same behaviour you previously got only by holding Shift). Hold **Alt** to bypass.
2920
+
2921
+ ================================================================================
2922
+ ADDED — Editor settings persistence (Windows / Linux / macOS)
2923
+ ================================================================================
2924
+
2925
+ The editor now uses `QSettings` to remember preferences across sessions:
2926
+
2927
+ - Window geometry (size + position)
2928
+ - Snap-to-grid on/off
2929
+ - Show alignment guides on/off
2930
+ - Recent files list (up to 10)
2931
+
2932
+ Settings live in standard system locations (registry on Windows, `~/.config/edof/editor.conf` on Linux, `~/Library/Preferences/edof.plist` on macOS). Delete to reset.
2933
+
2934
+ ================================================================================
2935
+ ADDED — Validate enhancements
2936
+ ================================================================================
2937
+
2938
+ `doc.validate()` now also reports:
2939
+ - **Duplicate object IDs** anywhere in the document (recursing into groups). Useful when programmatically copying objects without resetting `obj.id`.
2940
+ - **Objects positioned entirely off-page** (i.e. their bounding box has no overlap with the page). Partially-off objects (overlapping the edge) are NOT flagged — that's a deliberate design choice (e.g. bleed marks).
2941
+
2942
+ These join the existing checks for missing-resource references, undefined variable references, and unset required variables. The function still returns an empty list when the document is fully valid.
2943
+
2944
+ ================================================================================
2945
+ ADDED — CLI: 6 new subcommands + password support on existing ones
2946
+ ================================================================================
2947
+
2948
+ The CLI now exposes the rest of the public API. New subcommands:
2949
+
2950
+ - `edof-cli batch <template> <csv> -o <pattern>` — generate one output file per CSV row, auto-filling variables. Supports `{n}`, `{column}` in output pattern; `--start`, `--limit`, `--continue-on-error`. Accepts PDF/PNG/JPEG/SVG output formats.
2951
+ - `edof-cli import <pdf> -o <edof>` — convert a PDF to an editable .edof (best-effort PDF reconstruction, requires `[pdf]` extra). `--no-tables`, `--no-images`, `--no-paths`, `--heading-threshold` flags.
2952
+ - `edof-cli convert <input> -o <output>` — migrate any legacy archive to current v4 format.
2953
+ - `edof-cli to-v3 <input> -o <output>` — save as v3-compatible (lossy: tables flatten, runs collapse, paths sample, gradients average).
2954
+ - `edof-cli set-password <input> --level admin --password <pwd>` — manage encryption from the command line. Supports `--remove`, `--clear-all`, `--current-password`, `--recovery-key`. Recovery key is shown once on first password.
2955
+ - `edof-cli unlock-render <encrypted> <out.pdf> --password <pwd>` — decrypt + render in one step. The decrypted document is never written to disk.
2956
+
2957
+ Existing commands (`info`, `objects`, `validate`, `export`) gained:
2958
+ - `--password` / `--recovery-key` flags for working with encrypted templates.
2959
+ - `--vector` / `--raster` flags on PDF export.
2960
+ - SVG output support on `export` (auto-detected from `.svg` extension or via `--format svg`).
2961
+
2962
+ `info` on an encrypted file without a password now shows public manifest data (encryption mode, permission levels, KDF parameters) instead of failing.
2963
+
2964
+ Exit codes are now consistent with the documentation:
2965
+ - `0` success
2966
+ - `1` usage error
2967
+ - `2` file not found
2968
+ - `3` encryption error (wrong password / missing crypto extra)
2969
+ - `4` validation failure
2970
+ - `5` unknown internal error
2971
+
2972
+ ================================================================================
2973
+ DOCUMENTATION
2974
+ ================================================================================
2975
+
2976
+ Comprehensive documentation added under `docs/`. Covers every public symbol with signatures, examples, and conventions. Hosted on GitHub Pages at https://davidschobl.github.io/edof/ (after the deploy below). Includes:
2977
+
2978
+ - Installation, quick start, and conventions
2979
+ - Full API reference (Document, Page, all object types, styles, variables, export, import, encryption, editor, CLI, helpers)
2980
+ - Five cookbook recipes (certificate, invoice, batch PDF, encrypted template, PDF import)
2981
+ - Advanced topics (file format internals, extending, troubleshooting)
2982
+
2983
+ `pyproject.toml` now declares `[project.urls]` (Documentation, Repository, Changelog, Issues), which appear on the PyPI project page.
2984
+
2985
+ The README badges and content link to the documentation site.
2986
+
2987
+ ================================================================================
2988
+
2989
+ ## [4.0.1] - 2026-05-04
2990
+
2991
+ Maintenance + protection release. Adds AES-256 encryption, multi-level password protection, real (not XOR) document security, plus editor enhancements.
2992
+
2993
+ ================================================================================
2994
+ ADDED — Encryption & multi-level password protection
2995
+ ================================================================================
2996
+
2997
+ This is the headline feature for 4.0.1.
2998
+
2999
+ By default, documents remain plain (no encryption, no friction — same as 4.0.0). When an admin password is set, the document switches to encrypted mode on the next save. Encryption requires the optional `cryptography` extra: `pip install edof[crypto]`.
3000
+
3001
+ **Cryptography**
3002
+ - AES-256-GCM authenticated encryption for content
3003
+ - PBKDF2-SHA256 key derivation with 600,000 iterations
3004
+ - 16-byte random salt per slot, 12-byte random nonce per ciphertext
3005
+ - GCM authentication tag detects tampering on load
3006
+ - Real protection: no XOR, no obfuscation theatre
3007
+
3008
+ **Permission levels (hierarchical)**
3009
+ - `view` — render, print, export. No modifications.
3010
+ - `fill` — view + change variable values (template filling). No structural / textual edits.
3011
+ - `edit` — fill + change object .text content (and rich-text run text segments).
3012
+ - `design` — edit + change styles, layout, add / remove objects and pages.
3013
+ - `admin` — design + manage passwords, recovery keys, lock_level overrides.
3014
+
3015
+ Higher levels imply all lower levels.
3016
+
3017
+ **Multi-slot key wrapping**
3018
+ - Each password independently wraps the same 32-byte content key.
3019
+ - Setting an `admin` password also generates a 24-character alphanumeric recovery key.
3020
+ - The recovery key is shown exactly once at first password setup; it cannot be retrieved later.
3021
+ - Recovery key always grants ADMIN; designed for owner self-recovery.
3022
+ - Changing one password does not re-encrypt the bulk payload (just rewraps that one slot).
3023
+
3024
+ **Encryption modes**
3025
+ - `full` — entire document content (and resources) encrypted as a single AES blob inside the ZIP. Manifest leaks only KDF parameters and slot count. Title, page count, all metadata are hidden.
3026
+ - `partial` — only sensitive content fields encrypted (text content, rich-text runs, image data, QR data, table cell text). Structure (positions, sizes, fonts, alignment, page count, title) remains visible. Useful for "design template" sharing where layout is public but content is private.
3027
+ - `none` — current 4.0 behaviour, plain ZIP, no encryption.
3028
+
3029
+ In partial mode without a password, the document loads with redacted content (a placeholder character `█` replaces text). The user can see the layout and structure but no content. With a password, the full content is decrypted and accessible.
3030
+
3031
+ **Per-object locks (independent of doc-level encryption)**
3032
+ - `obj.lock_level = "design"` — modifying this object requires at least the named permission, regardless of the user's general permission level.
3033
+ - `obj.lock_text = True` — hard text lock; even ADMIN cannot edit `.text` or `.runs` until clearing this flag (which itself requires ADMIN).
3034
+ - `obj.can_modify(doc) -> bool` — programmatic check.
3035
+ - `obj.can_modify_text(doc) -> bool` — also honors `lock_text`.
3036
+
3037
+ **Document API**
3038
+ ```python
3039
+ rk = doc.set_password("admin", "mySecret123")
3040
+ doc.set_password("design", "designerPwd")
3041
+ doc.set_password("edit", "editorPwd")
3042
+ doc.set_password("fill", "templateFiller")
3043
+
3044
+ doc.encryption_mode = "partial" # or "full" (default after first password)
3045
+ doc.save("template.edof")
3046
+
3047
+ doc = edof.load("template.edof", password="editorPwd")
3048
+ print(doc.permission_level) # Permission.EDIT
3049
+ doc.can(edof.crypto.DESIGN) # False
3050
+ doc.require(edof.crypto.EDIT) # OK, no exception
3051
+
3052
+ doc.change_password("edit", "old", "new") # rotate without re-encrypting payload
3053
+ doc.remove_password("fill") # requires ADMIN
3054
+ doc.clear_all_protection() # requires ADMIN
3055
+
3056
+ doc = edof.load("template.edof", recovery_key="ABCD-EFGH-...") # recovers as ADMIN
3057
+ ```
3058
+
3059
+ **Editor UI**
3060
+ - File → Open: detects encrypted files automatically and prompts for password / recovery key. Three-strikes-and-out; Cancel on any prompt aborts the open.
3061
+ - Document → Unlock for editing… (Ctrl+Shift+L): shows password prompt when an encrypted document was opened with insufficient privileges, then displays a dialog listing exactly what the granted level can and cannot do.
3062
+ - Document → Protection… : full management UI for setting / changing / removing passwords and switching between full and partial encryption modes. Confirmation dialog before plain → encrypted upgrade.
3063
+ - Document → Re-lock: forgets the cached content key for the session.
3064
+ - Status bar shows protection state at all times: 🔓 Plain / 🔒 Locked / 🔓 Unlocked: <level>.
3065
+ - Permission-aware action gating: pressing a button (Add TextBox, Delete, Duplicate, etc.) without sufficient permission shows a clear dialog explaining what level is needed.
3066
+ - Canvas drag respects `obj.can_modify()`; locked objects cannot be moved.
3067
+ - Recovery key dialog uses fixed-width font, clipboard copy button, "I have saved this key" confirmation gate.
3068
+
3069
+ **EDOF 2 → 4 password upgrade flow**
3070
+ - When opening a legacy EDOF 2 archive that had an XOR-obfuscated password, the editor offers to set up real AES-256 encryption with a clear explanation of why the old password was insecure.
3071
+
3072
+ ================================================================================
3073
+ ADDED — Editor improvements (carry-over completed in 4.0.1)
3074
+ ================================================================================
3075
+
3076
+ - Snap-to-grid: View → Snap to Grid (Ctrl+G), 5 mm grid, hold Alt to bypass.
3077
+ - Alignment guides: View → Show Alignment Guides; magnetic snap to other objects' edges and centers during drag, threshold 1.5 mm.
3078
+ - Multi-select: Ctrl+click adds / removes from selection; group drag moves all selected objects together; group delete removes them all.
3079
+ - Cursor position in mm in the status bar (live during mouse move).
3080
+ - Find & Replace dialog (Ctrl+F): searches all TextBoxes on all pages, with case-sensitive and regex options.
3081
+ - Gradient Editor dialog: visual stop list, add/remove/recolor stops, switch between linear and radial.
3082
+ - Template gallery (File → New from Template…): Blank A4 P/L, Business Card, Certificate, Invoice with Table.
3083
+ - File → Save as v3 (downgrade)…: produces a v3-compatible .edof with all v4-only features flattened.
3084
+ - File → Import PDF…
3085
+ - File → Export SVG…
3086
+
3087
+ ================================================================================
3088
+ ADDED — `doc.export_3x(path)` API
3089
+ ================================================================================
3090
+
3091
+ Programmatic API for downgrading a v4 document to v3 format.
3092
+
3093
+ Best-effort lossy conversion:
3094
+ - Tables flattened to a Group of TextBoxes plus line shapes for borders.
3095
+ - Rich-text runs collapsed to plain `obj.text` (formatting lost).
3096
+ - Path shapes rasterised to polygon shapes (Beziers sampled at 12 segments per curve).
3097
+ - Gradients replaced with the average color of their stops.
3098
+ - `visible_if` evaluated once at export time and baked into `.visible`.
3099
+ - `blend_mode` reset to `"normal"`.
3100
+
3101
+ The original document is not mutated; a deep copy is made first. Manifest in the output explicitly says `format_version: 3.1.0` so v3 readers don't show a "newer version" warning.
3102
+
3103
+ ```python
3104
+ doc.export_3x("for_v3_users.edof")
3105
+ ```
3106
+
3107
+ ================================================================================
3108
+ ADDED — Real EDOF 2 import (`edof/utils/legacy_v2.py`)
3109
+ ================================================================================
3110
+
3111
+ Replaces the placeholder scaffolding from 4.0.0 with a complete migration path based on the actual EDOF 2 schema (versions ≤ 2.2):
3112
+
3113
+ - ZIP with `data.json` at root (no manifest).
3114
+ - Float `version` field (e.g. `2.2`).
3115
+ - ARGB hex colors `#AARRGGBB` correctly converted to v4 RGB tuples (alpha dropped, RGB preserved — alpha is not part of TextStyle.color in v4).
3116
+ - `font_weight ≥ 600` → `bold = True`.
3117
+ - `max_font_size_pt > font_point_size` → `auto_shrink = True`, `font_size = max`.
3118
+ - `h_align` / `v_align` mapped to v4 `alignment` / `vertical_align`.
3119
+ - Embedded images extracted from the `images/` directory and added as v4 resources with detected MIME type.
3120
+ - `z_value` → `layer` (preserves stacking order).
3121
+ - `allow_non_uniform_scale` → `fit_mode = "stretch"` or `"contain"`.
3122
+ - `edit_mode` other than "all" → informational warning in `doc.errors`.
3123
+ - `edit_password_xor` → ignored, with explicit warning that XOR provided no real security; editor offers to set up real AES encryption.
3124
+
3125
+ Auto-detection: `edof.load(path)` checks for v2 markers (`data.json` at root, version < 3.0, no `manifest.json`) and routes to the legacy loader transparently.
3126
+
3127
+ ================================================================================
3128
+ ADDED — Optional dependency
3129
+ ================================================================================
3130
+
3131
+ ```toml
3132
+ [project.optional-dependencies]
3133
+ crypto = ["cryptography>=42.0"]
3134
+ all = [..., "cryptography>=42.0"]
3135
+ ```
3136
+
3137
+ Encryption is opt-in. Without `cryptography` installed, all plain-mode functionality continues to work; only `set_password()` and friends raise `EdofCryptoUnavailable` with installation instructions.
3138
+
3139
+ ================================================================================
3140
+ FILE FORMAT
3141
+ ================================================================================
3142
+
3143
+ - Format version bumped to 4.0.1.
3144
+ - New optional `protection` block in the manifest:
3145
+ ```json
3146
+ {
3147
+ "protection": {
3148
+ "mode": "full" | "partial",
3149
+ "format": "edof-aes-256-gcm-v1",
3150
+ "slots": [
3151
+ {"permission": "fill", "kdf": "pbkdf2-sha256", "iterations": 600000,
3152
+ "salt": "<base64>", "wrapped_key": "<base64>"},
3153
+ ...
3154
+ ]
3155
+ }
3156
+ }
3157
+ ```
3158
+ - New file inside encrypted archives: `encrypted_payload.bin` (AES-GCM ciphertext: 12 B nonce || 16 B GCM tag || ciphertext).
3159
+ - 4.0.0 files load unchanged (mode defaults to "none").
3160
+ - Plain 4.0.1 files are bit-identical to 4.0.0 format.
3161
+
3162
+ ================================================================================
3163
+ SECURITY MODEL
3164
+ ================================================================================
3165
+
3166
+ What encryption protects against:
3167
+ - Reading content without a password
3168
+ - Detection of any tampering with the ciphertext
3169
+ - Brute-forcing weak passwords (PBKDF2 with 600k iterations is intentionally slow)
3170
+
3171
+ What it does NOT protect against:
3172
+ - A user with sufficient access running their own decryption code (they have the password)
3173
+ - Side-channel attacks on the host (memory dumps, keyloggers, etc.)
3174
+ - Loss of all passwords AND the recovery key — the document is mathematically unrecoverable
3175
+ - A malicious EDOF library — verify the source
3176
+
3177
+ Recovery key is treated as an additional ADMIN-level slot keyed by the recovery string. If you lose it, the only way to regenerate one is to remove all passwords and re-protect the document (which requires the admin password).
3178
+
3179
+ ================================================================================
3180
+ FIXED
3181
+ ================================================================================
3182
+
3183
+ - `EdofSerializer` now reads `FORMAT_VERSION_STR` dynamically through the version module, so `export_3x()` can override it for the duration of a single save without leaking into other operations.
3184
+ - Editor `_gradient_editor` method properly registered (it was lost during 4.0.0 development).
3185
+
3186
+ ================================================================================
3187
+
3188
+
3189
+
3190
+ Major release: rich text, vector graphics, custom PDF writer, PDF import, formatted tables, and legacy EDOF 2 read support.
3191
+ This is a major version bump because the renderer, PDF subsystem, and Shape model received fundamental architectural changes. File-format compatibility is preserved - 3.x files load with automatic migration, and EDOF 2 files (legacy unreleased format) are now also readable in best-effort mode.
3192
+
3193
+ ================================================================================
3194
+ ADDED - Rich text & formatting
3195
+ ================================================================================
3196
+
3197
+ Rich text runs in TextBox
3198
+ - New TextRun dataclass: text segment with its own font_family, font_size, bold, italic, underline, strikethrough, color, background
3199
+ - TextBox.runs: list[TextRun] - when non-empty, replaces plain text + style rendering
3200
+ - Run-based layout engine: per-run measurement, wrap across run boundaries, mixed font sizes on the same line
3201
+ - Auto-shrink / auto-fill with runs: global scale factor s found by binary search and applied to all font_size values, preserving relative size ratios between runs
3202
+ - Per-run underline, strikethrough, background highlight rendering with correct horizontal extents
3203
+ - Backwards compatible: runs == [] keeps the original plain-text behaviour
3204
+
3205
+ Formatted tables
3206
+ - New Table object type (separate from Group)
3207
+ - TableCell with full styling: own TextStyle or runs[] for rich text, bg_color (RGBA), per-side border (top/right/bottom/left) with own color and width, padding, colspan, rowspan
3208
+ - Per-row and per-column custom widths/heights; auto-distribution if not specified
3209
+ - Cell content clipped at cell boundary
3210
+ - Editor: click to select cell, double-click to edit, right-click for cell formatting menu
3211
+
3212
+ ================================================================================
3213
+ ADDED - Vector graphics
3214
+ ================================================================================
3215
+
3216
+ Bezier path Shape
3217
+ - New shape type "path" - arbitrary vector path with line segments and Bezier curves
3218
+ - Shape.path_data: list[PathCommand] - SVG-style commands: M (moveto), L (lineto), C (cubic Bezier), Q (quadratic Bezier), Z (close)
3219
+ - Pixel-correct rendering via Pillow ImageDraw.line() for segments + de Casteljau subdivision for curves
3220
+ - Direct SVG path string parsing: Shape.from_svg_path("M 10 10 L 50 50 C ...")
3221
+
3222
+ Linear and radial gradients
3223
+ - FillStyle.gradient - replaces solid fill with multi-stop gradient
3224
+ - Linear: gradient_type="linear" with gradient_angle (deg) and gradient_stops=[(offset, rgba), ...]
3225
+ - Radial: gradient_type="radial" with gradient_center=(cx, cy) and gradient_radius
3226
+ - Renderer creates per-object gradient mask; full RGBA interpolation between stops
3227
+
3228
+ Path-based stroke styling
3229
+ - StrokeStyle.dash_pattern - list of mm values, e.g. [3, 2] for dashed line
3230
+ - StrokeStyle.cap - "butt", "round", "square"
3231
+ - StrokeStyle.join - "miter", "round", "bevel"
3232
+
3233
+ Blend modes
3234
+ - obj.blend_mode - "normal", "multiply", "screen", "overlay", "darken", "lighten"
3235
+ - Compositing via Pillow with custom pixel ops
3236
+
3237
+ ================================================================================
3238
+ ADDED - Custom vector PDF writer
3239
+ ================================================================================
3240
+
3241
+ Pure-Python PDF 1.7 writer (no reportlab dependency)
3242
+ - Native implementation of cross-reference table, object catalog, page tree, content streams
3243
+ - Standard 14 PDF fonts (Helvetica, Times, Courier with bold/italic) - zero embedding for these
3244
+ - TTF embedding for custom fonts (Type0/CID font + ToUnicode CMap + cidset)
3245
+ - System font name mapping (Arial -> Helvetica, Times New Roman -> Times-Roman, ...)
3246
+ - WinAnsiEncoding for Latin-1 incl. Czech diacritics; UTF-16BE for CID fonts
3247
+ - Vector text - searchable, copyable, selectable in PDF readers
3248
+ - Vector shapes (rect, ellipse, line, polygon, Bezier path)
3249
+ - Linear / radial gradient as PDF shading patterns
3250
+ - Images as XObject with FlateDecode (PNG-style) or DCTDecode (JPEG passthrough)
3251
+ - Multi-page support with shared resources
3252
+ - PDF metadata (title, author, subject, keywords, creator) via Info dictionary
3253
+ - Vector PDFs typically 5-15x smaller than rasterised PDFs
3254
+ - Default mode is vector; raster fallback: doc.export_pdf(path, vector=False)
3255
+
3256
+ ================================================================================
3257
+ ADDED - PDF -> EDOF import
3258
+ ================================================================================
3259
+
3260
+ edof.import_pdf(path) -> Document
3261
+
3262
+ Bidirectional PDF support - open existing PDFs as editable EDOF documents.
3263
+
3264
+ Text reconstruction
3265
+ - Per-page text spans extracted via pymupdf with bbox, font, size, color, bold/italic flags
3266
+ - Block clustering algorithm detects formatted text blocks:
3267
+ * Same font + size (5% tolerance) -> grouped together
3268
+ * Vertical gap <= font_size x 1.5 -> same paragraph
3269
+ * Similar X-alignment (left, center, justified) -> same column
3270
+ * Line-spacing tolerance - variable line gaps within a paragraph are merged when consistent
3271
+ * Indented paragraphs detected by first-line offset relative to subsequent lines
3272
+ * Hanging indents for bulleted lists detected separately
3273
+ - Heading detection: spans with significantly larger font size than median -> standalone TextBox marked as heading
3274
+ - List detection: spans starting with bullet/dash/number prefixes -> list item TextBoxes with proper indent
3275
+ - Mixed inline formatting within a block -> produces a rich-text TextBox with runs[]
3276
+
3277
+ Font handling
3278
+ - Standard 14 PDF fonts -> mapped via the alias system, no embedding needed
3279
+ - Fully embedded TrueType/OpenType -> font bytes extracted directly into .edof resources
3280
+ - Subsetted fonts ("AAAAAA+Arial" prefix), most common case:
3281
+ * First tries to find the full font locally via the alias system -> uses local copy (full editing capability)
3282
+ * If not found, embeds the subset anyway -> existing text renders correctly, but adding new characters to that font will warn in doc.errors
3283
+ * Both cases logged in doc.errors with the substitution decision
3284
+ - Type3 fonts (vector glyphs) -> individual glyphs converted to Shape path objects when extractable; otherwise replaced with the closest local font and logged
3285
+ - CID fonts (Asian scripts) -> handled transparently by pymupdf, embedded as full TTFs
3286
+
3287
+ Image extraction
3288
+ - Embedded raster images extracted with original encoding (PNG / JPEG)
3289
+ - Pixel position and clip preserved
3290
+ - One ImageBox per detected image
3291
+
3292
+ Vector graphics
3293
+ - PDF stroked/filled paths converted to Shape objects with "path" type
3294
+ - Color and stroke properties preserved
3295
+ - Bezier curves preserved as C commands (no rasterization)
3296
+
3297
+ Tables (heuristic)
3298
+ - Optional pdfplumber dependency: detects tabular grids from horizontal/vertical lines + clustered text spans
3299
+ - Detected tables become Table objects with TableCells preserving cell content and basic styling
3300
+ - When detection is uncertain, falls back to individual TextBoxes (logged in doc.errors)
3301
+
3302
+ API:
3303
+ doc = edof.import_pdf("template.pdf",
3304
+ detect_tables=True,
3305
+ merge_paragraphs=True,
3306
+ heading_threshold=1.4) # font_size > median * 1.4
3307
+
3308
+ CLI:
3309
+ edof-cli import template.pdf -o template.edof --detect-tables
3310
+
3311
+ Editor: File -> Import PDF...
3312
+
3313
+ ================================================================================
3314
+ ADDED - Legacy EDOF 2 read support
3315
+ ================================================================================
3316
+
3317
+ EDOF 2 was an internal pre-release format that was never publicly distributed. It had architectural problems that led to the redesign in EDOF 3. To support users who have legacy EDOF 2 archives, the loader now performs a best-effort migration.
3318
+
3319
+ - edof.load(path) auto-detects the file format (EDOF 4, EDOF 3, or EDOF 2)
3320
+ - EDOF 2 files identified by manifest version field or legacy structure markers
3321
+ - Best-effort migration:
3322
+ * Object types mapped to EDOF 4 equivalents where possible
3323
+ * Style properties translated (legacy enum values -> string constants)
3324
+ * Embedded resources preserved
3325
+ * Variable system mapped to new VariableStore (legacy unstructured names normalised)
3326
+ - Migration warnings recorded in doc.errors (does not abort)
3327
+ - One-way conversion only: EDOF 2 -> EDOF 4. The output cannot be saved back to EDOF 2.
3328
+ - CLI: edof-cli convert legacy.edof -o new.edof
3329
+ - After conversion, save as a current EDOF file: doc.save("new.edof")
3330
+
3331
+ ================================================================================
3332
+ ADDED - SVG export
3333
+ ================================================================================
3334
+
3335
+ - doc.export_svg(path, page=0) - one SVG file per page
3336
+ - Text rendered as <text> elements (searchable in browsers, indexable, copyable)
3337
+ - Shapes as native SVG: <rect>, <ellipse>, <line>, <polygon>, <path> (with full Bezier)
3338
+ - Gradients rendered as <linearGradient> / <radialGradient> definitions
3339
+ - Images embedded as base64 data URIs (PNG / JPEG)
3340
+ - Custom fonts embedded via @font-face with data URI
3341
+
3342
+ ================================================================================
3343
+ ADDED - Templating
3344
+ ================================================================================
3345
+
3346
+ Conditional visibility
3347
+ - obj.visible_if = "score > 90" - Python-style expression evaluated against document variables at render time
3348
+ - Safe evaluator: literals, comparisons (<, <=, ==, !=, >=, >), arithmetic, and/or/not, in/not in; no function calls, no imports, no attribute access
3349
+ - Syntax errors recorded in doc.errors without aborting render
3350
+ - Editor displays a small (i) badge on objects with conditions
3351
+
3352
+ Repeating sections
3353
+ - page.repeat_objects(template_objs, data_list, gap=2.0) - duplicates a group of objects for each row of data_list
3354
+ - Variable substitution per row: {column_name} placeholders inside text, runs[].text, qrcode.data, imagebox.variable are replaced with row values
3355
+ - Auto-pagination: when a row would overflow the page, a new page is created automatically with the same dimensions
3356
+ - Page-level header/footer objects can be marked repeat_on_pages=True so they appear on every generated page
3357
+
3358
+ ================================================================================
3359
+ ADDED - High-level API helpers
3360
+ ================================================================================
3361
+
3362
+ Configurable text padding
3363
+ - TextStyle.padding (mm) - default 1 mm (was hardcoded 2 mm); set to 0 for edge-to-edge text
3364
+ - Small textboxes (< 6 mm tall) are now usable
3365
+
3366
+ Font fallback & cross-platform aliases
3367
+ - load_font_safe() emits EdofMissingFontWarning instead of silently using a bitmap fallback
3368
+ - Fallback chain: DejaVu Sans -> Liberation Sans -> FreeSans
3369
+ - Cross-platform aliases for Arial, Helvetica, Times New Roman, Courier New, Calibri, Cambria, Verdana, Tahoma, Trebuchet MS, Georgia, Segoe UI, Comic Sans MS, Impact
3370
+
3371
+ High-level widgets
3372
+ - page.add_card(x, y, w, h, title, body, accent_color) - accent header + title + body
3373
+ - page.add_metric(x, y, w, h, label, value, subtitle, value_color) - large-value tile
3374
+ - page.add_table(x, y, w, rows, header, alternating, row_height) - quick table (now uses new Table object internally)
3375
+ - page.add_kv_list(x, y, w, items, key_width_frac) - key-value list
3376
+
3377
+ Layout helpers
3378
+ - page.row(y, gap, height) -> _RowContext with add_textbox, add_image, add_shape, skip, next_x
3379
+ - page.column(x, gap, width) -> _ColumnContext with add_textbox, add_textbox_auto, add_image, add_shape, skip, next_y
3380
+
3381
+ Auto-height textbox
3382
+ - page.add_textbox_auto(x, y, w, text, min_height, **style) - height computed from content
3383
+ - edof.measure_text_height(text, style, width_mm, dpi) - standalone helper
3384
+
3385
+ ================================================================================
3386
+ ADDED - Editor
3387
+ ================================================================================
3388
+
3389
+ Existing 3.x features retained
3390
+ - edof-editor and edof-cli console scripts
3391
+ - edof/editor_lang/en.json for translations (add XX.json for other languages)
3392
+ - Async rendering, type-aware property panel, object list panel
3393
+ - Inline text editor with WYSIWYG sizing across all zoom levels and Windows DPI scaling
3394
+ - Double-click QR / Image actions
3395
+ - 60-step undo/redo
3396
+
3397
+ New in 4.0
3398
+ - Rich text inline editor: double-click a TextBox with runs opens a formatting toolbar (bold, italic, underline, color, font, size) for selected text
3399
+ - Cell editor for Table objects: click cell to select, double-click to edit content, right-click for cell formatting
3400
+ - Path drawing tool: draw arbitrary Bezier paths; convert any shape to path for editing
3401
+ - Cursor position in mm in status bar (live update during mouse move)
3402
+ - Find & Replace dialog (Ctrl+F): searches all TextBoxes on all pages, optional case-sensitive, regex, whole-word
3403
+ - Snap-to-grid: toggleable grid snap during drag, configurable spacing
3404
+ - Alignment guides: magnetic alignment to other objects' edges/centres while dragging
3405
+ - Multi-select: Ctrl+click adds to selection, lasso drag-rectangle, group move + batch property edit
3406
+ - Template gallery: File -> New from Template (invoice, certificate, business card, A4 label sheet)
3407
+ - CSV batch export: File -> Batch Fill from CSV...
3408
+ - Import PDF: File -> Import PDF...
3409
+ - Convert legacy EDOF 2: File -> Open... auto-detects and converts on load
3410
+ - Gradient editor: visual stop editor for fill gradients
3411
+ - Layer panel: dedicated dock with drag-to-reorder, eye/lock toggles per object
3412
+
3413
+ Print preview fixes (carried from 3.x)
3414
+ - Raw PIL bytes via QImage constructor -> bypasses Qt 256 MB image allocation limit
3415
+ - painter.viewport() for correct page rect (was blank pages)
3416
+ - QPageSize(QPageSize.PageSizeId.A4) (PyQt6-correct API)
3417
+ - Preview renders at <= 150 dpi regardless of printer DPI
3418
+
3419
+ ================================================================================
3420
+ ADDED - CLI
3421
+ ================================================================================
3422
+
3423
+ - edof-cli info template.edof - metadata, variables, editable fields, fonts used
3424
+ - edof-cli objects template.edof - all objects with layer, type, variable
3425
+ - edof-cli validate template.edof - structural validation
3426
+ - edof-cli export template.edof out.png --set name=Jan - fill and export
3427
+ - edof-cli batch template.edof data.csv -o "out_{n}.png" - CSV batch export
3428
+ - edof-cli import template.pdf -o template.edof - PDF -> EDOF
3429
+ - edof-cli convert legacy.edof -o template.edof - EDOF 2 -> EDOF 4 conversion
3430
+ - --vector / --raster flag for PDF export
3431
+ - --svg for SVG export
3432
+ - --all-pages, --dpi, --format, --color-space overrides
3433
+
3434
+ ================================================================================
3435
+ FIXED - carried from 3.x development
3436
+ ================================================================================
3437
+
3438
+ - Inline text editor keyboard input (replaced QGraphicsProxyWidget with QPlainTextEdit viewport child)
3439
+ - Inline text editor font size = font_pt x RDPI x zoom / logical_dpi (correct WYSIWYG at all zoom + Windows DPI scaling)
3440
+ - Auto-shrink / auto-fill DPI conversion (pt -> px = pt x dpi / 72)
3441
+ - Rotation handle direction sign error
3442
+ - Rotated object resize keeps anchor fixed
3443
+ - Middle-mouse pan (scroll bar delta)
3444
+ - Toolbar/menu items missing (semicolon bug if key: ...; addAction())
3445
+ - Layer ordering proper swap
3446
+ - QR codes with non-black colors (B&W render then colorise)
3447
+ - get_resolved_text falls back to obj.text for empty variable values
3448
+ - Hidden objects show as ghost outline, still selectable
3449
+
3450
+ ================================================================================
3451
+ CHANGED - breaking
3452
+ ================================================================================
3453
+
3454
+ - doc.export_pdf() defaults to vector mode (was raster); doc.export_pdf(path, vector=False) for raster fallback
3455
+ - Internal add_table helper produces a Table object (was a Group of TextBoxes); existing .edof files with the old layout still load via auto-migration
3456
+ - Shape.path_data field added - old Shape instances without this field load with path_data = [] (no behaviour change)
3457
+ - TextStyle.padding default 1.0 mm (was hardcoded 2.0 mm in renderer)
3458
+ - FillStyle.gradient field added - old FillStyle instances load with gradient = None (no behaviour change)
3459
+
3460
+ ================================================================================
3461
+ CHANGED - non-breaking
3462
+ ================================================================================
3463
+
3464
+ - reportlab is no longer a hard requirement - only used as fallback if vector=False is requested with reportlab installed
3465
+ - pymupdf added to edof[pdf] extras for the new PDF writer and PDF importer
3466
+ - pdfplumber added to edof[pdf] extras as optional table-detection helper
3467
+
3468
+ ================================================================================
3469
+ FILE FORMAT
3470
+ ================================================================================
3471
+
3472
+ - Format version bumped to 4.0.0
3473
+ - Forward compat: 3.x files load and migrate automatically; new 4.x fields default to neutral values that preserve existing rendering
3474
+ - Legacy EDOF 2 read support: best-effort migration on load (one-way; output is always 4.x)
3475
+ - Backward compat for 3.x consumers: 4.x files using only 3.x features can be downgraded with doc.export_3x() (best-effort: rich-text runs collapsed to plain text, paths rasterised to bitmap shapes, tables flattened to groups)
3476
+
3477
+ ================================================================================
3478
+ REMOVED
3479
+ ================================================================================
3480
+
3481
+ - Old raster-only pdf.py writer (replaced by vector writer with raster fallback)
3482
+
3483
+ ================================================================================
3484
+ MIGRATION GUIDE (3.x -> 4.x)
3485
+ ================================================================================
3486
+
3487
+ - All 3.x scripts continue to work without changes
3488
+ - doc.export_pdf("out.pdf") now produces vector PDF; if you specifically need raster (e.g., for compatibility with old PDF/A profiles), pass vector=False
3489
+ - If you used add_table and relied on iterating its child TextBoxes, switch to Table.cells instead
3490
+ - New rich-text features are opt-in; plain TextBox.text continues to work
3491
+ - Legacy EDOF 2 files: loading works automatically. To convert in bulk:
3492
+ for f in glob("legacy/*.edof"):
3493
+ doc = edof.load(f)
3494
+ doc.save(f.replace("legacy/", "converted/"))
3495
+
3496
+ ================================================================================
3497
+ ================================================================================
3498
+
3499
+ ## [3.0.2] - 2025-04-15
3500
+
3501
+ ### Fixed
3502
+ - attestations: false in CI workflow to fix failed PyPI publish via GitHub Actions
3503
+
3504
+ ================================================================================
3505
+
3506
+ ## [3.0.1] - 2025-04-14
3507
+
3508
+ ### Added
3509
+ - edof-editor and edof-cli console scripts
3510
+ - editor_lang/en.json for editor i18n
3511
+
3512
+ ### Fixed
3513
+ - Print preview blank pages, Qt 256 MB image limit, QPrinter.PageSize API
3514
+ - Inline text editor keyboard input and WYSIWYG font sizing
3515
+ - Auto-shrink / auto-fill DPI conversion
3516
+ - Rotation handle direction, rotated resize anchor, middle-mouse pan
3517
+ - Toolbar items missing (semicolon bug)
3518
+ - Layer ordering swap logic
3519
+ - QR codes with non-black colors
3520
+ - Variable binding clearing text
3521
+ - Hidden objects unselectable
3522
+
3523
+ ================================================================================
3524
+
3525
+ ## [3.0.0] - 2025-01-01
3526
+
3527
+ Initial public release.
3528
+
3529
+ - Document model: TextBox, ImageBox, Shape, Line, QRCode, Group
3530
+ - Variable/template system with type validation and batch fill
3531
+ - Pillow RGBA renderer; RGB/RGBA/L/1/CMYK; 8/16-bit
3532
+ - .edof ZIP format with embedded assets
3533
+ - Export: PNG/JPEG/TIFF/BMP/PDF; CLI tool; PyQt6 desktop editor
3534
+ - Command API with undo/redo
3535
+
3536
+ ================================================================================
3537
+
3538
+ Note: Versions 1.x were internal iterations not publicly released.
3539
+ EDOF 2 was a separate pre-release format with architectural problems that led to the redesign in EDOF 3. EDOF 2 archives can be read by EDOF 4+ in best-effort mode but cannot be written back to.