inklet 2.5.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.
- inklet/__init__.py +451 -0
- inklet/__main__.py +2 -0
- inklet/assets/__init__.py +42 -0
- inklet/assets/asset.py +374 -0
- inklet/assets/cache.py +134 -0
- inklet/assets/cutout.py +205 -0
- inklet/assets/deps.py +83 -0
- inklet/assets/harmonise.py +191 -0
- inklet/assets/lineart.py +313 -0
- inklet/assets/mask.py +117 -0
- inklet/assets/provenance.py +135 -0
- inklet/assets/raster.py +157 -0
- inklet/assets/sidecar.py +112 -0
- inklet/assets/silhouette.py +300 -0
- inklet/cli.py +197 -0
- inklet/components.py +143 -0
- inklet/core/__init__.py +26 -0
- inklet/core/diagram.py +574 -0
- inklet/core/envelope.py +269 -0
- inklet/core/geom.py +201 -0
- inklet/core/prims.py +342 -0
- inklet/core/style.py +179 -0
- inklet/core/trace.py +165 -0
- inklet/core/units.py +61 -0
- inklet/diagnostics/__init__.py +126 -0
- inklet/diagnostics/abut.py +54 -0
- inklet/diagnostics/break_rules.py +292 -0
- inklet/diagnostics/color.py +149 -0
- inklet/diagnostics/cross.py +97 -0
- inklet/diagnostics/image.py +164 -0
- inklet/diagnostics/key_rules.py +570 -0
- inklet/diagnostics/link_rules.py +572 -0
- inklet/diagnostics/path_rules.py +886 -0
- inklet/diagnostics/plot_rules.py +122 -0
- inklet/diagnostics/report.py +58 -0
- inklet/diagnostics/rules.py +3137 -0
- inklet/diagnostics/three_rules.py +218 -0
- inklet/document/__init__.py +15 -0
- inklet/document/compiler.py +504 -0
- inklet/document/composition.py +233 -0
- inklet/document/data.py +301 -0
- inklet/document/module.py +75 -0
- inklet/document/publication.py +61 -0
- inklet/document/spec.py +255 -0
- inklet/draw/__init__.py +51 -0
- inklet/draw/annotate.py +892 -0
- inklet/draw/clip.py +531 -0
- inklet/draw/coords.py +283 -0
- inklet/draw/path.py +249 -0
- inklet/draw/place.py +155 -0
- inklet/draw/shapes.py +226 -0
- inklet/figure.py +418 -0
- inklet/layout/__init__.py +27 -0
- inklet/layout/fit.py +228 -0
- inklet/layout/flow.py +754 -0
- inklet/layout/graph.py +712 -0
- inklet/layout/graph_force.py +202 -0
- inklet/layout/graph_layered.py +1129 -0
- inklet/layout/graph_tree.py +226 -0
- inklet/layout/labels.py +490 -0
- inklet/layout/sankey.py +1054 -0
- inklet/links/__init__.py +29 -0
- inklet/links/curves.py +173 -0
- inklet/links/link.py +2464 -0
- inklet/plot/__init__.py +73 -0
- inklet/plot/axis.py +523 -0
- inklet/plot/breaks.py +429 -0
- inklet/plot/categories.py +130 -0
- inklet/plot/facets.py +255 -0
- inklet/plot/inset.py +310 -0
- inklet/plot/key.py +202 -0
- inklet/plot/marks.py +1150 -0
- inklet/plot/notes.py +220 -0
- inklet/plot/panel.py +1775 -0
- inklet/plot/png.py +125 -0
- inklet/plot/polar.py +1717 -0
- inklet/plot/ramp.py +74 -0
- inklet/plot/raster.py +191 -0
- inklet/plot/ribbon.py +187 -0
- inklet/plot/scale.py +1202 -0
- inklet/plot/scatter_raster.py +161 -0
- inklet/plot/series.py +138 -0
- inklet/plot/timescale.py +417 -0
- inklet/render/__init__.py +18 -0
- inklet/render/bundle.py +75 -0
- inklet/render/fontembed.py +391 -0
- inklet/render/glyphs.py +200 -0
- inklet/render/outline.py +173 -0
- inklet/render/paint.py +60 -0
- inklet/render/pathdata.py +207 -0
- inklet/render/pdf.py +853 -0
- inklet/render/pdftext.py +213 -0
- inklet/render/preview.py +70 -0
- inklet/render/review.py +94 -0
- inklet/render/revision.py +47 -0
- inklet/render/svg.py +1005 -0
- inklet/themes/__init__.py +46 -0
- inklet/themes/color.py +384 -0
- inklet/themes/palettes.py +192 -0
- inklet/themes/theme.py +422 -0
- inklet/three/__init__.py +136 -0
- inklet/three/api.py +1545 -0
- inklet/three/backend.py +942 -0
- inklet/three/blender/__init__.py +58 -0
- inklet/three/blender/discover.py +204 -0
- inklet/three/blender/lineart.py +451 -0
- inklet/three/blender/options.py +133 -0
- inklet/three/blender/script.py +541 -0
- inklet/three/blender/svgread.py +214 -0
- inklet/three/blender/tracing.py +220 -0
- inklet/three/blender_backend.py +202 -0
- inklet/three/camera.py +326 -0
- inklet/three/deps.py +93 -0
- inklet/three/depth.py +196 -0
- inklet/three/drill.py +1129 -0
- inklet/three/edges.py +478 -0
- inklet/three/hlr.py +463 -0
- inklet/three/linalg.py +195 -0
- inklet/three/mesh.py +462 -0
- inklet/three/occlude.py +372 -0
- inklet/three/order.py +945 -0
- inklet/three/parse.py +602 -0
- inklet/three/place.py +156 -0
- inklet/three/protein.py +679 -0
- inklet/three/shade.py +828 -0
- inklet/three/solids.py +594 -0
- inklet/typeset/__init__.py +42 -0
- inklet/typeset/fonts.py +383 -0
- inklet/typeset/markup.py +421 -0
- inklet/typeset/onpath.py +760 -0
- inklet/typeset/outline.py +275 -0
- inklet/typeset/shaping.py +761 -0
- inklet-2.5.0.dist-info/METADATA +171 -0
- inklet-2.5.0.dist-info/RECORD +138 -0
- inklet-2.5.0.dist-info/WHEEL +4 -0
- inklet-2.5.0.dist-info/entry_points.txt +2 -0
- inklet-2.5.0.dist-info/licenses/LICENSE +21 -0
- inklet-2.5.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +67 -0
inklet/__init__.py
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
"""inklet -- publication-quality diagrams that lay themselves out.
|
|
2
|
+
|
|
3
|
+
The premise: an author should never compute a coordinate. Boxes size themselves
|
|
4
|
+
to their text, stacks space themselves, and arrows find the boundary of what
|
|
5
|
+
they point at. What is left to write is what the diagram *means*.
|
|
6
|
+
|
|
7
|
+
import inklet
|
|
8
|
+
|
|
9
|
+
encoder = inklet.box("Encoder\\n(ViT-B/16)")
|
|
10
|
+
decoder = inklet.box("Decoder")
|
|
11
|
+
panel = inklet.vstack([encoder, decoder], gap=6)
|
|
12
|
+
|
|
13
|
+
fig = inklet.figure(width="89mm")
|
|
14
|
+
fig.add(panel)
|
|
15
|
+
fig.link(encoder, decoder, label="latent z")
|
|
16
|
+
print(fig.report())
|
|
17
|
+
fig.save("fig1.svg")
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from dataclasses import replace as _replace
|
|
23
|
+
|
|
24
|
+
from .assets import asset
|
|
25
|
+
from .core import (
|
|
26
|
+
COLUMN_DOUBLE, COLUMN_SINGLE, Affine, Diagram, DiagramError, Envelope, Rect, Style, StyleError,
|
|
27
|
+
Vec2,
|
|
28
|
+
mm, pt, resolve, text_features,
|
|
29
|
+
)
|
|
30
|
+
from .figure import Figure, apply_theme, figure
|
|
31
|
+
from .layout import (
|
|
32
|
+
Graph, GraphEdge, GraphError, LabelChoice, LabelWeights,
|
|
33
|
+
Sankey, SankeyError, SankeyFlow, SankeyNode,
|
|
34
|
+
align_to, beside, box as _box_container, fit, flow, frame, graph, grid,
|
|
35
|
+
hstack, label_plan, overlay, pad, place_labels, sankey, spacer, stack,
|
|
36
|
+
vstack,
|
|
37
|
+
)
|
|
38
|
+
from .links import Link, link, route, route_all
|
|
39
|
+
from .diagnostics import (Diagnostic, abutting, crossing, format_report,
|
|
40
|
+
lint)
|
|
41
|
+
from .draw import (
|
|
42
|
+
annotate, annotation_side, arc, as_drawn, bracket, clip, curve, dimension,
|
|
43
|
+
drawn, encoded, label_slot, label_specs, letters, marker, path, place,
|
|
44
|
+
placed_anchor, plot_area, polygon, polyline, scalebar, sector,
|
|
45
|
+
)
|
|
46
|
+
from .plot.categories import CategorySet, categories
|
|
47
|
+
from .components import database, feature_matrix, sequence
|
|
48
|
+
from .render import outline_text, save_pdf, save_svg, to_pdf, to_svg
|
|
49
|
+
from .plot import (
|
|
50
|
+
Panel, Ramp, Scale, axis, band, grouped_band, broken, colorbar, column, dates, facets,
|
|
51
|
+
histogram, inset, legend, linear, log, panel, ramp, ribbon, row, symlog,
|
|
52
|
+
)
|
|
53
|
+
from .plot import (
|
|
54
|
+
PolarPanel, circular_histogram, circular_mean, polar, theta_ticks,
|
|
55
|
+
)
|
|
56
|
+
from .three import (
|
|
57
|
+
Mat4, Mesh, Vec3, anchor3d, axes, cartoon, model, outline_of, scene, solid,
|
|
58
|
+
)
|
|
59
|
+
from .typeset import (Baseline, baseline, baseline_arc, escape_markup, measure,
|
|
60
|
+
shape, strip_markup, theme_colors)
|
|
61
|
+
from .typeset import onpath as _onpath
|
|
62
|
+
from .themes import (THEMES, Theme, contrast_ratio, darken, lighten, mix,
|
|
63
|
+
readable, theme)
|
|
64
|
+
|
|
65
|
+
from contextvars import ContextVar
|
|
66
|
+
|
|
67
|
+
_current: Theme = theme("nature")
|
|
68
|
+
_theme_context = ContextVar("inklet_theme", default=None)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def use_theme(name: str | Theme) -> Theme:
|
|
72
|
+
"""Set the theme new content is built against.
|
|
73
|
+
|
|
74
|
+
Colour is resolved late, at figure build time, but *geometry* -- padding,
|
|
75
|
+
corner radius, gaps -- has to be decided while shapes are being made. This
|
|
76
|
+
is where those defaults come from.
|
|
77
|
+
"""
|
|
78
|
+
global _current
|
|
79
|
+
chosen = theme(name) if isinstance(name, str) else name
|
|
80
|
+
if _theme_context.get() is not None:
|
|
81
|
+
_theme_context.set(chosen)
|
|
82
|
+
else:
|
|
83
|
+
_current = chosen
|
|
84
|
+
return chosen
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def current_theme() -> Theme:
|
|
88
|
+
"""The theme new nodes are being styled with right now.
|
|
89
|
+
|
|
90
|
+
Read it for the design tokens rather than typing numbers: `t.stroke`,
|
|
91
|
+
`t.hairline`, `t.thick`, `t.ink`, `t.muted`, `t.paper`, `t.accent`,
|
|
92
|
+
`t.color(i)` for the categorical series, `t.gap("m")` for the standard
|
|
93
|
+
spacings. Styling from these is what keeps a figure looking like one thing.
|
|
94
|
+
"""
|
|
95
|
+
return _theme_context.get() or _current
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def text(content: str, *, size: float | str | None = None, font: str | None = None,
|
|
99
|
+
weight: str | None = None, align: str = "center",
|
|
100
|
+
width: float | str | None = None, line_height: float | None = None,
|
|
101
|
+
features: dict[str, bool | int] | None = None, markup: bool = True,
|
|
102
|
+
angle: float = 0.0, kind: str = "text", **style) -> Diagram:
|
|
103
|
+
"""Shaped text as a diagram. Its envelope is the real inked extent, which is
|
|
104
|
+
what lets a box around it actually fit.
|
|
105
|
+
|
|
106
|
+
Inline markup, every piece of it escapable with `\\` and composable with
|
|
107
|
+
the rest:
|
|
108
|
+
|
|
109
|
+
* `**bold**` and `//italic//`, set in the real bold and italic faces of the
|
|
110
|
+
family and measured in them, so a bold phrase in a justified column takes
|
|
111
|
+
the width it will draw at. Doubled delimiters because a lone `*` belongs
|
|
112
|
+
to `*CO` and a lone `/` to a URL.
|
|
113
|
+
* `{accent|text}` colours a span: a theme token (`ink`, `muted`, `accent`,
|
|
114
|
+
`paper`, `grid`, `series0`...) or any literal fill, `{#c1121f|text}`.
|
|
115
|
+
* `H_{2}O` and `x^{2}` set sub- and superscripts; the braces are the
|
|
116
|
+
markup, so `file_name` and `m^-1` are typed as they are.
|
|
117
|
+
|
|
118
|
+
`markup=False` turns all of it off for a string that must reach the page
|
|
119
|
+
exactly as typed. `weight` sets the face for the whole block (`"bold"`,
|
|
120
|
+
`"bold italic"`); `features` are OpenType tags, e.g. `{"tnum": True}` for
|
|
121
|
+
the tabular figures an axis wants.
|
|
122
|
+
|
|
123
|
+
`angle` turns the block. **Degrees, and positive is clockwise on the page**
|
|
124
|
+
-- y grows downward in inklet, so the rotation carrying +x toward +y is the
|
|
125
|
+
one a reader sees turn clockwise, and `angle=-90` is the bottom-to-top
|
|
126
|
+
y-axis label. The *shaped block* turns, not the letters one at a time: the
|
|
127
|
+
line was measured once, horizontally, and is then placed, so a turned
|
|
128
|
+
label is tracked exactly like the upright one. What the node reports turns
|
|
129
|
+
with it, so `hstack` packs the diagonal of a 45-degree label rather than
|
|
130
|
+
its upright box and `inklet.lint` measures clearance to the letters where
|
|
131
|
+
they actually are.
|
|
132
|
+
"""
|
|
133
|
+
_check_string("text", content)
|
|
134
|
+
th = current_theme()
|
|
135
|
+
asked = weight if weight is not None else style.get("font_weight") or "regular"
|
|
136
|
+
# A slant asked for as a style field is the same request as one written
|
|
137
|
+
# into the weight, and either way the block has to be *measured* in the
|
|
138
|
+
# sloped face: italic is a different design, not the upright leaned over,
|
|
139
|
+
# and a viewer re-shaping it inside a box built for the upright overruns.
|
|
140
|
+
if style.get("font_style") == "italic" and not _slanted(asked):
|
|
141
|
+
asked = f"{asked} italic"
|
|
142
|
+
prim = shape(
|
|
143
|
+
content,
|
|
144
|
+
font=font or th.font_family,
|
|
145
|
+
size=mm(size) if size is not None else th.font_size,
|
|
146
|
+
weight=asked,
|
|
147
|
+
align=align,
|
|
148
|
+
width=mm(width) if width is not None else None,
|
|
149
|
+
line_height=line_height or th.line_height,
|
|
150
|
+
features=features,
|
|
151
|
+
markup=markup,
|
|
152
|
+
colors=theme_colors(th),
|
|
153
|
+
)
|
|
154
|
+
# Record what it was shaped with. Anything that reshapes the block later --
|
|
155
|
+
# outlining it to paths, placing live glyphs -- must ask for the same
|
|
156
|
+
# features or it positions glyphs by different rules than these advances,
|
|
157
|
+
# and ten tabular digits drift 2.8mm. `getattr` because `shape()` may
|
|
158
|
+
# already have stamped it (M13).
|
|
159
|
+
if features and not getattr(prim, "features", ()):
|
|
160
|
+
prim = _replace(prim, features=text_features(features))
|
|
161
|
+
# The block was measured in the face `weight` names, so the live `<text>`
|
|
162
|
+
# has to ask for the same one or a viewer re-shapes it inside a box built
|
|
163
|
+
# for something else. Weight and slant are separate fields on `Style`, so
|
|
164
|
+
# `weight="bold italic"` sets both and neither can be said by the other.
|
|
165
|
+
if weight is not None:
|
|
166
|
+
words = weight.replace("-", " ").replace("_", " ").split()
|
|
167
|
+
upright = " ".join(w for w in words if w.lower() not in _SLANT_WORDS)
|
|
168
|
+
if upright and upright != "regular" and "font_weight" not in style:
|
|
169
|
+
style["font_weight"] = upright
|
|
170
|
+
if _slanted(weight) and "font_style" not in style and _STYLE_TAKES_SLANT:
|
|
171
|
+
style["font_style"] = "italic"
|
|
172
|
+
node = Diagram(prim=prim, kind=kind,
|
|
173
|
+
envelope_override=_halo_envelope(prim, style.get("halo")))
|
|
174
|
+
node = node.styled(**style) if style else node
|
|
175
|
+
return node if not angle else node.rotated(angle)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _halo_envelope(prim, halo) -> Envelope | None:
|
|
179
|
+
"""The space a haloed block claims: the block, grown by the paper that
|
|
180
|
+
shows around each stem.
|
|
181
|
+
|
|
182
|
+
A halo is a stroke painted *under* the glyphs, half of which the glyph
|
|
183
|
+
itself covers, so `halo=0.4` puts 0.2mm of ink outside the block on every
|
|
184
|
+
side. Layout spaces by the envelope and `inklet.lint` measures gaps with it,
|
|
185
|
+
so without this a haloed label is packed as though the halo were not there
|
|
186
|
+
and the first thing it touches is its neighbour. Nothing is shaped
|
|
187
|
+
differently -- a halo has no advance -- and the trace is deliberately left
|
|
188
|
+
alone, exactly as padding leaves it alone: an arrow aimed at a label should
|
|
189
|
+
still land on the letters.
|
|
190
|
+
"""
|
|
191
|
+
if not halo:
|
|
192
|
+
return None
|
|
193
|
+
return prim.envelope().pad(mm(halo) / 2)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def label(content: str, **kwargs) -> Diagram:
|
|
197
|
+
"""Smaller, quieter text -- for annotating rather than naming."""
|
|
198
|
+
_check_string("label", content)
|
|
199
|
+
kwargs.setdefault("size", current_theme().font_size_small)
|
|
200
|
+
kwargs.setdefault("kind", "label")
|
|
201
|
+
return text(content, **kwargs)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def title(content: str, **kwargs) -> Diagram:
|
|
205
|
+
"""`text` at the theme's large size, tagged as a title.
|
|
206
|
+
|
|
207
|
+
The tag is what lets a theme style titles apart from body text later; the
|
|
208
|
+
size is the theme's, so titles across a figure agree without being told to.
|
|
209
|
+
|
|
210
|
+
The weight is the theme's too, and it is taken *here* rather than left to
|
|
211
|
+
the role applied at build time: a title the theme sets bold has to be
|
|
212
|
+
measured in the bold face, or the box it was stacked into was sized for a
|
|
213
|
+
lighter one and the type overruns it.
|
|
214
|
+
"""
|
|
215
|
+
_check_string("title", content)
|
|
216
|
+
kwargs.setdefault("size", current_theme().font_size_large)
|
|
217
|
+
kwargs.setdefault("kind", "title")
|
|
218
|
+
kwargs.setdefault("weight", _role_face(current_theme().style_for("panel-title")))
|
|
219
|
+
return text(content, **kwargs)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def text_on_path(content: str | Diagram, along, **kwargs) -> Diagram:
|
|
223
|
+
"""Set a line of text along a curve, one shaping cluster per station.
|
|
224
|
+
|
|
225
|
+
ring = inklet.arc(18, -140, -40)
|
|
226
|
+
inklet.drawn([ring, inklet.text_on_path("excitation", ring, size=2.4,
|
|
227
|
+
lift=0.8)])
|
|
228
|
+
|
|
229
|
+
`along` is a drawn node, a `inklet.typeset.Baseline`, or anything
|
|
230
|
+
`inklet.baseline()` takes. A string is shaped here with `inklet.text`'s options
|
|
231
|
+
(`size=`, `weight=`, `fill=`, markup and all), which is why they are
|
|
232
|
+
accepted alongside the placement's own `align`, `start_offset`, `lift`,
|
|
233
|
+
`side`, `flip`, `overflow`, `spacing` and `pivot` -- see
|
|
234
|
+
`inklet.typeset.text_on_path` for those and for the sign convention, which is
|
|
235
|
+
the library's: positive is clockwise. If the figure also *draws* the
|
|
236
|
+
curve, pass `lift=` to raise the type off the stroke.
|
|
237
|
+
"""
|
|
238
|
+
return _onpath.text_on_path(_to_set(content, kwargs), along, **kwargs)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def text_on_arc(content: str | Diagram, radius: float, angle: float,
|
|
242
|
+
**kwargs) -> Diagram:
|
|
243
|
+
"""Set a line of text around a circle, centred on the bearing `angle`.
|
|
244
|
+
|
|
245
|
+
inklet.text_on_arc("270", 21, 90, side="outside", gap=1.0)
|
|
246
|
+
|
|
247
|
+
Degrees, 0 due east and increasing clockwise on the page. `side` is named
|
|
248
|
+
for the circle here -- `"outside"` keeps the ink clear of `radius` on the
|
|
249
|
+
far side from the centre, `"inside"` on the near side, by `gap` mm -- and
|
|
250
|
+
the run turns itself over on the half of the circle where it would
|
|
251
|
+
otherwise read upside-down. `inklet.typeset.text_on_arc` has the rest.
|
|
252
|
+
"""
|
|
253
|
+
return _onpath.text_on_arc(_to_set(content, kwargs), radius, angle, **kwargs)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _to_set(content: str | Diagram, kwargs: dict) -> Diagram:
|
|
257
|
+
"""A string becomes a shaped block, taking the `inklet.text` options out of
|
|
258
|
+
the placement's keywords; a node the caller already built is left alone."""
|
|
259
|
+
if isinstance(content, Diagram):
|
|
260
|
+
return content
|
|
261
|
+
_check_string("text_on_path", content)
|
|
262
|
+
opts = {name: kwargs.pop(name) for name in list(kwargs)
|
|
263
|
+
if name not in _PLACEMENT_ARGS}
|
|
264
|
+
return text(content, **opts)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
#: The keywords that belong to the placement rather than to the typesetting,
|
|
268
|
+
#: so that `inklet.text_on_arc("x", 10, 0, size=2, gap=1)` can carry both.
|
|
269
|
+
_PLACEMENT_ARGS = frozenset({
|
|
270
|
+
"align", "start_offset", "lift", "side", "flip", "overflow", "spacing",
|
|
271
|
+
"pivot", "kind", "gap", "centre", "sweep",
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
#: Slant tokens a `weight=` string may carry, e.g. `weight="bold italic"`.
|
|
276
|
+
_SLANT_WORDS = ("italic", "oblique")
|
|
277
|
+
|
|
278
|
+
#: Whether this build of core can carry a slant on a `Style`. Read rather than
|
|
279
|
+
#: assumed, so a figure asking for italic on an older core is set in the italic
|
|
280
|
+
#: face and merely says so less precisely in the file.
|
|
281
|
+
_STYLE_TAKES_SLANT = hasattr(Style(), "font_style")
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _slanted(weight: str) -> bool:
|
|
285
|
+
"""Whether a weight string asks for a sloped face."""
|
|
286
|
+
return any(word.lower() in _SLANT_WORDS
|
|
287
|
+
for word in weight.replace("-", " ").replace("_", " ").split())
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _role_face(role: Style) -> str:
|
|
291
|
+
"""A theme role's face as one `weight=` string.
|
|
292
|
+
|
|
293
|
+
A role may set a slant as well as a weight, and both have to reach the
|
|
294
|
+
typesetter: the role is applied at build time, long after the title was
|
|
295
|
+
measured and stacked, so a title a theme sets bold italic has to be
|
|
296
|
+
measured in the bold italic or the box it went into was sized for another
|
|
297
|
+
face. Same reasoning as the weight, which has been read here all along.
|
|
298
|
+
"""
|
|
299
|
+
weight = role.font_weight or "regular"
|
|
300
|
+
if getattr(role, "font_style", None) == "italic":
|
|
301
|
+
return f"{weight} italic"
|
|
302
|
+
return weight
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def box(content: str | Diagram | None = None, *, pad: float | str | None = None,
|
|
306
|
+
radius: float | str | None = None, shape_: str = "rect",
|
|
307
|
+
width: float | str | None = None, height: float | str | None = None,
|
|
308
|
+
**style) -> Diagram:
|
|
309
|
+
"""A labelled container that sizes itself to what is inside it.
|
|
310
|
+
|
|
311
|
+
The first argument is what goes *in* the box -- a string or a diagram --
|
|
312
|
+
not how big it is: `width=` and `height=` are minimum sizes, and with no
|
|
313
|
+
content at all they are the whole story, so `box(width=16, height=10)` is
|
|
314
|
+
an empty 16x10 box.
|
|
315
|
+
"""
|
|
316
|
+
th = current_theme()
|
|
317
|
+
if radius is not None:
|
|
318
|
+
style.setdefault("corner_radius", mm(radius))
|
|
319
|
+
_check_content("box", content, width, height)
|
|
320
|
+
if content is None:
|
|
321
|
+
content = spacer()
|
|
322
|
+
inner = text(content, width=width) if isinstance(content, str) else content
|
|
323
|
+
node = _box_container(
|
|
324
|
+
inner,
|
|
325
|
+
pad=th.gap("m") if pad is None else mm(pad),
|
|
326
|
+
radius=th.radius if radius is None else mm(radius),
|
|
327
|
+
shape=shape_,
|
|
328
|
+
min_width=mm(width) if width is not None else None,
|
|
329
|
+
min_height=mm(height) if height is not None else None,
|
|
330
|
+
)
|
|
331
|
+
# The name is what the linter and `fig.report()` quote back, so it is what
|
|
332
|
+
# the reader sees rather than what was typed: `box("**(a)** Cell")` is
|
|
333
|
+
# named "(a) Cell". The markup drew the label; repeating the delimiters in
|
|
334
|
+
# a diagnostic only makes the diagnostic harder to read.
|
|
335
|
+
node = node.named(strip_markup(content)) if isinstance(content, str) else node
|
|
336
|
+
return node.styled(**style) if style else node
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def circle(content: str | Diagram | None = None, **kwargs) -> Diagram:
|
|
340
|
+
"""`box` with an elliptical outline, taking the same keywords.
|
|
341
|
+
|
|
342
|
+
It sizes itself to its content like a box does, which means a long label
|
|
343
|
+
gives a wide ellipse rather than a big circle. A circle of a stated size is
|
|
344
|
+
`circle(width=16, height=16)` -- the first argument is the label, not the
|
|
345
|
+
diameter, so that one function covers both and neither reading is a guess.
|
|
346
|
+
"""
|
|
347
|
+
_check_content("circle", content, kwargs.get("width"), kwargs.get("height"))
|
|
348
|
+
kwargs["shape_"] = "ellipse"
|
|
349
|
+
return box(content, **kwargs)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _check_string(what: str, content) -> None:
|
|
353
|
+
"""Refuse anything but a string where the words go, at the door and by name.
|
|
354
|
+
|
|
355
|
+
Same reasoning as `_check_content`, and the same two mistakes: `text(16)`
|
|
356
|
+
is either a number someone meant to set, or a size they expected the first
|
|
357
|
+
argument to be. Left alone it raises `argument of type 'int' is not
|
|
358
|
+
iterable` from inside the markup scanner, three frames down and naming
|
|
359
|
+
neither the function nor what was wrong with the call.
|
|
360
|
+
"""
|
|
361
|
+
if isinstance(content, str):
|
|
362
|
+
return
|
|
363
|
+
if isinstance(content, (int, float)) and not isinstance(content, bool):
|
|
364
|
+
raise TypeError(
|
|
365
|
+
f"{what}() takes the words to set, not a size: write "
|
|
366
|
+
f"{what}({str(content)!r}) to set the number as text, or "
|
|
367
|
+
f"{what}('...', size={content!r}) to set the type size"
|
|
368
|
+
)
|
|
369
|
+
raise TypeError(
|
|
370
|
+
f"{what}() takes a string, not {type(content).__name__} ({content!r})"
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _check_content(what: str, content, width, height) -> None:
|
|
375
|
+
"""Refuse a size where the content goes, at the door and by name.
|
|
376
|
+
|
|
377
|
+
`inklet.circle(16)` is the reading everyone tries first, and left alone it
|
|
378
|
+
raises `'int' object has no attribute 'envelope'` four frames inside
|
|
379
|
+
`layout`, naming neither the function nor the argument. A number here is
|
|
380
|
+
never anything but this mistake, so it is worth one check to say so.
|
|
381
|
+
"""
|
|
382
|
+
if content is None or isinstance(content, (str, Diagram)):
|
|
383
|
+
return
|
|
384
|
+
if isinstance(content, (int, float)):
|
|
385
|
+
size = f"width={content!r}" + ("" if width or height else
|
|
386
|
+
f", height={content!r}")
|
|
387
|
+
raise TypeError(
|
|
388
|
+
f"{what}() takes the label that goes inside it, not a size: "
|
|
389
|
+
f"write {what}({size}) for the size, or "
|
|
390
|
+
f"{what}('text', {size}) for both"
|
|
391
|
+
)
|
|
392
|
+
raise TypeError(
|
|
393
|
+
f"{what}() takes a string or a Diagram, not "
|
|
394
|
+
f"{type(content).__name__} ({content!r})"
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
from .document import (PublicationProfile, publication, subfigure, Composition, LayoutValue, composition, ModuleSpec, module, Document, CompiledFigure, LayoutError, document, PlotSpec,
|
|
399
|
+
ComponentSpec, plot_spec, component, Dataset, DataRef, Source,
|
|
400
|
+
Series, SharedScale, dataset, shared_scale, CategoryEncoding, FileRef, DerivedData, derive)
|
|
401
|
+
|
|
402
|
+
__all__ = [
|
|
403
|
+
# live documents
|
|
404
|
+
"PublicationProfile", "publication",
|
|
405
|
+
"subfigure", "Composition", "LayoutValue", "composition", "ModuleSpec", "module",
|
|
406
|
+
"Document", "CompiledFigure", "LayoutError", "document", "PlotSpec", "ComponentSpec",
|
|
407
|
+
"plot_spec", "component", "Dataset", "DataRef", "Source", "Series", "SharedScale",
|
|
408
|
+
"dataset", "shared_scale", "CategoryEncoding", "FileRef", "DerivedData", "derive",
|
|
409
|
+
# authoring
|
|
410
|
+
"text", "label", "title", "box", "circle", "asset", "escape_markup",
|
|
411
|
+
"strip_markup",
|
|
412
|
+
"text_on_path", "text_on_arc", "baseline", "baseline_arc", "Baseline",
|
|
413
|
+
# drawing
|
|
414
|
+
"path", "polyline", "polygon", "curve", "arc", "sector", "marker", "place",
|
|
415
|
+
"clip", "encoded", "drawn", "as_drawn", "placed_anchor", "plot_area",
|
|
416
|
+
# annotating
|
|
417
|
+
"annotate", "annotation_side", "bracket", "dimension", "scalebar",
|
|
418
|
+
"letters", "label_slot", "label_specs",
|
|
419
|
+
"place_labels", "label_plan", "LabelChoice", "LabelWeights",
|
|
420
|
+
# scientific diagram components
|
|
421
|
+
"database", "feature_matrix", "sequence",
|
|
422
|
+
# plotting
|
|
423
|
+
"panel", "Panel", "row", "column", "axis", "colorbar", "legend",
|
|
424
|
+
"linear", "log", "symlog", "band", "grouped_band", "broken", "dates", "Scale",
|
|
425
|
+
"ramp", "Ramp", "CategorySet", "categories",
|
|
426
|
+
"inset", "ribbon", "facets", "histogram",
|
|
427
|
+
"polar", "PolarPanel", "theta_ticks",
|
|
428
|
+
"circular_mean", "circular_histogram",
|
|
429
|
+
"model", "solid", "scene", "axes", "cartoon",
|
|
430
|
+
"Mesh", "Vec3", "Mat4", "anchor3d", "outline_of",
|
|
431
|
+
"hstack", "vstack", "stack", "grid", "flow", "overlay", "pad", "frame",
|
|
432
|
+
"spacer",
|
|
433
|
+
"beside", "align_to", "fit",
|
|
434
|
+
"graph", "Graph", "GraphEdge", "GraphError",
|
|
435
|
+
"sankey", "Sankey", "SankeyError", "SankeyFlow", "SankeyNode",
|
|
436
|
+
"link", "Link", "route", "route_all",
|
|
437
|
+
"figure", "Figure",
|
|
438
|
+
# theming
|
|
439
|
+
"theme", "Theme", "THEMES", "use_theme", "current_theme", "contrast_ratio",
|
|
440
|
+
"mix", "lighten", "darken", "readable",
|
|
441
|
+
# inspection and output
|
|
442
|
+
"lint", "abutting", "crossing", "Diagnostic", "format_report",
|
|
443
|
+
"to_svg", "save_svg",
|
|
444
|
+
"to_pdf", "save_pdf", "outline_text",
|
|
445
|
+
"shape", "measure", "apply_theme", "resolve",
|
|
446
|
+
# core types and units
|
|
447
|
+
"Diagram", "DiagramError", "Style", "StyleError", "Vec2", "Rect", "Affine",
|
|
448
|
+
"mm", "pt", "COLUMN_SINGLE", "COLUMN_DOUBLE",
|
|
449
|
+
]
|
|
450
|
+
|
|
451
|
+
__version__ = "2.5.0"
|
inklet/__main__.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Images that behave like diagrams.
|
|
2
|
+
|
|
3
|
+
A scientist has a JPEG of a mouse and wants it in figure 1. Pasted into
|
|
4
|
+
Illustrator it is a rectangle with a white background: arrows stop at the
|
|
5
|
+
picture frame, the stack spaces itself around empty margin, and its colours
|
|
6
|
+
belong to whoever took the photograph. `inklet.asset()` returns a `Diagram`
|
|
7
|
+
instead -- one that knows its own silhouette, sizes itself to the subject
|
|
8
|
+
rather than to the canvas, carries named points, and remembers where it came
|
|
9
|
+
from.
|
|
10
|
+
|
|
11
|
+
mouse = inklet.asset("mouse.png", width=18)
|
|
12
|
+
fig.add(inklet.hstack([mouse, inklet.box("V1")], gap=6))
|
|
13
|
+
fig.link(mouse.at("nose"), stimulus) # anchors from mouse.inklet.json
|
|
14
|
+
print(credit_lines(fig.build()[0])) # what to put under the figure
|
|
15
|
+
|
|
16
|
+
Nothing here is imported until it is called, so `import inklet` still works with
|
|
17
|
+
no image libraries installed at all. The default path needs only Pillow and
|
|
18
|
+
NumPy (`pip install "inklet[images]"`); `rembg` and `potrace` are optional and
|
|
19
|
+
each says in its own docstring what it is good for.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from .asset import ASSET_KIND, DEFAULT_WIDTH, SILHOUETTE_KIND, asset
|
|
25
|
+
from .cache import PIPELINE_VERSION, cache_root, content_hash
|
|
26
|
+
from .cutout import Cutout, cutout_backends, register_cutout
|
|
27
|
+
from .deps import AssetError, MissingDependency
|
|
28
|
+
from .harmonise import Harmonise
|
|
29
|
+
from .lineart import LineArt, potrace_available
|
|
30
|
+
from .provenance import Provenance, credit_lines, credits, provenance_of
|
|
31
|
+
from .sidecar import Sidecar, load_sidecar, sidecar_path
|
|
32
|
+
from .silhouette import Silhouette
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"asset", "DEFAULT_WIDTH", "ASSET_KIND", "SILHOUETTE_KIND",
|
|
36
|
+
"Cutout", "LineArt", "Harmonise", "Silhouette",
|
|
37
|
+
"register_cutout", "cutout_backends", "potrace_available",
|
|
38
|
+
"Provenance", "credits", "credit_lines", "provenance_of",
|
|
39
|
+
"Sidecar", "load_sidecar", "sidecar_path",
|
|
40
|
+
"AssetError", "MissingDependency",
|
|
41
|
+
"cache_root", "content_hash", "PIPELINE_VERSION",
|
|
42
|
+
]
|