jekyll-imgflow 0.1.5 → 0.1.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 41492bf948a18c7820bd6251596966353e63411cc891f2808671cb15d8536d90
4
- data.tar.gz: 838d154fcc3bd85b9e77d15e62a11cf0b14b84fea4d18504ff131221a62d57a9
3
+ metadata.gz: 4a8e1eb8739d53b1ebcdbc440d4c77279ce895794ffb1beda8de9df159227d91
4
+ data.tar.gz: 730145b3e3017a1a63281f7e68c518cdc82cce708e55d942cdabdba002900d7c
5
5
  SHA512:
6
- metadata.gz: 02a8519c3b0dd50f28c71989ba60925c1aa091529d2a10ca30dff688abf0713170601a5c90ad8595da4917ca86677b592d4af3f4d17b729e55d472b719b7d87f
7
- data.tar.gz: fa306b6f147c4ee45cc0c92fa0fe586f9630a0ca1d6f11515309d91461860b9b4841a0cd43fdeaefb467fbf798c8b8cfb7760ba1b847ada3ebf970ad4136183f
6
+ metadata.gz: 23a45cd5749d4c389b65ffdbd54ca3240465441a325861d739acfedcaf342bd3e111eb04165b925d790f8b862a79d40c9bcbe85979181b184ea7bc3253065e00
7
+ data.tar.gz: 9b71032b277287766586518ef618cde9aa0d787f2ed3e7b061b609685f68221a46a7d5a09b53e8a4907512bf11611028a9b8add638f37477b08b607e9f38aa89
data/README.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  A [Jekyll](https://jekyllrb.com/) plugin for automatic image optimization with multiple providers and formats.
4
4
 
5
+ [![DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/gundestrup/jekyll-imgflow)
6
+ [![Status](https://img.shields.io/badge/status-active-success)](https://github.com/gundestrup/jekyll-imgflow)
7
+ [![codecov](https://codecov.io/gh/gundestrup/jekyll-imgflow/branch/main/graph/badge.svg)](https://codecov.io/gh/gundestrup/jekyll-imgflow)
8
+ [![License](https://img.shields.io/github/license/gundestrup/jekyll-imgflow)](LICENSE)
9
+
5
10
  ## 🚀 Quick Start
6
11
 
7
12
  ```bash
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JekyllImgFlow
4
+ # Detects whether a GIF file is animated (contains multiple frames)
5
+ #
6
+ # Animated GIFs must not be resized or format-converted because the
7
+ # operation would destroy the animation. This class parses the GIF
8
+ # binary format and counts Image Descriptor blocks to determine
9
+ # whether more than one frame is present.
10
+ class AnimatedGifDetector
11
+ # GIF block introducer bytes
12
+ EXTENSION_INTRODUCER = 0x21
13
+ IMAGE_DESCRIPTOR = 0x2C
14
+ TRAILER = 0x3B
15
+
16
+ # Check whether the file at +path+ is an animated GIF.
17
+ #
18
+ # @param path [String] Path to the image file
19
+ # @return [Boolean] true if the file is a GIF with more than one frame
20
+ def self.animated?(path)
21
+ new(path).animated?
22
+ end
23
+
24
+ # @param path [String] Path to the image file
25
+ def initialize(path)
26
+ @path = path
27
+ end
28
+
29
+ def animated?
30
+ return false unless gif?
31
+
32
+ frame_count > 1
33
+ end
34
+
35
+ private
36
+
37
+ def gif?
38
+ return false unless File.exist?(@path)
39
+
40
+ magic = File.binread(@path, 6)
41
+ %w[GIF87a GIF89a].include?(magic)
42
+ rescue StandardError
43
+ false
44
+ end
45
+
46
+ # Count the number of Image Descriptor (0x2C) blocks in the GIF.
47
+ # Each Image Descriptor marks the start of a frame.
48
+ # @return [Integer] Number of frames found
49
+ def frame_count
50
+ data = File.binread(@path)
51
+ reader = GifReader.new(data)
52
+
53
+ reader.skip_header_and_global_color_table
54
+ reader.count_image_descriptors
55
+ end
56
+
57
+ # Minimal forward-only GIF block reader.
58
+ # Walks the block structure to locate Image Descriptor markers
59
+ # without misinterpreting bytes that appear inside compressed data.
60
+ class GifReader
61
+ def initialize(data)
62
+ @data = data
63
+ @pos = 0
64
+ end
65
+
66
+ # Advance past the GIF header + Logical Screen Descriptor and
67
+ # the optional Global Color Table.
68
+ def skip_header_and_global_color_table
69
+ # Signature (3) + Version (3) already validated by caller;
70
+ # skip to Logical Screen Descriptor packed byte.
71
+ @pos = 10 # 6 (magic) + 4 (width/height)
72
+ packed = @data.getbyte(@pos)
73
+ @pos += 1 # consume packed byte
74
+
75
+ global_color_table_flag = packed.anybits?(0x80)
76
+ return unless global_color_table_flag
77
+
78
+ size_of_global_color_table = packed & 0x07
79
+ table_size = 3 * (2**(size_of_global_color_table + 1))
80
+ @pos += table_size
81
+ end
82
+
83
+ # Walk remaining blocks, counting Image Descriptors.
84
+ # @return [Integer] frame count
85
+ def count_image_descriptors
86
+ count = 0
87
+
88
+ while @pos < @data.length
89
+ introducer = @data.getbyte(@pos)
90
+
91
+ case introducer
92
+ when IMAGE_DESCRIPTOR
93
+ count += 1
94
+ skip_image_data
95
+ when EXTENSION_INTRODUCER
96
+ skip_extension
97
+ when TRAILER
98
+ break
99
+ else
100
+ # Unknown block — advance one byte to avoid infinite loop
101
+ @pos += 1
102
+ end
103
+ end
104
+
105
+ count
106
+ end
107
+
108
+ private
109
+
110
+ # Skip an Image Descriptor block + its Local Color Table +
111
+ # the LZW-compressed image data sub-blocks.
112
+ def skip_image_data
113
+ # Image Descriptor is 10 bytes (introducer + 9)
114
+ @pos += 10
115
+
116
+ packed = @data.getbyte(@pos - 1)
117
+ local_color_table_flag = packed.anybits?(0x80)
118
+
119
+ if local_color_table_flag
120
+ size_of_local_color_table = packed & 0x07
121
+ table_size = 3 * (2**(size_of_local_color_table + 1))
122
+ @pos += table_size
123
+ end
124
+
125
+ # LZW Minimum Code Size (1 byte)
126
+ @pos += 1
127
+
128
+ skip_sub_blocks
129
+ end
130
+
131
+ # Skip an Extension block (0x21 + label + sub-blocks).
132
+ def skip_extension
133
+ @pos += 2 # introducer + label
134
+ skip_sub_blocks
135
+ end
136
+
137
+ # Skip a sequence of sub-blocks: each starts with a size byte
138
+ # (1-255) followed by that many data bytes. A size of 0
139
+ # terminates the sequence.
140
+ def skip_sub_blocks
141
+ loop do
142
+ break if @pos >= @data.length
143
+
144
+ size = @data.getbyte(@pos)
145
+ @pos += 1
146
+ break if size.zero?
147
+
148
+ @pos += size
149
+ end
150
+ end
151
+ end
152
+
153
+ private_constant :GifReader
154
+ end
155
+ end
@@ -109,6 +109,7 @@ module JekyllImgFlow
109
109
  input_formats = @config.input_formats
110
110
 
111
111
  Dir.glob(File.join(originals_dir, "**", "*.{#{input_formats.join(',')}}"))
112
+ .reject { |path| AnimatedGifDetector.animated?(path) }
112
113
  end
113
114
 
114
115
  # Check if image needs processing
@@ -53,8 +53,18 @@ module JekyllImgFlow
53
53
  # Ensure output directory exists before processing
54
54
  FileUtils.mkdir_p(File.dirname(actual_output_path))
55
55
 
56
- # Process the operation (create the image)
57
- process_single_operation(type, input_path, actual_output_path, params)
56
+ # Animated GIFs must not be resized or converted — the operation
57
+ # would destroy the animation. Copy the original file as-is so the
58
+ # manifest can track it and HTML references stay valid.
59
+ if AnimatedGifDetector.animated?(input_path)
60
+ Jekyll.logger.warn "🖼️ ImgFlow: Skipping resize for animated GIF " \
61
+ "'#{original_name}' — copying original as-is to " \
62
+ "preserve animation."
63
+ FileUtils.cp(input_path, actual_output_path)
64
+ else
65
+ # Process the operation (create the image)
66
+ process_single_operation(type, input_path, actual_output_path, params)
67
+ end
58
68
 
59
69
  # Register in manifest
60
70
  if @manifest
@@ -60,7 +60,6 @@ module JekyllImgFlow
60
60
  crop_height = opts[:calculated_height]
61
61
 
62
62
  operations << "g:sm"
63
- operations << "c:#{crop_width}:#{crop_height}"
64
63
  else
65
64
  # Use basic cropping with explicit coordinates
66
65
  if operation[:ratio]
@@ -77,8 +76,8 @@ module JekyllImgFlow
77
76
  # imgproxy crop format: c:width:height:gravity
78
77
  # Use nowe (north-west) gravity with x/y offsets for pixel-precise cropping
79
78
  operations << "g:nowe:#{crop_x}:#{crop_y}"
80
- operations << "c:#{crop_width}:#{crop_height}"
81
79
  end
80
+ operations << "c:#{crop_width}:#{crop_height}"
82
81
 
83
82
  when :quality
84
83
  imgproxy_quality = translate_quality_to_imgproxy(operation[:quality])
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JekyllImgFlow
4
- VERSION = "0.1.5"
4
+ VERSION = "0.1.6"
5
5
  end
@@ -12,6 +12,7 @@ require_relative "jekyll-imgflow/manifest_manager"
12
12
  require_relative "jekyll-imgflow/tag_scanner"
13
13
  require_relative "jekyll-imgflow/path_resolver"
14
14
  require_relative "jekyll-imgflow/filename_generator"
15
+ require_relative "jekyll-imgflow/animated_gif_detector"
15
16
  require_relative "jekyll-imgflow/operation_processor"
16
17
  require_relative "jekyll-imgflow/batch_manager"
17
18
  require_relative "jekyll-imgflow/preset_manager"
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll-imgflow
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Svend Gundestrup
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-07-29 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: fastimage
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '2.4'
18
+ version: 2.4.1
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: '2.4'
25
+ version: 2.4.1
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: jekyll
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -43,154 +43,154 @@ dependencies:
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: '0.9'
46
+ version: 0.9.3
47
47
  type: :development
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
- version: '0.9'
53
+ version: 0.9.3
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: rake
56
56
  requirement: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - "~>"
59
59
  - !ruby/object:Gem::Version
60
- version: '13.0'
60
+ version: 13.4.2
61
61
  type: :development
62
62
  prerelease: false
63
63
  version_requirements: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
- version: '13.0'
67
+ version: 13.4.2
68
68
  - !ruby/object:Gem::Dependency
69
69
  name: reek
70
70
  requirement: !ruby/object:Gem::Requirement
71
71
  requirements:
72
72
  - - "~>"
73
73
  - !ruby/object:Gem::Version
74
- version: '6.0'
74
+ version: 6.5.0
75
75
  type: :development
76
76
  prerelease: false
77
77
  version_requirements: !ruby/object:Gem::Requirement
78
78
  requirements:
79
79
  - - "~>"
80
80
  - !ruby/object:Gem::Version
81
- version: '6.0'
81
+ version: 6.5.0
82
82
  - !ruby/object:Gem::Dependency
83
83
  name: rspec
84
84
  requirement: !ruby/object:Gem::Requirement
85
85
  requirements:
86
86
  - - "~>"
87
87
  - !ruby/object:Gem::Version
88
- version: '3.13'
88
+ version: 3.13.2
89
89
  type: :development
90
90
  prerelease: false
91
91
  version_requirements: !ruby/object:Gem::Requirement
92
92
  requirements:
93
93
  - - "~>"
94
94
  - !ruby/object:Gem::Version
95
- version: '3.13'
95
+ version: 3.13.2
96
96
  - !ruby/object:Gem::Dependency
97
97
  name: rubocop
98
98
  requirement: !ruby/object:Gem::Requirement
99
99
  requirements:
100
100
  - - "~>"
101
101
  - !ruby/object:Gem::Version
102
- version: '1.88'
102
+ version: 1.89.0
103
103
  type: :development
104
104
  prerelease: false
105
105
  version_requirements: !ruby/object:Gem::Requirement
106
106
  requirements:
107
107
  - - "~>"
108
108
  - !ruby/object:Gem::Version
109
- version: '1.88'
109
+ version: 1.89.0
110
110
  - !ruby/object:Gem::Dependency
111
111
  name: rubocop-markdown
112
112
  requirement: !ruby/object:Gem::Requirement
113
113
  requirements:
114
114
  - - "~>"
115
115
  - !ruby/object:Gem::Version
116
- version: '0.2'
116
+ version: 0.2.0
117
117
  type: :development
118
118
  prerelease: false
119
119
  version_requirements: !ruby/object:Gem::Requirement
120
120
  requirements:
121
121
  - - "~>"
122
122
  - !ruby/object:Gem::Version
123
- version: '0.2'
123
+ version: 0.2.0
124
124
  - !ruby/object:Gem::Dependency
125
125
  name: rubocop-performance
126
126
  requirement: !ruby/object:Gem::Requirement
127
127
  requirements:
128
128
  - - "~>"
129
129
  - !ruby/object:Gem::Version
130
- version: '1.26'
130
+ version: 1.26.1
131
131
  type: :development
132
132
  prerelease: false
133
133
  version_requirements: !ruby/object:Gem::Requirement
134
134
  requirements:
135
135
  - - "~>"
136
136
  - !ruby/object:Gem::Version
137
- version: '1.26'
137
+ version: 1.26.1
138
138
  - !ruby/object:Gem::Dependency
139
139
  name: rubocop-rake
140
140
  requirement: !ruby/object:Gem::Requirement
141
141
  requirements:
142
142
  - - "~>"
143
143
  - !ruby/object:Gem::Version
144
- version: '0.7'
144
+ version: 0.7.1
145
145
  type: :development
146
146
  prerelease: false
147
147
  version_requirements: !ruby/object:Gem::Requirement
148
148
  requirements:
149
149
  - - "~>"
150
150
  - !ruby/object:Gem::Version
151
- version: '0.7'
151
+ version: 0.7.1
152
152
  - !ruby/object:Gem::Dependency
153
153
  name: rubocop-rspec
154
154
  requirement: !ruby/object:Gem::Requirement
155
155
  requirements:
156
156
  - - "~>"
157
157
  - !ruby/object:Gem::Version
158
- version: '3.10'
158
+ version: 3.10.2
159
159
  type: :development
160
160
  prerelease: false
161
161
  version_requirements: !ruby/object:Gem::Requirement
162
162
  requirements:
163
163
  - - "~>"
164
164
  - !ruby/object:Gem::Version
165
- version: '3.10'
165
+ version: 3.10.2
166
166
  - !ruby/object:Gem::Dependency
167
167
  name: simplecov
168
168
  requirement: !ruby/object:Gem::Requirement
169
169
  requirements:
170
170
  - - "~>"
171
171
  - !ruby/object:Gem::Version
172
- version: '1.0'
172
+ version: 1.1.1
173
173
  type: :development
174
174
  prerelease: false
175
175
  version_requirements: !ruby/object:Gem::Requirement
176
176
  requirements:
177
177
  - - "~>"
178
178
  - !ruby/object:Gem::Version
179
- version: '1.0'
179
+ version: 1.1.1
180
180
  - !ruby/object:Gem::Dependency
181
181
  name: yard
182
182
  requirement: !ruby/object:Gem::Requirement
183
183
  requirements:
184
184
  - - "~>"
185
185
  - !ruby/object:Gem::Version
186
- version: '0.9'
186
+ version: 0.9.45
187
187
  type: :development
188
188
  prerelease: false
189
189
  version_requirements: !ruby/object:Gem::Requirement
190
190
  requirements:
191
191
  - - "~>"
192
192
  - !ruby/object:Gem::Version
193
- version: '0.9'
193
+ version: 0.9.45
194
194
  description: ImgFlow provides automatic image optimization for Jekyll with support
195
195
  for multiple providers (Sharp, Imgproxy, ImageMagick, LibVips), multiple formats
196
196
  (AVIF, WebP, PNG, JPG), and responsive image generation with modern picture tags.
@@ -202,6 +202,7 @@ files:
202
202
  - LICENSE
203
203
  - README.md
204
204
  - lib/jekyll-imgflow.rb
205
+ - lib/jekyll-imgflow/animated_gif_detector.rb
205
206
  - lib/jekyll-imgflow/batch_manager.rb
206
207
  - lib/jekyll-imgflow/build_time_processor.rb
207
208
  - lib/jekyll-imgflow/config.rb
@@ -248,14 +249,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
248
249
  requirements:
249
250
  - - ">="
250
251
  - !ruby/object:Gem::Version
251
- version: 3.3.0
252
+ version: 3.4.0
252
253
  required_rubygems_version: !ruby/object:Gem::Requirement
253
254
  requirements:
254
255
  - - ">="
255
256
  - !ruby/object:Gem::Version
256
257
  version: '0'
257
258
  requirements: []
258
- rubygems_version: 3.6.2
259
+ rubygems_version: 3.6.9
259
260
  specification_version: 4
260
261
  summary: A modern, multi-provider, multi-format image optimization engine for Jekyll
261
262
  test_files: []