png_conform 0.1.2 → 0.1.3
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_todo.yml +116 -6
- data/Gemfile +1 -1
- data/config/validation_profiles.yml +105 -0
- data/lib/png_conform/analyzers/comparison_analyzer.rb +41 -7
- data/lib/png_conform/analyzers/metrics_analyzer.rb +6 -9
- data/lib/png_conform/analyzers/optimization_analyzer.rb +30 -24
- data/lib/png_conform/analyzers/resolution_analyzer.rb +31 -32
- data/lib/png_conform/cli.rb +12 -0
- data/lib/png_conform/commands/check_command.rb +118 -53
- data/lib/png_conform/configuration.rb +147 -0
- data/lib/png_conform/container.rb +113 -0
- data/lib/png_conform/models/validation_result.rb +30 -4
- data/lib/png_conform/pipelines/pipeline_result.rb +39 -0
- data/lib/png_conform/pipelines/stages/analysis_stage.rb +35 -0
- data/lib/png_conform/pipelines/stages/base_stage.rb +23 -0
- data/lib/png_conform/pipelines/stages/chunk_validation_stage.rb +74 -0
- data/lib/png_conform/pipelines/stages/sequence_validation_stage.rb +77 -0
- data/lib/png_conform/pipelines/stages/signature_validation_stage.rb +41 -0
- data/lib/png_conform/pipelines/validation_pipeline.rb +90 -0
- data/lib/png_conform/readers/full_load_reader.rb +13 -4
- data/lib/png_conform/readers/streaming_reader.rb +27 -2
- data/lib/png_conform/reporters/color_reporter.rb +17 -14
- data/lib/png_conform/reporters/visual_elements.rb +22 -16
- data/lib/png_conform/services/analysis_manager.rb +120 -0
- data/lib/png_conform/services/chunk_processor.rb +195 -0
- data/lib/png_conform/services/file_signature.rb +226 -0
- data/lib/png_conform/services/file_strategy.rb +78 -0
- data/lib/png_conform/services/lru_cache.rb +170 -0
- data/lib/png_conform/services/parallel_validator.rb +118 -0
- data/lib/png_conform/services/profile_manager.rb +41 -12
- data/lib/png_conform/services/result_builder.rb +299 -0
- data/lib/png_conform/services/validation_cache.rb +210 -0
- data/lib/png_conform/services/validation_orchestrator.rb +188 -0
- data/lib/png_conform/services/validation_service.rb +53 -337
- data/lib/png_conform/services/validator_pool.rb +142 -0
- data/lib/png_conform/utils/colorizer.rb +149 -0
- data/lib/png_conform/validators/chunk_registry.rb +12 -0
- data/lib/png_conform/validators/streaming_idat_validator.rb +123 -0
- data/lib/png_conform/version.rb +1 -1
- data/png_conform.gemspec +1 -0
- metadata +38 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8bcefecf4f13700db28e89f3ec81ad93df86ef0d5774057a3366ef9c74121169
|
|
4
|
+
data.tar.gz: bfd053fc3545d3ede27936476e8fce4d483c85e227d5eb5824e9faaa4f7c8872
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8b16cfe85d3e17edc7dcdd5c4a7b1b9e416fd91be3825ac758bb2f95de19015cacc3c8c3301aabbac7174dc651e180930862b026a42968959539d4e1520857b6
|
|
7
|
+
data.tar.gz: c966fa4713fc24c7a4439e41ada2382814395d7be7bbe2e782fb388bf59d3f723ee5d36db5cac431c078c02b1dbdd9f517dbfa341ee0132ffee8403208185a69
|
data/.rubocop_todo.yml
CHANGED
|
@@ -1,18 +1,93 @@
|
|
|
1
1
|
# This configuration was generated by
|
|
2
2
|
# `rubocop --auto-gen-config`
|
|
3
|
-
# on
|
|
3
|
+
# on 2026-01-19 08:52:09 UTC using RuboCop version 1.81.1.
|
|
4
4
|
# The point is for the user to remove these configuration records
|
|
5
5
|
# one by one as the offenses are removed from the code base.
|
|
6
6
|
# Note that changes in the inspected code, or installation of new
|
|
7
7
|
# versions of RuboCop, may require this file to be generated again.
|
|
8
8
|
|
|
9
|
-
# Offense count:
|
|
9
|
+
# Offense count: 6
|
|
10
|
+
# This cop supports safe autocorrection (--autocorrect).
|
|
11
|
+
# Configuration parameters: EnforcedStyle, IndentationWidth.
|
|
12
|
+
# SupportedStyles: with_first_argument, with_fixed_indentation
|
|
13
|
+
Layout/ArgumentAlignment:
|
|
14
|
+
Exclude:
|
|
15
|
+
- 'lib/png_conform/commands/check_command.rb'
|
|
16
|
+
- 'lib/png_conform/services/chunk_processor.rb'
|
|
17
|
+
- 'lib/png_conform/services/profile_manager.rb'
|
|
18
|
+
- 'lib/png_conform/services/validator_pool.rb'
|
|
19
|
+
|
|
20
|
+
# Offense count: 4
|
|
21
|
+
# This cop supports safe autocorrection (--autocorrect).
|
|
22
|
+
# Configuration parameters: EnforcedStyleAlignWith.
|
|
23
|
+
# SupportedStylesAlignWith: either, start_of_block, start_of_line
|
|
24
|
+
Layout/BlockAlignment:
|
|
25
|
+
Exclude:
|
|
26
|
+
- 'lib/png_conform/analyzers/comparison_analyzer.rb'
|
|
27
|
+
- 'lib/png_conform/analyzers/optimization_analyzer.rb'
|
|
28
|
+
- 'lib/png_conform/services/file_signature.rb'
|
|
29
|
+
- 'lib/png_conform/services/lru_cache.rb'
|
|
30
|
+
|
|
31
|
+
# Offense count: 4
|
|
32
|
+
# This cop supports safe autocorrection (--autocorrect).
|
|
33
|
+
Layout/BlockEndNewline:
|
|
34
|
+
Exclude:
|
|
35
|
+
- 'lib/png_conform/analyzers/comparison_analyzer.rb'
|
|
36
|
+
- 'lib/png_conform/analyzers/optimization_analyzer.rb'
|
|
37
|
+
- 'lib/png_conform/services/file_signature.rb'
|
|
38
|
+
- 'lib/png_conform/services/lru_cache.rb'
|
|
39
|
+
|
|
40
|
+
# Offense count: 1
|
|
41
|
+
# This cop supports safe autocorrection (--autocorrect).
|
|
42
|
+
Layout/ElseAlignment:
|
|
43
|
+
Exclude:
|
|
44
|
+
- 'lib/png_conform/services/validation_cache.rb'
|
|
45
|
+
|
|
46
|
+
# Offense count: 1
|
|
47
|
+
# This cop supports safe autocorrection (--autocorrect).
|
|
48
|
+
# Configuration parameters: EnforcedStyleAlignWith, Severity.
|
|
49
|
+
# SupportedStylesAlignWith: keyword, variable, start_of_line
|
|
50
|
+
Layout/EndAlignment:
|
|
51
|
+
Exclude:
|
|
52
|
+
- 'lib/png_conform/services/validation_cache.rb'
|
|
53
|
+
|
|
54
|
+
# Offense count: 2
|
|
55
|
+
# This cop supports safe autocorrection (--autocorrect).
|
|
56
|
+
# Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle.
|
|
57
|
+
# SupportedHashRocketStyles: key, separator, table
|
|
58
|
+
# SupportedColonStyles: key, separator, table
|
|
59
|
+
# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit
|
|
60
|
+
Layout/HashAlignment:
|
|
61
|
+
Exclude:
|
|
62
|
+
- 'lib/png_conform/services/validator_pool.rb'
|
|
63
|
+
|
|
64
|
+
# Offense count: 10
|
|
65
|
+
# This cop supports safe autocorrection (--autocorrect).
|
|
66
|
+
# Configuration parameters: Width, AllowedPatterns.
|
|
67
|
+
Layout/IndentationWidth:
|
|
68
|
+
Exclude:
|
|
69
|
+
- 'lib/png_conform/analyzers/comparison_analyzer.rb'
|
|
70
|
+
- 'lib/png_conform/analyzers/optimization_analyzer.rb'
|
|
71
|
+
- 'lib/png_conform/services/file_signature.rb'
|
|
72
|
+
- 'lib/png_conform/services/lru_cache.rb'
|
|
73
|
+
- 'lib/png_conform/services/validation_cache.rb'
|
|
74
|
+
|
|
75
|
+
# Offense count: 267
|
|
10
76
|
# This cop supports safe autocorrection (--autocorrect).
|
|
11
77
|
# Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings.
|
|
12
78
|
# URISchemes: http, https
|
|
13
79
|
Layout/LineLength:
|
|
14
80
|
Enabled: false
|
|
15
81
|
|
|
82
|
+
# Offense count: 3
|
|
83
|
+
# This cop supports safe autocorrection (--autocorrect).
|
|
84
|
+
# Configuration parameters: AllowInHeredoc.
|
|
85
|
+
Layout/TrailingWhitespace:
|
|
86
|
+
Exclude:
|
|
87
|
+
- 'lib/png_conform/commands/check_command.rb'
|
|
88
|
+
- 'lib/png_conform/services/profile_manager.rb'
|
|
89
|
+
- 'lib/png_conform/services/validator_pool.rb'
|
|
90
|
+
|
|
16
91
|
# Offense count: 1
|
|
17
92
|
Lint/BinaryOperatorWithIdenticalOperands:
|
|
18
93
|
Exclude:
|
|
@@ -27,7 +102,16 @@ Lint/DuplicateBranch:
|
|
|
27
102
|
- 'lib/png_conform/validators/ancillary/sbit_validator.rb'
|
|
28
103
|
- 'spec/pngsuite/helpers/semantic_validator.rb'
|
|
29
104
|
|
|
30
|
-
# Offense count:
|
|
105
|
+
# Offense count: 4
|
|
106
|
+
# Configuration parameters: AllowedParentClasses.
|
|
107
|
+
Lint/MissingSuper:
|
|
108
|
+
Exclude:
|
|
109
|
+
- 'lib/png_conform/pipelines/stages/analysis_stage.rb'
|
|
110
|
+
- 'lib/png_conform/pipelines/stages/chunk_validation_stage.rb'
|
|
111
|
+
- 'lib/png_conform/pipelines/stages/sequence_validation_stage.rb'
|
|
112
|
+
- 'lib/png_conform/pipelines/stages/signature_validation_stage.rb'
|
|
113
|
+
|
|
114
|
+
# Offense count: 124
|
|
31
115
|
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
|
|
32
116
|
Metrics/AbcSize:
|
|
33
117
|
Enabled: false
|
|
@@ -36,14 +120,14 @@ Metrics/AbcSize:
|
|
|
36
120
|
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode.
|
|
37
121
|
# AllowedMethods: refine
|
|
38
122
|
Metrics/BlockLength:
|
|
39
|
-
Max:
|
|
123
|
+
Max: 52
|
|
40
124
|
|
|
41
|
-
# Offense count:
|
|
125
|
+
# Offense count: 62
|
|
42
126
|
# Configuration parameters: AllowedMethods, AllowedPatterns, Max.
|
|
43
127
|
Metrics/CyclomaticComplexity:
|
|
44
128
|
Enabled: false
|
|
45
129
|
|
|
46
|
-
# Offense count:
|
|
130
|
+
# Offense count: 193
|
|
47
131
|
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
|
|
48
132
|
Metrics/MethodLength:
|
|
49
133
|
Max: 66
|
|
@@ -114,6 +198,20 @@ RSpec/SpecFilePathFormat:
|
|
|
114
198
|
RSpec/VerifiedDoubles:
|
|
115
199
|
Enabled: false
|
|
116
200
|
|
|
201
|
+
# Offense count: 4
|
|
202
|
+
# This cop supports safe autocorrection (--autocorrect).
|
|
203
|
+
# Configuration parameters: EnforcedStyle, ProceduralMethods, FunctionalMethods, AllowedMethods, AllowedPatterns, AllowBracesOnProceduralOneLiners, BracesRequiredMethods.
|
|
204
|
+
# SupportedStyles: line_count_based, semantic, braces_for_chaining, always_braces
|
|
205
|
+
# ProceduralMethods: benchmark, bm, bmbm, create, each_with_object, measure, new, realtime, tap, with_object
|
|
206
|
+
# FunctionalMethods: let, let!, subject, watch
|
|
207
|
+
# AllowedMethods: lambda, proc, it
|
|
208
|
+
Style/BlockDelimiters:
|
|
209
|
+
Exclude:
|
|
210
|
+
- 'lib/png_conform/analyzers/comparison_analyzer.rb'
|
|
211
|
+
- 'lib/png_conform/analyzers/optimization_analyzer.rb'
|
|
212
|
+
- 'lib/png_conform/services/file_signature.rb'
|
|
213
|
+
- 'lib/png_conform/services/lru_cache.rb'
|
|
214
|
+
|
|
117
215
|
# Offense count: 41
|
|
118
216
|
# This cop supports safe autocorrection (--autocorrect).
|
|
119
217
|
# Configuration parameters: MaxUnannotatedPlaceholdersAllowed, Mode, AllowedMethods, AllowedPatterns.
|
|
@@ -125,3 +223,15 @@ Style/FormatStringToken:
|
|
|
125
223
|
Style/MissingRespondToMissing:
|
|
126
224
|
Exclude:
|
|
127
225
|
- 'lib/png_conform/cli.rb'
|
|
226
|
+
|
|
227
|
+
# Offense count: 1
|
|
228
|
+
# This cop supports safe autocorrection (--autocorrect).
|
|
229
|
+
Style/MultilineTernaryOperator:
|
|
230
|
+
Exclude:
|
|
231
|
+
- 'lib/png_conform/services/validation_cache.rb'
|
|
232
|
+
|
|
233
|
+
# Offense count: 2
|
|
234
|
+
# This cop supports safe autocorrection (--autocorrect).
|
|
235
|
+
Style/RedundantParentheses:
|
|
236
|
+
Exclude:
|
|
237
|
+
- 'lib/png_conform/services/validation_cache.rb'
|
data/Gemfile
CHANGED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# PNG Conform Validation Profiles
|
|
2
|
+
#
|
|
3
|
+
# This file defines validation profiles for PNG files. Each profile specifies
|
|
4
|
+
# which chunks are required, optional, or prohibited.
|
|
5
|
+
#
|
|
6
|
+
# Profile structure:
|
|
7
|
+
# name: Display name
|
|
8
|
+
# description: Human-readable description
|
|
9
|
+
# required_chunks: List of chunk types that MUST be present
|
|
10
|
+
# optional_chunks: List of chunk types that MAY be present (use "*" for all)
|
|
11
|
+
# prohibited_chunks: List of chunk types that MUST NOT be present
|
|
12
|
+
#
|
|
13
|
+
|
|
14
|
+
minimal:
|
|
15
|
+
name: "Minimal validation"
|
|
16
|
+
description: "Only critical chunks required"
|
|
17
|
+
required_chunks:
|
|
18
|
+
- IHDR
|
|
19
|
+
- IDAT
|
|
20
|
+
- IEND
|
|
21
|
+
optional_chunks: "*"
|
|
22
|
+
prohibited_chunks: []
|
|
23
|
+
|
|
24
|
+
web:
|
|
25
|
+
name: "Web-optimized"
|
|
26
|
+
description: "Recommended for web images"
|
|
27
|
+
required_chunks:
|
|
28
|
+
- IHDR
|
|
29
|
+
- IDAT
|
|
30
|
+
- IEND
|
|
31
|
+
- gAMA
|
|
32
|
+
- sRGB
|
|
33
|
+
optional_chunks:
|
|
34
|
+
- tRNS
|
|
35
|
+
- bKGD
|
|
36
|
+
- tEXt
|
|
37
|
+
- iTXt
|
|
38
|
+
- zTXt
|
|
39
|
+
- pHYs
|
|
40
|
+
prohibited_chunks: []
|
|
41
|
+
|
|
42
|
+
print:
|
|
43
|
+
name: "Print-ready"
|
|
44
|
+
description: "For high-quality print output"
|
|
45
|
+
required_chunks:
|
|
46
|
+
- IHDR
|
|
47
|
+
- IDAT
|
|
48
|
+
- IEND
|
|
49
|
+
- pHYs
|
|
50
|
+
optional_chunks:
|
|
51
|
+
- gAMA
|
|
52
|
+
- cHRM
|
|
53
|
+
- sRGB
|
|
54
|
+
- iCCP
|
|
55
|
+
- tRNS
|
|
56
|
+
- bKGD
|
|
57
|
+
prohibited_chunks: []
|
|
58
|
+
|
|
59
|
+
archive:
|
|
60
|
+
name: "Archive quality"
|
|
61
|
+
description: "For long-term image storage"
|
|
62
|
+
required_chunks:
|
|
63
|
+
- IHDR
|
|
64
|
+
- IDAT
|
|
65
|
+
- IEND
|
|
66
|
+
optional_chunks:
|
|
67
|
+
- gAMA
|
|
68
|
+
- cHRM
|
|
69
|
+
- sRGB
|
|
70
|
+
- iCCP
|
|
71
|
+
- tEXt
|
|
72
|
+
- iTXt
|
|
73
|
+
- zTXt
|
|
74
|
+
- tIME
|
|
75
|
+
prohibited_chunks: []
|
|
76
|
+
|
|
77
|
+
strict:
|
|
78
|
+
name: "Strict PNG specification"
|
|
79
|
+
description: "Full compliance with PNG specification"
|
|
80
|
+
required_chunks:
|
|
81
|
+
- IHDR
|
|
82
|
+
- IDAT
|
|
83
|
+
- IEND
|
|
84
|
+
optional_chunks:
|
|
85
|
+
- PLTE
|
|
86
|
+
- gAMA
|
|
87
|
+
- cHRM
|
|
88
|
+
- sRGB
|
|
89
|
+
- iCCP
|
|
90
|
+
- tRNS
|
|
91
|
+
- bKGD
|
|
92
|
+
- hIST
|
|
93
|
+
- tEXt
|
|
94
|
+
- zTXt
|
|
95
|
+
- iTXt
|
|
96
|
+
- pHYs
|
|
97
|
+
- sPLT
|
|
98
|
+
- sBIT
|
|
99
|
+
- oFFs
|
|
100
|
+
- pCAL
|
|
101
|
+
- sCAL
|
|
102
|
+
- tIME
|
|
103
|
+
- cICP
|
|
104
|
+
- mDCv
|
|
105
|
+
prohibited_chunks: []
|
|
@@ -1,18 +1,51 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../configuration"
|
|
4
|
+
require_relative "../services/file_signature"
|
|
5
|
+
|
|
3
6
|
module PngConform
|
|
4
7
|
module Analyzers
|
|
5
8
|
# Compares two PNG files and reports differences
|
|
6
9
|
class ComparisonAnalyzer
|
|
7
|
-
|
|
8
|
-
METADATA_CHUNKS = %w[tEXt zTXt iTXt tIME].freeze
|
|
9
|
-
|
|
10
|
-
def initialize(result1, result2)
|
|
10
|
+
def initialize(result1, result2, config: Configuration.instance)
|
|
11
11
|
@result1 = result1
|
|
12
12
|
@result2 = result2
|
|
13
|
+
@config = config
|
|
14
|
+
|
|
15
|
+
# Fast path: compute signatures for quick equality check
|
|
16
|
+
@sig1 = Services::FileSignature.from_result(result1).compute_signature
|
|
17
|
+
@sig2 = Services::FileSignature.from_result(result2).compute_signature
|
|
13
18
|
end
|
|
14
19
|
|
|
15
20
|
def analyze
|
|
21
|
+
# Fast return if signatures are identical
|
|
22
|
+
return identical_result if @sig1 == @sig2
|
|
23
|
+
|
|
24
|
+
# Full comparison for different files
|
|
25
|
+
full_comparison
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
# Return result for identical files
|
|
31
|
+
#
|
|
32
|
+
# @return [Hash] Analysis result for identical files
|
|
33
|
+
def identical_result
|
|
34
|
+
{
|
|
35
|
+
files: {
|
|
36
|
+
file1: @result1.filename,
|
|
37
|
+
file2: @result2.filename,
|
|
38
|
+
identical: true,
|
|
39
|
+
signature: @sig1.short_signature,
|
|
40
|
+
},
|
|
41
|
+
summary: ["Files are binary identical"],
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Full comparison for different files
|
|
46
|
+
#
|
|
47
|
+
# @return [Hash] Complete comparison analysis
|
|
48
|
+
def full_comparison
|
|
16
49
|
{
|
|
17
50
|
files: file_comparison,
|
|
18
51
|
image: image_comparison,
|
|
@@ -23,8 +56,6 @@ module PngConform
|
|
|
23
56
|
}
|
|
24
57
|
end
|
|
25
58
|
|
|
26
|
-
private
|
|
27
|
-
|
|
28
59
|
def file_comparison
|
|
29
60
|
size1 = @result1.file_size
|
|
30
61
|
size2 = @result2.file_size
|
|
@@ -166,7 +197,10 @@ module PngConform
|
|
|
166
197
|
end
|
|
167
198
|
|
|
168
199
|
def metadata_count(result)
|
|
169
|
-
|
|
200
|
+
# Use only text and time chunks from metadata (excluding pHYs which is physical)
|
|
201
|
+
result.chunks.count do |c|
|
|
202
|
+
@config.text_chunks.include?(c.type) || c.type == "tIME"
|
|
203
|
+
end
|
|
170
204
|
end
|
|
171
205
|
|
|
172
206
|
def format_size_change(diff, percent)
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../configuration"
|
|
4
|
+
|
|
3
5
|
module PngConform
|
|
4
6
|
module Analyzers
|
|
5
7
|
# Generates comprehensive metrics for CI/CD and automation
|
|
6
8
|
class MetricsAnalyzer
|
|
7
|
-
|
|
8
|
-
TEXT_CHUNKS = %w[tEXt zTXt iTXt].freeze
|
|
9
|
-
|
|
10
|
-
# Metadata chunk types including time
|
|
11
|
-
METADATA_CHUNKS = %w[tEXt zTXt iTXt tIME].freeze
|
|
12
|
-
|
|
13
|
-
def initialize(result)
|
|
9
|
+
def initialize(result, config: Configuration.instance)
|
|
14
10
|
@result = result
|
|
11
|
+
@config = config
|
|
15
12
|
ihdr = result.ihdr_chunk
|
|
16
13
|
@width = ihdr ? get_width(ihdr) : 0
|
|
17
14
|
@height = ihdr ? get_height(ihdr) : 0
|
|
@@ -149,10 +146,10 @@ module PngConform
|
|
|
149
146
|
has_iccp: @result.has_chunk?("iCCP"),
|
|
150
147
|
has_transparency: @result.has_chunk?("tRNS"),
|
|
151
148
|
has_metadata: @result.chunks.any? do |c|
|
|
152
|
-
|
|
149
|
+
@config.text_chunks.include?(c.type)
|
|
153
150
|
end,
|
|
154
151
|
metadata_chunks_count: @result.chunks.count do |c|
|
|
155
|
-
|
|
152
|
+
@config.metadata_chunks.include?(c.type)
|
|
156
153
|
end,
|
|
157
154
|
bytes_per_pixel: calculate_bytes_per_pixel,
|
|
158
155
|
}
|
|
@@ -1,20 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../configuration"
|
|
4
|
+
|
|
3
5
|
module PngConform
|
|
4
6
|
module Analyzers
|
|
5
7
|
# Analyzes PNG files for optimization opportunities
|
|
6
8
|
class OptimizationAnalyzer
|
|
7
|
-
|
|
8
|
-
UNNECESSARY_FOR_WEB = %w[tIME pHYs oFFs pCAL sCAL sTER].freeze
|
|
9
|
-
|
|
10
|
-
# Text chunk types
|
|
11
|
-
TEXT_CHUNKS = %w[tEXt zTXt iTXt].freeze
|
|
12
|
-
|
|
13
|
-
# Metadata chunk types
|
|
14
|
-
METADATA_CHUNKS = %w[tEXt zTXt iTXt tIME pHYs].freeze
|
|
15
|
-
|
|
16
|
-
def initialize(result)
|
|
9
|
+
def initialize(result, config: Configuration.instance)
|
|
17
10
|
@result = result
|
|
11
|
+
@config = config
|
|
18
12
|
@suggestions = []
|
|
19
13
|
end
|
|
20
14
|
|
|
@@ -37,7 +31,7 @@ module PngConform
|
|
|
37
31
|
|
|
38
32
|
def check_unnecessary_chunks
|
|
39
33
|
unnecessary = @result.chunks.select do |c|
|
|
40
|
-
|
|
34
|
+
@config.unnecessary_web_chunks.include?(c.type)
|
|
41
35
|
end
|
|
42
36
|
return if unnecessary.empty?
|
|
43
37
|
|
|
@@ -61,14 +55,17 @@ module PngConform
|
|
|
61
55
|
# Estimate if 8-bit would be sufficient
|
|
62
56
|
if could_use_8_bit?
|
|
63
57
|
current_size = @result.file_size
|
|
64
|
-
estimated_savings = (
|
|
58
|
+
estimated_savings = (
|
|
59
|
+
current_size *
|
|
60
|
+
@config.optimization_percentages[:bit_depth_reduction] / 100.0
|
|
61
|
+
).to_i
|
|
65
62
|
|
|
66
63
|
@suggestions << {
|
|
67
64
|
type: :reduce_bit_depth,
|
|
68
65
|
priority: :high,
|
|
69
66
|
savings_bytes: estimated_savings,
|
|
70
67
|
description: "Convert from 16-bit to 8-bit depth " \
|
|
71
|
-
"(estimated
|
|
68
|
+
"(estimated ~#{@config.optimization_percentages[:bit_depth_reduction]}% file size reduction)",
|
|
72
69
|
current: "16-bit",
|
|
73
70
|
recommended: "8-bit",
|
|
74
71
|
}
|
|
@@ -79,15 +76,18 @@ module PngConform
|
|
|
79
76
|
# Get color type from IHDR
|
|
80
77
|
ihdr = @result.ihdr_chunk
|
|
81
78
|
return unless ihdr && get_color_type(ihdr) == 2 # RGB
|
|
82
|
-
return if @result.file_size <
|
|
79
|
+
return if @result.file_size < @config.size_thresholds[:palette_opportunity]
|
|
83
80
|
|
|
84
81
|
# If it's RGB but could be palette
|
|
85
82
|
@suggestions << {
|
|
86
83
|
type: :convert_to_palette,
|
|
87
84
|
priority: :medium,
|
|
88
|
-
savings_bytes: (
|
|
85
|
+
savings_bytes: (
|
|
86
|
+
@result.file_size *
|
|
87
|
+
@config.optimization_percentages[:palette_conversion] / 100.0
|
|
88
|
+
).to_i,
|
|
89
89
|
description: "Consider converting to palette mode if using limited colors " \
|
|
90
|
-
"(potential
|
|
90
|
+
"(potential ~#{@config.optimization_percentages[:palette_conversion]}% reduction)",
|
|
91
91
|
current: "RGB (Truecolor)",
|
|
92
92
|
recommended: "Indexed (Palette)",
|
|
93
93
|
}
|
|
@@ -99,25 +99,30 @@ module PngConform
|
|
|
99
99
|
return unless ihdr && get_interlace_method(ihdr) == 1
|
|
100
100
|
|
|
101
101
|
# Interlaced PNGs are larger
|
|
102
|
-
savings = (
|
|
102
|
+
savings = (
|
|
103
|
+
@result.file_size *
|
|
104
|
+
@config.optimization_percentages[:interlace_removal] / 100.0
|
|
105
|
+
).to_i
|
|
103
106
|
|
|
104
107
|
@suggestions << {
|
|
105
108
|
type: :remove_interlacing,
|
|
106
109
|
priority: :low,
|
|
107
110
|
savings_bytes: savings,
|
|
108
111
|
description: "Remove interlacing for smaller file size " \
|
|
109
|
-
"(
|
|
112
|
+
"(~#{@config.optimization_percentages[:interlace_removal]}% reduction, but slower initial display)",
|
|
110
113
|
current: "Adam7 interlaced",
|
|
111
114
|
recommended: "Non-interlaced",
|
|
112
115
|
}
|
|
113
116
|
end
|
|
114
117
|
|
|
115
118
|
def check_text_chunks
|
|
116
|
-
text_chunks = @result.chunks.select
|
|
119
|
+
text_chunks = @result.chunks.select do |c|
|
|
120
|
+
@config.text_chunks.include?(c.type)
|
|
121
|
+
end
|
|
117
122
|
return if text_chunks.empty?
|
|
118
123
|
|
|
119
124
|
total_text_size = text_chunks.sum { |c| c.length + 12 }
|
|
120
|
-
return if total_text_size <
|
|
125
|
+
return if total_text_size < @config.size_thresholds[:text_metadata]
|
|
121
126
|
|
|
122
127
|
@suggestions << {
|
|
123
128
|
type: :reduce_metadata,
|
|
@@ -131,14 +136,15 @@ module PngConform
|
|
|
131
136
|
|
|
132
137
|
def check_metadata_size
|
|
133
138
|
metadata_chunks = @result.chunks.select do |c|
|
|
134
|
-
|
|
139
|
+
@config.metadata_chunks.include?(c.type)
|
|
135
140
|
end
|
|
136
141
|
|
|
137
142
|
total_metadata = metadata_chunks.sum { |c| c.length + 12 }
|
|
138
143
|
file_size = @result.file_size
|
|
139
144
|
|
|
140
|
-
# If metadata is more than
|
|
141
|
-
|
|
145
|
+
# If metadata is more than threshold percent of file size
|
|
146
|
+
threshold = @config.optimization_percentages[:metadata_threshold]
|
|
147
|
+
return unless total_metadata > file_size * threshold / 100.0
|
|
142
148
|
|
|
143
149
|
@suggestions << {
|
|
144
150
|
type: :excessive_metadata,
|
|
@@ -173,7 +179,7 @@ module PngConform
|
|
|
173
179
|
def could_use_8_bit?
|
|
174
180
|
# Conservative heuristic: suggest 8-bit for smaller files
|
|
175
181
|
# Without pixel analysis, we're conservative
|
|
176
|
-
@result.file_size <
|
|
182
|
+
@result.file_size < @config.size_thresholds[:small_file]
|
|
177
183
|
end
|
|
178
184
|
|
|
179
185
|
def calculate_total_savings
|
|
@@ -1,22 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../configuration"
|
|
4
|
+
|
|
3
5
|
module PngConform
|
|
4
6
|
module Analyzers
|
|
5
7
|
# Analyzes PNG resolution and DPI for various use cases
|
|
6
8
|
class ResolutionAnalyzer
|
|
7
|
-
|
|
8
|
-
SCREEN_DPI = 72
|
|
9
|
-
PRINT_DPI_LOW = 150
|
|
10
|
-
PRINT_DPI_STANDARD = 300
|
|
11
|
-
PRINT_DPI_HIGH = 600
|
|
12
|
-
|
|
13
|
-
# Retina display densities
|
|
14
|
-
RETINA_1X = 1.0
|
|
15
|
-
RETINA_2X = 2.0
|
|
16
|
-
RETINA_3X = 3.0
|
|
17
|
-
|
|
18
|
-
def initialize(result)
|
|
9
|
+
def initialize(result, config: Configuration.instance)
|
|
19
10
|
@result = result
|
|
11
|
+
@config = config
|
|
20
12
|
ihdr = result.ihdr_chunk
|
|
21
13
|
@width = ihdr ? get_width(ihdr) : 0
|
|
22
14
|
@height = ihdr ? get_height(ihdr) : 0
|
|
@@ -63,9 +55,9 @@ module PngConform
|
|
|
63
55
|
def retina_analysis
|
|
64
56
|
analysis = {
|
|
65
57
|
is_retina_ready: check_retina_ready,
|
|
66
|
-
at_1x: calculate_physical_size(
|
|
67
|
-
at_2x: calculate_physical_size(
|
|
68
|
-
at_3x: calculate_physical_size(
|
|
58
|
+
at_1x: calculate_physical_size(@config.retina_scalers["1x"]),
|
|
59
|
+
at_2x: calculate_physical_size(@config.retina_scalers["2x"]),
|
|
60
|
+
at_3x: calculate_physical_size(@config.retina_scalers["3x"]),
|
|
69
61
|
}
|
|
70
62
|
|
|
71
63
|
analysis[:recommended_density] = recommend_density
|
|
@@ -82,7 +74,7 @@ module PngConform
|
|
|
82
74
|
height_inches = @height.to_f / @dpi
|
|
83
75
|
|
|
84
76
|
{
|
|
85
|
-
capable: @dpi >=
|
|
77
|
+
capable: @dpi >= @config.print_dpi_thresholds[:minimum],
|
|
86
78
|
dpi: @dpi,
|
|
87
79
|
physical_size: {
|
|
88
80
|
width_inches: width_inches.round(2),
|
|
@@ -97,9 +89,11 @@ module PngConform
|
|
|
97
89
|
|
|
98
90
|
def web_analysis
|
|
99
91
|
{
|
|
100
|
-
suitable_for_web: @width <=
|
|
92
|
+
suitable_for_web: @width <= @config.size_thresholds[:large_for_web] &&
|
|
93
|
+
@height <= @config.size_thresholds[:large_for_web],
|
|
101
94
|
typical_screen_size: calculate_screen_coverage,
|
|
102
|
-
mobile_friendly: @width <=
|
|
95
|
+
mobile_friendly: @width <= @config.size_thresholds[:large_for_mobile] &&
|
|
96
|
+
@height <= @config.size_thresholds[:large_for_mobile],
|
|
103
97
|
retina_optimized: @width >= 1000 && @height >= 1000,
|
|
104
98
|
load_time_estimate: estimate_load_time,
|
|
105
99
|
}
|
|
@@ -119,15 +113,15 @@ module PngConform
|
|
|
119
113
|
end
|
|
120
114
|
|
|
121
115
|
def check_retina_ready
|
|
122
|
-
@width >=
|
|
116
|
+
@width >= @config.size_thresholds[:retina_ready_min] &&
|
|
117
|
+
@height >= @config.size_thresholds[:retina_ready_min]
|
|
123
118
|
end
|
|
124
119
|
|
|
125
120
|
def calculate_physical_size(density)
|
|
126
|
-
|
|
127
|
-
effective_dpi = css_reference_dpi * density
|
|
121
|
+
effective_dpi = @config.css_reference_dpi * density
|
|
128
122
|
|
|
129
|
-
width_points = (@width.to_f / effective_dpi *
|
|
130
|
-
height_points = (@height.to_f / effective_dpi *
|
|
123
|
+
width_points = (@width.to_f / effective_dpi * @config.screen_dpi).round(1)
|
|
124
|
+
height_points = (@height.to_f / effective_dpi * @config.screen_dpi).round(1)
|
|
131
125
|
|
|
132
126
|
{
|
|
133
127
|
width_points: width_points,
|
|
@@ -181,15 +175,19 @@ module PngConform
|
|
|
181
175
|
return "Unknown" unless @dpi
|
|
182
176
|
|
|
183
177
|
case @dpi
|
|
184
|
-
when 0
|
|
185
|
-
|
|
186
|
-
when
|
|
187
|
-
|
|
178
|
+
when 0...@config.print_dpi_thresholds[:minimum]
|
|
179
|
+
"Not suitable"
|
|
180
|
+
when @config.print_dpi_thresholds[:minimum]...@config.print_dpi_thresholds[:good]
|
|
181
|
+
"Acceptable"
|
|
182
|
+
when @config.print_dpi_thresholds[:good]...@config.print_dpi_thresholds[:excellent]
|
|
183
|
+
"Good"
|
|
184
|
+
else
|
|
185
|
+
"Excellent"
|
|
188
186
|
end
|
|
189
187
|
end
|
|
190
188
|
|
|
191
189
|
def suitable_print_sizes
|
|
192
|
-
return [] unless @dpi && @dpi >=
|
|
190
|
+
return [] unless @dpi && @dpi >= @config.print_dpi_thresholds[:minimum]
|
|
193
191
|
|
|
194
192
|
width_in = @width.to_f / @dpi
|
|
195
193
|
height_in = @height.to_f / @dpi
|
|
@@ -227,8 +225,7 @@ module PngConform
|
|
|
227
225
|
|
|
228
226
|
def estimate_load_time
|
|
229
227
|
file_size = @result.file_size
|
|
230
|
-
|
|
231
|
-
bytes_per_second = (mbps * 1_000_000 / 8).to_i
|
|
228
|
+
bytes_per_second = @config.network_speeds[:fast]
|
|
232
229
|
seconds = file_size.to_f / bytes_per_second
|
|
233
230
|
|
|
234
231
|
if seconds < 0.1
|
|
@@ -243,7 +240,8 @@ module PngConform
|
|
|
243
240
|
def generate_recommendations
|
|
244
241
|
recs = []
|
|
245
242
|
|
|
246
|
-
if @width <
|
|
243
|
+
if @width < @config.size_thresholds[:retina_ready_min] &&
|
|
244
|
+
@height < @config.size_thresholds[:retina_ready_min]
|
|
247
245
|
recs << {
|
|
248
246
|
category: :retina,
|
|
249
247
|
priority: :high,
|
|
@@ -259,7 +257,8 @@ module PngConform
|
|
|
259
257
|
}
|
|
260
258
|
end
|
|
261
259
|
|
|
262
|
-
if @width >
|
|
260
|
+
if @width > @config.size_thresholds[:very_large] ||
|
|
261
|
+
@height > @config.size_thresholds[:very_large]
|
|
263
262
|
recs << {
|
|
264
263
|
category: :web,
|
|
265
264
|
priority: :high,
|