figquilt 0.1.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.
figquilt/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
figquilt/cli.py ADDED
@@ -0,0 +1,65 @@
1
+ import argparse
2
+ import sys
3
+ from pathlib import Path
4
+ from .parser import parse_layout
5
+ from .errors import FigQuiltError
6
+
7
+ def main():
8
+ parser = argparse.ArgumentParser(description="FigQuilt: Compose figures from multiple panels.")
9
+ parser.add_argument("layout", type=Path, help="Path to layout YAML file")
10
+ parser.add_argument("output", type=Path, help="Path to output file (PDF/SVG/PNG)")
11
+ parser.add_argument("--format", choices=["pdf", "svg", "png"], help="Override output format")
12
+ parser.add_argument("--check", action="store_true", help="Validate layout only")
13
+ parser.add_argument("--verbose", action="store_true", help="Enable verbose output")
14
+
15
+ args = parser.parse_args()
16
+
17
+ try:
18
+ layout = parse_layout(args.layout)
19
+ print(f"Layout parsed successfully: {args.layout}")
20
+ print(f"Page size: {layout.page.width}x{layout.page.height} {layout.page.units}")
21
+ print(f"Panels: {len(layout.panels)}")
22
+
23
+ if args.check:
24
+ sys.exit(0)
25
+
26
+ # Determine output format
27
+ suffix = args.output.suffix.lower()
28
+ fmt = args.format or suffix.lstrip('.')
29
+
30
+ if fmt == 'pdf':
31
+ from .compose_pdf import PDFComposer
32
+ composer = PDFComposer(layout)
33
+ composer.compose(args.output)
34
+ print(f"Successfully created: {args.output}")
35
+
36
+ elif fmt == 'svg':
37
+ from .compose_svg import SVGComposer
38
+ composer = SVGComposer(layout)
39
+ composer.compose(args.output)
40
+ print(f"Successfully created: {args.output}")
41
+
42
+ elif fmt == 'png':
43
+ from .compose_pdf import PDFComposer
44
+ composer = PDFComposer(layout)
45
+ doc = composer.build()
46
+ # Rasterize first page
47
+ page = doc[0]
48
+ pix = page.get_pixmap(dpi=layout.page.dpi)
49
+ pix.save(str(args.output))
50
+ doc.close()
51
+ print(f"Successfully created: {args.output}")
52
+
53
+ else:
54
+ print(f"Unsupported format: {fmt}", file=sys.stderr)
55
+ sys.exit(1)
56
+
57
+ except FigQuiltError as e:
58
+ print(f"Error: {e}", file=sys.stderr)
59
+ sys.exit(1)
60
+ except Exception as e:
61
+ print(f"Unexpected error: {e}", file=sys.stderr)
62
+ sys.exit(1)
63
+
64
+ if __name__ == "__main__":
65
+ main()
@@ -0,0 +1,143 @@
1
+ import fitz
2
+ from pathlib import Path
3
+ from .layout import Layout, Panel
4
+ from .units import mm_to_pt
5
+ from .errors import FigQuiltError
6
+
7
+ class PDFComposer:
8
+ def __init__(self, layout: Layout):
9
+ self.layout = layout
10
+ self.width_pt = mm_to_pt(layout.page.width)
11
+ self.height_pt = mm_to_pt(layout.page.height)
12
+
13
+ def compose(self, output_path: Path):
14
+ doc = self.build()
15
+ doc.save(str(output_path))
16
+ doc.close()
17
+
18
+ def build(self) -> fitz.Document:
19
+ doc = fitz.open()
20
+ page = doc.new_page(width=self.width_pt, height=self.height_pt)
21
+
22
+ # Draw background if specified
23
+ if self.layout.page.background:
24
+ # simple color parsing: strict names or hex?
25
+ # PyMuPDF draw_rect color expects sequence of floats (0..1) or nothing.
26
+ # We need to robustly parse the color string from layout (e.g. "white", "#f0f0f0").
27
+ # fitz.utils.getColor? No, fitz doesn't have a robust color parser built-in for all CSS names.
28
+ # But the user example uses "#f0f0f0".
29
+ # Minimal hex parser:
30
+ col = self._parse_color(self.layout.page.background)
31
+ if col:
32
+ page.draw_rect(page.rect, color=col, fill=col)
33
+
34
+ # Draw panels
35
+ for i, panel in enumerate(self.layout.panels):
36
+ self._place_panel(doc, page, panel, index=i)
37
+
38
+ return doc
39
+
40
+ def _parse_color(self, color_str: str):
41
+ # Very basic hex support
42
+ if color_str.startswith("#"):
43
+ h = color_str.lstrip('#')
44
+ try:
45
+ rgb = tuple(int(h[i:i+2], 16)/255.0 for i in (0, 2, 4))
46
+ return rgb
47
+ except:
48
+ return None
49
+ # Basic name support mapping could be added here
50
+ # For now, just support hex or fallback to None (skip)
51
+ # Using PIL ImageColor is an option if we import it, but we want minimal dep for this file?
52
+ # We process images, so PIL is available.
53
+ try:
54
+ from PIL import ImageColor
55
+ rgb = ImageColor.getrgb(color_str)
56
+ return tuple(c/255.0 for c in rgb)
57
+ except:
58
+ return None
59
+
60
+ def _place_panel(self, doc: fitz.Document, page: fitz.Page, panel: Panel, index: int):
61
+ # Calculate position and size first
62
+ x = mm_to_pt(panel.x)
63
+ y = mm_to_pt(panel.y)
64
+ w = mm_to_pt(panel.width)
65
+
66
+ # Determine height from aspect ratio if needed
67
+ # We need to open the source to get aspect ratio
68
+ try:
69
+ # fitz.open can handle PDF, PNG, JPEG, SVG...
70
+ src_doc = fitz.open(panel.file)
71
+ except Exception as e:
72
+ raise FigQuiltError(f"Failed to open panel file {panel.file}: {e}")
73
+
74
+ # Get source dimension
75
+ if src_doc.is_pdf:
76
+ src_page = src_doc[0]
77
+ src_rect = src_page.rect
78
+ else:
79
+ # For images/SVG, fitz doc acts like a list of pages too?
80
+ # Yes, usually page[0] is the image/svg content.
81
+ src_page = src_doc[0]
82
+ src_rect = src_page.rect
83
+
84
+ aspect = src_rect.height / src_rect.width
85
+
86
+ if panel.height is not None:
87
+ h = mm_to_pt(panel.height)
88
+ else:
89
+ h = w * aspect
90
+
91
+ rect = fitz.Rect(x, y, x + w, y + h)
92
+
93
+ if src_doc.is_pdf:
94
+ page.show_pdf_page(rect, src_doc, 0)
95
+ elif panel.file.suffix.lower() == ".svg":
96
+ # Convert SVG to PDF in memory to allow vector embedding
97
+ pdf_bytes = src_doc.convert_to_pdf()
98
+ src_pdf = fitz.open("pdf", pdf_bytes)
99
+ page.show_pdf_page(rect, src_pdf, 0)
100
+ else:
101
+ # Insert as image (works for PNG/JPEG)
102
+ page.insert_image(rect, filename=panel.file)
103
+
104
+ # Labels
105
+ self._draw_label(page, panel, rect, index)
106
+
107
+ def _draw_label(self, page: fitz.Page, panel: Panel, rect: fitz.Rect, index: int):
108
+ # Determine effective label settings
109
+ style = panel.label_style if panel.label_style else self.layout.page.label
110
+
111
+ if not style.enabled:
112
+ return
113
+
114
+ text = panel.label
115
+ if text is None and style.auto_sequence:
116
+ text = chr(65 + index) # A, B, C...
117
+
118
+ if not text:
119
+ return
120
+
121
+ if style.uppercase:
122
+ text = text.upper()
123
+
124
+ # Position logic
125
+ # Design doc: offset is relative to top-left.
126
+ # SVG implementation uses 'hanging' baseline, so (0,0) is top-left of text char.
127
+ # PyMuPDF insert_text uses 'baseline', so (0,0) is bottom-left of text char.
128
+ # We need to shift Y down by approximately the font sizing to match SVG visual.
129
+
130
+ pos_x = rect.x0 + mm_to_pt(style.offset_x_mm)
131
+ raw_y = rect.y0 + mm_to_pt(style.offset_y_mm)
132
+
133
+ # Approximate baseline shift: font_size
134
+ # (A more precise way uses font.ascender, but for basic standard fonts, size is decent proxy for visual top->baseline)
135
+ pos_y = raw_y + style.font_size_pt
136
+
137
+ # Font - PyMuPDF supports base 14 fonts by name
138
+ fontname = "helv" # default mapping for Helvetica
139
+ if style.bold:
140
+ fontname = "HeBo" # Helvetica-Bold
141
+
142
+ # Insert text
143
+ page.insert_text((pos_x, pos_y), text, fontsize=style.font_size_pt, fontname=fontname)
@@ -0,0 +1,153 @@
1
+ from pathlib import Path
2
+ import base64
3
+ from lxml import etree
4
+ from .layout import Layout, Panel
5
+ from .units import mm_to_pt
6
+ from .errors import FigQuiltError
7
+ import fitz
8
+
9
+ class SVGComposer:
10
+ def __init__(self, layout: Layout):
11
+ self.layout = layout
12
+ self.width_pt = mm_to_pt(layout.page.width)
13
+ self.height_pt = mm_to_pt(layout.page.height)
14
+
15
+ def compose(self, output_path: Path):
16
+ # Create root SVG element
17
+ nsmap = {None: "http://www.w3.org/2000/svg", "xlink": "http://www.w3.org/1999/xlink"}
18
+ root = etree.Element("svg", nsmap=nsmap)
19
+ root.set("width", f"{self.layout.page.width}mm")
20
+ root.set("height", f"{self.layout.page.height}mm")
21
+ root.set("viewBox", f"0 0 {self.width_pt} {self.height_pt}")
22
+ root.set("version", "1.1")
23
+
24
+ if self.layout.page.background:
25
+ # Draw background
26
+ bg = etree.SubElement(root, "rect")
27
+ bg.set("width", "100%")
28
+ bg.set("height", "100%")
29
+ bg.set("fill", self.layout.page.background)
30
+
31
+ # Draw panels
32
+ for i, panel in enumerate(self.layout.panels):
33
+ self._place_panel(root, panel, i)
34
+
35
+ # Write to file
36
+ tree = etree.ElementTree(root)
37
+ with open(output_path, "wb") as f:
38
+ tree.write(f, pretty_print=True, xml_declaration=True, encoding="utf-8")
39
+
40
+ def _place_panel(self, root: etree.Element, panel: Panel, index: int):
41
+ x = mm_to_pt(panel.x)
42
+ y = mm_to_pt(panel.y)
43
+ w = mm_to_pt(panel.width)
44
+
45
+ # Determine content sizing
46
+ # For simplicity in V0, relying on fitz for aspect ratio of all inputs (robust)
47
+ try:
48
+ src_doc = fitz.open(panel.file)
49
+ src_page = src_doc[0]
50
+ src_rect = src_page.rect
51
+ aspect = src_rect.height / src_rect.width
52
+ except Exception as e:
53
+ raise FigQuiltError(f"Failed to inspect panel {panel.file}: {e}")
54
+
55
+ if panel.height is not None:
56
+ h = mm_to_pt(panel.height)
57
+ else:
58
+ h = w * aspect
59
+
60
+ # Group for the panel
61
+ g = etree.SubElement(root, "g")
62
+ g.set("transform", f"translate({x}, {y})")
63
+
64
+ # Insert content
65
+ # Check if SVG
66
+ suffix = panel.file.suffix.lower()
67
+ if suffix == ".svg":
68
+ # Embed SVG by creating an <image> tag with data URI to avoid DOM conflicts
69
+ # This is safer than merging trees for V0.
70
+ # Merging trees requires stripping root, handling viewbox/transform matching.
71
+ # <image> handles scaling automatically.
72
+ data_uri = self._get_data_uri(panel.file, "image/svg+xml")
73
+ img = etree.SubElement(g, "image")
74
+ img.set("width", str(w))
75
+ img.set("height", str(h))
76
+ img.set("{http://www.w3.org/1999/xlink}href", data_uri)
77
+ else:
78
+ # PDF or Raster Image
79
+ # For PDF, we rasterize to PNG (easiest for SVG compatibility without huge libs) or embed as image/pdf?
80
+ # Browsers don't support image/pdf in SVG.
81
+ # So if PDF, convert to PNG.
82
+ # If PNG/JPG, embed directly.
83
+
84
+ mime = "image/png"
85
+ if suffix in [".jpg", ".jpeg"]:
86
+ mime = "image/jpeg"
87
+ data_path = panel.file
88
+ elif suffix == ".png":
89
+ mime = "image/png"
90
+ data_path = panel.file
91
+ elif suffix == ".pdf":
92
+ # Rasterize page to PNG
93
+ pix = src_page.get_pixmap(dpi=300) # Use decent DPI
94
+ data = pix.tobytes("png")
95
+ # We can't use _get_data_uri directly on file, we have bytes
96
+ b64 = base64.b64encode(data).decode("utf-8")
97
+ data_uri = f"data:image/png;base64,{b64}"
98
+ data_path = None # signal that we have URI
99
+ else:
100
+ # Fallback
101
+ mime = "application/octet-stream"
102
+ data_path = panel.file
103
+
104
+ if data_path:
105
+ data_uri = self._get_data_uri(data_path, mime)
106
+
107
+ img = etree.SubElement(g, "image")
108
+ img.set("width", str(w))
109
+ img.set("height", str(h))
110
+ img.set("{http://www.w3.org/1999/xlink}href", data_uri)
111
+
112
+ # Label
113
+ self._draw_label(g, panel, w, h, index)
114
+
115
+ def _get_data_uri(self, path: Path, mime: str) -> str:
116
+ with open(path, "rb") as f:
117
+ data = f.read()
118
+ b64 = base64.b64encode(data).decode("utf-8")
119
+ return f"data:{mime};base64,{b64}"
120
+
121
+ def _draw_label(self, parent: etree.Element, panel: Panel, w: float, h: float, index: int):
122
+ style = panel.label_style if panel.label_style else self.layout.page.label
123
+ if not style.enabled:
124
+ return
125
+
126
+ text_str = panel.label
127
+ if text_str is None and style.auto_sequence:
128
+ text_str = chr(65 + index)
129
+ if not text_str:
130
+ return
131
+ if style.uppercase:
132
+ text_str = text_str.upper()
133
+
134
+ # Offset (relative to panel top-left, which is 0,0 inside the group)
135
+ x = mm_to_pt(style.offset_x_mm)
136
+ y = mm_to_pt(style.offset_y_mm)
137
+
138
+ # Create text element
139
+ txt = etree.SubElement(parent, "text")
140
+ txt.text = text_str
141
+ txt.set("x", str(x))
142
+ txt.set("y", str(y))
143
+
144
+ # Style
145
+ # Font family is tricky in SVG (system fonts).
146
+ txt.set("font-family", style.font_family)
147
+ txt.set("font-size", f"{style.font_size_pt}pt")
148
+ if style.bold:
149
+ txt.set("font-weight", "bold")
150
+
151
+ # Baseline alignment? SVG text y is usually baseline.
152
+ # If we want top-left of text at (x,y), we should adjust or use dominant-baseline.
153
+ txt.set("dominant-baseline", "hanging") # Matches top-down coordinate logic
figquilt/errors.py ADDED
@@ -0,0 +1,11 @@
1
+ class FigQuiltError(Exception):
2
+ """Base exception for figquilt."""
3
+ pass
4
+
5
+ class LayoutError(FigQuiltError):
6
+ """Raised when there is an issue with the layout configuration."""
7
+ pass
8
+
9
+ class AssetMissingError(FigQuiltError):
10
+ """Raised when an input file cannot be found."""
11
+ pass
figquilt/images.py ADDED
@@ -0,0 +1,27 @@
1
+ from pathlib import Path
2
+ from typing import Tuple, Optional
3
+ from PIL import Image
4
+ import fitz
5
+
6
+ def get_image_size(path: Path) -> Tuple[float, float]:
7
+ """Returns (width, height) in pixels/points."""
8
+ try:
9
+ with Image.open(path) as img:
10
+ return float(img.width), float(img.height)
11
+ except Exception:
12
+ # Try fitz for PDF/SVG
13
+ try:
14
+ doc = fitz.open(path)
15
+ page = doc[0]
16
+ return page.rect.width, page.rect.height
17
+ except Exception:
18
+ raise ValueError(f"Could not determine size of {path}")
19
+
20
+ def is_image(path: Path) -> bool:
21
+ """Checks if the file is a raster image supported by Pillow."""
22
+ try:
23
+ with Image.open(path) as img:
24
+ img.verify()
25
+ return True
26
+ except Exception:
27
+ return False
figquilt/layout.py ADDED
@@ -0,0 +1,42 @@
1
+ from typing import Optional, List, Tuple, Union
2
+ from pathlib import Path
3
+ from pydantic import BaseModel, Field, field_validator
4
+
5
+ class LabelStyle(BaseModel):
6
+ enabled: bool = True
7
+ auto_sequence: bool = True
8
+ font_family: str = "Helvetica"
9
+ font_size_pt: float = 8.0
10
+ offset_x_mm: float = 2.0
11
+ offset_y_mm: float = 2.0
12
+ bold: bool = True
13
+ uppercase: bool = True
14
+
15
+ class Panel(BaseModel):
16
+ id: str
17
+ file: Path
18
+ x: float
19
+ y: float
20
+ width: float
21
+ height: Optional[float] = None # If None, compute from aspect ratio
22
+ label: Optional[str] = None
23
+ label_style: Optional[LabelStyle] = None
24
+
25
+ @field_validator("file")
26
+ @classmethod
27
+ def validate_file(cls, v: Path) -> Path:
28
+ # We don't check existence here to allow validation without side effects,
29
+ # but we could add it if desired. The parser/logic will check it.
30
+ return v
31
+
32
+ class Page(BaseModel):
33
+ width: float
34
+ height: float
35
+ units: str = "mm"
36
+ dpi: int = 300
37
+ background: Optional[str] = "white"
38
+ label: LabelStyle = Field(default_factory=LabelStyle)
39
+
40
+ class Layout(BaseModel):
41
+ page: Page
42
+ panels: List[Panel]
figquilt/parser.py ADDED
@@ -0,0 +1,32 @@
1
+ from pathlib import Path
2
+ import yaml
3
+ from .layout import Layout
4
+ from .errors import LayoutError, AssetMissingError
5
+
6
+ def parse_layout(layout_path: Path) -> Layout:
7
+ """Parses a YAML layout file and returns a Layout object."""
8
+ if not layout_path.exists():
9
+ raise LayoutError(f"Layout file not found: {layout_path}")
10
+
11
+ try:
12
+ with open(layout_path, 'r') as f:
13
+ data = yaml.safe_load(f)
14
+ except yaml.YAMLError as e:
15
+ raise LayoutError(f"Failed to parse YAML: {e}")
16
+
17
+ try:
18
+ layout = Layout(**data)
19
+ except Exception as e:
20
+ raise LayoutError(f"Layout validation failed: {e}")
21
+
22
+ # Validate assets exist relative to the layout file
23
+ base_dir = layout_path.parent
24
+ for panel in layout.panels:
25
+ # Resolve path relative to layout file if not absolute
26
+ if not panel.file.is_absolute():
27
+ panel.file = base_dir / panel.file
28
+
29
+ if not panel.file.exists():
30
+ raise AssetMissingError(f"Asset for panel '{panel.id}' not found: {panel.file}")
31
+
32
+ return layout
figquilt/units.py ADDED
@@ -0,0 +1,7 @@
1
+ def mm_to_pt(mm: float) -> float:
2
+ """Converts millimeters to points (1 inch = 25.4 mm = 72 pts)."""
3
+ return mm * 72 / 25.4
4
+
5
+ def pt_to_mm(pt: float) -> float:
6
+ """Converts points to millimeters."""
7
+ return pt * 25.4 / 72
@@ -0,0 +1,80 @@
1
+ Metadata-Version: 2.3
2
+ Name: figquilt
3
+ Version: 0.1.0
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
+ Author: YY Ahn
6
+ Author-email: YY Ahn <yongyeol@gmail.com>
7
+ License: MIT
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Dist: lxml>=6.0.2
13
+ Requires-Dist: matplotlib>=3.10.7
14
+ Requires-Dist: pillow>=12.0.0
15
+ Requires-Dist: pydantic>=2.12.5
16
+ Requires-Dist: pymupdf>=1.26.6
17
+ Requires-Dist: pyyaml>=6.0.3
18
+ Requires-Dist: seaborn>=0.13.2
19
+ Requires-Python: >=3.12
20
+ Project-URL: Homepage, https://github.com/yy/figquilt
21
+ Project-URL: Repository, https://github.com/yy/figquilt
22
+ Description-Content-Type: text/markdown
23
+
24
+ # figquilt
25
+
26
+ **Figure quilter**: A CLI tool to compositing multiple figures (PDF, SVG, PNG) into a publication-ready figure layout.
27
+
28
+ `figquilt` takes a simple layout file (YAML) describing panels and their positions, composed of various inputs (plots from R/Python, diagrams, photos), and stitches them into a single output file (PDF, SVG) with precise dimension control and automatic labeling.
29
+
30
+ ## Features
31
+
32
+ - **Precise Layout**: Define exact physical dimensions (mm) for the page and panels.
33
+ - **Mixed Media**: Combine PDF, SVG, and PNG inputs in one figure.
34
+ - **Automated Labeling**: Automatically add subfigure labels (A, B, C...) with consistent styling.
35
+ - **Reproducible**: Layouts are defined in version-controllable text files (YAML).
36
+ - **Language Agnostic**: It is a CLI tool, so it works with outputs from any tool (R, Python, Julia, Inkscape, etc.).
37
+
38
+ ## Installation
39
+
40
+ This project uses `uv` for dependency management.
41
+
42
+ ```bash
43
+ # Clone the repository
44
+ git clone https://github.com/yy/figquilt.git
45
+ cd figquilt
46
+
47
+ # Install dependencies and set up the environment
48
+ uv sync
49
+
50
+ # Install the package in editable mode
51
+ uv pip install -e .
52
+ ```
53
+
54
+ ## Usage
55
+
56
+ Define a layout in a YAML file (e.g., `figure1.yaml`):
57
+
58
+ ```yaml
59
+ page:
60
+ width: 180 # mm
61
+ height: 120 # mm
62
+
63
+ panels:
64
+ - id: A
65
+ file: "plots/scatter.pdf"
66
+ width: 80
67
+ x: 0
68
+ y: 0
69
+ - id: B
70
+ file: "diagrams/schematic.svg"
71
+ width: 80
72
+ x: 90
73
+ y: 0
74
+ ```
75
+
76
+ Run `figquilt` to generate the figure:
77
+
78
+ ```bash
79
+ figquilt figure1.yaml figure1.pdf
80
+ ```
@@ -0,0 +1,13 @@
1
+ figquilt/__init__.py,sha256=91447944015cec709e8aa7655f7e9d64e1e4508e7023a57fe3746911c0fc6fed,22
2
+ figquilt/cli.py,sha256=481535c3c2d77a73692d76c1be8dfd02056a2b20edb7e9877edca0edf73d67ab,2352
3
+ figquilt/compose_pdf.py,sha256=4da4d5f9b656cd1da6c1b58e3e92a18955d60aa498bd1720050fc6a5988f0e4c,5385
4
+ figquilt/compose_svg.py,sha256=2a4f01369be6c4acca62f823967bc3027b9b341b339e55391186ed5eb6f0a5fb,5921
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=0c1b6b3f7380fb8ebf51ae81fb093c1830ae4a240b9151191333a3b0af16cd84,233
10
+ figquilt-0.1.0.dist-info/WHEEL,sha256=76443c98c0efcfdd1191eac5fa1d8223dba1c474dbd47676674a255e7ca48770,79
11
+ figquilt-0.1.0.dist-info/entry_points.txt,sha256=8f70ce07f585bed28aca569052c7f0029384ac67c5e738faeb0daeb31695bc85,48
12
+ figquilt-0.1.0.dist-info/METADATA,sha256=91ec1ccc28fee0be464922bae83d7f154447d1ff69c014b64b007ef949ef640b,2518
13
+ figquilt-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.8.12
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ figquilt = figquilt.cli:main
3
+