png_conform 0.1.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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +19 -0
- data/.rubocop_todo.yml +197 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/CONTRIBUTING.md +323 -0
- data/Gemfile +13 -0
- data/LICENSE +43 -0
- data/README.adoc +859 -0
- data/Rakefile +10 -0
- data/SECURITY.md +147 -0
- data/docs/ARCHITECTURE.adoc +681 -0
- data/docs/CHUNK_TYPES.adoc +450 -0
- data/docs/CLI_OPTIONS.adoc +913 -0
- data/docs/COMPATIBILITY.adoc +616 -0
- data/examples/README.adoc +398 -0
- data/examples/advanced_usage.rb +304 -0
- data/examples/basic_usage.rb +210 -0
- data/exe/png_conform +6 -0
- data/lib/png_conform/analyzers/comparison_analyzer.rb +230 -0
- data/lib/png_conform/analyzers/metrics_analyzer.rb +176 -0
- data/lib/png_conform/analyzers/optimization_analyzer.rb +190 -0
- data/lib/png_conform/analyzers/resolution_analyzer.rb +274 -0
- data/lib/png_conform/bindata/chunk_structure.rb +153 -0
- data/lib/png_conform/bindata/jng_file.rb +79 -0
- data/lib/png_conform/bindata/mng_file.rb +97 -0
- data/lib/png_conform/bindata/png_file.rb +162 -0
- data/lib/png_conform/cli.rb +116 -0
- data/lib/png_conform/commands/check_command.rb +323 -0
- data/lib/png_conform/commands/list_command.rb +67 -0
- data/lib/png_conform/models/chunk.rb +84 -0
- data/lib/png_conform/models/chunk_info.rb +71 -0
- data/lib/png_conform/models/compression_info.rb +49 -0
- data/lib/png_conform/models/decoded_chunk_data.rb +143 -0
- data/lib/png_conform/models/file_analysis.rb +181 -0
- data/lib/png_conform/models/file_info.rb +91 -0
- data/lib/png_conform/models/image_info.rb +52 -0
- data/lib/png_conform/models/validation_error.rb +89 -0
- data/lib/png_conform/models/validation_result.rb +137 -0
- data/lib/png_conform/readers/full_load_reader.rb +113 -0
- data/lib/png_conform/readers/streaming_reader.rb +180 -0
- data/lib/png_conform/reporters/base_reporter.rb +53 -0
- data/lib/png_conform/reporters/color_reporter.rb +65 -0
- data/lib/png_conform/reporters/json_reporter.rb +18 -0
- data/lib/png_conform/reporters/palette_reporter.rb +48 -0
- data/lib/png_conform/reporters/quiet_reporter.rb +18 -0
- data/lib/png_conform/reporters/reporter_factory.rb +108 -0
- data/lib/png_conform/reporters/summary_reporter.rb +65 -0
- data/lib/png_conform/reporters/text_reporter.rb +66 -0
- data/lib/png_conform/reporters/verbose_reporter.rb +87 -0
- data/lib/png_conform/reporters/very_verbose_reporter.rb +33 -0
- data/lib/png_conform/reporters/visual_elements.rb +66 -0
- data/lib/png_conform/reporters/yaml_reporter.rb +18 -0
- data/lib/png_conform/services/profile_manager.rb +242 -0
- data/lib/png_conform/services/validation_service.rb +457 -0
- data/lib/png_conform/services/zlib_validator.rb +270 -0
- data/lib/png_conform/validators/ancillary/bkgd_validator.rb +140 -0
- data/lib/png_conform/validators/ancillary/chrm_validator.rb +178 -0
- data/lib/png_conform/validators/ancillary/cicp_validator.rb +202 -0
- data/lib/png_conform/validators/ancillary/gama_validator.rb +105 -0
- data/lib/png_conform/validators/ancillary/hist_validator.rb +147 -0
- data/lib/png_conform/validators/ancillary/iccp_validator.rb +243 -0
- data/lib/png_conform/validators/ancillary/itxt_validator.rb +280 -0
- data/lib/png_conform/validators/ancillary/mdcv_validator.rb +201 -0
- data/lib/png_conform/validators/ancillary/offs_validator.rb +132 -0
- data/lib/png_conform/validators/ancillary/pcal_validator.rb +289 -0
- data/lib/png_conform/validators/ancillary/phys_validator.rb +107 -0
- data/lib/png_conform/validators/ancillary/sbit_validator.rb +176 -0
- data/lib/png_conform/validators/ancillary/scal_validator.rb +180 -0
- data/lib/png_conform/validators/ancillary/splt_validator.rb +223 -0
- data/lib/png_conform/validators/ancillary/srgb_validator.rb +117 -0
- data/lib/png_conform/validators/ancillary/ster_validator.rb +111 -0
- data/lib/png_conform/validators/ancillary/text_validator.rb +129 -0
- data/lib/png_conform/validators/ancillary/time_validator.rb +132 -0
- data/lib/png_conform/validators/ancillary/trns_validator.rb +154 -0
- data/lib/png_conform/validators/ancillary/ztxt_validator.rb +173 -0
- data/lib/png_conform/validators/apng/actl_validator.rb +81 -0
- data/lib/png_conform/validators/apng/fctl_validator.rb +155 -0
- data/lib/png_conform/validators/apng/fdat_validator.rb +117 -0
- data/lib/png_conform/validators/base_validator.rb +241 -0
- data/lib/png_conform/validators/chunk_registry.rb +219 -0
- data/lib/png_conform/validators/critical/idat_validator.rb +77 -0
- data/lib/png_conform/validators/critical/iend_validator.rb +68 -0
- data/lib/png_conform/validators/critical/ihdr_validator.rb +160 -0
- data/lib/png_conform/validators/critical/plte_validator.rb +120 -0
- data/lib/png_conform/validators/jng/jdat_validator.rb +66 -0
- data/lib/png_conform/validators/jng/jhdr_validator.rb +116 -0
- data/lib/png_conform/validators/jng/jsep_validator.rb +66 -0
- data/lib/png_conform/validators/mng/back_validator.rb +87 -0
- data/lib/png_conform/validators/mng/clip_validator.rb +65 -0
- data/lib/png_conform/validators/mng/clon_validator.rb +45 -0
- data/lib/png_conform/validators/mng/defi_validator.rb +104 -0
- data/lib/png_conform/validators/mng/dhdr_validator.rb +104 -0
- data/lib/png_conform/validators/mng/disc_validator.rb +44 -0
- data/lib/png_conform/validators/mng/endl_validator.rb +65 -0
- data/lib/png_conform/validators/mng/fram_validator.rb +91 -0
- data/lib/png_conform/validators/mng/loop_validator.rb +75 -0
- data/lib/png_conform/validators/mng/mend_validator.rb +31 -0
- data/lib/png_conform/validators/mng/mhdr_validator.rb +69 -0
- data/lib/png_conform/validators/mng/move_validator.rb +61 -0
- data/lib/png_conform/validators/mng/save_validator.rb +39 -0
- data/lib/png_conform/validators/mng/seek_validator.rb +42 -0
- data/lib/png_conform/validators/mng/show_validator.rb +52 -0
- data/lib/png_conform/validators/mng/term_validator.rb +84 -0
- data/lib/png_conform/version.rb +5 -0
- data/lib/png_conform.rb +101 -0
- data/png_conform.gemspec +43 -0
- metadata +201 -0
data/README.adoc
ADDED
|
@@ -0,0 +1,859 @@
|
|
|
1
|
+
= PngConform: A Comprehensive PNG Validator in Ruby
|
|
2
|
+
|
|
3
|
+
image:https://img.shields.io/gem/v/png_conform.svg[Gem Version, link="https://rubygems.org/gems/png_conform"]
|
|
4
|
+
image:https://img.shields.io/github/license/claricle/png_conform.svg[License, link="https://github.com/claricle/png_conform/blob/main/LICENSE"]
|
|
5
|
+
image:https://github.com/claricle/png_conform/actions/workflows/rake.yml/badge.svg[Build Status, link="https://github.com/claricle/png_conform/actions/workflows/rake.yml"]
|
|
6
|
+
|
|
7
|
+
== Purpose
|
|
8
|
+
|
|
9
|
+
PngConform is a pure Ruby PNG file validator with comprehensive chunk validation
|
|
10
|
+
and profile support. It validates file structure, chunk validity, CRC checksums,
|
|
11
|
+
chunk ordering, and profile conformance for PNG files.
|
|
12
|
+
|
|
13
|
+
The library provides pngcheck-compatible validation output while leveraging
|
|
14
|
+
modern Ruby object-oriented design principles and a layered architecture for
|
|
15
|
+
extensibility and maintainability.
|
|
16
|
+
|
|
17
|
+
== Features
|
|
18
|
+
|
|
19
|
+
* *46 Chunk Validators*: Complete validation of PNG (24), APNG (3), MNG (16), and JNG (3) chunks
|
|
20
|
+
* *PNG 3rd Edition Support*: Includes cICP and mDCv chunks for HDR content
|
|
21
|
+
* *MNG/JNG Support*: Full Multiple-image Network Graphics and JPEG Network Graphics validation
|
|
22
|
+
* *CRC Validation*: CRC-32 checksum verification for all chunks with detailed error reporting
|
|
23
|
+
* *Chunk Ordering*: Validates proper chunk sequence and dependencies
|
|
24
|
+
* *6 Validation Profiles*: Minimal, web, print, archive, strict, and default profiles
|
|
25
|
+
* *Enhanced Output Formats*:
|
|
26
|
+
** Text output with colors and emojis (✅/❌/⚠️)
|
|
27
|
+
** Comprehensive YAML output with image info, resolution, and recommendations
|
|
28
|
+
** Comprehensive JSON output for CI/CD integration
|
|
29
|
+
** Compression ratio reporting
|
|
30
|
+
* *Analysis Features*:
|
|
31
|
+
** *Retina/Resolution Analysis*: @1x/@2x/@3x calculations for mobile developers
|
|
32
|
+
** *Optimization Suggestions*: File size reduction recommendations
|
|
33
|
+
** *Comprehensive Metrics*: Detailed metrics for CI/CD automation
|
|
34
|
+
** *iOS/Android Support*: Asset catalog and density bucket suggestions
|
|
35
|
+
* *Multiple Output Modes*: Summary, verbose, very verbose, quiet, with optional palette/text/color output
|
|
36
|
+
* *Clean Architecture*: Layered OO design following MECE principles
|
|
37
|
+
* *CLI Interface*: Thor-based command-line tool with pngcheck-compatible options plus modern analysis features
|
|
38
|
+
* *Comprehensive Testing*: Extensive RSpec test suite (936 examples, 100% passing)
|
|
39
|
+
|
|
40
|
+
== Architecture
|
|
41
|
+
|
|
42
|
+
PngConform uses a layered architecture with strict separation of concerns:
|
|
43
|
+
|
|
44
|
+
.Layered Architecture
|
|
45
|
+
[source]
|
|
46
|
+
----
|
|
47
|
+
╔════════════════════════════════════════════════════════════╗
|
|
48
|
+
║ Presentation Layer ║
|
|
49
|
+
║ (CLI, Reporters) ║
|
|
50
|
+
╚════════════════════════════════════════════════════════════╝
|
|
51
|
+
│
|
|
52
|
+
╔════════════════════════════════════════════════════════════╗
|
|
53
|
+
║ Service Layer ║
|
|
54
|
+
║ (ValidationService, ProfileManager) ║
|
|
55
|
+
╚════════════════════════════════════════════════════════════╝
|
|
56
|
+
│
|
|
57
|
+
╔════════════════════════════════════════════════════════════╗
|
|
58
|
+
║ Business Logic Layer ║
|
|
59
|
+
║ (Validators, ChunkRegistry, Readers) ║
|
|
60
|
+
╚════════════════════════════════════════════════════════════╝
|
|
61
|
+
│
|
|
62
|
+
╔════════════════════════════════════════════════════════════╗
|
|
63
|
+
║ Domain Model Layer ║
|
|
64
|
+
║ (ChunkInfo, ValidationResult, FileAnalysis) ║
|
|
65
|
+
╚════════════════════════════════════════════════════════════╝
|
|
66
|
+
│
|
|
67
|
+
╔════════════════════════════════════════════════════════════╗
|
|
68
|
+
║ Binary Parsing Layer ║
|
|
69
|
+
║ (BinData structures) ║
|
|
70
|
+
╚════════════════════════════════════════════════════════════╝
|
|
71
|
+
----
|
|
72
|
+
|
|
73
|
+
.Module Structure
|
|
74
|
+
[source]
|
|
75
|
+
----
|
|
76
|
+
PngConform
|
|
77
|
+
├── BinData (Binary parsing structures)
|
|
78
|
+
│ ├── ChunkStructure
|
|
79
|
+
│ ├── PngFile
|
|
80
|
+
│ ├── MngFile ✅
|
|
81
|
+
│ └── JngFile ✅
|
|
82
|
+
├── Models (Domain models)
|
|
83
|
+
│ ├── ChunkInfo
|
|
84
|
+
│ ├── ValidationResult
|
|
85
|
+
│ ├── ValidationError
|
|
86
|
+
│ ├── FileAnalysis
|
|
87
|
+
│ ├── ImageInfo
|
|
88
|
+
│ ├── CompressionInfo
|
|
89
|
+
│ └── DecodedChunkData
|
|
90
|
+
├── Validators (Validation logic)
|
|
91
|
+
│ ├── BaseValidator
|
|
92
|
+
│ ├── ChunkRegistry
|
|
93
|
+
│ ├── Critical (IHDR, PLTE, IDAT, IEND)
|
|
94
|
+
│ └── Ancillary (gAMA, tRNS, tEXt, etc. - 20 validators)
|
|
95
|
+
├── Services (Orchestration)
|
|
96
|
+
│ ├── ValidationService
|
|
97
|
+
│ └── ProfileManager
|
|
98
|
+
├── Readers (File reading strategies)
|
|
99
|
+
│ ├── StreamingReader
|
|
100
|
+
│ └── FullLoadReader
|
|
101
|
+
├── Reporters (Output formatting)
|
|
102
|
+
│ ├── BaseReporter
|
|
103
|
+
│ ├── SummaryReporter
|
|
104
|
+
│ ├── VerboseReporter
|
|
105
|
+
│ ├── VeryVerboseReporter
|
|
106
|
+
│ ├── QuietReporter
|
|
107
|
+
│ ├── PaletteReporter (decorator)
|
|
108
|
+
│ ├── TextReporter (decorator)
|
|
109
|
+
│ ├── ColorReporter (decorator)
|
|
110
|
+
│ └── ReporterFactory
|
|
111
|
+
├── Commands (CLI commands)
|
|
112
|
+
│ ├── CheckCommand
|
|
113
|
+
│ └── ListCommand
|
|
114
|
+
└── Cli (Thor-based CLI)
|
|
115
|
+
----
|
|
116
|
+
|
|
117
|
+
.Data Flow
|
|
118
|
+
[source]
|
|
119
|
+
----
|
|
120
|
+
File Input
|
|
121
|
+
│
|
|
122
|
+
▼
|
|
123
|
+
CLI (CheckCommand)
|
|
124
|
+
│
|
|
125
|
+
├─► ProfileManager (load profile)
|
|
126
|
+
│
|
|
127
|
+
└─► ReporterFactory (create reporter)
|
|
128
|
+
│
|
|
129
|
+
▼
|
|
130
|
+
ValidationService
|
|
131
|
+
│
|
|
132
|
+
├─► BinData Parser (read PNG structure)
|
|
133
|
+
│
|
|
134
|
+
├─► ChunkRegistry (lookup validators)
|
|
135
|
+
│
|
|
136
|
+
└─► Validators (validate chunks)
|
|
137
|
+
│
|
|
138
|
+
├─► BaseValidator methods (check_crc, check_length, etc.)
|
|
139
|
+
│
|
|
140
|
+
└─► ValidationContext (store state)
|
|
141
|
+
│
|
|
142
|
+
▼
|
|
143
|
+
ValidationResult
|
|
144
|
+
│
|
|
145
|
+
├─► FileAnalysis (file-level results)
|
|
146
|
+
├─► ChunkInfo[] (chunk-level results)
|
|
147
|
+
└─► ValidationError[] (errors/warnings/info)
|
|
148
|
+
│
|
|
149
|
+
▼
|
|
150
|
+
Reporter (format output)
|
|
151
|
+
│
|
|
152
|
+
├─► SummaryReporter
|
|
153
|
+
├─► VerboseReporter
|
|
154
|
+
├─► VeryVerboseReporter
|
|
155
|
+
└─► QuietReporter
|
|
156
|
+
│
|
|
157
|
+
▼
|
|
158
|
+
Output (STDOUT)
|
|
159
|
+
----
|
|
160
|
+
|
|
161
|
+
== Installation
|
|
162
|
+
|
|
163
|
+
Add this line to your application's Gemfile:
|
|
164
|
+
|
|
165
|
+
[source,ruby]
|
|
166
|
+
----
|
|
167
|
+
gem "png_conform"
|
|
168
|
+
----
|
|
169
|
+
|
|
170
|
+
And then execute:
|
|
171
|
+
|
|
172
|
+
[source,shell]
|
|
173
|
+
----
|
|
174
|
+
bundle install
|
|
175
|
+
----
|
|
176
|
+
|
|
177
|
+
Or install it yourself as:
|
|
178
|
+
|
|
179
|
+
[source,shell]
|
|
180
|
+
----
|
|
181
|
+
gem install png_conform
|
|
182
|
+
----
|
|
183
|
+
|
|
184
|
+
[[cli-usage]]
|
|
185
|
+
== CLI Usage
|
|
186
|
+
|
|
187
|
+
=== General
|
|
188
|
+
|
|
189
|
+
PngConform provides a command-line interface with pngcheck-compatible options.
|
|
190
|
+
|
|
191
|
+
=== Basic usage
|
|
192
|
+
|
|
193
|
+
[source,shell]
|
|
194
|
+
----
|
|
195
|
+
png_conform check FILE [FILE...]
|
|
196
|
+
----
|
|
197
|
+
|
|
198
|
+
.Basic validation (successful)
|
|
199
|
+
[example]
|
|
200
|
+
====
|
|
201
|
+
[source,shell]
|
|
202
|
+
----
|
|
203
|
+
$ png_conform check image.png
|
|
204
|
+
✅ OK: image.png (800x600, 8-bit/color RGB, non-interlaced)
|
|
205
|
+
----
|
|
206
|
+
====
|
|
207
|
+
|
|
208
|
+
.Basic validation (with errors)
|
|
209
|
+
[example]
|
|
210
|
+
====
|
|
211
|
+
[source,shell]
|
|
212
|
+
----
|
|
213
|
+
$ png_conform check corrupted.png
|
|
214
|
+
❌ ERROR: corrupted.png, (PNG, 1024 bytes, 3 chunks)
|
|
215
|
+
ERROR: IHDR: invalid bit depth 3 for color type 2
|
|
216
|
+
ERROR: CRC error in chunk IDAT
|
|
217
|
+
WARNING: Missing recommended chunk gAMA
|
|
218
|
+
----
|
|
219
|
+
====
|
|
220
|
+
|
|
221
|
+
=== CLI options
|
|
222
|
+
|
|
223
|
+
==== Verbosity options
|
|
224
|
+
|
|
225
|
+
`-v, --verbose`:: Display chunk-level information
|
|
226
|
+
`-vv, --very-verbose`:: Display detailed chunk data
|
|
227
|
+
`-q, --quiet`:: Suppress all output except errors
|
|
228
|
+
|
|
229
|
+
.Verbose output example (with colors and emojis)
|
|
230
|
+
[example]
|
|
231
|
+
====
|
|
232
|
+
[source,shell]
|
|
233
|
+
----
|
|
234
|
+
$ png_conform check -v image.png
|
|
235
|
+
📄 image.png (32768 bytes)
|
|
236
|
+
|
|
237
|
+
✓ 📦 IHDR at 0x0000c (13 bytes)
|
|
238
|
+
✓ 📦 gAMA at 0x00025 (4 bytes)
|
|
239
|
+
✓ 📦 cHRM at 0x00035 (32 bytes)
|
|
240
|
+
✓ 📦 IDAT at 0x00061 (65445 bytes)
|
|
241
|
+
✓ 📦 IEND at 0x10072 (0 bytes)
|
|
242
|
+
|
|
243
|
+
✓ No errors detected in image.png (5 chunks)
|
|
244
|
+
----
|
|
245
|
+
====
|
|
246
|
+
|
|
247
|
+
.Verbose with errors
|
|
248
|
+
[example]
|
|
249
|
+
====
|
|
250
|
+
[source,shell]
|
|
251
|
+
----
|
|
252
|
+
$ png_conform check -v corrupted.png
|
|
253
|
+
📄 corrupted.png (1024 bytes)
|
|
254
|
+
|
|
255
|
+
✗ 📦 IHDR at 0x0000c (13 bytes)
|
|
256
|
+
✓ 📦 PLTE at 0x00025 (12 bytes)
|
|
257
|
+
✗ 📦 IDAT at 0x00035 (891 bytes)
|
|
258
|
+
✓ 📦 IEND at 0x003ec (0 bytes)
|
|
259
|
+
|
|
260
|
+
VALIDATION ERRORS:
|
|
261
|
+
❌ ERROR: IHDR: invalid bit depth 3 for color type 2
|
|
262
|
+
❌ ERROR: CRC error in chunk IDAT (expected 0x12345678, got 0x87654321)
|
|
263
|
+
⚠️ WARNING: Missing recommended chunk gAMA
|
|
264
|
+
|
|
265
|
+
❌ 2 errors, 1 warning in corrupted.png (4 chunks)
|
|
266
|
+
----
|
|
267
|
+
====
|
|
268
|
+
|
|
269
|
+
.Comprehensive YAML output (includes image info, resolution, and recommendations)
|
|
270
|
+
[example]
|
|
271
|
+
====
|
|
272
|
+
[source,shell]
|
|
273
|
+
----
|
|
274
|
+
$ png_conform check --format yaml image.png
|
|
275
|
+
---
|
|
276
|
+
filename: "./spec/fixtures/pngsuite/background/bgwn6a08.png"
|
|
277
|
+
file_type: PNG
|
|
278
|
+
file_size: 202
|
|
279
|
+
compression_ratio: -97.3
|
|
280
|
+
crc_errors_count: 0
|
|
281
|
+
valid: true
|
|
282
|
+
image:
|
|
283
|
+
width: 32
|
|
284
|
+
height: 32
|
|
285
|
+
bit_depth: 8
|
|
286
|
+
color_type: 6
|
|
287
|
+
color_type_name: RGBA
|
|
288
|
+
interlaced: false
|
|
289
|
+
chunks:
|
|
290
|
+
total: 5
|
|
291
|
+
types:
|
|
292
|
+
- IDAT
|
|
293
|
+
- IEND
|
|
294
|
+
- IHDR
|
|
295
|
+
- bKGD
|
|
296
|
+
- gAMA
|
|
297
|
+
resolution:
|
|
298
|
+
dimensions: 32x32
|
|
299
|
+
megapixels: 0.0
|
|
300
|
+
dpi:
|
|
301
|
+
retina:
|
|
302
|
+
at_1x: 14.1x14.1pt
|
|
303
|
+
at_2x: 7.1x7.1pt
|
|
304
|
+
at_3x: 4.7x4.7pt
|
|
305
|
+
recommended: "@1x (too small for higher densities)"
|
|
306
|
+
ios:
|
|
307
|
+
- Custom size
|
|
308
|
+
android: ldpi or mdpi
|
|
309
|
+
recommendations:
|
|
310
|
+
- Image is too small for Retina displays - consider @2x/@3x versions
|
|
311
|
+
- Add pHYs chunk with DPI information for print compatibility
|
|
312
|
+
----
|
|
313
|
+
====
|
|
314
|
+
|
|
315
|
+
.JSON output example
|
|
316
|
+
[example]
|
|
317
|
+
====
|
|
318
|
+
[source,shell]
|
|
319
|
+
----
|
|
320
|
+
$ png_conform check --format json image.png
|
|
321
|
+
{
|
|
322
|
+
"filename": "image.png",
|
|
323
|
+
"file_type": "PNG",
|
|
324
|
+
"file_size": 32768,
|
|
325
|
+
"crc_errors_count": 0,
|
|
326
|
+
"compression_ratio": -89.2
|
|
327
|
+
}
|
|
328
|
+
----
|
|
329
|
+
====
|
|
330
|
+
.YAML with errors
|
|
331
|
+
[example]
|
|
332
|
+
====
|
|
333
|
+
[source,shell]
|
|
334
|
+
----
|
|
335
|
+
$ png_conform check --format yaml corrupted.png
|
|
336
|
+
---
|
|
337
|
+
filename: corrupted.png
|
|
338
|
+
file_type: PNG
|
|
339
|
+
file_size: 1024
|
|
340
|
+
crc_errors_count: 1
|
|
341
|
+
valid: false
|
|
342
|
+
errors:
|
|
343
|
+
- severity: error
|
|
344
|
+
message: 'IHDR: invalid bit depth 3 for color type 2'
|
|
345
|
+
chunk_type: IHDR
|
|
346
|
+
- severity: error
|
|
347
|
+
message: 'CRC error in chunk IDAT'
|
|
348
|
+
chunk_type: IDAT
|
|
349
|
+
expected: '0x12345678'
|
|
350
|
+
actual: '0x87654321'
|
|
351
|
+
- severity: warning
|
|
352
|
+
message: Missing recommended chunk gAMA
|
|
353
|
+
----
|
|
354
|
+
====
|
|
355
|
+
|
|
356
|
+
.JSON with errors
|
|
357
|
+
[example]
|
|
358
|
+
====
|
|
359
|
+
[source,shell]
|
|
360
|
+
----
|
|
361
|
+
$ png_conform check --format json corrupted.png
|
|
362
|
+
{
|
|
363
|
+
"filename": "corrupted.png",
|
|
364
|
+
"file_type": "PNG",
|
|
365
|
+
"file_size": 1024,
|
|
366
|
+
"crc_errors_count": 1,
|
|
367
|
+
"valid": false,
|
|
368
|
+
"errors": [
|
|
369
|
+
{
|
|
370
|
+
"severity": "error",
|
|
371
|
+
"message": "IHDR: invalid bit depth 3 for color type 2",
|
|
372
|
+
"chunk_type": "IHDR"
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
"severity": "error",
|
|
376
|
+
"message": "CRC error in chunk IDAT",
|
|
377
|
+
"chunk_type": "IDAT",
|
|
378
|
+
"expected": "0x12345678",
|
|
379
|
+
"actual": "0x87654321"
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
"severity": "warning",
|
|
383
|
+
"message": "Missing recommended chunk gAMA"
|
|
384
|
+
}
|
|
385
|
+
]
|
|
386
|
+
}
|
|
387
|
+
----
|
|
388
|
+
====
|
|
389
|
+
|
|
390
|
+
.Disable colors
|
|
391
|
+
[example]
|
|
392
|
+
====
|
|
393
|
+
[source,shell]
|
|
394
|
+
----
|
|
395
|
+
$ png_conform check --no-color image.png
|
|
396
|
+
OK: image.png (800x600, 8-bit/color RGB, non-interlaced)
|
|
397
|
+
----
|
|
398
|
+
====
|
|
399
|
+
|
|
400
|
+
==== Output options
|
|
401
|
+
|
|
402
|
+
`-f, --format FORMAT`:: Output format: text (default), yaml, json
|
|
403
|
+
`--no-color`:: Disable colored output
|
|
404
|
+
`-c, --color`:: Display RGB color values (only with `-p` or `-t`)
|
|
405
|
+
`-p, --palette`:: Display palette entries
|
|
406
|
+
`-t, --text`:: Display text chunk contents
|
|
407
|
+
`-7, --seven-bit`:: Check for 7-bit ASCII in tEXt chunks
|
|
408
|
+
|
|
409
|
+
.Palette display example
|
|
410
|
+
[example]
|
|
411
|
+
====
|
|
412
|
+
[source,shell]
|
|
413
|
+
----
|
|
414
|
+
$ png_conform check -p indexed.png
|
|
415
|
+
OK: indexed.png (32x32, 2-bit palette, non-interlaced)
|
|
416
|
+
chunk IHDR at offset 0x0000c, length 13
|
|
417
|
+
chunk PLTE at offset 0x00025, length 12
|
|
418
|
+
0: (255,255,255) = gray100
|
|
419
|
+
1: (204,204,204) = gray80
|
|
420
|
+
2: (153,153,153) = gray60
|
|
421
|
+
3: (102,102,102) = gray40
|
|
422
|
+
chunk IDAT at offset 0x0003d, length 147
|
|
423
|
+
chunk IEND at offset 0x000d4, length 0
|
|
424
|
+
----
|
|
425
|
+
====
|
|
426
|
+
|
|
427
|
+
==== Profile options
|
|
428
|
+
|
|
429
|
+
`--profile PROFILE`:: Use validation profile (minimal, web, print, archive, strict, default)
|
|
430
|
+
`--strict`:: Shortcut for `--profile strict`
|
|
431
|
+
|
|
432
|
+
.Profile validation example
|
|
433
|
+
[example]
|
|
434
|
+
====
|
|
435
|
+
[source,shell]
|
|
436
|
+
----
|
|
437
|
+
$ png_conform check --profile web image.png
|
|
438
|
+
WARN: image.png: missing required chunk gAMA (web profile)
|
|
439
|
+
WARN: image.png: missing required chunk sRGB (web profile)
|
|
440
|
+
OK: image.png (800x600, 8-bit/color RGB, non-interlaced)
|
|
441
|
+
----
|
|
442
|
+
====
|
|
443
|
+
|
|
444
|
+
==== Analysis options
|
|
445
|
+
|
|
446
|
+
`--resolution`:: Display resolution and Retina analysis (@1x/@2x/@3x)
|
|
447
|
+
`--optimize`:: Show file size optimization suggestions
|
|
448
|
+
`--metrics`:: Display comprehensive metrics for CI/CD
|
|
449
|
+
`--mobile-ready`:: Check mobile and Retina readiness
|
|
450
|
+
|
|
451
|
+
.Resolution and Retina analysis (shown by default)
|
|
452
|
+
[example]
|
|
453
|
+
====
|
|
454
|
+
[source,shell]
|
|
455
|
+
----
|
|
456
|
+
$ png_conform check icon.png
|
|
457
|
+
✅ OK: icon.png, (PNG, 202 bytes, 5 chunks, 🗜️ -97.3%)
|
|
458
|
+
|
|
459
|
+
RESOLUTION ANALYSIS:
|
|
460
|
+
Dimensions: 32x32 (0.0 megapixels)
|
|
461
|
+
DPI: Not specified
|
|
462
|
+
|
|
463
|
+
Retina Analysis:
|
|
464
|
+
@1x: 14.1x14.1pt (Small icon)
|
|
465
|
+
@2x: 7.1x7.1pt (Small icon)
|
|
466
|
+
@3x: 4.7x4.7pt (Small icon)
|
|
467
|
+
Recommended: @1x (too small for higher densities)
|
|
468
|
+
iOS: Custom size
|
|
469
|
+
Android: ldpi or mdpi
|
|
470
|
+
|
|
471
|
+
Recommendations:
|
|
472
|
+
[HIGH] Image is too small for Retina displays - consider @2x/@3x versions
|
|
473
|
+
[MEDIUM] Add pHYs chunk with DPI information for print compatibility
|
|
474
|
+
----
|
|
475
|
+
====
|
|
476
|
+
|
|
477
|
+
.Optimization suggestions
|
|
478
|
+
[example]
|
|
479
|
+
====
|
|
480
|
+
[source,shell]
|
|
481
|
+
----
|
|
482
|
+
$ png_conform check --optimize large-photo.png
|
|
483
|
+
✅ OK: large-photo.png, (PNG, 4.5 MB, 8 chunks, 🗜️ -42.1%)
|
|
484
|
+
|
|
485
|
+
OPTIMIZATION SUGGESTIONS:
|
|
486
|
+
1. [HIGH] Convert from 16-bit to 8-bit depth (saves ~45% = 2.1MB)
|
|
487
|
+
2. [MEDIUM] Remove 3 unnecessary chunks (tIME, pHYs, oFFs) (saves 156 bytes)
|
|
488
|
+
3. [LOW] Remove interlacing for smaller file size (saves ~15% = 680KB)
|
|
489
|
+
|
|
490
|
+
Total Potential Savings: 2.8MB (62.2%)
|
|
491
|
+
----
|
|
492
|
+
====
|
|
493
|
+
|
|
494
|
+
.Mobile and Retina readiness check
|
|
495
|
+
[example]
|
|
496
|
+
====
|
|
497
|
+
[source,shell]
|
|
498
|
+
----
|
|
499
|
+
$ png_conform check --mobile-ready app-icon@2x.png
|
|
500
|
+
✅ OK: app-icon@2x.png, (PNG, 2.3 KB, 5 chunks, 🗜️ -88.5%)
|
|
501
|
+
|
|
502
|
+
MOBILE & RETINA READINESS:
|
|
503
|
+
Status: ✓ READY
|
|
504
|
+
|
|
505
|
+
Checks:
|
|
506
|
+
Retina Ready: ✓
|
|
507
|
+
Mobile Friendly: ✓
|
|
508
|
+
Web Suitable: ✓
|
|
509
|
+
|
|
510
|
+
Retina Densities:
|
|
511
|
+
@1x: 44.0x44.0pt
|
|
512
|
+
@2x: 22.0x22.0pt
|
|
513
|
+
@3x: 14.7x14.7pt
|
|
514
|
+
Recommended: @2x
|
|
515
|
+
|
|
516
|
+
Screen Coverage:
|
|
517
|
+
Mobile (375x667): 23.5% x 13.2%
|
|
518
|
+
Desktop (1920x1080): 4.6% x 8.1%
|
|
519
|
+
|
|
520
|
+
Load Time: 25ms (fast)
|
|
521
|
+
----
|
|
522
|
+
====
|
|
523
|
+
|
|
524
|
+
=== List profiles
|
|
525
|
+
|
|
526
|
+
Display available validation profiles:
|
|
527
|
+
|
|
528
|
+
[source,shell]
|
|
529
|
+
----
|
|
530
|
+
png_conform list
|
|
531
|
+
----
|
|
532
|
+
|
|
533
|
+
.Profile listing output
|
|
534
|
+
[example]
|
|
535
|
+
====
|
|
536
|
+
[source,shell]
|
|
537
|
+
----
|
|
538
|
+
$ png_conform list
|
|
539
|
+
Available validation profiles:
|
|
540
|
+
|
|
541
|
+
minimal
|
|
542
|
+
Required chunks: IHDR, IDAT, IEND
|
|
543
|
+
Optional chunks: (any)
|
|
544
|
+
Prohibited chunks: (none)
|
|
545
|
+
|
|
546
|
+
web
|
|
547
|
+
Required chunks: IHDR, IDAT, IEND, gAMA, sRGB
|
|
548
|
+
Optional chunks: tRNS, bKGD, tEXt, iTXt, zTXt
|
|
549
|
+
Prohibited chunks: (none)
|
|
550
|
+
|
|
551
|
+
print
|
|
552
|
+
Required chunks: IHDR, IDAT, IEND, iCCP
|
|
553
|
+
Optional chunks: gAMA, cHRM, tRNS, bKGD
|
|
554
|
+
Prohibited chunks: sRGB
|
|
555
|
+
|
|
556
|
+
archive
|
|
557
|
+
Required chunks: IHDR, IDAT, IEND, tIME
|
|
558
|
+
Optional chunks: (any)
|
|
559
|
+
Prohibited chunks: (none)
|
|
560
|
+
|
|
561
|
+
strict
|
|
562
|
+
Required chunks: IHDR, IDAT, IEND
|
|
563
|
+
Optional chunks: (critical chunks only)
|
|
564
|
+
Prohibited chunks: (unknown chunks)
|
|
565
|
+
|
|
566
|
+
default
|
|
567
|
+
Required chunks: IHDR, IDAT, IEND
|
|
568
|
+
Optional chunks: (any)
|
|
569
|
+
Prohibited chunks: (none)
|
|
570
|
+
----
|
|
571
|
+
====
|
|
572
|
+
|
|
573
|
+
[[ruby-api]]
|
|
574
|
+
== Ruby API
|
|
575
|
+
|
|
576
|
+
=== General
|
|
577
|
+
|
|
578
|
+
PngConform provides a comprehensive Ruby API for programmatic validation.
|
|
579
|
+
|
|
580
|
+
=== Basic validation
|
|
581
|
+
|
|
582
|
+
[source,ruby]
|
|
583
|
+
----
|
|
584
|
+
require "png_conform"
|
|
585
|
+
|
|
586
|
+
# Validate a file
|
|
587
|
+
service = PngConform::Services::ValidationService.new
|
|
588
|
+
result = service.validate_file("image.png")
|
|
589
|
+
|
|
590
|
+
if result.valid?
|
|
591
|
+
puts "File is valid"
|
|
592
|
+
puts "Image: #{result.image_info.width}x#{result.image_info.height}"
|
|
593
|
+
puts "Chunks: #{result.chunks.count}"
|
|
594
|
+
else
|
|
595
|
+
puts "Validation errors:"
|
|
596
|
+
result.errors.each do |error|
|
|
597
|
+
puts " #{error.severity}: #{error.message}"
|
|
598
|
+
end
|
|
599
|
+
end
|
|
600
|
+
----
|
|
601
|
+
|
|
602
|
+
=== Using profiles
|
|
603
|
+
|
|
604
|
+
[source,ruby]
|
|
605
|
+
----
|
|
606
|
+
# Load a specific profile
|
|
607
|
+
profile_mgr = PngConform::Services::ProfileManager.new
|
|
608
|
+
profile = profile_mgr.load_profile("web")
|
|
609
|
+
|
|
610
|
+
# Validate with profile
|
|
611
|
+
service = PngConform::Services::ValidationService.new
|
|
612
|
+
result = service.validate_file("image.png", profile: profile)
|
|
613
|
+
|
|
614
|
+
# Check profile conformance
|
|
615
|
+
profile_result = profile_mgr.validate_file_against_profile(
|
|
616
|
+
"image.png",
|
|
617
|
+
"web"
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
if profile_result.profile_errors.any?
|
|
621
|
+
puts "Profile violations:"
|
|
622
|
+
profile_result.profile_errors.each do |error|
|
|
623
|
+
puts " #{error.message}"
|
|
624
|
+
end
|
|
625
|
+
end
|
|
626
|
+
----
|
|
627
|
+
|
|
628
|
+
=== Custom reporters
|
|
629
|
+
|
|
630
|
+
[source,ruby]
|
|
631
|
+
----
|
|
632
|
+
# Use specific reporter
|
|
633
|
+
reporter = PngConform::Reporters::VerboseReporter.new
|
|
634
|
+
service = PngConform::Services::ValidationService.new
|
|
635
|
+
result = service.validate_file("image.png")
|
|
636
|
+
|
|
637
|
+
reporter.report(result)
|
|
638
|
+
|
|
639
|
+
# Create custom reporter combinations
|
|
640
|
+
factory = PngConform::Reporters::ReporterFactory.new
|
|
641
|
+
reporter = factory.create_reporter(
|
|
642
|
+
verbose: true,
|
|
643
|
+
show_palette: true,
|
|
644
|
+
show_text: true,
|
|
645
|
+
colorize: true
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
reporter.report(result)
|
|
649
|
+
----
|
|
650
|
+
|
|
651
|
+
=== Accessing chunk data
|
|
652
|
+
|
|
653
|
+
[source,ruby]
|
|
654
|
+
----
|
|
655
|
+
result = service.validate_file("image.png")
|
|
656
|
+
|
|
657
|
+
# Iterate through chunks
|
|
658
|
+
result.chunks.each do |chunk|
|
|
659
|
+
puts "Chunk: #{chunk.type}"
|
|
660
|
+
puts " Offset: #{chunk.offset}"
|
|
661
|
+
puts " Length: #{chunk.length}"
|
|
662
|
+
puts " CRC: 0x#{chunk.crc.to_s(16).upcase}"
|
|
663
|
+
|
|
664
|
+
# Access decoded data
|
|
665
|
+
if chunk.decoded_data
|
|
666
|
+
case chunk.type
|
|
667
|
+
when "IHDR"
|
|
668
|
+
puts " Dimensions: #{chunk.decoded_data.width}x#{chunk.decoded_data.height}"
|
|
669
|
+
puts " Bit depth: #{chunk.decoded_data.bit_depth}"
|
|
670
|
+
puts " Color type: #{chunk.decoded_data.color_type}"
|
|
671
|
+
when "gAMA"
|
|
672
|
+
puts " Gamma: #{chunk.decoded_data.gamma}"
|
|
673
|
+
when "tEXt"
|
|
674
|
+
puts " Keyword: #{chunk.decoded_data.keyword}"
|
|
675
|
+
puts " Text: #{chunk.decoded_data.text}"
|
|
676
|
+
end
|
|
677
|
+
end
|
|
678
|
+
end
|
|
679
|
+
----
|
|
680
|
+
|
|
681
|
+
[[chunk-validation]]
|
|
682
|
+
== Chunk Validation
|
|
683
|
+
|
|
684
|
+
=== General
|
|
685
|
+
|
|
686
|
+
PngConform validates all standard PNG chunk types with dedicated validators.
|
|
687
|
+
|
|
688
|
+
=== Critical chunks
|
|
689
|
+
|
|
690
|
+
==== IHDR (image header)
|
|
691
|
+
|
|
692
|
+
The IHDR chunk must be the first chunk and contains image metadata.
|
|
693
|
+
|
|
694
|
+
Validation checks:
|
|
695
|
+
|
|
696
|
+
* Width and height must be non-zero
|
|
697
|
+
* Bit depth must be valid for color type
|
|
698
|
+
* Color type must be 0, 2, 3, 4, or 6
|
|
699
|
+
* Compression method must be 0 (deflate)
|
|
700
|
+
* Filter method must be 0 (adaptive)
|
|
701
|
+
* Interlace method must be 0 (none) or 1 (Adam7)
|
|
702
|
+
|
|
703
|
+
==== PLTE (palette)
|
|
704
|
+
|
|
705
|
+
Required for indexed-color images, optional for truecolor.
|
|
706
|
+
|
|
707
|
+
Validation checks:
|
|
708
|
+
|
|
709
|
+
* Length must be divisible by 3 (RGB triplets)
|
|
710
|
+
* Number of entries must not exceed 2^bit_depth
|
|
711
|
+
* Must appear before IDAT chunks
|
|
712
|
+
* Required for color type 3
|
|
713
|
+
* Must not appear for grayscale images
|
|
714
|
+
|
|
715
|
+
==== IDAT (image data)
|
|
716
|
+
|
|
717
|
+
Contains compressed image data. Multiple IDAT chunks must be consecutive.
|
|
718
|
+
|
|
719
|
+
Validation checks:
|
|
720
|
+
|
|
721
|
+
* IDAT chunks must be consecutive
|
|
722
|
+
* Compressed data must be valid zlib format
|
|
723
|
+
* Decompressed data must match expected size
|
|
724
|
+
* Filter types must be valid (0-4)
|
|
725
|
+
|
|
726
|
+
==== IEND (image trailer)
|
|
727
|
+
|
|
728
|
+
Marks the end of the PNG file.
|
|
729
|
+
|
|
730
|
+
Validation checks:
|
|
731
|
+
|
|
732
|
+
* Must be the last chunk
|
|
733
|
+
* Data length must be 0
|
|
734
|
+
* Must appear exactly once
|
|
735
|
+
|
|
736
|
+
=== Ancillary chunks
|
|
737
|
+
|
|
738
|
+
==== Color space chunks
|
|
739
|
+
|
|
740
|
+
`gAMA`:: Gamma correction value
|
|
741
|
+
`cHRM`:: Primary chromaticities and white point
|
|
742
|
+
`sRGB`:: Standard RGB color space
|
|
743
|
+
`iCCP`:: ICC color profile
|
|
744
|
+
`cICP`:: Coding-independent code points (HDR)
|
|
745
|
+
|
|
746
|
+
==== Transparency chunks
|
|
747
|
+
|
|
748
|
+
`tRNS`:: Transparency information
|
|
749
|
+
`bKGD`:: Background color
|
|
750
|
+
|
|
751
|
+
==== Text chunks
|
|
752
|
+
|
|
753
|
+
`tEXt`:: Uncompressed Latin-1 text
|
|
754
|
+
`zTXt`:: Compressed Latin-1 text
|
|
755
|
+
`iTXt`:: International UTF-8 text
|
|
756
|
+
|
|
757
|
+
==== Physical dimensions
|
|
758
|
+
|
|
759
|
+
`pHYs`:: Physical pixel dimensions
|
|
760
|
+
`sCAL`:: Physical scale
|
|
761
|
+
|
|
762
|
+
==== Time stamp
|
|
763
|
+
|
|
764
|
+
`tIME`:: Last modification time
|
|
765
|
+
|
|
766
|
+
==== Other chunks
|
|
767
|
+
|
|
768
|
+
`hIST`:: Palette histogram
|
|
769
|
+
`sPLT`:: Suggested palette
|
|
770
|
+
`sBIT`:: Significant bits
|
|
771
|
+
`oFFs`:: Image offset
|
|
772
|
+
`pCAL`:: Pixel calibration
|
|
773
|
+
`sTER`:: Stereo image indicator
|
|
774
|
+
`mDCv`:: Mastering display color volume (HDR)
|
|
775
|
+
|
|
776
|
+
[[validation-profiles]]
|
|
777
|
+
== Validation Profiles
|
|
778
|
+
|
|
779
|
+
=== General
|
|
780
|
+
|
|
781
|
+
Profiles define conformance requirements for different use cases.
|
|
782
|
+
|
|
783
|
+
=== Available profiles
|
|
784
|
+
|
|
785
|
+
==== minimal
|
|
786
|
+
|
|
787
|
+
Basic PNG structure validation only.
|
|
788
|
+
|
|
789
|
+
* Required: IHDR, IDAT, IEND
|
|
790
|
+
* Optional: Any chunks
|
|
791
|
+
* Prohibited: None
|
|
792
|
+
|
|
793
|
+
==== web
|
|
794
|
+
|
|
795
|
+
Optimized for web display.
|
|
796
|
+
|
|
797
|
+
* Required: IHDR, IDAT, IEND, gAMA, sRGB
|
|
798
|
+
* Optional: tRNS, bKGD, tEXt, iTXt, zTXt
|
|
799
|
+
* Prohibited: None
|
|
800
|
+
|
|
801
|
+
==== print
|
|
802
|
+
|
|
803
|
+
For high-quality print reproduction.
|
|
804
|
+
|
|
805
|
+
* Required: IHDR, IDAT, IEND, iCCP
|
|
806
|
+
* Optional: gAMA, cHRM, tRNS, bKGD
|
|
807
|
+
* Prohibited: sRGB
|
|
808
|
+
|
|
809
|
+
==== archive
|
|
810
|
+
|
|
811
|
+
Long-term preservation requirements.
|
|
812
|
+
|
|
813
|
+
* Required: IHDR, IDAT, IEND, tIME
|
|
814
|
+
* Optional: Any chunks
|
|
815
|
+
* Prohibited: None
|
|
816
|
+
|
|
817
|
+
==== strict
|
|
818
|
+
|
|
819
|
+
Only standard chunks allowed.
|
|
820
|
+
|
|
821
|
+
* Required: IHDR, IDAT, IEND
|
|
822
|
+
* Optional: Critical chunks only
|
|
823
|
+
* Prohibited: Unknown chunks
|
|
824
|
+
|
|
825
|
+
==== default
|
|
826
|
+
|
|
827
|
+
Standard PNG validation.
|
|
828
|
+
|
|
829
|
+
* Required: IHDR, IDAT, IEND
|
|
830
|
+
* Optional: Any chunks
|
|
831
|
+
* Prohibited: None
|
|
832
|
+
|
|
833
|
+
[[development]]
|
|
834
|
+
== Development
|
|
835
|
+
|
|
836
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
|
837
|
+
`rake spec` to run the tests. You can also run `bin/console` for an interactive
|
|
838
|
+
prompt that will allow you to experiment.
|
|
839
|
+
|
|
840
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
|
841
|
+
|
|
842
|
+
== Contributing
|
|
843
|
+
|
|
844
|
+
Bug reports and pull requests are welcome on GitHub at
|
|
845
|
+
https://github.com/claricle/png_conform.
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
== Credits
|
|
849
|
+
|
|
850
|
+
PngConform is inspired by https://github.com/pnggroup/pngcheck/[pngcheck],
|
|
851
|
+
originally developed by Greg Roelofs and contributors, and now maintained by the
|
|
852
|
+
PNG Development Group.
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
== Copyright and license
|
|
856
|
+
|
|
857
|
+
Copyright Ribose.
|
|
858
|
+
|
|
859
|
+
The gem is available as open source under the Ribose BSD-2-Clause License.
|