figquilt 0.1.3__py3-none-any.whl → 0.1.4__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.
- figquilt/compose_pdf.py +17 -4
- figquilt/compose_svg.py +46 -15
- figquilt/layout.py +8 -1
- figquilt/units.py +45 -0
- {figquilt-0.1.3.dist-info → figquilt-0.1.4.dist-info}/METADATA +1 -1
- figquilt-0.1.4.dist-info/RECORD +13 -0
- figquilt-0.1.3.dist-info/RECORD +0 -13
- {figquilt-0.1.3.dist-info → figquilt-0.1.4.dist-info}/WHEEL +0 -0
- {figquilt-0.1.3.dist-info → figquilt-0.1.4.dist-info}/entry_points.txt +0 -0
figquilt/compose_pdf.py
CHANGED
|
@@ -87,14 +87,27 @@ class PDFComposer:
|
|
|
87
87
|
src_page = src_doc[0]
|
|
88
88
|
src_rect = src_page.rect
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
src_aspect = src_rect.height / src_rect.width
|
|
91
91
|
|
|
92
|
+
# Calculate cell height
|
|
92
93
|
if panel.height is not None:
|
|
93
94
|
h = to_pt(panel.height, self.units)
|
|
94
95
|
else:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
# No height specified: use source aspect ratio
|
|
97
|
+
h = w * src_aspect
|
|
98
|
+
|
|
99
|
+
# Calculate content rect using fit mode
|
|
100
|
+
from .units import calculate_fit
|
|
101
|
+
|
|
102
|
+
content_w, content_h, offset_x, offset_y = calculate_fit(
|
|
103
|
+
src_aspect, w, h, panel.fit
|
|
104
|
+
)
|
|
105
|
+
rect = fitz.Rect(
|
|
106
|
+
x + offset_x,
|
|
107
|
+
y + offset_y,
|
|
108
|
+
x + offset_x + content_w,
|
|
109
|
+
y + offset_y + content_h,
|
|
110
|
+
)
|
|
98
111
|
|
|
99
112
|
if src_doc.is_pdf:
|
|
100
113
|
page.show_pdf_page(rect, src_doc, 0)
|
figquilt/compose_svg.py
CHANGED
|
@@ -59,30 +59,50 @@ class SVGComposer:
|
|
|
59
59
|
try:
|
|
60
60
|
src_page = src_doc[0]
|
|
61
61
|
src_rect = src_page.rect
|
|
62
|
-
|
|
62
|
+
src_aspect = src_rect.height / src_rect.width
|
|
63
63
|
|
|
64
64
|
if panel.height is not None:
|
|
65
65
|
h = to_pt(panel.height, self.units)
|
|
66
66
|
else:
|
|
67
|
-
h = w *
|
|
67
|
+
h = w * src_aspect
|
|
68
|
+
|
|
69
|
+
# Calculate content dimensions using fit mode
|
|
70
|
+
from .units import calculate_fit
|
|
71
|
+
|
|
72
|
+
content_w, content_h, offset_x, offset_y = calculate_fit(
|
|
73
|
+
src_aspect, w, h, panel.fit
|
|
74
|
+
)
|
|
68
75
|
|
|
69
76
|
# Group for the panel
|
|
70
77
|
g = etree.SubElement(root, "g")
|
|
71
78
|
g.set("transform", f"translate({x}, {y})")
|
|
72
79
|
|
|
80
|
+
# For cover mode, add a clip path to crop the overflow
|
|
81
|
+
if panel.fit == "cover":
|
|
82
|
+
clip_id = f"clip-{panel.id}"
|
|
83
|
+
defs = etree.SubElement(g, "defs")
|
|
84
|
+
clip_path = etree.SubElement(defs, "clipPath")
|
|
85
|
+
clip_path.set("id", clip_id)
|
|
86
|
+
clip_rect = etree.SubElement(clip_path, "rect")
|
|
87
|
+
clip_rect.set("x", "0")
|
|
88
|
+
clip_rect.set("y", "0")
|
|
89
|
+
clip_rect.set("width", str(w))
|
|
90
|
+
clip_rect.set("height", str(h))
|
|
91
|
+
|
|
73
92
|
# Insert content
|
|
74
93
|
# Check if SVG
|
|
75
94
|
suffix = panel.file.suffix.lower()
|
|
76
95
|
if suffix == ".svg":
|
|
77
96
|
# Embed SVG by creating an <image> tag with data URI to avoid DOM conflicts
|
|
78
|
-
# This is safer than merging trees for V0.
|
|
79
|
-
# Merging trees requires stripping root, handling viewbox/transform matching.
|
|
80
|
-
# <image> handles scaling automatically.
|
|
81
97
|
data_uri = self._get_data_uri(panel.file, "image/svg+xml")
|
|
82
98
|
img = etree.SubElement(g, "image")
|
|
83
|
-
img.set("
|
|
84
|
-
img.set("
|
|
99
|
+
img.set("x", str(offset_x))
|
|
100
|
+
img.set("y", str(offset_y))
|
|
101
|
+
img.set("width", str(content_w))
|
|
102
|
+
img.set("height", str(content_h))
|
|
85
103
|
img.set("{http://www.w3.org/1999/xlink}href", data_uri)
|
|
104
|
+
if panel.fit == "cover":
|
|
105
|
+
img.set("clip-path", f"url(#{clip_id})")
|
|
86
106
|
else:
|
|
87
107
|
# PDF or Raster Image
|
|
88
108
|
# For PDF, we rasterize to PNG (easiest for SVG compatibility without huge libs)
|
|
@@ -112,12 +132,16 @@ class SVGComposer:
|
|
|
112
132
|
data_uri = self._get_data_uri(data_path, mime)
|
|
113
133
|
|
|
114
134
|
img = etree.SubElement(g, "image")
|
|
115
|
-
img.set("
|
|
116
|
-
img.set("
|
|
135
|
+
img.set("x", str(offset_x))
|
|
136
|
+
img.set("y", str(offset_y))
|
|
137
|
+
img.set("width", str(content_w))
|
|
138
|
+
img.set("height", str(content_h))
|
|
117
139
|
img.set("{http://www.w3.org/1999/xlink}href", data_uri)
|
|
140
|
+
if panel.fit == "cover":
|
|
141
|
+
img.set("clip-path", f"url(#{clip_id})")
|
|
118
142
|
|
|
119
|
-
# Label
|
|
120
|
-
self._draw_label(g, panel,
|
|
143
|
+
# Label (positioned relative to content, not cell)
|
|
144
|
+
self._draw_label(g, panel, content_w, content_h, offset_x, offset_y, index)
|
|
121
145
|
finally:
|
|
122
146
|
src_doc.close()
|
|
123
147
|
|
|
@@ -128,7 +152,14 @@ class SVGComposer:
|
|
|
128
152
|
return f"data:{mime};base64,{b64}"
|
|
129
153
|
|
|
130
154
|
def _draw_label(
|
|
131
|
-
self,
|
|
155
|
+
self,
|
|
156
|
+
parent: etree.Element,
|
|
157
|
+
panel: Panel,
|
|
158
|
+
content_w: float,
|
|
159
|
+
content_h: float,
|
|
160
|
+
offset_x: float,
|
|
161
|
+
offset_y: float,
|
|
162
|
+
index: int,
|
|
132
163
|
):
|
|
133
164
|
style = panel.label_style if panel.label_style else self.layout.page.label
|
|
134
165
|
if not style.enabled:
|
|
@@ -142,9 +173,9 @@ class SVGComposer:
|
|
|
142
173
|
if style.uppercase:
|
|
143
174
|
text_str = text_str.upper()
|
|
144
175
|
|
|
145
|
-
# Offset
|
|
146
|
-
x = mm_to_pt(style.offset_x_mm)
|
|
147
|
-
y = mm_to_pt(style.offset_y_mm)
|
|
176
|
+
# Offset relative to the content position
|
|
177
|
+
x = offset_x + mm_to_pt(style.offset_x_mm)
|
|
178
|
+
y = offset_y + mm_to_pt(style.offset_y_mm)
|
|
148
179
|
|
|
149
180
|
# Create text element
|
|
150
181
|
txt = etree.SubElement(parent, "text")
|
figquilt/layout.py
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
from typing import Optional, List
|
|
1
|
+
from typing import Literal, Optional, List
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
from pydantic import BaseModel, Field, field_validator
|
|
4
4
|
|
|
5
|
+
FitMode = Literal["contain", "cover"]
|
|
6
|
+
|
|
7
|
+
|
|
5
8
|
class LabelStyle(BaseModel):
|
|
6
9
|
enabled: bool = True
|
|
7
10
|
auto_sequence: bool = True
|
|
@@ -12,6 +15,7 @@ class LabelStyle(BaseModel):
|
|
|
12
15
|
bold: bool = True
|
|
13
16
|
uppercase: bool = True
|
|
14
17
|
|
|
18
|
+
|
|
15
19
|
class Panel(BaseModel):
|
|
16
20
|
id: str
|
|
17
21
|
file: Path
|
|
@@ -19,6 +23,7 @@ class Panel(BaseModel):
|
|
|
19
23
|
y: float
|
|
20
24
|
width: float
|
|
21
25
|
height: Optional[float] = None # If None, compute from aspect ratio
|
|
26
|
+
fit: FitMode = "contain"
|
|
22
27
|
label: Optional[str] = None
|
|
23
28
|
label_style: Optional[LabelStyle] = None
|
|
24
29
|
|
|
@@ -29,6 +34,7 @@ class Panel(BaseModel):
|
|
|
29
34
|
# but we could add it if desired. The parser/logic will check it.
|
|
30
35
|
return v
|
|
31
36
|
|
|
37
|
+
|
|
32
38
|
class Page(BaseModel):
|
|
33
39
|
width: float
|
|
34
40
|
height: float
|
|
@@ -37,6 +43,7 @@ class Page(BaseModel):
|
|
|
37
43
|
background: Optional[str] = "white"
|
|
38
44
|
label: LabelStyle = Field(default_factory=LabelStyle)
|
|
39
45
|
|
|
46
|
+
|
|
40
47
|
class Layout(BaseModel):
|
|
41
48
|
page: Page
|
|
42
49
|
panels: List[Panel]
|
figquilt/units.py
CHANGED
|
@@ -23,3 +23,48 @@ def to_pt(value: float, units: str) -> float:
|
|
|
23
23
|
return value
|
|
24
24
|
else:
|
|
25
25
|
raise ValueError(f"Unknown unit: {units}")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def calculate_fit(
|
|
29
|
+
src_aspect: float, cell_w: float, cell_h: float, fit_mode: str
|
|
30
|
+
) -> tuple[float, float, float, float]:
|
|
31
|
+
"""
|
|
32
|
+
Calculate content dimensions and offset based on fit mode.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
src_aspect: Source aspect ratio (height / width)
|
|
36
|
+
cell_w: Cell width in points
|
|
37
|
+
cell_h: Cell height in points
|
|
38
|
+
fit_mode: "contain" or "cover"
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Tuple of (content_w, content_h, offset_x, offset_y)
|
|
42
|
+
"""
|
|
43
|
+
cell_aspect = cell_h / cell_w
|
|
44
|
+
|
|
45
|
+
if fit_mode == "cover":
|
|
46
|
+
# Scale to cover entire cell (may crop)
|
|
47
|
+
if src_aspect > cell_aspect:
|
|
48
|
+
# Source is taller: scale by width, crop top/bottom
|
|
49
|
+
content_w = cell_w
|
|
50
|
+
content_h = cell_w * src_aspect
|
|
51
|
+
else:
|
|
52
|
+
# Source is wider: scale by height, crop left/right
|
|
53
|
+
content_h = cell_h
|
|
54
|
+
content_w = cell_h / src_aspect
|
|
55
|
+
else: # contain (default)
|
|
56
|
+
# Scale to fit within cell, preserving aspect ratio
|
|
57
|
+
if src_aspect > cell_aspect:
|
|
58
|
+
# Source is taller: fit by height
|
|
59
|
+
content_h = cell_h
|
|
60
|
+
content_w = cell_h / src_aspect
|
|
61
|
+
else:
|
|
62
|
+
# Source is wider: fit by width
|
|
63
|
+
content_w = cell_w
|
|
64
|
+
content_h = cell_w * src_aspect
|
|
65
|
+
|
|
66
|
+
# Center in cell
|
|
67
|
+
offset_x = (cell_w - content_w) / 2
|
|
68
|
+
offset_y = (cell_h - content_h) / 2
|
|
69
|
+
|
|
70
|
+
return content_w, content_h, offset_x, offset_y
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: figquilt
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: figquilt is a small, language-agnostic CLI tool that composes multiple figures (PDF/SVG/PNG) into a single publication-ready figure, based on a simple layout file (YAML/JSON). The key function is creating a PDF by composing multiple PDFs and adding subfigure labels and minimal annotations.
|
|
5
5
|
Author: YY Ahn
|
|
6
6
|
Author-email: YY Ahn <yongyeol@gmail.com>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
figquilt/__init__.py,sha256=91447944015cec709e8aa7655f7e9d64e1e4508e7023a57fe3746911c0fc6fed,22
|
|
2
|
+
figquilt/cli.py,sha256=d73f2f97ca86b0d932e5a4dac8596e3137017517e2b7017916920bfe8e36a789,6930
|
|
3
|
+
figquilt/compose_pdf.py,sha256=b9418a415eb52da82b3126501cdded8048083418ba69cb6e9c281f0ecc0ebe0f,6172
|
|
4
|
+
figquilt/compose_svg.py,sha256=3a3499c9805508c38906568e984d46cd391f1432ab2fea21b3fe2be783816c81,7418
|
|
5
|
+
figquilt/errors.py,sha256=6f4001dcae85d2171f7aa7df4161926771dbe8c21068ccb70a7865298f05cf2b,298
|
|
6
|
+
figquilt/images.py,sha256=c613655fb3a0790fca98182c558c584e632a8822225220a5feb6080c7c68eb9e,815
|
|
7
|
+
figquilt/layout.py,sha256=12a1169f05630ba97b3b29f2d269708b38da697357e5d511e12fad5e484fbfbe,1226
|
|
8
|
+
figquilt/parser.py,sha256=d33d21178721072bbf681be940312b5fda0275f451fca3be2affad707f80a7fb,1065
|
|
9
|
+
figquilt/units.py,sha256=25098b6d7559cb78214fe1a9889fd38ce88633993d0c92819cc609510654275d,2124
|
|
10
|
+
figquilt-0.1.4.dist-info/WHEEL,sha256=76443c98c0efcfdd1191eac5fa1d8223dba1c474dbd47676674a255e7ca48770,79
|
|
11
|
+
figquilt-0.1.4.dist-info/entry_points.txt,sha256=8f70ce07f585bed28aca569052c7f0029384ac67c5e738faeb0daeb31695bc85,48
|
|
12
|
+
figquilt-0.1.4.dist-info/METADATA,sha256=a21301d1e2a2d00ae70e43531d0e2145f79cc0cffa93eac07be19fadc39a4463,3330
|
|
13
|
+
figquilt-0.1.4.dist-info/RECORD,,
|
figquilt-0.1.3.dist-info/RECORD
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
figquilt/__init__.py,sha256=91447944015cec709e8aa7655f7e9d64e1e4508e7023a57fe3746911c0fc6fed,22
|
|
2
|
-
figquilt/cli.py,sha256=d73f2f97ca86b0d932e5a4dac8596e3137017517e2b7017916920bfe8e36a789,6930
|
|
3
|
-
figquilt/compose_pdf.py,sha256=5c0391e2259c830cfaa760a5f06b33b3173ecfd8fb067ca63321dae15ea252dc,5700
|
|
4
|
-
figquilt/compose_svg.py,sha256=161042f0067d87891cc0d95e363b759fb6b3d40e3c55d50673d7c2bfe9011ba2,6246
|
|
5
|
-
figquilt/errors.py,sha256=6f4001dcae85d2171f7aa7df4161926771dbe8c21068ccb70a7865298f05cf2b,298
|
|
6
|
-
figquilt/images.py,sha256=c613655fb3a0790fca98182c558c584e632a8822225220a5feb6080c7c68eb9e,815
|
|
7
|
-
figquilt/layout.py,sha256=44514c72f8883afe02f5dd05e674410440dc52c40966a8028f1d9693d4be3364,1159
|
|
8
|
-
figquilt/parser.py,sha256=d33d21178721072bbf681be940312b5fda0275f451fca3be2affad707f80a7fb,1065
|
|
9
|
-
figquilt/units.py,sha256=cb14151b8b456a53d823ac5021b339bddefe3aac09e945d81a8306c1370f49fd,684
|
|
10
|
-
figquilt-0.1.3.dist-info/WHEEL,sha256=76443c98c0efcfdd1191eac5fa1d8223dba1c474dbd47676674a255e7ca48770,79
|
|
11
|
-
figquilt-0.1.3.dist-info/entry_points.txt,sha256=8f70ce07f585bed28aca569052c7f0029384ac67c5e738faeb0daeb31695bc85,48
|
|
12
|
-
figquilt-0.1.3.dist-info/METADATA,sha256=fda0ebf7ae6118a48a337b983a8e9bd09c31c1df4bb8f5b329af404ae46a37fb,3330
|
|
13
|
-
figquilt-0.1.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|