edof 3.0.0__py3-none-any.whl

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/__init__.py ADDED
@@ -0,0 +1,91 @@
1
+ # edof/__init__.py
2
+ """
3
+ from __future__ import annotations
4
+ edof – Easy Document Format 3.0
5
+ ================================
6
+ Programmatic document creation, template filling, and export.
7
+
8
+ Quick start::
9
+
10
+ import edof
11
+
12
+ doc = edof.Document(title="Hello")
13
+ page = doc.add_page()
14
+ tb = page.add_textbox(10, 10, 120, 20, "Hello, World!")
15
+ tb.style.font_size = 36
16
+ tb.style.auto_shrink = True
17
+ doc.save("hello.edof")
18
+ doc.export_bitmap("hello.png", dpi=300)
19
+ """
20
+
21
+ from edof.version import __version__, FORMAT_VERSION_STR # noqa: F401
22
+ from edof.exceptions import ( # noqa: F401
23
+ EdofError, EdofVersionError, EdofResourceError,
24
+ EdofRenderError, EdofVariableError, EdofAPIError,
25
+ EdofValidationError, EdofPrintError,
26
+ EdofNewerVersionWarning, EdofMissingOptionalWarning,
27
+ )
28
+
29
+ from edof.format.document import Document, Page, ResourceStore # noqa: F401
30
+ from edof.format.objects import ( # noqa: F401
31
+ EdofObject, TextBox, ImageBox, Shape, QRCode, Group,
32
+ SHAPE_RECT, SHAPE_ELLIPSE, SHAPE_LINE, SHAPE_POLYGON, SHAPE_ARROW,
33
+ )
34
+ from edof.format.styles import TextStyle, StrokeStyle, FillStyle, ShadowStyle # noqa: F401
35
+ from edof.format.variables import VariableStore, VariableDef # noqa: F401
36
+ from edof.format.variables import ( # noqa: F401
37
+ VAR_TEXT, VAR_IMAGE, VAR_NUMBER, VAR_DATE, VAR_BOOL, VAR_QR, VAR_URL,
38
+ )
39
+ from edof.format.serializer import EdofSerializer # noqa: F401
40
+ from edof.format.document import ( # noqa: F401
41
+ CS_RGB, CS_RGBA, CS_GRAY, CS_BW, CS_CMYK, BD_8, BD_16,
42
+ )
43
+ from edof.engine.transform import Transform, to_mm, from_mm, mm_to_px # noqa: F401
44
+ from edof.engine.renderer import render_page, render_document # noqa: F401
45
+ from edof.export.bitmap import ( # noqa: F401
46
+ export_page_bitmap, export_all_pages, export_to_bytes,
47
+ )
48
+
49
+ # ── Convenience aliases ───────────────────────────────────────────────────────
50
+
51
+ def load(path: str) -> Document:
52
+ """Load an .edof file and return a Document."""
53
+ return Document.load(path)
54
+
55
+
56
+ def new(width: float = 210.0, height: float = 297.0, **kwargs) -> Document:
57
+ """Create a new blank Document."""
58
+ return Document(width=width, height=height, **kwargs)
59
+
60
+
61
+ __all__ = [
62
+ # Core
63
+ "Document", "Page", "ResourceStore",
64
+ # Objects
65
+ "EdofObject", "TextBox", "ImageBox", "Shape", "QRCode", "Group",
66
+ "SHAPE_RECT", "SHAPE_ELLIPSE", "SHAPE_LINE", "SHAPE_POLYGON", "SHAPE_ARROW",
67
+ # Styles
68
+ "TextStyle", "StrokeStyle", "FillStyle", "ShadowStyle",
69
+ # Variables
70
+ "VariableStore", "VariableDef",
71
+ "VAR_TEXT", "VAR_IMAGE", "VAR_NUMBER", "VAR_DATE", "VAR_BOOL", "VAR_QR", "VAR_URL",
72
+ # Serializer
73
+ "EdofSerializer",
74
+ # Color-space / bit-depth constants
75
+ "CS_RGB", "CS_RGBA", "CS_GRAY", "CS_BW", "CS_CMYK", "BD_8", "BD_16",
76
+ # Transform
77
+ "Transform", "to_mm", "from_mm", "mm_to_px",
78
+ # Render
79
+ "render_page", "render_document",
80
+ # Export helpers
81
+ "export_page_bitmap", "export_all_pages", "export_to_bytes",
82
+ # Convenience
83
+ "load", "new",
84
+ # Version
85
+ "__version__", "FORMAT_VERSION_STR",
86
+ # Exceptions
87
+ "EdofError", "EdofVersionError", "EdofResourceError",
88
+ "EdofRenderError", "EdofVariableError", "EdofAPIError",
89
+ "EdofValidationError", "EdofPrintError",
90
+ "EdofNewerVersionWarning", "EdofMissingOptionalWarning",
91
+ ]
edof/api/__init__.py ADDED
File without changes
edof/api/commands.py ADDED
@@ -0,0 +1,326 @@
1
+ # edof/api/commands.py
2
+ """
3
+ Command API – a string-based command system usable from:
4
+ • editor undo/redo stacks
5
+ • external scripts / automation
6
+ • network / IPC APIs
7
+
8
+ Every command is a plain dict:
9
+ {"cmd": "set_text", "object_id": "...", "text": "Hello"}
10
+
11
+ The CommandRegistry maps command names → handler functions.
12
+ The CommandHistory manages undo/redo.
13
+ """
14
+
15
+ from __future__ import annotations
16
+ from dataclasses import dataclass, field
17
+ from typing import Any, Callable, Dict, List, Optional, TYPE_CHECKING
18
+
19
+ if TYPE_CHECKING:
20
+ from edof.format.document import Document
21
+
22
+ Handler = Callable[["Document", dict], Any]
23
+
24
+
25
+ # ── Registry ──────────────────────────────────────────────────────────────────
26
+
27
+ class CommandRegistry:
28
+ def __init__(self) -> None:
29
+ self._handlers: Dict[str, Handler] = {}
30
+ self._register_builtins()
31
+
32
+ def register(self, name: str, handler: Handler) -> None:
33
+ self._handlers[name] = handler
34
+
35
+ def execute(self, doc: "Document", command: dict) -> Any:
36
+ from edof.exceptions import EdofAPIError
37
+ name = command.get("cmd")
38
+ if not name:
39
+ raise EdofAPIError("Command dict must have a 'cmd' key.")
40
+ handler = self._handlers.get(name)
41
+ if handler is None:
42
+ raise EdofAPIError(f"Unknown command: {name!r}")
43
+ return handler(doc, command)
44
+
45
+ # ── Built-in commands ──────────────────────────────────────────────────────
46
+
47
+ def _register_builtins(self) -> None:
48
+ self.register("set_text", _cmd_set_text)
49
+ self.register("set_variable", _cmd_set_variable)
50
+ self.register("fill_variables", _cmd_fill_variables)
51
+ self.register("add_page", _cmd_add_page)
52
+ self.register("remove_page", _cmd_remove_page)
53
+ self.register("add_textbox", _cmd_add_textbox)
54
+ self.register("add_image", _cmd_add_image)
55
+ self.register("add_shape", _cmd_add_shape)
56
+ self.register("add_qrcode", _cmd_add_qrcode)
57
+ self.register("remove_object", _cmd_remove_object)
58
+ self.register("move_object", _cmd_move_object)
59
+ self.register("resize_object", _cmd_resize_object)
60
+ self.register("rotate_object", _cmd_rotate_object)
61
+ self.register("set_style", _cmd_set_style)
62
+ self.register("set_visibility", _cmd_set_visibility)
63
+ self.register("set_layer", _cmd_set_layer)
64
+ self.register("export_bitmap", _cmd_export_bitmap)
65
+ self.register("export_pdf", _cmd_export_pdf)
66
+ self.register("save", _cmd_save)
67
+ self.register("validate", _cmd_validate)
68
+
69
+
70
+ # ── Command handlers ──────────────────────────────────────────────────────────
71
+
72
+ def _get_obj(doc: "Document", command: dict):
73
+ from edof.exceptions import EdofAPIError
74
+ oid = command.get("object_id")
75
+ if not oid:
76
+ raise EdofAPIError("'object_id' required.")
77
+ page_idx = int(command.get("page", 0))
78
+ page = doc.pages[page_idx]
79
+ obj = page.get_object(oid)
80
+ if obj is None:
81
+ raise EdofAPIError(f"Object {oid!r} not found on page {page_idx}.")
82
+ return obj
83
+
84
+
85
+ def _cmd_set_text(doc, cmd):
86
+ obj = _get_obj(doc, cmd)
87
+ from edof.format.objects import TextBox
88
+ if not isinstance(obj, TextBox):
89
+ from edof.exceptions import EdofAPIError
90
+ raise EdofAPIError("Object is not a TextBox.")
91
+ obj.text = str(cmd.get("text", ""))
92
+ return obj.id
93
+
94
+
95
+ def _cmd_set_variable(doc, cmd):
96
+ name = cmd.get("name")
97
+ value = cmd.get("value", "")
98
+ doc.set_variable(name, value)
99
+ return name
100
+
101
+
102
+ def _cmd_fill_variables(doc, cmd):
103
+ mapping = cmd.get("mapping", {})
104
+ doc.fill_variables(mapping)
105
+ return list(mapping.keys())
106
+
107
+
108
+ def _cmd_add_page(doc, cmd):
109
+ page = doc.add_page(
110
+ width=cmd.get("width"), height=cmd.get("height"),
111
+ dpi=cmd.get("dpi"), color_space=cmd.get("color_space"),
112
+ )
113
+ return page.id
114
+
115
+
116
+ def _cmd_remove_page(doc, cmd):
117
+ doc.remove_page(int(cmd.get("index", 0)))
118
+
119
+
120
+ def _cmd_add_textbox(doc, cmd):
121
+ page = doc.pages[int(cmd.get("page", 0))]
122
+ tb = page.add_textbox(
123
+ x=float(cmd.get("x", 0)),
124
+ y=float(cmd.get("y", 0)),
125
+ width=float(cmd.get("width", 80)),
126
+ height=float(cmd.get("height", 20)),
127
+ text=str(cmd.get("text", "")),
128
+ unit=cmd.get("unit", "mm"),
129
+ )
130
+ if "variable" in cmd:
131
+ tb.variable = cmd["variable"]
132
+ return tb.id
133
+
134
+
135
+ def _cmd_add_image(doc, cmd):
136
+ page = doc.pages[int(cmd.get("page", 0))]
137
+ rid = cmd.get("resource_id") or doc.add_resource_from_file(cmd["path"])
138
+ ib = page.add_image(resource_id=rid,
139
+ x=float(cmd.get("x", 0)),
140
+ y=float(cmd.get("y", 0)),
141
+ width=float(cmd.get("width", 50)),
142
+ height=float(cmd.get("height", 50)),
143
+ unit=cmd.get("unit", "mm"))
144
+ return ib.id
145
+
146
+
147
+ def _cmd_add_shape(doc, cmd):
148
+ page = doc.pages[int(cmd.get("page", 0))]
149
+ sh = page.add_shape(
150
+ shape_type=cmd.get("shape_type", "rect"),
151
+ x=float(cmd.get("x", 0)), y=float(cmd.get("y", 0)),
152
+ width=float(cmd.get("width", 50)), height=float(cmd.get("height", 30)),
153
+ unit=cmd.get("unit", "mm"),
154
+ )
155
+ return sh.id
156
+
157
+
158
+ def _cmd_add_qrcode(doc, cmd):
159
+ page = doc.pages[int(cmd.get("page", 0))]
160
+ qr = page.add_qrcode(
161
+ data=cmd.get("data", ""),
162
+ x=float(cmd.get("x", 0)), y=float(cmd.get("y", 0)),
163
+ size=float(cmd.get("size", 30)),
164
+ unit=cmd.get("unit", "mm"),
165
+ error_correction=cmd.get("error_correction", "M"),
166
+ )
167
+ return qr.id
168
+
169
+
170
+ def _cmd_remove_object(doc, cmd):
171
+ page = doc.pages[int(cmd.get("page", 0))]
172
+ return page.remove_object(cmd.get("object_id", ""))
173
+
174
+
175
+ def _cmd_move_object(doc, cmd):
176
+ obj = _get_obj(doc, cmd)
177
+ obj.transform.translate(
178
+ float(cmd.get("dx", 0)),
179
+ float(cmd.get("dy", 0)),
180
+ cmd.get("unit", "mm"),
181
+ )
182
+
183
+
184
+ def _cmd_resize_object(doc, cmd):
185
+ obj = _get_obj(doc, cmd)
186
+ if "factor" in cmd:
187
+ obj.transform.resize_uniform(float(cmd["factor"]))
188
+ else:
189
+ obj.transform.resize_free(
190
+ float(cmd.get("width", obj.transform.width)),
191
+ float(cmd.get("height", obj.transform.height)),
192
+ cmd.get("unit", "mm"),
193
+ cmd.get("anchor", "top-left"),
194
+ )
195
+
196
+
197
+ def _cmd_rotate_object(doc, cmd):
198
+ obj = _get_obj(doc, cmd)
199
+ if "angle_absolute" in cmd:
200
+ obj.transform.rotate_to(float(cmd["angle_absolute"]))
201
+ else:
202
+ obj.transform.rotate(float(cmd.get("angle", 0)))
203
+
204
+
205
+ def _cmd_set_style(doc, cmd):
206
+ obj = _get_obj(doc, cmd)
207
+ from edof.format.objects import TextBox
208
+ if isinstance(obj, TextBox):
209
+ for k, v in cmd.get("style", {}).items():
210
+ if hasattr(obj.style, k):
211
+ setattr(obj.style, k, v)
212
+
213
+
214
+ def _cmd_set_visibility(doc, cmd):
215
+ obj = _get_obj(doc, cmd)
216
+ obj.visible = bool(cmd.get("visible", True))
217
+
218
+
219
+ def _cmd_set_layer(doc, cmd):
220
+ obj = _get_obj(doc, cmd)
221
+ obj.layer = int(cmd.get("layer", 0))
222
+
223
+
224
+ def _cmd_export_bitmap(doc, cmd):
225
+ from edof.export.bitmap import export_page_bitmap
226
+ export_page_bitmap(
227
+ doc,
228
+ page_index=int(cmd.get("page", 0)),
229
+ path=cmd["path"],
230
+ dpi=cmd.get("dpi"),
231
+ color_space=cmd.get("color_space"),
232
+ format=cmd.get("format", "PNG"),
233
+ )
234
+
235
+
236
+ def _cmd_export_pdf(doc, cmd):
237
+ from edof.export.pdf import export_pdf
238
+ export_pdf(doc, cmd["path"])
239
+
240
+
241
+ def _cmd_save(doc, cmd):
242
+ doc.save(cmd["path"])
243
+
244
+
245
+ def _cmd_validate(doc, cmd):
246
+ return doc.validate()
247
+
248
+
249
+ # ── History (undo/redo) ────────────────────────────────────────────────────────
250
+
251
+ @dataclass
252
+ class HistoryEntry:
253
+ description: str
254
+ snapshot: bytes # serialised document state
255
+
256
+
257
+ class CommandHistory:
258
+ """
259
+ Undo/redo stack based on document snapshots.
260
+ Each snapshot is a full .edof file in memory (~fast for small documents).
261
+ """
262
+
263
+ def __init__(self, max_undo: int = 50) -> None:
264
+ self._stack: List[HistoryEntry] = []
265
+ self._pointer: int = -1
266
+ self._max: int = max_undo
267
+
268
+ def _snapshot(self, doc: "Document") -> bytes:
269
+ from edof.format.serializer import EdofSerializer
270
+ return EdofSerializer.to_bytes(doc)
271
+
272
+ def _restore(self, data: bytes) -> "Document":
273
+ from edof.format.serializer import EdofSerializer
274
+ return EdofSerializer.from_bytes(data)
275
+
276
+ def push(self, doc: "Document", description: str = "") -> None:
277
+ # Drop any redo states ahead of current pointer
278
+ self._stack = self._stack[:self._pointer + 1]
279
+ entry = HistoryEntry(description=description,
280
+ snapshot=self._snapshot(doc))
281
+ self._stack.append(entry)
282
+ if len(self._stack) > self._max:
283
+ self._stack.pop(0)
284
+ self._pointer = len(self._stack) - 1
285
+
286
+ def undo(self, doc: "Document") -> Optional["Document"]:
287
+ if self._pointer <= 0:
288
+ return None
289
+ self._pointer -= 1
290
+ return self._restore(self._stack[self._pointer].snapshot)
291
+
292
+ def redo(self, doc: "Document") -> Optional["Document"]:
293
+ if self._pointer >= len(self._stack) - 1:
294
+ return None
295
+ self._pointer += 1
296
+ return self._restore(self._stack[self._pointer].snapshot)
297
+
298
+ def can_undo(self) -> bool:
299
+ return self._pointer > 0
300
+
301
+ def can_redo(self) -> bool:
302
+ return self._pointer < len(self._stack) - 1
303
+
304
+ def description_undo(self) -> str:
305
+ if self._pointer > 0:
306
+ return self._stack[self._pointer].description
307
+ return ""
308
+
309
+ def description_redo(self) -> str:
310
+ if self._pointer < len(self._stack) - 1:
311
+ return self._stack[self._pointer + 1].description
312
+ return ""
313
+
314
+ def clear(self) -> None:
315
+ self._stack.clear()
316
+ self._pointer = -1
317
+
318
+
319
+ # ── Module-level singleton registry ───────────────────────────────────────────
320
+
321
+ registry = CommandRegistry()
322
+
323
+
324
+ def execute(doc: "Document", command: dict) -> Any:
325
+ """Execute a command dict against a document using the global registry."""
326
+ return registry.execute(doc, command)
@@ -0,0 +1 @@
1
+ # edof sub-package
edof/engine/color.py ADDED
@@ -0,0 +1,90 @@
1
+ # edof/engine/color.py
2
+ """
3
+ Colour-space helpers.
4
+ Conversion is delegated to Pillow; this module provides a clean interface
5
+ and validates the settings chosen in the document.
6
+ """
7
+
8
+ from __future__ import annotations
9
+ from typing import Optional
10
+ from PIL import Image
11
+
12
+ # Mapping from edof CS constants → Pillow mode
13
+ PILLOW_MODE: dict[str, str] = {
14
+ "RGB": "RGB",
15
+ "RGBA": "RGBA",
16
+ "L": "L", # 8-bit grayscale
17
+ "1": "1", # 1-bit B&W
18
+ "CMYK": "CMYK",
19
+ }
20
+
21
+ VALID_COLOR_SPACES = set(PILLOW_MODE.keys())
22
+ VALID_BIT_DEPTHS = {8, 16}
23
+
24
+
25
+ def validate(color_space: str, bit_depth: int) -> None:
26
+ from edof.exceptions import EdofValidationError
27
+ if color_space not in VALID_COLOR_SPACES:
28
+ raise EdofValidationError(
29
+ f"Unknown colour space {color_space!r}. "
30
+ f"Valid: {sorted(VALID_COLOR_SPACES)}"
31
+ )
32
+ if bit_depth not in VALID_BIT_DEPTHS:
33
+ raise EdofValidationError(
34
+ f"Unsupported bit depth {bit_depth}. Valid: {VALID_BIT_DEPTHS}"
35
+ )
36
+
37
+
38
+ def pillow_mode(color_space: str) -> str:
39
+ return PILLOW_MODE.get(color_space, "RGB")
40
+
41
+
42
+ def convert_image(img: Image.Image,
43
+ color_space: str,
44
+ bit_depth: int = 8) -> Image.Image:
45
+ """
46
+ Convert a Pillow image to the target colour space and bit depth.
47
+ Always works non-destructively (returns new image).
48
+ """
49
+ validate(color_space, bit_depth)
50
+ mode = pillow_mode(color_space)
51
+
52
+ # ── Colour-space conversion ────────────────────────────────────────────────
53
+ if img.mode != mode:
54
+ if mode == "1":
55
+ # Convert to grayscale first for clean B&W threshold
56
+ img = img.convert("L").convert("1")
57
+ elif mode == "CMYK" and img.mode in ("RGBA", "LA"):
58
+ img = img.convert("RGB").convert("CMYK")
59
+ else:
60
+ try:
61
+ img = img.convert(mode)
62
+ except Exception:
63
+ img = img.convert("RGB").convert(mode)
64
+
65
+ # ── Bit depth ─────────────────────────────────────────────────────────────
66
+ if bit_depth == 16 and mode in ("RGB", "L", "RGBA"):
67
+ import numpy as np
68
+ arr = np.array(img, dtype=np.uint16) * 257 # 8-bit → 16-bit range
69
+ # Pillow doesn't natively save 16-bit RGB; return as I;16 for TIFF
70
+ if mode == "L":
71
+ img = Image.fromarray(arr.astype(np.uint16), mode="I;16")
72
+ # For RGB 16-bit export Pillow uses mode "I;16" only for grayscale;
73
+ # callers should save as TIFF with bit_depth hint in metadata.
74
+
75
+ return img
76
+
77
+
78
+ def background_image(width_px: int, height_px: int,
79
+ color_space: str,
80
+ background: tuple = (255, 255, 255)) -> Image.Image:
81
+ """Create a blank canvas in the correct mode."""
82
+ mode = pillow_mode(color_space)
83
+ if mode == "1":
84
+ return Image.new("1", (width_px, height_px), 1)
85
+ if mode == "CMYK":
86
+ return Image.new("CMYK", (width_px, height_px), (0, 0, 0, 0))
87
+ # Clamp alpha for RGBA
88
+ if mode == "RGBA" and len(background) == 3:
89
+ background = (*background, 255)
90
+ return Image.new(mode, (width_px, height_px), background[:len(background)])