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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +10 -1
  3. data/LICENSE +25 -0
  4. data/README.adoc +439 -157
  5. data/TODO.plan/00-overview.md +502 -0
  6. data/TODO.plan/01-core.md +296 -0
  7. data/TODO.plan/02-inspect.md +163 -0
  8. data/TODO.plan/03-conform.md +208 -0
  9. data/TODO.plan/04-convert.md +216 -0
  10. data/exe/claricle +1 -1
  11. data/lib/claricle/batch.rb +240 -0
  12. data/lib/claricle/cli.rb +514 -40
  13. data/lib/claricle/conformance_options.rb +49 -0
  14. data/lib/claricle/conversion_engine.rb +21 -0
  15. data/lib/claricle/convert.rb +21 -0
  16. data/lib/claricle/detector.rb +696 -0
  17. data/lib/claricle/emf_header.rb +33 -0
  18. data/lib/claricle/errors.rb +73 -0
  19. data/lib/claricle/fault.rb +69 -0
  20. data/lib/claricle/handlers/base.rb +169 -0
  21. data/lib/claricle/handlers/extension_points.rb +124 -0
  22. data/lib/claricle/handlers/metafile.rb +1011 -0
  23. data/lib/claricle/handlers/pdf.rb +961 -0
  24. data/lib/claricle/handlers/pdf_arlington/definitions.rb +54 -0
  25. data/lib/claricle/handlers/pdf_arlington/kind.rb +62 -0
  26. data/lib/claricle/handlers/pdf_arlington/predicate_gate.rb +39 -0
  27. data/lib/claricle/handlers/pdf_arlington/rules.rb +86 -0
  28. data/lib/claricle/handlers/pdf_arlington/walker.rb +131 -0
  29. data/lib/claricle/handlers/pdf_arlington.rb +38 -0
  30. data/lib/claricle/handlers/pdf_profiles.rb +66 -0
  31. data/lib/claricle/handlers/png.rb +1107 -0
  32. data/lib/claricle/handlers/postscript.rb +1357 -0
  33. data/lib/claricle/handlers/svg.rb +787 -0
  34. data/lib/claricle/image.rb +272 -0
  35. data/lib/claricle/lossiness/levels.rb +13 -0
  36. data/lib/claricle/lossiness.rb +864 -0
  37. data/lib/claricle/models/base.rb +460 -0
  38. data/lib/claricle/models/batch_item.rb +127 -0
  39. data/lib/claricle/models/conversion.rb +167 -0
  40. data/lib/claricle/models/format_capability.rb +40 -0
  41. data/lib/claricle/models/free_form_hash.rb +77 -0
  42. data/lib/claricle/models/inspection.rb +188 -0
  43. data/lib/claricle/models/issue.rb +23 -0
  44. data/lib/claricle/models/location.rb +51 -0
  45. data/lib/claricle/models/report.rb +54 -0
  46. data/lib/claricle/models/uncoerced_integer.rb +38 -0
  47. data/lib/claricle/postscript_fidelity/verdict.rb +37 -0
  48. data/lib/claricle/postscript_fidelity/view_box.rb +82 -0
  49. data/lib/claricle/postscript_fidelity.rb +160 -0
  50. data/lib/claricle/registry.rb +125 -0
  51. data/lib/claricle/version.rb +1 -1
  52. data/lib/claricle/writer.rb +468 -0
  53. data/lib/claricle.rb +244 -7
  54. metadata +201 -19
  55. data/claricle.gemspec +0 -42
  56. 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/ribose/claricle.svg[License]
5
- image:https://github.com/ribose/claricle/actions/workflows/test.yml/badge.svg["Build Status", link="https://github.com/ribose/claricle/actions/workflows/test.yml"]
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 comprehensive image utility gem that provides tools for image
10
- validation, format conversion, compression, and vector graphics processing.
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
- The gem integrates functionality from multiple specialized tools including
13
- pngconform, svgconform, and vectory into a unified interface, making it easier
14
- to work with various image formats in Ruby applications.
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
- This library is particularly useful for applications that need to:
17
-
18
- * Validate image files for conformance to standards
19
- * Convert between different image formats
20
- * Compress images while maintaining quality
21
- * Process vector graphics programmatically
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
- [cols="1,2,3",options="header"]
26
- |===
27
- |Part
28
- |Root
29
- |Meaning
30
+ *Claricle* = *Clari* (clarity) + *cle* (particle)
30
31
 
31
- |clari-
32
- |Latin _clārus_
33
- |"clear, bright, famous" -- root of clarity, clarify
32
+ *Combined:* "Clear particle" or "Bright essence"
34
33
 
35
- |-cle
36
- |Latin _cle_ (from _particula_)
37
- |"small particle" -- like particle, fascicle
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
- *Combined:* "Clear particle" or "Bright essence"
118
+ == Command Line Interface
41
119
 
42
- Perfect metaphor:
120
+ === Available commands
43
121
 
44
- * *Validation* → clarify every pixel/particle
45
- * *Conversion* → preserve the clear essence
46
- * *Compression* → bright clarity in every bit
47
- * *Ruby* → gem-like, small but brilliant
122
+ [cols="1,3"]
123
+ |===
124
+ | Command | What it does
48
125
 
49
- == Features
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
- [NOTE]
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
- Planned features:
135
+ The operations are added as they are implemented, so this table never
136
+ lists a command that does nothing.
58
137
 
59
- * PNG validation and conformance checking (via pngconform)
60
- * SVG validation and processing (via svgconform)
61
- * Vector graphics manipulation (via vectory)
62
- * Image format conversion utilities
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
- == Installation
143
+ === Inspecting a file
70
144
 
71
- Add this line to your application's Gemfile:
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,ruby]
148
+ [source,sh]
74
149
  ----
75
- gem 'claricle'
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
- And then execute:
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
- $ bundle install
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
- Or install it yourself as:
178
+ === Listing formats
86
179
 
87
180
  [source,sh]
88
181
  ----
89
- $ gem install claricle
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
- == Command Line Interface
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
- The `claricle` command provides utilities for working with image files.
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
- === Available Commands
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
- Get help on available commands:
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 help
103
- Commands:
104
- claricle compress FILE_PATTERN # Compress image files (placeholder)
105
- claricle convert SOURCE DEST # Convert image formats (placeholder)
106
- claricle help [COMMAND] # Describe available commands or one specific command
107
- claricle validate FILE_PATTERN # Validate image files (placeholder)
108
- claricle version # Display Claricle version
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
- === Version Information
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
- Display the installed version:
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
- === Validation (Placeholder)
263
+ === Exit codes
122
264
 
123
- [NOTE]
124
- ====
125
- This functionality is not yet implemented in this shim release.
126
- ====
265
+ [cols="1,4"]
266
+ |===
267
+ | Code | Meaning
127
268
 
128
- Future usage:
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
- [source,sh]
131
- ----
132
- # Validate a single image file
133
- $ claricle validate image.png
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
- # Validate multiple files
136
- $ claricle validate *.svg
314
+ Detect a format from a path or from bytes:
137
315
 
138
- # Validate files using patterns
139
- $ claricle validate "images/*.png"
316
+ [source,ruby]
140
317
  ----
318
+ require "claricle"
141
319
 
142
- === Conversion (Placeholder)
320
+ Claricle.detect(File.binread("logo.png")) # => :png
321
+ File.open("logo.png", "rb") { |io| Claricle.detect(io) } # => :png
143
322
 
144
- [NOTE]
145
- ====
146
- This functionality is not yet implemented in this shim release.
147
- ====
323
+ image = Claricle::Image.from_path("logo.png")
324
+ image.format # => :png
325
+ image.content # read lazily, once
326
+ ----
148
327
 
149
- Future usage:
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,sh]
364
+ [source,ruby]
152
365
  ----
153
- # Convert between formats
154
- $ claricle convert image.png image.svg
155
- $ claricle convert logo.svg logo.png
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
- === Compression (Placeholder)
371
+ Hand it a source instead when the reader only needs a prefix:
159
372
 
160
- [NOTE]
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
- # Compress image files
170
- $ claricle compress image.png
171
- $ claricle compress *.jpg
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
- == Usage
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
- === Basic Example (Future)
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
- require 'claricle'
389
+ conversion = Claricle.convert("diagram.emf", to: :svg) # writes diagram.svg
390
+ conversion.output_path
391
+ conversion.lossiness
181
392
 
182
- # Validate an image
183
- validator = Claricle::Validator.new
184
- result = validator.validate('image.png')
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
- if result.valid?
187
- puts "Image is valid!"
188
- else
189
- puts "Validation errors:"
190
- result.errors.each { |error| puts " - #{error}" }
191
- end
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
- # Convert image format
194
- converter = Claricle::Converter.new
195
- converter.convert('image.png', 'image.svg')
405
+ == Architecture
196
406
 
197
- # Compress image
198
- compressor = Claricle::Compressor.new
199
- compressor.compress('image.png', quality: 85)
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
- == Architecture
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
- === Current Structure
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
- [source]
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
- Claricle (Gem)
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
- === Future Architecture
494
+ And then execute:
224
495
 
225
- The planned architecture will integrate specialized tools:
496
+ [source,sh]
497
+ ----
498
+ $ bundle install
499
+ ----
226
500
 
227
- [source]
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/ribose/claricle.
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/ribose/claricle/blob/main/CODE_OF_CONDUCT.md[code of conduct].
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/ribose/claricle/blob/main/CODE_OF_CONDUCT.md[code of conduct].
573
+ https://github.com/claricle/claricle/blob/main/CODE_OF_CONDUCT.md[code of conduct].