sevgi-sundries 0.95.0 → 1.0.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +221 -2
- data/README.md +15 -9
- data/lib/sevgi/sundries/export/native.rb +60 -58
- data/lib/sevgi/sundries/export/system.rb +10 -8
- data/lib/sevgi/sundries/export.rb +46 -26
- data/lib/sevgi/sundries/grid.rb +106 -34
- data/lib/sevgi/sundries/ruler.rb +105 -31
- data/lib/sevgi/sundries/tile.rb +67 -13
- data/lib/sevgi/sundries/version.rb +1 -1
- data/lib/sevgi/sundries.rb +13 -3
- metadata +10 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 20ba96c5308412cca2773c3678698968da83f06814ccdcd9b2d64c9ee522312c
|
|
4
|
+
data.tar.gz: 52b7e80ff6e5828ca83359eeeca6e593440f1dcd74943142987a1a7f149a5a95
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4a899ba87fcb53cd0652e27b587ecfe2ec5d8b184e23c27f114ae08b001aa30b3f9d06497ff921738c8fe7482e212aed5bd0cef71f9e221e9169e855c43faa99
|
|
7
|
+
data.tar.gz: 218c824c2748b155e8d2005df3e8176a6de4b9e80780cba56659ddac4987481d42c23e34b23b4c040d5f5a68d946436ae91640f43d68b94d3f5f841afd9273e8
|
data/CHANGELOG.md
CHANGED
|
@@ -3,7 +3,226 @@
|
|
|
3
3
|
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) and follows
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
5
|
|
|
6
|
-
##
|
|
6
|
+
## 1.0.0 - 2026-09-16
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- Added Hatch and Orb Showcase examples and displayed both arc lengths in the Arc example.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Matched documentation page widths to the homepage Showcase.
|
|
15
|
+
- Aligned Derender user guidance with UTF-8 normalization and safe subtree selection rules.
|
|
16
|
+
- Clarified the packaged skill's script invocation, subtree copying, and Derender selection and encoding rules.
|
|
17
|
+
- Allowed Canvas and Paper values as the first `SVG` argument when using the default document profile.
|
|
18
|
+
- Made element `dup` and `clone` own independent subtrees while retaining IDs and Ruby copy semantics.
|
|
19
|
+
- Made Segment equality compare length and direction; ordering still compares length only.
|
|
20
|
+
- Limited Tile cell, row, and column queries to the requested geometry and accelerated XML character validation.
|
|
21
|
+
- Simplified the first-run guide and aligned library constructor examples.
|
|
22
|
+
- Updated the packaged skill for canvas origins, alignment, Paper lookup, curved geometry, and export constraints.
|
|
23
|
+
- Revised DSL catalog examples and packaged Sevgi skill guidance to use statement-style calls and show inspection results.
|
|
24
|
+
- Refined Showcase example tabs, made Ruby and XML views fill their panels, and hid executable preambles from Ruby views.
|
|
25
|
+
- Documented non-rendering element data and clarified the related DSL catalog examples.
|
|
26
|
+
- Refined Showcase heading spacing, page-title colors, and DSL catalog cards across light and dark themes.
|
|
27
|
+
- Removed an obsolete ruler example and unused site code.
|
|
28
|
+
- Kept the Squared and Copperplate guidesheet examples together at the end of the Showcase.
|
|
29
|
+
- Updated the packaged skill to avoid duplicate semantic shapes and unnecessary paint passes.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- Kept horizontal scrollbars visible in Showcase example code panels.
|
|
34
|
+
- Read Derender input in its declared XML encoding and normalized generated source, XML declarations, and markup to UTF-8.
|
|
35
|
+
- Preserved open-path endpoints and derived edges independently of display precision.
|
|
36
|
+
- Kept near-axis and near-parallel line equations and intersections independent of angle and slope rounding.
|
|
37
|
+
- Corrected the Showcase social image dimensions.
|
|
38
|
+
- Added SVG namespaces to standalone drawing examples and regenerated their outputs.
|
|
39
|
+
- Made failed Sevgi Rake tasks stop dependent tasks and captured required-library failures in executor results.
|
|
40
|
+
- Removed executor signal-handler replacement so the host and shell retain SIGINT ownership.
|
|
41
|
+
- Escaped late export CSS as XML text and inserted it only at a supported final root closing tag.
|
|
42
|
+
- Preserved Derender processing instructions and rejected custom entity references and unsupported trailing document nodes before inclusion.
|
|
43
|
+
- Included both box origins in `Align`, unified Tile index validation, and isolated custom numeric attribute snapshots.
|
|
44
|
+
- Prevented avoidable overflow and underflow in point distances.
|
|
45
|
+
- Corrected browser source-panel expectations and added executable documentation, YARD, and packaged-skill checks.
|
|
46
|
+
- Rendered inline code in DSL catalog summaries and kept linked task themes visible below the sticky header.
|
|
47
|
+
- Restored Showcase tab icons with embedded Font Awesome SVGs.
|
|
48
|
+
|
|
49
|
+
## 0.100.0 - 2026-09-09
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
|
|
53
|
+
- Added `ArcTo` and `ArcBy` for SVG-native elliptical arc paths.
|
|
54
|
+
- Added Geometry `Arc`, `Ellipse`, and `Circle` with bounds, length, predicates, affine transformations, and
|
|
55
|
+
linear/quadratic intersections. Closed ellipses and circles support sweeps and hatching.
|
|
56
|
+
- Added Geometry `Point.midpoint`, `Element#closed?`, lined `vertices`, open-path traversal, and `Rect#center`.
|
|
57
|
+
- Added Geometry aggregate bounding boxes through `Operation.box`, point collinearity through `Point.collinear?`, and
|
|
58
|
+
polygon `simple?`, `convex?`, and `concave?` predicates.
|
|
59
|
+
- Added Protractor and Arc examples and documented the drawing and calculation APIs.
|
|
60
|
+
|
|
61
|
+
### Changed
|
|
62
|
+
|
|
63
|
+
- Removed redundant parent scans while duplicating element subtrees.
|
|
64
|
+
- Reused source bounds across Tile cells to reduce repeated calculations and object allocations.
|
|
65
|
+
|
|
66
|
+
### Fixed
|
|
67
|
+
|
|
68
|
+
- Preserved Showcase card HTML through Markdown rendering so all examples and their tabs remain visible.
|
|
69
|
+
- Preserved XML declarations, pre-root comments, CDATA sections, inline comments, and namespace-only leaves in
|
|
70
|
+
Derender output and evaluation.
|
|
71
|
+
- Limited PDF stamp recoloring to replaced text and carried graphics state across a page's content streams.
|
|
72
|
+
- Corrected decimal interval fitting and made Grid point queries honor the active numeric precision.
|
|
73
|
+
- Prevented implicit shell interpretation of single executable arguments in `F.sh` and `F.sh!`, including paths with spaces or shell metacharacters.
|
|
74
|
+
- Preserved inherited namespaces on detached Derender nodes in generated source and direct evaluation, including imports through `EvaluateChildren` and `IncludeChildren`.
|
|
75
|
+
- Prevented sweep and Hatch lines from crossing the exterior between two tangent vertices of a concave polygon.
|
|
76
|
+
- Aligned Tile cells with their declared bounds for circles, rotated shapes, and polygons whose first vertex differs from their bounding-box origin.
|
|
77
|
+
- Limited `Load` chains to 128 active sources with `Executor::LoadDepthError`, preserving cycle detection and nested load diagnostics.
|
|
78
|
+
- Rejected nil and false aggregate-box inputs with the documented geometry error.
|
|
79
|
+
- Restored precision-aware rejection of zero-length Triangle sides.
|
|
80
|
+
- Made point-set collinearity independent of input order and removed eager polygon edge-pair storage.
|
|
81
|
+
- Applied the collinearity tolerance to every three-point subset, removing coordinate-axis-dependent baseline selection.
|
|
82
|
+
|
|
83
|
+
## 0.99.0 - 2026-08-09
|
|
84
|
+
|
|
85
|
+
### Added
|
|
86
|
+
|
|
87
|
+
- Added `SVG.Module` for building anonymous callable drawing modules with their contract installed before definition.
|
|
88
|
+
- Added responsive Mermaid diagrams for the Derender round trip and SVG validation lifecycle.
|
|
89
|
+
|
|
90
|
+
### Changed
|
|
91
|
+
|
|
92
|
+
- Reorganized the documentation around getting started, usage, composition, documents, layout, output, and examples.
|
|
93
|
+
- Documented how the `igves` and `igsev` command names reflect their conversion directions.
|
|
94
|
+
- Normalized DSL catalog provider labels to lowercase and simplified polyfill-backed editor entries to `inkscape`.
|
|
95
|
+
|
|
96
|
+
### Fixed
|
|
97
|
+
|
|
98
|
+
- Allowed A5-on-A4 pdfcpu conversion to replace an existing output file.
|
|
99
|
+
- Pre-rendered Mermaid diagrams as inline SVG instead of parsing them in site visitors' browsers.
|
|
100
|
+
- Removed duplicate provider and context labels from DSL catalog entries.
|
|
101
|
+
|
|
102
|
+
## 0.98.2 - 2026-07-25
|
|
103
|
+
|
|
104
|
+
### Fixed
|
|
105
|
+
|
|
106
|
+
- Restored the `sevgi` CLI's main-object scope so bare toolkit calls remain available inside script helper classes.
|
|
107
|
+
|
|
108
|
+
## 0.98.1 - 2026-07-20
|
|
109
|
+
|
|
110
|
+
### Changed
|
|
111
|
+
|
|
112
|
+
- Changed the `sevgi` CLI to use the executor's isolated scope by default.
|
|
113
|
+
|
|
114
|
+
### Removed
|
|
115
|
+
|
|
116
|
+
- Removed the redundant `-n` and `--nomain` CLI options; library consumers can still select `main: true` through
|
|
117
|
+
`Sevgi.execute` and `Sevgi.execute_file` when needed.
|
|
118
|
+
|
|
119
|
+
## 0.98.0 - 2026-07-20
|
|
120
|
+
|
|
121
|
+
### Added
|
|
122
|
+
|
|
123
|
+
- Added standard-input support to `sevgi`, `igves`, and `igsev`; `sevgi --as NAME` and
|
|
124
|
+
`Sevgi.execute_file(..., as: NAME)` supply the logical source basename used by implicit SVG, PDF, and PNG
|
|
125
|
+
destinations without changing physical load identity.
|
|
126
|
+
|
|
127
|
+
## 0.97.0 - 2026-07-19
|
|
128
|
+
|
|
129
|
+
### Added
|
|
130
|
+
|
|
131
|
+
- Added `sevgi-appendix`, containing the Sevgi agent skill and a RuboCop plugin for preserving readable `.sevgi` DSL
|
|
132
|
+
source alongside rubyfmt-formatted Ruby; the umbrella `sevgi` gem installs the matching Appendix version.
|
|
133
|
+
- Added `sevgi --skill` to report the validated path of the matching packaged agent skill for product-neutral setup.
|
|
134
|
+
- Added `igsev` to the umbrella gem for normalizing an SVG file through a complete SVG-to-Sevgi-to-SVG round trip.
|
|
135
|
+
|
|
136
|
+
### Changed
|
|
137
|
+
|
|
138
|
+
- Made Homebrew the recommended complete CLI installation while retaining Bundler and focused component gems for Ruby
|
|
139
|
+
application dependencies.
|
|
140
|
+
|
|
141
|
+
## 0.96.0 - 2026-07-18
|
|
142
|
+
|
|
143
|
+
### Added
|
|
144
|
+
|
|
145
|
+
- Added exact, subtree-wide attribute omission to Derender content/file conversion, evaluation, and inclusion APIs;
|
|
146
|
+
`igves --omit` exposes the same behavior from the command line.
|
|
147
|
+
- Added `Sevgi.SVG` as the explicit namespaced form of the top-level SVG document entrypoint.
|
|
148
|
+
- Added the opt-in recursive `SVG::Modules` contract for callable drawing namespaces.
|
|
149
|
+
- Added callable drawing-module `base` blocks, document and paper registry introspection, Canvas structural equality,
|
|
150
|
+
renderer options on `RenderChildren`, axis translation helpers, and a CC BY RDF helper.
|
|
151
|
+
- Formalized non-rendering `-` metadata and `+` attribute updates as `Attributes::META_PREFIX` and
|
|
152
|
+
`Attributes::UPDATE_SUFFIX`; repeated Array updates now concatenate into a stable flat value.
|
|
153
|
+
|
|
154
|
+
### Changed
|
|
155
|
+
|
|
156
|
+
- Breaking: replaced the `SVG = Sevgi::Graphics` alias with an independent SVG facade. Facade operations now use
|
|
157
|
+
capitalized names such as `SVG.Canvas` and `SVG.Document`; lowercase component helpers remain on
|
|
158
|
+
`Sevgi::Graphics`, and the stuttering `SVG.SVG` form was removed.
|
|
159
|
+
- Breaking: made low-level `Executor.execute` and `execute_file` runners private; consumers use
|
|
160
|
+
`Sevgi.execute` and `Sevgi.execute_file` while retaining the public result and error types.
|
|
161
|
+
- Breaking: removed the abstract `Document::Base` layer from the selectable document-profile registry; `Minimal` and
|
|
162
|
+
`Default` are now sibling concrete profiles, and custom profile hierarchies should derive from `Base`.
|
|
163
|
+
- Breaking: renamed Parallelogram segment and constraint arguments by geometric role: `base`, `side`, and
|
|
164
|
+
`constraint` replace axis-implying constructor names.
|
|
165
|
+
- Made degree-based sine and cosine exact at integer quarter turns, eliminating cardinal Geometry residue.
|
|
166
|
+
- Documented complete file-system failure families for file comparison, output, touch, and upward location helpers.
|
|
167
|
+
- Formalized generated Tile ids, positional CSS classes, template placement, and per-use callback signatures.
|
|
168
|
+
- Formalized String and Symbol ids across Derender selection APIs and documented file-read failures consistently.
|
|
169
|
+
- Breaking: made top-level `Decompile`, `Derender`, `Evaluate`, and `EvaluateChildren` consume inline SVG/XML;
|
|
170
|
+
file inputs now use the corresponding `File`-suffixed entrypoints.
|
|
171
|
+
- Breaking: replaced callable drawing module `call` block registration with argument-independent `base` blocks;
|
|
172
|
+
inherited bases run parent-first, and modules with one drawing method conventionally name it `call`.
|
|
173
|
+
- Breaking: made bracket and call notation the canonical Geometry constructors. Among Data value types, bracket notation
|
|
174
|
+
is public only for Point, Segment, LengthAngle, Margin, and Paper; use `.new` for Location and result carriers, and
|
|
175
|
+
`Stay` for traversal stop tokens.
|
|
176
|
+
- Breaking: aligned Point and Segment comparison with Ruby `Comparable`; malformed or unrelated comparison operands now
|
|
177
|
+
return nil from `<=>`.
|
|
178
|
+
- Breaking: executor entrypoints now return immutable `Executor::Result` values and expose only `execute` and
|
|
179
|
+
`execute_file`; inspect `result.error`, `result.stack`, and `result.value` instead of executor scopes or lifecycle state.
|
|
180
|
+
- Breaking: wrapper attributes and callable arguments use distinct channels, `With` rejects parentless receivers, and
|
|
181
|
+
unsupported direct constructors are private; use the documented factories for Content, concrete Geometry lined
|
|
182
|
+
elements, and Grid query values.
|
|
183
|
+
- Breaking: Canvas uses `.new` for explicit fields, `.from_paper(paper, **overrides)` for paper conversion, and
|
|
184
|
+
`.call`/`Graphics.canvas` for dispatch; the misleading keyword-only `.from_paper(width:, height:)` form was removed.
|
|
185
|
+
- Breaking: `Save`, `Write`, `PNG`, `PDF`, and Sundries native export normalize successful destinations to expanded
|
|
186
|
+
String paths and create missing parent directories; change-aware SVG writes still return nil when unchanged.
|
|
187
|
+
- Breaking: renamed `Derender.evaluate_file_children` to `evaluate_children_file`; decompiled nodes now own immutable
|
|
188
|
+
public state while parser, strategy, and construction plumbing remain private.
|
|
189
|
+
- Expanded runtime/YARD parity checks and exact contracts for inherited, extended, generated, and dynamic API surfaces,
|
|
190
|
+
forwarded options, error channels, path ownership, whitespace, namespaces, nil behavior, and return values.
|
|
191
|
+
|
|
192
|
+
### Fixed
|
|
193
|
+
|
|
194
|
+
- Rejected directories from default file discovery while preserving custom locator matchers.
|
|
195
|
+
- Kept `Ancestral` context in non-rendering `-context` metadata instead of leaking it into SVG attributes.
|
|
196
|
+
- Preserved source Canvas units and names when deriving fitted Grid canvases.
|
|
197
|
+
- Prevented stale documentation assets from mixing old layouts with new HTML, and moved sidebar navigation into the
|
|
198
|
+
tablet menu at 1024px and below.
|
|
199
|
+
- Normalized numeric slots owned by path, shape, transform, tile, and Inkscape page helpers to finite SVG number spelling;
|
|
200
|
+
arbitrary user-supplied attributes remain untouched.
|
|
201
|
+
- Made named callable wrapper and symbol ids stable while omitting defaults for anonymous modules, and indexed every
|
|
202
|
+
rendered id value, including false and numeric values, through its serialized string.
|
|
203
|
+
- Preserved signed Geometry constraint directions, rejected invalid sweep/export channels through Sevgi error families,
|
|
204
|
+
and validated raw output paths before expansion or rendering.
|
|
205
|
+
- Made callable module configuration copy-owned and freeze-aware, document subclasses inherit their nearest profile, and
|
|
206
|
+
false executor boot receivers remain explicit rather than defaulting to an internal scope.
|
|
207
|
+
- Made element trees, attributes, identifiers, locator results, document profiles, Derender nodes, shell results, and
|
|
208
|
+
executor results retain owned immutable snapshots where their public contracts promise value semantics.
|
|
209
|
+
- Corrected Shell combined-output separators, nil export density errors, document render-option routing, executor source
|
|
210
|
+
snapshots, selected-node namespace/whitespace documentation, and Standard character-data validation.
|
|
211
|
+
|
|
212
|
+
### Removed
|
|
213
|
+
|
|
214
|
+
- Removed public access to pluralization tables and the internal SVG save extension; pluralization rules are now deeply
|
|
215
|
+
immutable.
|
|
216
|
+
- Removed public documentation and constant access for command-line implementation modules; the `sevgi` and `igves`
|
|
217
|
+
executables remain unchanged.
|
|
218
|
+
- Removed eager loading and public documentation of private Showcase build/test support; explicit support entrypoints
|
|
219
|
+
now keep the harness under the Showcase namespace.
|
|
220
|
+
- Removed public access to document profile name normalizers; registry operations retain them as private plumbing.
|
|
221
|
+
- Removed the redundant `Margin.margin` constructor; use canonical bracket notation.
|
|
222
|
+
- Removed public executor orchestration, obsolete callable-module hooks, the old public attribute syntax constants, and
|
|
223
|
+
direct construction of abstract or internally wired Content, Element, and Grid query types.
|
|
224
|
+
- Removed accidental public access to abstract Lined factories, internal element-name/export maps, result/location
|
|
225
|
+
bracket constructors, and direct Stop construction.
|
|
7
226
|
|
|
8
227
|
## 0.95.0 - 2026-07-11
|
|
9
228
|
|
|
@@ -87,7 +306,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|
|
87
306
|
|
|
88
307
|
### Changed
|
|
89
308
|
|
|
90
|
-
- Added the
|
|
309
|
+
- Added the checkerboard example to the documentation showcase flow.
|
|
91
310
|
- Consolidated Victor Book attribution for adapted showcase examples into a single documentation note.
|
|
92
311
|
|
|
93
312
|
### Fixed
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Sevgi Sundries
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Sevgi Sundries contains shared layout objects and optional export tools.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -17,14 +17,20 @@ require "sevgi/sundries"
|
|
|
17
17
|
## Example
|
|
18
18
|
|
|
19
19
|
```ruby
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
x = Sevgi::Sundries::Ruler.new(brut: 80, unit: 1, multiple: 10, margins: [5])
|
|
21
|
+
y = Sevgi::Sundries::Ruler.new(brut: 50, unit: 1, multiple: 10, margins: [5])
|
|
22
|
+
grid = Sevgi::Sundries::Grid[x, y]
|
|
23
|
+
|
|
24
|
+
grid.x.major.lines.size # => 5
|
|
25
|
+
grid.canvas.margin.to_a # => [5.0, 5.0, 5.0, 5.0]
|
|
23
26
|
```
|
|
24
27
|
|
|
28
|
+
Rulers, grids, and tiles are inspectable Ruby values. They create no SVG elements by themselves. Pass their geometry
|
|
29
|
+
to Sevgi Graphics when the document needs it.
|
|
30
|
+
|
|
25
31
|
## Ruby compatibility
|
|
26
32
|
|
|
27
|
-
Requires Ruby 3.4.0 or newer. CI verifies Ruby 3.4
|
|
33
|
+
Requires Ruby 3.4.0 or newer. CI verifies the current Ruby 3.4 release and the development Ruby from `.ruby-version`.
|
|
28
34
|
|
|
29
35
|
## Native prerequisites
|
|
30
36
|
|
|
@@ -49,7 +55,7 @@ gem install cairo rsvg2 hexapdf
|
|
|
49
55
|
|
|
50
56
|
## Links
|
|
51
57
|
|
|
52
|
-
- Documentation: https://sevgi.roktas.dev
|
|
53
|
-
- API documentation: https://www.rubydoc.info/gems/sevgi-sundries
|
|
54
|
-
- Source: https://github.com/roktas/sevgi/tree/main/sundries
|
|
55
|
-
- Changelog: https://github.com/roktas/sevgi/blob/main/CHANGELOG.md
|
|
58
|
+
- Documentation: <https://sevgi.roktas.dev>
|
|
59
|
+
- API documentation: <https://www.rubydoc.info/gems/sevgi-sundries>
|
|
60
|
+
- Source: <https://github.com/roktas/sevgi/tree/main/sundries>
|
|
61
|
+
- Changelog: <https://github.com/roktas/sevgi/blob/main/CHANGELOG.md>
|
|
@@ -20,29 +20,33 @@ module Sevgi
|
|
|
20
20
|
|
|
21
21
|
# Exports SVG source to a PDF or PNG file using librsvg and Cairo.
|
|
22
22
|
# @param svg [String] SVG source content
|
|
23
|
-
#
|
|
23
|
+
# Relative paths are expanded, missing parent directories are created after all render inputs validate, and an
|
|
24
|
+
# existing output file is replaced. Directory paths are not expanded to a default file name.
|
|
25
|
+
# @param output [String, #to_path] output file path
|
|
24
26
|
# @param format [Symbol, String, nil] explicit output format, or nil to infer from output extension
|
|
25
27
|
# @param width [Numeric, nil] finite positive target width in output pixels for PNG, or CSS pixels before PDF point conversion
|
|
26
28
|
# @param height [Numeric, nil] finite positive target height in output pixels for PNG, or CSS pixels before PDF point conversion
|
|
27
|
-
# @param dpi [Numeric] finite positive CSS pixel density
|
|
29
|
+
# @param dpi [Numeric] finite positive CSS pixel density. Omission uses {DEFAULT_DPI}, but explicit nil is invalid
|
|
28
30
|
# @param css [String, nil] CSS inserted before the closing svg tag before rendering
|
|
29
31
|
# @yield [svg] optional source transformation applied before rendering
|
|
30
32
|
# @yieldparam svg [String] SVG source after optional CSS injection
|
|
31
33
|
# @yieldreturn [String] SVG source to render
|
|
32
|
-
# @return [
|
|
33
|
-
# @raise [Sevgi::ArgumentError] when output,
|
|
34
|
-
#
|
|
34
|
+
# @return [String] expanded output path
|
|
35
|
+
# @raise [Sevgi::ArgumentError] when output is blank, invalid, or a directory, or CSS/transformed SVG has an
|
|
36
|
+
# invalid type
|
|
37
|
+
# @raise [Sevgi::Sundries::Export::ExportError] when format, numeric options, CSS insertion, SVG parsing, SVG dimensions, or render dimensions are invalid
|
|
38
|
+
# @raise [SystemCallError] when the output directory or file cannot be created or written
|
|
35
39
|
def call(svg, output, format: nil, width: nil, height: nil, dpi: DEFAULT_DPI, css: nil, &block)
|
|
36
40
|
ArgumentError.("SVG content must be a String") unless svg.is_a?(String)
|
|
37
|
-
original_output = output
|
|
38
41
|
output = output_path(output)
|
|
39
|
-
format = format_for
|
|
42
|
+
format = format_for(format, output)
|
|
40
43
|
width = dimension(width, "width")
|
|
41
44
|
height = dimension(height, "height")
|
|
42
|
-
dpi = dimension(dpi, "dpi")
|
|
45
|
+
dpi = dimension(dpi, "dpi", optional: false)
|
|
43
46
|
ArgumentError.("Export CSS must be a String") unless css.nil? || css.is_a?(String)
|
|
47
|
+
ArgumentError.("Export CSS must be valid text") if css && !css.valid_encoding?
|
|
44
48
|
|
|
45
|
-
svg =
|
|
49
|
+
svg = styled(svg, css) if css && !css.strip.empty?
|
|
46
50
|
svg = block.call(svg) if block
|
|
47
51
|
ArgumentError.("SVG content must be a String") unless svg.is_a?(String)
|
|
48
52
|
|
|
@@ -62,6 +66,7 @@ module Sevgi
|
|
|
62
66
|
tw, th = target_size(iw, ih, width, height)
|
|
63
67
|
ExportError.("Invalid export dimensions") unless target_size?(format, tw, th)
|
|
64
68
|
|
|
69
|
+
::FileUtils.mkdir_p(::File.dirname(output))
|
|
65
70
|
renderer.call(
|
|
66
71
|
handle: handle,
|
|
67
72
|
output: output,
|
|
@@ -75,7 +80,7 @@ module Sevgi
|
|
|
75
80
|
ExportError.("Render error: #{e.message}")
|
|
76
81
|
end
|
|
77
82
|
|
|
78
|
-
|
|
83
|
+
output
|
|
79
84
|
end
|
|
80
85
|
|
|
81
86
|
# Replaces exact placeholder text objects in PDF streams.
|
|
@@ -88,24 +93,18 @@ module Sevgi
|
|
|
88
93
|
# @param placeholder [String] placeholder text to replace
|
|
89
94
|
# @return [Boolean] true when at least one matching placeholder was replaced
|
|
90
95
|
# @raise [Sevgi::Sundries::Export::ExportError] when the PDF cannot be read, rewritten, or stamped
|
|
91
|
-
# @note
|
|
96
|
+
# @note Pages with inline images, unknown operators, or unbalanced graphics-state/text-object operators are left unchanged.
|
|
92
97
|
def stamp(infile, outfile, stamp:, placeholder:)
|
|
93
98
|
doc = HexaPDF::Document.open(infile)
|
|
94
99
|
replacements = 0
|
|
95
100
|
|
|
96
101
|
doc.pages.each do |page|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
next unless obj.respond_to?(:stream)
|
|
102
|
+
data, count = stamp_stream(page.contents, stamp:, placeholder:)
|
|
103
|
+
next if count.zero?
|
|
100
104
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
replacements += count
|
|
105
|
-
|
|
106
|
-
obj.stream = data
|
|
107
|
-
obj.set_filter(:FlateDecode)
|
|
108
|
-
end
|
|
105
|
+
replacements += count
|
|
106
|
+
page.contents = data
|
|
107
|
+
doc.deref(page[:Contents]).set_filter(:FlateDecode)
|
|
109
108
|
end
|
|
110
109
|
|
|
111
110
|
doc.write(outfile, optimize: true) if replacements.positive?
|
|
@@ -122,7 +121,7 @@ module Sevgi
|
|
|
122
121
|
# @param placeholder [String] placeholder text to replace
|
|
123
122
|
# @return [Boolean] true when at least one matching placeholder was replaced
|
|
124
123
|
# @raise [Sevgi::Sundries::Export::ExportError] when the PDF cannot be read, rewritten, stamped, or replaced
|
|
125
|
-
# @note
|
|
124
|
+
# @note Pages with inline images, unknown operators, or unbalanced graphics-state/text-object operators are left unchanged.
|
|
126
125
|
def stamp!(infile, stamp:, placeholder:)
|
|
127
126
|
temp = Tempfile.new(%w[stamp .pdf], File.dirname(infile))
|
|
128
127
|
stamped = stamp(infile, temp.path, stamp:, placeholder:)
|
|
@@ -181,7 +180,6 @@ module Sevgi
|
|
|
181
180
|
serializer = HexaPDF::Serializer.new
|
|
182
181
|
operands = []
|
|
183
182
|
state = {
|
|
184
|
-
fill_span: nil,
|
|
185
183
|
fill_white: false,
|
|
186
184
|
font_size: nil,
|
|
187
185
|
in_text: false,
|
|
@@ -202,8 +200,12 @@ module Sevgi
|
|
|
202
200
|
next
|
|
203
201
|
end
|
|
204
202
|
|
|
203
|
+
# Inline image bytes are not PDF tokens; never interpret them as operators.
|
|
204
|
+
operator = object.to_sym
|
|
205
|
+
return [{}, 0] if operator == :BI || !HexaPDF::Content::Processor::OPERATOR_MESSAGE_NAME_MAP.key?(operator)
|
|
206
|
+
|
|
205
207
|
count += process_operator(
|
|
206
|
-
|
|
208
|
+
operator,
|
|
207
209
|
operands,
|
|
208
210
|
finish,
|
|
209
211
|
data,
|
|
@@ -223,11 +225,13 @@ module Sevgi
|
|
|
223
225
|
def process_operator(operator, operands, finish, data, stamp:, placeholder:, serializer:, state:, replacements:)
|
|
224
226
|
case operator
|
|
225
227
|
when :q
|
|
226
|
-
state[:stack] << state.values_at(:
|
|
228
|
+
state[:stack] << state.values_at(:fill_white, :font_size)
|
|
227
229
|
when :Q
|
|
228
230
|
state[:stack].any? ? restore_state(state) : state[:valid] = false
|
|
229
231
|
when :rg
|
|
230
|
-
set_fill_state(state, operands
|
|
232
|
+
set_fill_state(state, operands)
|
|
233
|
+
when :g, :k, :cs, :sc, :scn
|
|
234
|
+
state[:fill_white] = false
|
|
231
235
|
when :BT
|
|
232
236
|
state[:valid] = false if state[:in_text]
|
|
233
237
|
state[:in_text] = true
|
|
@@ -241,6 +245,7 @@ module Sevgi
|
|
|
241
245
|
operator,
|
|
242
246
|
operands,
|
|
243
247
|
data,
|
|
248
|
+
operator_finish: finish,
|
|
244
249
|
stamp:,
|
|
245
250
|
placeholder:,
|
|
246
251
|
serializer:,
|
|
@@ -253,31 +258,38 @@ module Sevgi
|
|
|
253
258
|
end
|
|
254
259
|
|
|
255
260
|
def restore_state(state)
|
|
256
|
-
state[:
|
|
261
|
+
state[:fill_white], state[:font_size] = state[:stack].pop
|
|
257
262
|
end
|
|
258
263
|
|
|
259
|
-
def set_fill_state(state, operands
|
|
264
|
+
def set_fill_state(state, operands)
|
|
260
265
|
state[:fill_white] = operands.size == 3 && operands.all? { |value, _start, _finish| value == 1 }
|
|
261
|
-
state[:fill_span] = [operands.first[1], finish] if state[:fill_white]
|
|
262
|
-
state[:fill_span] = nil unless state[:fill_white]
|
|
263
266
|
end
|
|
264
267
|
|
|
265
|
-
def add_text_replacement(
|
|
268
|
+
def add_text_replacement(
|
|
269
|
+
operator,
|
|
270
|
+
operands,
|
|
271
|
+
data,
|
|
272
|
+
operator_finish:,
|
|
273
|
+
stamp:,
|
|
274
|
+
placeholder:,
|
|
275
|
+
serializer:,
|
|
276
|
+
state:,
|
|
277
|
+
replacements:
|
|
278
|
+
)
|
|
266
279
|
replacement = text_replacement(data, operator, operands, stamp:, placeholder:, serializer:, state:)
|
|
267
280
|
return 0 unless replacement
|
|
268
281
|
|
|
269
|
-
start,
|
|
270
|
-
|
|
271
|
-
|
|
282
|
+
start, text_finish, text = replacement
|
|
283
|
+
color_start = operands.first.fetch(1)
|
|
284
|
+
text = " 0.101961 0.101961 0.101961 rg#{text}" if start == color_start
|
|
285
|
+
replacements[[start, text_finish]] = text
|
|
286
|
+
add_color_replacement(color_start, replacements) unless start == color_start
|
|
287
|
+
replacements[[operator_finish, operator_finish]] ||= " 1 1 1 rg"
|
|
272
288
|
1
|
|
273
289
|
end
|
|
274
290
|
|
|
275
|
-
def add_color_replacement(
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
color_start, color_finish = state[:fill_span]
|
|
279
|
-
prefix = data[color_start...color_finish].to_s[/\A\s*/]
|
|
280
|
-
replacements[[color_start, color_finish]] ||= "#{prefix}0.101961 0.101961 0.101961 rg"
|
|
291
|
+
def add_color_replacement(start, replacements)
|
|
292
|
+
replacements[[start, start]] ||= " 0.101961 0.101961 0.101961 rg"
|
|
281
293
|
end
|
|
282
294
|
|
|
283
295
|
def text_replacement(data, operator, operands, stamp:, placeholder:, serializer:, state:)
|
|
@@ -308,19 +320,21 @@ module Sevgi
|
|
|
308
320
|
def output_path(output)
|
|
309
321
|
ArgumentError.("Export output must be provided") if output.nil?
|
|
310
322
|
|
|
311
|
-
path = output.
|
|
312
|
-
ArgumentError.("Export output must be a String-like
|
|
323
|
+
path = output.respond_to?(:to_path) ? output.to_path : output
|
|
324
|
+
ArgumentError.("Export output must be a String or path-like object") unless path.is_a?(::String)
|
|
313
325
|
ArgumentError.("Export output must be provided") if path.strip.empty?
|
|
314
326
|
|
|
327
|
+
path = ::File.expand_path(path)
|
|
328
|
+
ArgumentError.("Export output must name a file") if ::File.directory?(path)
|
|
315
329
|
path
|
|
316
330
|
rescue ::StandardError => e
|
|
317
331
|
raise if e.is_a?(::Sevgi::ArgumentError)
|
|
318
332
|
|
|
319
|
-
ArgumentError.("Export output must be a String-like
|
|
333
|
+
ArgumentError.("Export output must be a String or path-like object: #{e.message}")
|
|
320
334
|
end
|
|
321
335
|
|
|
322
|
-
def dimension(value, field)
|
|
323
|
-
return if value.nil?
|
|
336
|
+
def dimension(value, field, optional: true)
|
|
337
|
+
return if value.nil? && optional
|
|
324
338
|
ExportError.(dimension_error(field)) unless value.is_a?(::Numeric)
|
|
325
339
|
|
|
326
340
|
number = begin
|
|
@@ -420,18 +434,6 @@ module Sevgi
|
|
|
420
434
|
module Renderer
|
|
421
435
|
extend self
|
|
422
436
|
|
|
423
|
-
# Returns a renderer method for a format.
|
|
424
|
-
# @param format [Symbol, String, nil] format name
|
|
425
|
-
# @return [Method, nil]
|
|
426
|
-
def [](format)
|
|
427
|
-
case format&.to_sym
|
|
428
|
-
when :png
|
|
429
|
-
method(:png)
|
|
430
|
-
when :pdf
|
|
431
|
-
method(:pdf)
|
|
432
|
-
end
|
|
433
|
-
end
|
|
434
|
-
|
|
435
437
|
# Renders SVG data to a PDF surface.
|
|
436
438
|
# @param handle [Rsvg::Handle] parsed SVG handle
|
|
437
439
|
# @param output [String] output file path
|
|
@@ -22,8 +22,10 @@ module Sevgi
|
|
|
22
22
|
# @raise [Errno::ENOENT] when the executable cannot be spawned
|
|
23
23
|
def a5_on_a4!(infile)
|
|
24
24
|
temp = Tempfile.new(%w[output .pdf], File.dirname(infile))
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
outfile = temp.path
|
|
26
|
+
temp.close!
|
|
27
|
+
a5_on_a4(infile, outfile)
|
|
28
|
+
FileUtils.mv(outfile, infile)
|
|
27
29
|
ensure
|
|
28
30
|
temp&.close!
|
|
29
31
|
end
|
|
@@ -40,7 +42,7 @@ module Sevgi
|
|
|
40
42
|
# @param page [Integer, String, nil] page selector passed to Inkscape
|
|
41
43
|
# @param css [String, nil] CSS inserted before exporting
|
|
42
44
|
# @return [Sevgi::Function::Shell::Result] command result
|
|
43
|
-
# @raise [Sevgi::Sundries::Export::ExportError] when format
|
|
45
|
+
# @raise [Sevgi::Sundries::Export::ExportError] when format, output extension, or CSS insertion is invalid
|
|
44
46
|
# @raise [Sevgi::Error] when Inkscape is missing or the command fails
|
|
45
47
|
# @raise [Errno::ENOENT] when the executable cannot be spawned
|
|
46
48
|
# @see https://inkscape.org/ Inkscape
|
|
@@ -59,11 +61,11 @@ module Sevgi
|
|
|
59
61
|
infile = File.expand_path(infile)
|
|
60
62
|
outfile ||= F.subext(".pdf", infile)
|
|
61
63
|
outfile = File.expand_path(outfile)
|
|
62
|
-
format = format_for
|
|
64
|
+
format = format_for(format, outfile)
|
|
63
65
|
|
|
64
66
|
if css
|
|
65
67
|
temp = Tempfile.new(%w[input .svg], File.dirname(infile))
|
|
66
|
-
::File.write(temp.path,
|
|
68
|
+
::File.write(temp.path, styled(::File.read(infile), css))
|
|
67
69
|
infile = temp.path
|
|
68
70
|
end
|
|
69
71
|
|
|
@@ -98,7 +100,7 @@ module Sevgi
|
|
|
98
100
|
# @param id [String, nil] SVG element id to export
|
|
99
101
|
# @param css [String, nil] CSS inserted before exporting
|
|
100
102
|
# @return [Sevgi::Function::Shell::Result] command result
|
|
101
|
-
# @raise [Sevgi::Sundries::Export::ExportError] when format
|
|
103
|
+
# @raise [Sevgi::Sundries::Export::ExportError] when format, output extension, or CSS insertion is invalid
|
|
102
104
|
# @raise [Sevgi::Error] when rsvg-convert is missing or the command fails
|
|
103
105
|
# @raise [Errno::ENOENT] when the executable cannot be spawned
|
|
104
106
|
# @see https://gitlab.gnome.org/GNOME/librsvg librsvg
|
|
@@ -115,11 +117,11 @@ module Sevgi
|
|
|
115
117
|
infile = File.expand_path(infile)
|
|
116
118
|
outfile ||= F.subext(".pdf", infile)
|
|
117
119
|
outfile = File.expand_path(outfile)
|
|
118
|
-
format = format_for
|
|
120
|
+
format = format_for(format, outfile)
|
|
119
121
|
|
|
120
122
|
if css
|
|
121
123
|
temp = Tempfile.new(%w[input .svg], File.dirname(infile))
|
|
122
|
-
::File.write(temp.path,
|
|
124
|
+
::File.write(temp.path, styled(::File.read(infile), css))
|
|
123
125
|
infile = temp.path
|
|
124
126
|
end
|
|
125
127
|
|