claricle 0.1.0 → 0.2.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/.rubocop.yml +10 -1
- data/LICENSE +25 -0
- data/README.adoc +439 -157
- data/TODO.plan/00-overview.md +502 -0
- data/TODO.plan/01-core.md +296 -0
- data/TODO.plan/02-inspect.md +163 -0
- data/TODO.plan/03-conform.md +208 -0
- data/TODO.plan/04-convert.md +216 -0
- data/exe/claricle +1 -1
- data/lib/claricle/batch.rb +240 -0
- data/lib/claricle/cli.rb +514 -40
- data/lib/claricle/conformance_options.rb +49 -0
- data/lib/claricle/conversion_engine.rb +21 -0
- data/lib/claricle/convert.rb +21 -0
- data/lib/claricle/detector.rb +696 -0
- data/lib/claricle/emf_header.rb +33 -0
- data/lib/claricle/errors.rb +73 -0
- data/lib/claricle/fault.rb +69 -0
- data/lib/claricle/handlers/base.rb +169 -0
- data/lib/claricle/handlers/extension_points.rb +124 -0
- data/lib/claricle/handlers/metafile.rb +1011 -0
- data/lib/claricle/handlers/pdf.rb +961 -0
- data/lib/claricle/handlers/pdf_arlington/definitions.rb +54 -0
- data/lib/claricle/handlers/pdf_arlington/kind.rb +62 -0
- data/lib/claricle/handlers/pdf_arlington/predicate_gate.rb +39 -0
- data/lib/claricle/handlers/pdf_arlington/rules.rb +86 -0
- data/lib/claricle/handlers/pdf_arlington/walker.rb +131 -0
- data/lib/claricle/handlers/pdf_arlington.rb +38 -0
- data/lib/claricle/handlers/pdf_profiles.rb +66 -0
- data/lib/claricle/handlers/png.rb +1107 -0
- data/lib/claricle/handlers/postscript.rb +1357 -0
- data/lib/claricle/handlers/svg.rb +787 -0
- data/lib/claricle/image.rb +272 -0
- data/lib/claricle/lossiness/levels.rb +13 -0
- data/lib/claricle/lossiness.rb +864 -0
- data/lib/claricle/models/base.rb +460 -0
- data/lib/claricle/models/batch_item.rb +127 -0
- data/lib/claricle/models/conversion.rb +167 -0
- data/lib/claricle/models/format_capability.rb +40 -0
- data/lib/claricle/models/free_form_hash.rb +77 -0
- data/lib/claricle/models/inspection.rb +188 -0
- data/lib/claricle/models/issue.rb +23 -0
- data/lib/claricle/models/location.rb +51 -0
- data/lib/claricle/models/report.rb +54 -0
- data/lib/claricle/models/uncoerced_integer.rb +38 -0
- data/lib/claricle/postscript_fidelity/verdict.rb +37 -0
- data/lib/claricle/postscript_fidelity/view_box.rb +82 -0
- data/lib/claricle/postscript_fidelity.rb +160 -0
- data/lib/claricle/registry.rb +125 -0
- data/lib/claricle/version.rb +1 -1
- data/lib/claricle/writer.rb +468 -0
- data/lib/claricle.rb +244 -7
- metadata +201 -19
- data/claricle.gemspec +0 -42
- data/sig/claricle.rbs +0 -4
data/README.adoc
CHANGED
|
@@ -1,116 +1,258 @@
|
|
|
1
1
|
= Claricle
|
|
2
2
|
|
|
3
3
|
image:https://img.shields.io/gem/v/claricle.svg[RubyGems Version]
|
|
4
|
-
image:https://img.shields.io/github/license/
|
|
5
|
-
image:https://github.com/
|
|
4
|
+
image:https://img.shields.io/github/license/claricle/claricle.svg[License]
|
|
5
|
+
image:https://github.com/claricle/claricle/actions/workflows/main.yml/badge.svg["Build Status", link="https://github.com/claricle/claricle/actions/workflows/main.yml"]
|
|
6
6
|
|
|
7
7
|
== Purpose
|
|
8
8
|
|
|
9
|
-
Claricle is a
|
|
10
|
-
|
|
9
|
+
Claricle is a Ruby library and CLI for working with image files. It
|
|
10
|
+
detects a file's format from its bytes and gives one object and one set
|
|
11
|
+
of models to work with, wrapping specialist tools behind a single API.
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
Detection, inspection, conformance checking and conversion ship today
|
|
14
|
+
across PNG, SVG, EMF, EPS, PS and PDF. Not every format supports every
|
|
15
|
+
operation -- `claricle formats` lists which, and "What does not work
|
|
16
|
+
yet" below says why.
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
[NOTE]
|
|
19
|
+
====
|
|
20
|
+
Version 0.1.0 was a name reservation. Its `validate`, `convert` and
|
|
21
|
+
`compress` commands printed "coming soon" and did nothing; they have
|
|
22
|
+
been removed rather than left in place. Its metadata also advertised
|
|
23
|
+
image compression, which was never implemented -- compression is not in
|
|
24
|
+
v1's scope. Rendering, editing and WMF conversion are likewise outside
|
|
25
|
+
v1.
|
|
26
|
+
====
|
|
22
27
|
|
|
23
28
|
== Etymology & Symbolism
|
|
24
29
|
|
|
25
|
-
|
|
26
|
-
|===
|
|
27
|
-
|Part
|
|
28
|
-
|Root
|
|
29
|
-
|Meaning
|
|
30
|
+
*Claricle* = *Clari* (clarity) + *cle* (particle)
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|Latin _clārus_
|
|
33
|
-
|"clear, bright, famous" -- root of clarity, clarify
|
|
32
|
+
*Combined:* "Clear particle" or "Bright essence"
|
|
34
33
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
The name reflects what the library is for:
|
|
35
|
+
|
|
36
|
+
* *Detection* -> know exactly what a file is
|
|
37
|
+
* *Validation* -> clarify every pixel
|
|
38
|
+
* *Conversion* -> preserve the clear essence
|
|
39
|
+
* *Ruby* -> gem-like, small but brilliant
|
|
40
|
+
|
|
41
|
+
== What works today
|
|
42
|
+
|
|
43
|
+
* Format detection for PNG, SVG, EMF, WMF, EPS, PS and PDF
|
|
44
|
+
* `inspect` for EMF -- dimensions, DPI and header metadata
|
|
45
|
+
* `inspect` for PNG -- dimensions, colour space, bit depth and DPI
|
|
46
|
+
* `inspect` for SVG -- dimensions from the root, absolute units
|
|
47
|
+
converted to CSS px; a relative unit such as `%` reports no dimension.
|
|
48
|
+
It stops at the root start tag, so its parse status means "the root
|
|
49
|
+
was readable", not "the document is well-formed". A file damaged
|
|
50
|
+
after that tag still reports `ok`. Judging the whole document is
|
|
51
|
+
conformance, which `conform` now does for SVG; `inspect` still only
|
|
52
|
+
reads the bounded root prefix
|
|
53
|
+
* `inspect` for EPS and PS -- dimensions from a **concrete bounding box
|
|
54
|
+
declared in the DSC header**, preferring `%%HiResBoundingBox` and
|
|
55
|
+
falling back to `%%BoundingBox`, plus the title, creator, creation
|
|
56
|
+
date and language level. Inspection parses a bounded header. When a
|
|
57
|
+
bounding box is declared as `(atend)`, it also scans the outer document
|
|
58
|
+
trailer while skipping nested documents and declared data sections. A
|
|
59
|
+
box is reported only when the parser reads the same values the file's
|
|
60
|
+
**first** declaration spells out in DSC syntax, or when one unambiguous
|
|
61
|
+
concrete trailer value resolves `(atend)`. A box that fails that check
|
|
62
|
+
is discarded rather than guessed at -- the other box, if the file
|
|
63
|
+
declares one, is still used, and dimensions are nil only when neither
|
|
64
|
+
survives. The page count is deliberately not
|
|
65
|
+
reported: the parser fills it in only for the two-argument
|
|
66
|
+
`%%Pages: n m` form, so it would be present for some files and absent
|
|
67
|
+
for others with nothing in `meta` to tell the cases apart
|
|
68
|
+
* `inspect` for PDF -- the document's version and the **declared** page
|
|
69
|
+
count from the page tree's `/Count`. The version is the higher of the
|
|
70
|
+
two the file states, compared as numbers: the `%PDF-` header, and the
|
|
71
|
+
catalog's `/Version` where one is present, well formed and no more than
|
|
72
|
+
1,024 bytes long. It comes
|
|
73
|
+
from the file's own bytes rather than the parser, which fabricates
|
|
74
|
+
`1.4` for an empty file. The count is a declaration and
|
|
75
|
+
nothing walks the page tree to check it, so a document declaring
|
|
76
|
+
`/Count 999` over one page reports 999. Dimensions are deliberately
|
|
77
|
+
not reported: reaching the first page's box safely needs cycle-guarded
|
|
78
|
+
page-tree walking the parser does not offer. The count is omitted
|
|
79
|
+
whenever it cannot be read as a non-negative whole number of at most
|
|
80
|
+
1,024 decimal digits -- including when the reference holding it is
|
|
81
|
+
refused. A deadline expiry after the version is settled leaves the file
|
|
82
|
+
inspecting as `ok` with the count omitted. An earlier expiry with no
|
|
83
|
+
failure cause already established reports `parse_status: "failed"` with
|
|
84
|
+
code `pdf.timeout`; an established failure keeps its own code
|
|
85
|
+
* `conform` for PNG, SVG, EMF and PDF -- a yes/no verdict plus typed
|
|
86
|
+
issues, exit 1 for a nonconformant file
|
|
87
|
+
* `convert` between SVG, EMF, EPS and PS, in every direction, each
|
|
88
|
+
conversion labelled `lossless`, `lossy` or `unknown` (see "Conversion
|
|
89
|
+
lossiness" below)
|
|
90
|
+
* `formats`, listing each format and what Claricle can do with it
|
|
91
|
+
* `Claricle::Image`, built from a path or from bytes
|
|
92
|
+
* A unified issue and report model, serialising to and from JSON
|
|
93
|
+
* An error taxonomy mapped to documented CLI exit codes
|
|
94
|
+
|
|
95
|
+
== What does not work yet
|
|
96
|
+
|
|
97
|
+
* `inspect` for WMF -- every other format has a handler, so WMF is the
|
|
98
|
+
one that still raises `Claricle::UnsupportedFormat`
|
|
99
|
+
* **A memory bound for PDF.** The five second deadline bounds TIME only.
|
|
100
|
+
A small crafted file can still make the parser build a very large
|
|
101
|
+
string before the clock fires -- measured, a 190 KB file whose object
|
|
102
|
+
stream decodes to 200 MB spent that memory inside the window and then
|
|
103
|
+
timed out. So `inspect` for PDF is not safe to run on untrusted input
|
|
104
|
+
in a long-lived server process. Run it on files you trust, or in a
|
|
105
|
+
separate process you can cap and kill
|
|
106
|
+
* `conform` for EPS and PS. The PostScript parser rejects only
|
|
107
|
+
unterminated strings -- it accepts raw binary -- so passing it says
|
|
108
|
+
almost nothing about the file, and calling that conformance would
|
|
109
|
+
mislead. Both raise
|
|
110
|
+
`Claricle::UnsupportedFormat` and the CLI exits 3
|
|
111
|
+
* `convert` to or from PNG and PDF. Both raise
|
|
112
|
+
`Claricle::UnsupportedFormat` and the CLI exits 3
|
|
113
|
+
* WMF beyond detection. `detect` returns `:wmf`, but no handler
|
|
114
|
+
registers it, so every operation on a WMF raises
|
|
115
|
+
`UnsupportedFormat`. That is deliberate: the upstream parser this
|
|
116
|
+
library would use reports "WMF parser not yet implemented"
|
|
39
117
|
|
|
40
|
-
|
|
118
|
+
== Command Line Interface
|
|
41
119
|
|
|
42
|
-
|
|
120
|
+
=== Available commands
|
|
43
121
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
* *Ruby* → gem-like, small but brilliant
|
|
122
|
+
[cols="1,3"]
|
|
123
|
+
|===
|
|
124
|
+
| Command | What it does
|
|
48
125
|
|
|
49
|
-
|
|
126
|
+
| `claricle inspect FILE` | Report a file's format and metadata
|
|
127
|
+
| `claricle conform FILE...` | Check files against their format's conformance rules
|
|
128
|
+
| `claricle convert SOURCE...` | Convert files to another format
|
|
129
|
+
| `claricle formats` | List the formats Claricle handles, and what it can do with each
|
|
130
|
+
| `claricle version` | Print the version
|
|
131
|
+
|===
|
|
50
132
|
|
|
51
|
-
|
|
52
|
-
====
|
|
53
|
-
This is currently a minimal shim release to reserve the gem name.
|
|
54
|
-
Full functionality will be implemented in future versions.
|
|
55
|
-
====
|
|
133
|
+
`claricle help` lists these, plus the commands Thor contributes itself.
|
|
56
134
|
|
|
57
|
-
|
|
135
|
+
The operations are added as they are implemented, so this table never
|
|
136
|
+
lists a command that does nothing.
|
|
58
137
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
* Image compression with quality preservation
|
|
64
|
-
* Command-line interface for all operations
|
|
65
|
-
* Ruby API for programmatic access
|
|
66
|
-
* Batch processing capabilities
|
|
67
|
-
* Comprehensive error reporting
|
|
138
|
+
`claricle inspect` prints a file's own metadata under `meta.` keys, and
|
|
139
|
+
shows control characters escaped. Both are there so a crafted file
|
|
140
|
+
cannot print a line that looks like Claricle's own. `--json` is
|
|
141
|
+
unaffected: it carries the real values.
|
|
68
142
|
|
|
69
|
-
|
|
143
|
+
=== Inspecting a file
|
|
70
144
|
|
|
71
|
-
|
|
145
|
+
Output below is a real transcript, run against a fixture in this repo
|
|
146
|
+
(`spec/fixtures/inspect/basic.eps`, an EPS with a DSC header):
|
|
72
147
|
|
|
73
|
-
[source,
|
|
148
|
+
[source,sh]
|
|
74
149
|
----
|
|
75
|
-
|
|
150
|
+
$ claricle inspect spec/fixtures/inspect/basic.eps
|
|
151
|
+
format: eps
|
|
152
|
+
dimensions: 100.0x50.0
|
|
153
|
+
meta.bounding_box: [0.0, 0.0, 100.0, 50.0]
|
|
154
|
+
meta.creation_date: 2026-08-20
|
|
155
|
+
meta.creator: claricle specs
|
|
156
|
+
meta.language_level: 2
|
|
157
|
+
meta.title: Baseline figure
|
|
158
|
+
parse status: ok
|
|
159
|
+
|
|
160
|
+
$ claricle inspect spec/fixtures/inspect/basic.eps --json
|
|
161
|
+
{"format":"eps","width":100.0,"height":50.0,"meta":{"bounding_box":[0.0,0.0,100.0,50.0],"title":"Baseline figure","creator":"claricle specs","creation_date":"2026-08-20","language_level":2},"parse_status":"ok","issues":[]}
|
|
76
162
|
----
|
|
77
163
|
|
|
78
|
-
|
|
164
|
+
A file Claricle cannot recognise, or cannot find, exits with the code
|
|
165
|
+
named below rather than a stack trace:
|
|
79
166
|
|
|
80
167
|
[source,sh]
|
|
81
168
|
----
|
|
82
|
-
$
|
|
169
|
+
$ claricle inspect not-an-image.txt; echo "exit: $?"
|
|
170
|
+
claricle: no known image signature
|
|
171
|
+
exit: 3
|
|
172
|
+
|
|
173
|
+
$ claricle inspect missing.png; echo "exit: $?"
|
|
174
|
+
claricle: No such file or directory @ rb_sysopen - missing.png
|
|
175
|
+
exit: 2
|
|
83
176
|
----
|
|
84
177
|
|
|
85
|
-
|
|
178
|
+
=== Listing formats
|
|
86
179
|
|
|
87
180
|
[source,sh]
|
|
88
181
|
----
|
|
89
|
-
$
|
|
182
|
+
$ claricle formats
|
|
183
|
+
emf inspect, conform, convert
|
|
184
|
+
eps inspect, convert
|
|
185
|
+
pdf inspect, conform
|
|
186
|
+
png inspect, conform
|
|
187
|
+
ps inspect, convert
|
|
188
|
+
svg inspect, conform, convert
|
|
189
|
+
|
|
190
|
+
$ claricle formats --json
|
|
191
|
+
[{"format":"emf","inspect":true,"conform":true,"convert":true,"convert_to":["svg","eps","ps"]},{"format":"eps","inspect":true,"conform":false,"convert":true,"convert_to":["svg","emf","ps"]},{"format":"pdf","inspect":true,"conform":true,"convert":false,"convert_to":[]},{"format":"png","inspect":true,"conform":true,"convert":false,"convert_to":[]},{"format":"ps","inspect":true,"conform":false,"convert":true,"convert_to":["svg","emf","eps"]},{"format":"svg","inspect":true,"conform":true,"convert":true,"convert_to":["eps","ps","emf"]}]
|
|
90
192
|
----
|
|
91
193
|
|
|
92
|
-
|
|
194
|
+
This is a derived fact, not a hand-maintained value -- `formats` reads
|
|
195
|
+
it off the handler classes actually loaded (see "Architecture" below),
|
|
196
|
+
so it cannot advertise an operation that does not run.
|
|
197
|
+
|
|
198
|
+
=== Checking conformance
|
|
199
|
+
|
|
200
|
+
The generic check is format-specific: PNG validates its chunk stream,
|
|
201
|
+
SVG checks well-formed structure and the `base` requirement set, EMF
|
|
202
|
+
checks record framing and its terminating EOF record, and PDF combines
|
|
203
|
+
pdfrb's structural validator with Claricle's Arlington document walk.
|
|
204
|
+
These are deliberately not interchangeable claims; each report records
|
|
205
|
+
the profile when a named requirement set ran.
|
|
93
206
|
|
|
94
|
-
|
|
207
|
+
SVG profiles are `base` (the generic default), `lucid_fix`, `metanorma`,
|
|
208
|
+
`no_external_css`, `svg_1_2_rfc`, and `svg_1_2_rfc_with_rdf`. PDF offers
|
|
209
|
+
`pdf_a`, `pdf_ua`, `pdf_x`, `pdf_vt`, `pades`, `ltv`, `pdf_2_af`, and
|
|
210
|
+
`tagged_pdf`. Four PDF families accept a level:
|
|
95
211
|
|
|
96
|
-
|
|
212
|
+
[cols="1,3"]
|
|
213
|
+
|===
|
|
214
|
+
| Profile | Accepted `--level` values
|
|
215
|
+
|
|
216
|
+
| `pdf_a` | `a1b`, `a1a`, `a2b`, `a2a`, `a3b`, `a3a`, `a4`
|
|
217
|
+
| `pdf_x` | `x1a`, `x3`, `x4`, `x6`
|
|
218
|
+
| `pdf_vt` | `vt1`, `vt2`
|
|
219
|
+
| `pades` | `b-b`, `b-t`, `b-lt`, `b-lta`
|
|
220
|
+
|===
|
|
97
221
|
|
|
98
|
-
|
|
222
|
+
An unknown profile, an invalid profile/format pairing, an unknown level,
|
|
223
|
+
or `--level` with a profile that takes none is a bad invocation and exits
|
|
224
|
+
2. Named PDF validation first requires the generic structural and
|
|
225
|
+
Arlington checks to pass.
|
|
99
226
|
|
|
100
227
|
[source,sh]
|
|
101
228
|
----
|
|
102
|
-
$ claricle
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
229
|
+
$ claricle conform valid.svg no_viewbox.svg; echo "exit: $?"
|
|
230
|
+
no_viewbox.svg: no
|
|
231
|
+
error [viewbox_required] SVG root element must have a viewBox attribute
|
|
232
|
+
error [viewbox_required] Trying to put in the attribute with value '0 0 64.0 64.0'
|
|
233
|
+
valid.svg: yes
|
|
234
|
+
exit: 1
|
|
235
|
+
|
|
236
|
+
$ claricle conform document.pdf --profile pdf_a --level a1b
|
|
237
|
+
document.pdf: no
|
|
238
|
+
error [6.1-2] PDF/A requires /Catalog/Metadata XMP stream (node_path: Catalog)
|
|
109
239
|
----
|
|
110
240
|
|
|
111
|
-
===
|
|
241
|
+
=== Converting a file
|
|
242
|
+
|
|
243
|
+
The summary line names the source, the target format, where the bytes
|
|
244
|
+
were written and the lossiness verdict:
|
|
245
|
+
|
|
246
|
+
[source,sh]
|
|
247
|
+
----
|
|
248
|
+
$ claricle convert rect_and_line.emf --to svg
|
|
249
|
+
rect_and_line.emf -> svg: ./rect_and_line.svg (unknown)
|
|
250
|
+
|
|
251
|
+
$ claricle convert gradient_linear.svg --to emf
|
|
252
|
+
gradient_linear.svg -> emf: ./gradient_linear.emf (lossy)
|
|
253
|
+
----
|
|
112
254
|
|
|
113
|
-
|
|
255
|
+
=== Version
|
|
114
256
|
|
|
115
257
|
[source,sh]
|
|
116
258
|
----
|
|
@@ -118,129 +260,269 @@ $ claricle version
|
|
|
118
260
|
Claricle version 0.1.0
|
|
119
261
|
----
|
|
120
262
|
|
|
121
|
-
===
|
|
263
|
+
=== Exit codes
|
|
122
264
|
|
|
123
|
-
[
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
====
|
|
265
|
+
[cols="1,4"]
|
|
266
|
+
|===
|
|
267
|
+
| Code | Meaning
|
|
127
268
|
|
|
128
|
-
|
|
269
|
+
| 0 | Success
|
|
270
|
+
| 1 | The file is not conformant
|
|
271
|
+
| 2 | Bad invocation -- unknown command, wrong arguments, missing file
|
|
272
|
+
| 3 | The format is unknown, or is recognised but unsupported for that operation
|
|
273
|
+
| 4 | The operation failed, or something unexpected went wrong
|
|
274
|
+
|===
|
|
129
275
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
276
|
+
A command may also exit with a status of its own choosing. A status in
|
|
277
|
+
0-255 is passed through unchanged; one outside that range becomes 4,
|
|
278
|
+
because a process status has to be a byte. The codes above are the ones
|
|
279
|
+
this library assigns.
|
|
280
|
+
|
|
281
|
+
Exit code 1 means a nonconformant file. 0 and 1 are reachable end to
|
|
282
|
+
end for every format with a conformance handler -- PNG, SVG, EMF and
|
|
283
|
+
PDF; EPS and PS answer 3.
|
|
284
|
+
|
|
285
|
+
`claricle conform` takes any number of files. A positional is a literal
|
|
286
|
+
path when it names an existing file and a glob otherwise, and
|
|
287
|
+
`--pattern` reads its argument as a glob whatever the filename looks
|
|
288
|
+
like -- which is how a file genuinely called `star[1].png` stays
|
|
289
|
+
reachable both ways. A pattern adds to the positionals rather than
|
|
290
|
+
replacing them, the combined set is deduplicated and processed in
|
|
291
|
+
sorted order, a file that fails does not stop the ones after it, and
|
|
292
|
+
the command exits with the highest code any of them reached. Matching
|
|
293
|
+
no files at all is a bad invocation, not a silent pass. `--json` always
|
|
294
|
+
emits an array, a single file included, so a consumer never gets one
|
|
295
|
+
shape for one file and another for many.
|
|
296
|
+
|
|
297
|
+
`claricle convert` takes the same positional/`--pattern` shape as
|
|
298
|
+
`conform`. `--to FORMAT` names the target; when it is omitted,
|
|
299
|
+
`--output FILE` must name a recognised format extension to infer it
|
|
300
|
+
from -- `--output -` (stdout) cannot, since stdout has no extension, so
|
|
301
|
+
that combination requires `--to`. `--to` and `--output`'s extension
|
|
302
|
+
disagreeing is a bad invocation, not a silent preference for one over
|
|
303
|
+
the other. `--output` names a single destination, so it is refused
|
|
304
|
+
whenever more than one source resolves; a multi-source run derives each
|
|
305
|
+
destination from its own source name and the target's extension
|
|
306
|
+
instead. The whole destination set -- derived or given -- is
|
|
307
|
+
preflighted for collisions and overwrites before anything is converted,
|
|
308
|
+
and an existing destination needs `--force` to be replaced. `--output
|
|
309
|
+
-` writes only the converted bytes to stdout, with no trailing newline;
|
|
310
|
+
everything else -- the summary, warnings, error text -- goes to stderr.
|
|
311
|
+
|
|
312
|
+
== Usage
|
|
134
313
|
|
|
135
|
-
|
|
136
|
-
$ claricle validate *.svg
|
|
314
|
+
Detect a format from a path or from bytes:
|
|
137
315
|
|
|
138
|
-
|
|
139
|
-
$ claricle validate "images/*.png"
|
|
316
|
+
[source,ruby]
|
|
140
317
|
----
|
|
318
|
+
require "claricle"
|
|
141
319
|
|
|
142
|
-
|
|
320
|
+
Claricle.detect(File.binread("logo.png")) # => :png
|
|
321
|
+
File.open("logo.png", "rb") { |io| Claricle.detect(io) } # => :png
|
|
143
322
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
323
|
+
image = Claricle::Image.from_path("logo.png")
|
|
324
|
+
image.format # => :png
|
|
325
|
+
image.content # read lazily, once
|
|
326
|
+
----
|
|
148
327
|
|
|
149
|
-
|
|
328
|
+
`Image.from_path` streams the file while detecting. `Claricle.detect`
|
|
329
|
+
accepts a String of bytes or an IO; an IO is read from wherever it
|
|
330
|
+
currently sits and is never rewound.
|
|
331
|
+
|
|
332
|
+
Detection reads the IO in chunks and reclassifies after each one, so it
|
|
333
|
+
stops on the first chunk that settles the format and never reads past
|
|
334
|
+
that chunk. Each read asks for the whole remaining probe allowance --
|
|
335
|
+
the most any detector probe consults -- so how far the IO advances is
|
|
336
|
+
set by how much the source hands over per read. A file or a `StringIO`
|
|
337
|
+
hands over the whole allowance at once, so a source longer than the
|
|
338
|
+
allowance is read that far even though a PNG signature settled it after
|
|
339
|
+
eight bytes. A pipe or a socket hands over what has arrived, so a short
|
|
340
|
+
complete image settles without waiting for bytes nothing needs.
|
|
341
|
+
|
|
342
|
+
A source beginning `%!PS` is the exception. `:ps` and `:eps` are told
|
|
343
|
+
apart by scanning the first line, and a byte that has not arrived yet
|
|
344
|
+
can still flip one into the other, so that verdict always reads to the
|
|
345
|
+
allowance or to the end of the stream.
|
|
346
|
+
|
|
347
|
+
Detection never reads beyond the allowance, and it leaves what it did
|
|
348
|
+
not read on the IO: after a source longer than that, the IO sits
|
|
349
|
+
mid-stream and `eof?` is false. A caller who needs the rest of the
|
|
350
|
+
stream, or cares whether it reached EOF, should check rather than
|
|
351
|
+
assume.
|
|
352
|
+
|
|
353
|
+
An SVG is recognised only when its root carries the SVG namespace. A
|
|
354
|
+
bare `<svg>` with no `xmlns` is not detected, because SVG requires that
|
|
355
|
+
declaration and the probe will not guess at one.
|
|
356
|
+
|
|
357
|
+
Bytes matching no known signature raise `Claricle::UnknownFormat`. A
|
|
358
|
+
format that is recognised but unhandled raises
|
|
359
|
+
`Claricle::UnsupportedFormat` -- two different answers to two different
|
|
360
|
+
questions.
|
|
361
|
+
|
|
362
|
+
Hand a delegate a real path, whichever way the image was built:
|
|
150
363
|
|
|
151
|
-
[source,
|
|
364
|
+
[source,ruby]
|
|
152
365
|
----
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
366
|
+
Claricle::Image.from_content(bytes, format: :png).with_path do |path|
|
|
367
|
+
# a temporary file exists here, and only here
|
|
368
|
+
end
|
|
156
369
|
----
|
|
157
370
|
|
|
158
|
-
|
|
371
|
+
Hand it a source instead when the reader only needs a prefix:
|
|
159
372
|
|
|
160
|
-
[
|
|
161
|
-
====
|
|
162
|
-
This functionality is not yet implemented in this shim release.
|
|
163
|
-
====
|
|
164
|
-
|
|
165
|
-
Future usage:
|
|
166
|
-
|
|
167
|
-
[source,sh]
|
|
373
|
+
[source,ruby]
|
|
168
374
|
----
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
375
|
+
Claricle::Image.from_path("logo.svg").with_source do |source|
|
|
376
|
+
# the open file here; the bytes, for a content-born image
|
|
377
|
+
end
|
|
172
378
|
----
|
|
173
379
|
|
|
174
|
-
|
|
380
|
+
A path-born image is never slurped to satisfy `with_source`. That is how
|
|
381
|
+
the SVG handler reads the root, so a 64 MiB file costs the 8 KiB it
|
|
382
|
+
actually reads.
|
|
175
383
|
|
|
176
|
-
|
|
384
|
+
Convert one file. The result is the `Models::Conversion`: where it was
|
|
385
|
+
written, how lossy it was, and the bytes.
|
|
177
386
|
|
|
178
387
|
[source,ruby]
|
|
179
388
|
----
|
|
180
|
-
|
|
389
|
+
conversion = Claricle.convert("diagram.emf", to: :svg) # writes diagram.svg
|
|
390
|
+
conversion.output_path
|
|
391
|
+
conversion.lossiness
|
|
181
392
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
393
|
+
Claricle.convert("diagram.emf", output: "out/copy.svg", force: true)
|
|
394
|
+
Claricle.convert("diagram.emf", to: :svg, output: "-") # bytes to stdout
|
|
395
|
+
----
|
|
185
396
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
397
|
+
`output:` defaults to the source's name with the target's extension,
|
|
398
|
+
beside the source. An existing file is refused unless `force: true`. This
|
|
399
|
+
is the same code as `Claricle.convert_batch` and `claricle convert`, so
|
|
400
|
+
the collision and overwrite rules are identical. A file that cannot be
|
|
401
|
+
converted raises its own error (`UnsupportedFormat`, `InvocationError`),
|
|
402
|
+
where `convert_batch` collects it per file; a name matching more than one
|
|
403
|
+
file raises `InvocationError`.
|
|
192
404
|
|
|
193
|
-
|
|
194
|
-
converter = Claricle::Converter.new
|
|
195
|
-
converter.convert('image.png', 'image.svg')
|
|
405
|
+
== Architecture
|
|
196
406
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
407
|
+
[source]
|
|
408
|
+
----
|
|
409
|
+
lib/claricle/
|
|
410
|
+
version.rb # VERSION
|
|
411
|
+
detector.rb # magic-byte and XML-root format detection
|
|
412
|
+
image.rb # the object every operation takes
|
|
413
|
+
lossiness.rb # what a conversion loses, read from the source document
|
|
414
|
+
registry.rb # frozen format -> handler map
|
|
415
|
+
writer.rb # preflights and writes conversion output
|
|
416
|
+
handlers/base.rb # what a handler subclasses
|
|
417
|
+
handlers/metafile.rb # EMF header metadata
|
|
418
|
+
handlers/pdf.rb # PDF version and declared page count
|
|
419
|
+
handlers/png.rb # PNG metadata
|
|
420
|
+
handlers/postscript.rb # EPS and PS DSC header metadata
|
|
421
|
+
handlers/svg.rb # SVG root metadata
|
|
422
|
+
models/ # Location, Issue, Report, Inspection, Conversion, BatchItem
|
|
423
|
+
batch.rb # one file set, one envelope per file
|
|
424
|
+
fault.rb # an exception's exit code and renderable message
|
|
425
|
+
errors.rb # Error, UnknownFormat, UnsupportedFormat, ...
|
|
426
|
+
cli.rb # Thor CLI and the exit-code Runner
|
|
200
427
|
----
|
|
201
428
|
|
|
202
|
-
|
|
429
|
+
`Detector`, `Registry`, `Batch`, `Fault`, `Handlers::Base`,
|
|
430
|
+
`Models::Base`, `Models::FreeFormHash`, `Models::UncoercedInteger`,
|
|
431
|
+
`Models::Validation`, `Models::Location::POSITIONS`,
|
|
432
|
+
`Models::BatchItem::CODES`, `Cli::Runner::Status::RANGE` and
|
|
433
|
+
`UnsupportedFormat::ABSENT` are private constants.
|
|
434
|
+
The public surface is `Claricle.detect`, `Claricle.conform?`,
|
|
435
|
+
`Claricle.conformance_report`, `Claricle.conformance_batch`,
|
|
436
|
+
`Claricle.convert`, `Claricle.convert_batch`,
|
|
437
|
+
`Claricle::Image`,
|
|
438
|
+
`Claricle::BatchResult`,
|
|
439
|
+
`Claricle::Cli` (including `Cli::Runner` and `Runner::Status`),
|
|
440
|
+
`Claricle::VERSION`, the error classes, the model classes
|
|
441
|
+
`Models::BatchError`, `Models::BatchItem`, `Models::FormatCapability`,
|
|
442
|
+
`Models::Inspection`, `Models::Issue`, `Models::Location` and `Models::Report`, and the two
|
|
443
|
+
vocabularies those models validate against,
|
|
444
|
+
`Inspection::PARSE_STATUSES` and `Issue::SEVERITIES`.
|
|
445
|
+
|
|
446
|
+
=== Adding a format
|
|
447
|
+
|
|
448
|
+
One file: a handler class in `lib/claricle/handlers/`. Every file in that
|
|
449
|
+
directory is loaded, and the registry is derived from the classes that
|
|
450
|
+
subclass `Handlers::Base` -- there is no list to extend.
|
|
203
451
|
|
|
204
|
-
|
|
452
|
+
[source,ruby]
|
|
453
|
+
----
|
|
454
|
+
module Claricle
|
|
455
|
+
module Handlers
|
|
456
|
+
class Qoi < Base
|
|
457
|
+
formats :qoi # what it owns
|
|
458
|
+
detect { |header| :qoi if header.start_with?("qoif") }
|
|
459
|
+
convert_from(:svg) { |content, to| encode(content) }
|
|
460
|
+
loss_rules lost: %i[gradient], kept: %i[basic_shape]
|
|
461
|
+
|
|
462
|
+
def inspection(image) = ... # each operation it supports
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
----
|
|
205
467
|
|
|
206
|
-
|
|
468
|
+
* `formats` makes the format routable and listed by `claricle formats`.
|
|
469
|
+
* `detect` is handed the first bytes (at most `Detector::HEADER_BYTES`) and
|
|
470
|
+
answers one of the handler's own formats, or nil. It runs after the
|
|
471
|
+
built-in probes, so it cannot change what an existing format detects.
|
|
472
|
+
* `convert_from` lets other formats convert INTO this one without editing
|
|
473
|
+
their handlers; it also adds this format to their `convert_to` column.
|
|
474
|
+
A source handler that already lists the target keeps its own path.
|
|
475
|
+
* `loss_rules` is optional; without it a conversion into this format
|
|
476
|
+
reports `unknown` lossiness, never `lossless`.
|
|
477
|
+
* Operations (`inspection`, `conformance_report`, `convert`) are the
|
|
478
|
+
methods the class overrides; `capabilities` is derived from them.
|
|
479
|
+
|
|
480
|
+
The five built-in formats still detect through `Detector`'s own ordered
|
|
481
|
+
probes, which are heterogeneous (fixed offsets, an XML root, a
|
|
482
|
+
PostScript line scan) and whose order is load-bearing; a handler's
|
|
483
|
+
`detect` is the extension point for everything added after them.
|
|
484
|
+
|
|
485
|
+
== Installation
|
|
486
|
+
|
|
487
|
+
Add this line to your application's Gemfile:
|
|
488
|
+
|
|
489
|
+
[source,ruby]
|
|
207
490
|
----
|
|
208
|
-
|
|
209
|
-
│
|
|
210
|
-
├── CLI (Thor-based command-line interface)
|
|
211
|
-
│ ├── validate command (placeholder)
|
|
212
|
-
│ ├── convert command (placeholder)
|
|
213
|
-
│ ├── compress command (placeholder)
|
|
214
|
-
│ └── version command (implemented)
|
|
215
|
-
│
|
|
216
|
-
└── Core Module (placeholder for future components)
|
|
217
|
-
├── Validator (future: PNG, SVG validation)
|
|
218
|
-
├── Converter (future: format conversion)
|
|
219
|
-
├── Compressor (future: image compression)
|
|
220
|
-
└── VectorProcessor (future: vector graphics)
|
|
491
|
+
gem 'claricle'
|
|
221
492
|
----
|
|
222
493
|
|
|
223
|
-
|
|
494
|
+
And then execute:
|
|
224
495
|
|
|
225
|
-
|
|
496
|
+
[source,sh]
|
|
497
|
+
----
|
|
498
|
+
$ bundle install
|
|
499
|
+
----
|
|
226
500
|
|
|
227
|
-
|
|
501
|
+
Or install it yourself as:
|
|
502
|
+
|
|
503
|
+
[source,sh]
|
|
228
504
|
----
|
|
229
|
-
|
|
230
|
-
│ Claricle Gem │
|
|
231
|
-
│ │
|
|
232
|
-
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
|
233
|
-
│ │ PNG Module │ │ SVG Module │ │Vector Module │ │
|
|
234
|
-
│ │ │ │ │ │ │ │
|
|
235
|
-
│ │ pngconform │ │ svgconform │ │ vectory │ │
|
|
236
|
-
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
|
237
|
-
│ │
|
|
238
|
-
│ ┌────────────────────────────────────────────────┐ │
|
|
239
|
-
│ │ Unified API & CLI Interface │ │
|
|
240
|
-
│ └────────────────────────────────────────────────┘ │
|
|
241
|
-
└─────────────────────────────────────────────────────────┘
|
|
505
|
+
$ gem install claricle
|
|
242
506
|
----
|
|
243
507
|
|
|
508
|
+
== Conversion lossiness
|
|
509
|
+
|
|
510
|
+
Whether a conversion loses anything is decided per conversion, from what the
|
|
511
|
+
source document actually contains -- not from a table of format pairs. The same
|
|
512
|
+
edge is clean for a rectangle and destructive for a gradient, so a per-edge
|
|
513
|
+
label would call the second one lossless on the first one's evidence.
|
|
514
|
+
|
|
515
|
+
A verdict is one of `lossless`, `lossy` or `unknown`, published as
|
|
516
|
+
`Models::Conversion::LOSSINESS_LEVELS`. `unknown` is the default: a `lossless`
|
|
517
|
+
verdict is a positive claim and is made only when every feature the document
|
|
518
|
+
contains has been shown to survive that target. `lossy` and `unknown` both tell
|
|
519
|
+
you to look; only `lossless` tells you not to, so only `lossless` can hide a
|
|
520
|
+
loss.
|
|
521
|
+
|
|
522
|
+
The model class `Models::Conversion` and its vocabulary
|
|
523
|
+
`Conversion::LOSSINESS_LEVELS` are public. `Lossiness` and
|
|
524
|
+
`Models::BinaryContent` are private constants.
|
|
525
|
+
|
|
244
526
|
== Development
|
|
245
527
|
|
|
246
528
|
After checking out the repo, run `bin/setup` to install dependencies.
|
|
@@ -269,11 +551,11 @@ tag, and push the `.gem` file to https://rubygems.org[RubyGems.org].
|
|
|
269
551
|
== Contributing
|
|
270
552
|
|
|
271
553
|
Bug reports and pull requests are welcome on GitHub at
|
|
272
|
-
https://github.com/
|
|
554
|
+
https://github.com/claricle/claricle.
|
|
273
555
|
|
|
274
556
|
This project is intended to be a safe, welcoming space for collaboration, and
|
|
275
557
|
contributors are expected to adhere to the
|
|
276
|
-
https://github.com/
|
|
558
|
+
https://github.com/claricle/claricle/blob/main/CODE_OF_CONDUCT.md[code of conduct].
|
|
277
559
|
|
|
278
560
|
== Copyright
|
|
279
561
|
|
|
@@ -288,4 +570,4 @@ https://opensource.org/licenses/BSD-2-Clause[BSD 2-Clause License].
|
|
|
288
570
|
|
|
289
571
|
Everyone interacting in the Claricle project's codebases, issue trackers,
|
|
290
572
|
chat rooms and mailing lists is expected to follow the
|
|
291
|
-
https://github.com/
|
|
573
|
+
https://github.com/claricle/claricle/blob/main/CODE_OF_CONDUCT.md[code of conduct].
|