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.
- edof-4.3.0/CHANGELOG.md +3539 -0
- edof-4.3.0/MANIFEST.in +6 -0
- {edof-4.2.4 → edof-4.3.0}/PKG-INFO +22 -5
- {edof-4.2.4 → edof-4.3.0}/README.md +19 -4
- edof-4.3.0/docs/INSTALL.md +141 -0
- edof-4.3.0/docs/QUICKSTART.md +208 -0
- edof-4.3.0/docs/README.md +65 -0
- edof-4.3.0/docs/advanced/extending.md +335 -0
- edof-4.3.0/docs/advanced/file-format.md +499 -0
- edof-4.3.0/docs/advanced/troubleshooting.md +353 -0
- edof-4.3.0/docs/cookbook/batch-pdf.md +314 -0
- edof-4.3.0/docs/cookbook/certificate.md +219 -0
- edof-4.3.0/docs/cookbook/effects-poster.md +84 -0
- edof-4.3.0/docs/cookbook/encrypted-template.md +289 -0
- edof-4.3.0/docs/cookbook/invoice.md +345 -0
- edof-4.3.0/docs/cookbook/pdf-import-edit.md +280 -0
- edof-4.3.0/docs/reference/01-document.md +431 -0
- edof-4.3.0/docs/reference/02-objects.md +443 -0
- edof-4.3.0/docs/reference/03-styles.md +308 -0
- edof-4.3.0/docs/reference/04-variables.md +309 -0
- edof-4.3.0/docs/reference/05-export.md +278 -0
- edof-4.3.0/docs/reference/06-import.md +160 -0
- edof-4.3.0/docs/reference/07-encryption.md +411 -0
- edof-4.3.0/docs/reference/08-editor.md +465 -0
- edof-4.3.0/docs/reference/09-cli.md +329 -0
- edof-4.3.0/docs/reference/10-helpers.md +267 -0
- edof-4.3.0/docs/reference/11-docx.md +132 -0
- edof-4.3.0/docs/reference/12-effects.md +133 -0
- edof-4.3.0/docs/reference/API.md +1874 -0
- {edof-4.2.4 → edof-4.3.0}/edof/_apps/assets/__init__.py +25 -0
- edof-4.3.0/edof/_apps/assets/cursors/cur-add_point.png +0 -0
- edof-4.3.0/edof/_apps/assets/cursors/cur-crosshair.png +0 -0
- edof-4.3.0/edof/_apps/assets/cursors/cur-eyedropper.png +0 -0
- edof-4.3.0/edof/_apps/assets/cursors/cur-hand.png +0 -0
- edof-4.3.0/edof/_apps/assets/cursors/cur-hand_grab.png +0 -0
- edof-4.3.0/edof/_apps/assets/cursors/cur-move.png +0 -0
- edof-4.3.0/edof/_apps/assets/cursors/cur-not_allowed.png +0 -0
- edof-4.3.0/edof/_apps/assets/cursors/cur-pen.png +0 -0
- edof-4.3.0/edof/_apps/assets/cursors/cur-pen_close.png +0 -0
- edof-4.3.0/edof/_apps/assets/cursors/cur-remove_point.png +0 -0
- edof-4.3.0/edof/_apps/assets/cursors/cur-resize_ew.png +0 -0
- edof-4.3.0/edof/_apps/assets/cursors/cur-resize_nesw.png +0 -0
- edof-4.3.0/edof/_apps/assets/cursors/cur-resize_ns.png +0 -0
- edof-4.3.0/edof/_apps/assets/cursors/cur-resize_nwse.png +0 -0
- edof-4.3.0/edof/_apps/assets/cursors/cur-rotate.png +0 -0
- edof-4.3.0/edof/_apps/assets/cursors/cur-tangent.png +0 -0
- edof-4.3.0/edof/_apps/assets/cursors/cur-text.png +0 -0
- edof-4.3.0/edof/_apps/assets/cursors/cur-zoom_in.png +0 -0
- edof-4.3.0/edof/_apps/assets/cursors/cur-zoom_out.png +0 -0
- edof-4.3.0/edof/_apps/assets/cursors/cursors.json +232 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-align-center.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-align-left.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-align-right.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-blend.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-bold.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-delete.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-duplicate.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-ellipse.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-export.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-eyedropper.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-fit.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-fx.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-grid.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-group.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-guides.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-hand.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-hidden.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-image.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-italic.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-layer-down.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-layer-up.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-line.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-lock.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-new.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-open.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-pen.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-print.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-qr.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-rect.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-redo.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-save.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-select.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-snap.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-strike.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-table.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-text.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-underline.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-undo.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-ungroup.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-unlock.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-visible.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-zoom-in.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-zoom-out.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/ic-zoom.png +0 -0
- edof-4.3.0/edof/_apps/assets/ui_icons/icons.json +356 -0
- {edof-4.2.4 → edof-4.3.0}/edof/_apps/editor.py +3804 -521
- {edof-4.2.4 → edof-4.3.0}/edof/_apps/edof_text_editor.py +61 -17
- {edof-4.2.4 → edof-4.3.0}/edof/_apps/viewer.py +1 -1
- {edof-4.2.4 → edof-4.3.0}/edof/api/commands.py +1 -1
- {edof-4.2.4 → edof-4.3.0}/edof/crypto/document_protection.py +1 -1
- {edof-4.2.4 → edof-4.3.0}/edof/engine/color.py +0 -1
- {edof-4.2.4 → edof-4.3.0}/edof/engine/debug_log.py +9 -2
- {edof-4.2.4 → edof-4.3.0}/edof/engine/document_flow.py +1 -1
- {edof-4.2.4 → edof-4.3.0}/edof/engine/document_paginate.py +44 -6
- edof-4.3.0/edof/engine/gpu.py +1054 -0
- edof-4.3.0/edof/engine/renderer.py +2723 -0
- {edof-4.2.4 → edof-4.3.0}/edof/engine/text_engine.py +43 -7
- {edof-4.2.4 → edof-4.3.0}/edof/engine/text_layout.py +112 -12
- {edof-4.2.4 → edof-4.3.0}/edof/export/pdf.py +0 -47
- {edof-4.2.4 → edof-4.3.0}/edof/export/pdf_writer.py +2 -7
- {edof-4.2.4 → edof-4.3.0}/edof/format/document.py +2 -1
- {edof-4.2.4 → edof-4.3.0}/edof/format/document_body.py +41 -1
- {edof-4.2.4 → edof-4.3.0}/edof/format/document_boxes.py +1 -1
- {edof-4.2.4 → edof-4.3.0}/edof/format/objects.py +5 -1
- {edof-4.2.4 → edof-4.3.0}/edof/format/serializer.py +1 -0
- {edof-4.2.4 → edof-4.3.0}/edof/format/styles.py +132 -3
- {edof-4.2.4 → edof-4.3.0}/edof/format/svg_io.py +0 -1
- {edof-4.2.4 → edof-4.3.0}/edof/gui/pyqt6_widget.py +2 -1
- {edof-4.2.4 → edof-4.3.0}/edof/gui/tkinter_canvas.py +0 -1
- {edof-4.2.4 → edof-4.3.0}/edof/utils/legacy_v2.py +1 -1
- {edof-4.2.4 → edof-4.3.0}/edof/utils/pdf_import.py +1 -1
- {edof-4.2.4 → edof-4.3.0}/edof/utils/qr.py +1 -1
- {edof-4.2.4 → edof-4.3.0}/edof/utils/rtf.py +1 -1
- {edof-4.2.4 → edof-4.3.0}/edof/utils/safe_eval.py +1 -1
- {edof-4.2.4 → edof-4.3.0}/edof/version.py +2 -2
- {edof-4.2.4 → edof-4.3.0}/edof.egg-info/PKG-INFO +22 -5
- edof-4.3.0/edof.egg-info/SOURCES.txt +188 -0
- {edof-4.2.4 → edof-4.3.0}/edof.egg-info/entry_points.txt +1 -0
- {edof-4.2.4 → edof-4.3.0}/edof.egg-info/requires.txt +3 -0
- edof-4.3.0/examples/01_hello_document.py +20 -0
- edof-4.3.0/examples/02_table_and_rich_text.py +22 -0
- edof-4.3.0/examples/03_vector_gradients.py +24 -0
- edof-4.3.0/examples/04_layer_effects.py +40 -0
- edof-4.3.0/examples/05_variables_template.py +16 -0
- edof-4.3.0/examples/06_save_load_roundtrip.py +15 -0
- edof-4.3.0/examples/README.md +19 -0
- {edof-4.2.4 → edof-4.3.0}/pyproject.toml +8 -1
- {edof-4.2.4 → edof-4.3.0}/tests/test_v403.py +11 -5
- edof-4.3.0/version.txt +1 -0
- edof-4.2.4/edof/engine/renderer.py +0 -1393
- edof-4.2.4/edof.egg-info/SOURCES.txt +0 -87
- {edof-4.2.4 → edof-4.3.0}/LICENSE +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/__init__.py +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/_apps/__init__.py +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/_apps/_assoc_dialog.py +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/_apps/assets/icons/__init__.py +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/_apps/assets/icons/edof-document-512.png +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/_apps/assets/icons/edof-document.icns +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/_apps/assets/icons/edof-document.ico +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/_apps/assets/icons/edof-document.png +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/_apps/assets/icons/edof-editor-512.png +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/_apps/assets/icons/edof-editor.icns +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/_apps/assets/icons/edof-editor.ico +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/_apps/assets/icons/edof-editor.png +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/_apps/assets/icons/edof-viewer-512.png +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/_apps/assets/icons/edof-viewer.icns +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/_apps/assets/icons/edof-viewer.ico +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/_apps/assets/icons/edof-viewer.png +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/_apps/cli.py +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/_apps/file_assoc.py +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/_apps/shortcuts.py +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/api/__init__.py +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/crypto/__init__.py +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/crypto/encryption.py +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/crypto/permissions.py +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/editor_lang/en.json +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/engine/__init__.py +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/engine/textbox_flow.py +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/engine/transform.py +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/exceptions.py +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/export/__init__.py +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/export/bitmap.py +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/export/legacy_v3.py +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/export/printer.py +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/export/svg.py +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/format/__init__.py +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/format/variables.py +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/gui/__init__.py +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/interop/__init__.py +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/interop/docx_io.py +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/py.typed +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/units.py +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/utils/__init__.py +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof/utils/compat.py +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof.egg-info/dependency_links.txt +0 -0
- {edof-4.2.4 → edof-4.3.0}/edof.egg-info/top_level.txt +0 -0
- {edof-4.2.4 → edof-4.3.0}/setup.cfg +0 -0
- {edof-4.2.4 → edof-4.3.0}/tests/test_v310.py +0 -0
- {edof-4.2.4 → edof-4.3.0}/tests/test_v400.py +0 -0
- {edof-4.2.4 → edof-4.3.0}/tests/test_v401.py +0 -0
- {edof-4.2.4 → edof-4.3.0}/tests/test_v402.py +0 -0
- {edof-4.2.4 → edof-4.3.0}/tests/test_v410.py +0 -0
edof-4.3.0/CHANGELOG.md
ADDED
|
@@ -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.
|