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.
Files changed (138) hide show
  1. inklet/__init__.py +451 -0
  2. inklet/__main__.py +2 -0
  3. inklet/assets/__init__.py +42 -0
  4. inklet/assets/asset.py +374 -0
  5. inklet/assets/cache.py +134 -0
  6. inklet/assets/cutout.py +205 -0
  7. inklet/assets/deps.py +83 -0
  8. inklet/assets/harmonise.py +191 -0
  9. inklet/assets/lineart.py +313 -0
  10. inklet/assets/mask.py +117 -0
  11. inklet/assets/provenance.py +135 -0
  12. inklet/assets/raster.py +157 -0
  13. inklet/assets/sidecar.py +112 -0
  14. inklet/assets/silhouette.py +300 -0
  15. inklet/cli.py +197 -0
  16. inklet/components.py +143 -0
  17. inklet/core/__init__.py +26 -0
  18. inklet/core/diagram.py +574 -0
  19. inklet/core/envelope.py +269 -0
  20. inklet/core/geom.py +201 -0
  21. inklet/core/prims.py +342 -0
  22. inklet/core/style.py +179 -0
  23. inklet/core/trace.py +165 -0
  24. inklet/core/units.py +61 -0
  25. inklet/diagnostics/__init__.py +126 -0
  26. inklet/diagnostics/abut.py +54 -0
  27. inklet/diagnostics/break_rules.py +292 -0
  28. inklet/diagnostics/color.py +149 -0
  29. inklet/diagnostics/cross.py +97 -0
  30. inklet/diagnostics/image.py +164 -0
  31. inklet/diagnostics/key_rules.py +570 -0
  32. inklet/diagnostics/link_rules.py +572 -0
  33. inklet/diagnostics/path_rules.py +886 -0
  34. inklet/diagnostics/plot_rules.py +122 -0
  35. inklet/diagnostics/report.py +58 -0
  36. inklet/diagnostics/rules.py +3137 -0
  37. inklet/diagnostics/three_rules.py +218 -0
  38. inklet/document/__init__.py +15 -0
  39. inklet/document/compiler.py +504 -0
  40. inklet/document/composition.py +233 -0
  41. inklet/document/data.py +301 -0
  42. inklet/document/module.py +75 -0
  43. inklet/document/publication.py +61 -0
  44. inklet/document/spec.py +255 -0
  45. inklet/draw/__init__.py +51 -0
  46. inklet/draw/annotate.py +892 -0
  47. inklet/draw/clip.py +531 -0
  48. inklet/draw/coords.py +283 -0
  49. inklet/draw/path.py +249 -0
  50. inklet/draw/place.py +155 -0
  51. inklet/draw/shapes.py +226 -0
  52. inklet/figure.py +418 -0
  53. inklet/layout/__init__.py +27 -0
  54. inklet/layout/fit.py +228 -0
  55. inklet/layout/flow.py +754 -0
  56. inklet/layout/graph.py +712 -0
  57. inklet/layout/graph_force.py +202 -0
  58. inklet/layout/graph_layered.py +1129 -0
  59. inklet/layout/graph_tree.py +226 -0
  60. inklet/layout/labels.py +490 -0
  61. inklet/layout/sankey.py +1054 -0
  62. inklet/links/__init__.py +29 -0
  63. inklet/links/curves.py +173 -0
  64. inklet/links/link.py +2464 -0
  65. inklet/plot/__init__.py +73 -0
  66. inklet/plot/axis.py +523 -0
  67. inklet/plot/breaks.py +429 -0
  68. inklet/plot/categories.py +130 -0
  69. inklet/plot/facets.py +255 -0
  70. inklet/plot/inset.py +310 -0
  71. inklet/plot/key.py +202 -0
  72. inklet/plot/marks.py +1150 -0
  73. inklet/plot/notes.py +220 -0
  74. inklet/plot/panel.py +1775 -0
  75. inklet/plot/png.py +125 -0
  76. inklet/plot/polar.py +1717 -0
  77. inklet/plot/ramp.py +74 -0
  78. inklet/plot/raster.py +191 -0
  79. inklet/plot/ribbon.py +187 -0
  80. inklet/plot/scale.py +1202 -0
  81. inklet/plot/scatter_raster.py +161 -0
  82. inklet/plot/series.py +138 -0
  83. inklet/plot/timescale.py +417 -0
  84. inklet/render/__init__.py +18 -0
  85. inklet/render/bundle.py +75 -0
  86. inklet/render/fontembed.py +391 -0
  87. inklet/render/glyphs.py +200 -0
  88. inklet/render/outline.py +173 -0
  89. inklet/render/paint.py +60 -0
  90. inklet/render/pathdata.py +207 -0
  91. inklet/render/pdf.py +853 -0
  92. inklet/render/pdftext.py +213 -0
  93. inklet/render/preview.py +70 -0
  94. inklet/render/review.py +94 -0
  95. inklet/render/revision.py +47 -0
  96. inklet/render/svg.py +1005 -0
  97. inklet/themes/__init__.py +46 -0
  98. inklet/themes/color.py +384 -0
  99. inklet/themes/palettes.py +192 -0
  100. inklet/themes/theme.py +422 -0
  101. inklet/three/__init__.py +136 -0
  102. inklet/three/api.py +1545 -0
  103. inklet/three/backend.py +942 -0
  104. inklet/three/blender/__init__.py +58 -0
  105. inklet/three/blender/discover.py +204 -0
  106. inklet/three/blender/lineart.py +451 -0
  107. inklet/three/blender/options.py +133 -0
  108. inklet/three/blender/script.py +541 -0
  109. inklet/three/blender/svgread.py +214 -0
  110. inklet/three/blender/tracing.py +220 -0
  111. inklet/three/blender_backend.py +202 -0
  112. inklet/three/camera.py +326 -0
  113. inklet/three/deps.py +93 -0
  114. inklet/three/depth.py +196 -0
  115. inklet/three/drill.py +1129 -0
  116. inklet/three/edges.py +478 -0
  117. inklet/three/hlr.py +463 -0
  118. inklet/three/linalg.py +195 -0
  119. inklet/three/mesh.py +462 -0
  120. inklet/three/occlude.py +372 -0
  121. inklet/three/order.py +945 -0
  122. inklet/three/parse.py +602 -0
  123. inklet/three/place.py +156 -0
  124. inklet/three/protein.py +679 -0
  125. inklet/three/shade.py +828 -0
  126. inklet/three/solids.py +594 -0
  127. inklet/typeset/__init__.py +42 -0
  128. inklet/typeset/fonts.py +383 -0
  129. inklet/typeset/markup.py +421 -0
  130. inklet/typeset/onpath.py +760 -0
  131. inklet/typeset/outline.py +275 -0
  132. inklet/typeset/shaping.py +761 -0
  133. inklet-2.5.0.dist-info/METADATA +171 -0
  134. inklet-2.5.0.dist-info/RECORD +138 -0
  135. inklet-2.5.0.dist-info/WHEEL +4 -0
  136. inklet-2.5.0.dist-info/entry_points.txt +2 -0
  137. inklet-2.5.0.dist-info/licenses/LICENSE +21 -0
  138. 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,2 @@
1
+ from .cli import main
2
+ raise SystemExit(main())
@@ -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
+ ]