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 CHANGED
@@ -87,14 +87,27 @@ class PDFComposer:
87
87
  src_page = src_doc[0]
88
88
  src_rect = src_page.rect
89
89
 
90
- aspect = src_rect.height / src_rect.width
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
- h = w * aspect
96
-
97
- rect = fitz.Rect(x, y, x + w, y + h)
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
- aspect = src_rect.height / src_rect.width
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 * aspect
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("width", str(w))
84
- img.set("height", str(h))
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("width", str(w))
116
- img.set("height", str(h))
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, w, h, index)
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, parent: etree.Element, panel: Panel, w: float, h: float, index: int
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 (relative to panel top-left, which is 0,0 inside the group)
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, Tuple, Union
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
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,,
@@ -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,,