jekyll-auto-thumbnails 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.
data/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # Automatic Image Thumbnails for Jekyll
2
+
3
+ Scans your rendered HTML for local images with `width` or `height` attributes, then automatically generates and uses appropriately-sized thumbnails for them, if the `src` image is bigger than that.
4
+
5
+ Can also take global maximum dimensions (such as for fixed-width layouts) and thumbnail images that don't have explicit size attributes, too.
6
+
7
+ Requires [ImageMagick](https://imagemagick.org) to be installed.
8
+
9
+ ## Installation
10
+
11
+ Add to your `Gemfile`:
12
+
13
+ ```ruby
14
+ group :jekyll_plugins do
15
+ gem "jekyll-auto-thumbnails"
16
+ end
17
+ ```
18
+
19
+ Run:
20
+
21
+ ```bash
22
+ bundle install
23
+ ```
24
+
25
+ **System Requirement**: ImageMagick must be installed (gem requires the `convert` and `identify` commands to be available).
26
+
27
+ ## Configuration
28
+
29
+ ```yaml
30
+ # _config.yml
31
+ auto_thumbnails:
32
+ enabled: true # default: true
33
+
34
+ # Maximum dimensions for automatic thumbnailing
35
+ # Images exceeding these get thumbnails even without explicit sizing
36
+ max_width: 1200 # pixels (optional)
37
+ max_height: 800 # pixels (optional)
38
+
39
+ # JPEG quality for generated thumbnails (0-100)
40
+ quality: 85 # default: 85
41
+ ```
42
+
43
+ ## How It Works
44
+
45
+ 1. **HTML Scanning**: After all plugins run, scans `<article>` tags for images
46
+ 2. **Size Detection**:
47
+ - Images with `width` or `height` attributes → uses those dimensions
48
+ - Unsized images exceeding max config → thumbnails to max dimensions
49
+ 3. **Generation**: Creates thumbnails with MD5-based caching in `.jekyll-cache/`
50
+ 4. **URL Replacement**: Updates `<img src>` to point to thumbnails
51
+ 5. **File Copying**: Copies thumbnails to `_site/` after build
52
+
53
+ ## Usage Examples
54
+
55
+ ### Explicit Sizing
56
+
57
+ ```html
58
+ <article>
59
+ <img src="/photo.jpg" width="300" height="200">
60
+ <!-- Generates: photo_thumb-abc123-300x200.jpg -->
61
+ </article>
62
+ ```
63
+
64
+ ### Automatic Optimization
65
+
66
+ ```yaml
67
+ # _config.yml
68
+ auto_thumbnails:
69
+ max_width: 800
70
+ ```
71
+
72
+ ```html
73
+ <article>
74
+ <img src="/big-photo.jpg">
75
+ <!-- If photo is 2000x1500, generates: big-photo_thumb-def456-800x600.jpg -->
76
+ </article>
77
+ ```
78
+
79
+ ### With Markdown
80
+
81
+ Works automatically with any Markdown processor, because it checks the rendered HTML!
82
+
83
+ ```markdown
84
+ ![Photo](photo.jpg) <!-- Auto-detects size -->
85
+ ```
86
+
87
+
88
+ ## Cache Behavior
89
+
90
+ - First build: Generates thumbnails, stores in `.jekyll-cache/`
91
+ - Subsequent builds: Reuses cached thumbnails (fast!)
92
+ - Source changed: MD5 mismatch detected, regenerates automatically
93
+ - Dimensions changed: Different filename, generates new thumbnail
94
+
95
+ ## Troubleshooting
96
+
97
+ ### ImageMagick Not Found
98
+
99
+ ```bash
100
+ # Ubuntu/Debian
101
+ sudo apt-get install imagemagick
102
+
103
+ # macOS
104
+ brew install imagemagick
105
+
106
+ # Verify installation
107
+ which convert identify
108
+ ```
109
+
110
+ ### Thumbnails Not Generating
111
+
112
+ Check build output for warnings:
113
+
114
+ ```bash
115
+ bundle exec jekyll build --verbose
116
+ # Look for "AutoThumbnails:" messages
117
+ ```
118
+
119
+ ### Clear Cache
120
+
121
+ ```bash
122
+ rm -rf .jekyll-cache/jekyll-auto-thumbnails/
123
+ ```
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JekyllAutoThumbnails
4
+ # Configuration parser for auto_thumbnails settings
5
+ #
6
+ # Parses Jekyll site config for auto_thumbnails options and provides
7
+ # accessor methods with appropriate defaults and validation.
8
+ class Configuration
9
+ attr_reader :max_width, :max_height, :quality, :cache_dir
10
+
11
+ # Initialize configuration from Jekyll site
12
+ #
13
+ # @param site [Jekyll::Site] The Jekyll site object
14
+ def initialize(site)
15
+ config_hash = site.config["auto_thumbnails"] || {}
16
+
17
+ @enabled = config_hash.fetch("enabled", true)
18
+ @max_width = parse_dimension(config_hash["max_width"])
19
+ @max_height = parse_dimension(config_hash["max_height"])
20
+ @quality = parse_quality(config_hash.fetch("quality", 85))
21
+ @cache_dir = File.join(site.source, ".jekyll-cache", "jekyll-auto-thumbnails")
22
+ end
23
+
24
+ # Check if image optimization is enabled
25
+ #
26
+ # @return [Boolean] true if enabled (default: true)
27
+ def enabled?
28
+ @enabled
29
+ end
30
+
31
+ private
32
+
33
+ # Parse dimension value (max_width or max_height)
34
+ #
35
+ # @param value [Object] dimension value from config
36
+ # @return [Integer, nil] positive integer or nil
37
+ def parse_dimension(value)
38
+ val = value.to_i
39
+ val.positive? ? val : nil
40
+ end
41
+
42
+ # Parse quality value (0-100)
43
+ #
44
+ # @param value [Object] quality value from config
45
+ # @return [Integer] quality value 0-100, or 85 if invalid
46
+ def parse_quality(value)
47
+ val = value.to_i
48
+ val.between?(0, 100) ? val : 85
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest/md5"
4
+
5
+ module JekyllAutoThumbnails
6
+ # MD5 digest calculation for cache keys
7
+ #
8
+ # Computes short MD5 digests of image files for use in thumbnail filenames.
9
+ module DigestCalculator
10
+ # Compute short (6-char) MD5 digest of file
11
+ #
12
+ # @param file_path [String] path to file
13
+ # @return [String] first 6 characters of MD5 hex digest
14
+ # @raise [Errno::ENOENT] if file doesn't exist
15
+ def self.short_digest(file_path)
16
+ Digest::MD5.file(file_path).hexdigest[0...6]
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module JekyllAutoThumbnails
6
+ # Thumbnail generation via ImageMagick
7
+ class Generator
8
+ # Initialize generator
9
+ #
10
+ # @param config [Configuration] configuration
11
+ # @param site_source [String] Jekyll site source directory
12
+ def initialize(config, site_source)
13
+ @config = config
14
+ @site_source = site_source
15
+ end
16
+
17
+ # Check if ImageMagick is available (cross-platform)
18
+ #
19
+ # @return [Boolean] true if convert command found in PATH
20
+ def imagemagick_available?
21
+ cmd_name = Gem.win_platform? ? "convert.exe" : "convert"
22
+ path_dirs = ENV["PATH"].to_s.split(File::PATH_SEPARATOR)
23
+
24
+ path_dirs.any? do |dir|
25
+ executable = File.join(dir, cmd_name)
26
+ File.executable?(executable)
27
+ end
28
+ end
29
+
30
+ # Generate thumbnail (with caching)
31
+ #
32
+ # @param url [String] image URL
33
+ # @param width [Integer, nil] target width
34
+ # @param height [Integer, nil] target height
35
+ # @return [String, nil] path to cached thumbnail or nil if failed
36
+ def generate(url, width, height)
37
+ # Resolve source file
38
+ source_path = UrlResolver.to_filesystem_path(url, @site_source)
39
+ return nil unless source_path && File.exist?(source_path)
40
+
41
+ # Compute digest
42
+ digest = DigestCalculator.short_digest(source_path)
43
+
44
+ # Build thumbnail filename
45
+ basename = File.basename(source_path, File.extname(source_path))
46
+ ext = File.extname(source_path)
47
+ thumb_filename = build_thumbnail_filename(basename, digest, width, height, ext)
48
+
49
+ # Check cache
50
+ cached_path = File.join(@config.cache_dir, thumb_filename)
51
+ return cached_path if File.exist?(cached_path)
52
+
53
+ # Generate
54
+ FileUtils.mkdir_p(@config.cache_dir)
55
+ success = shell_generate(source_path, cached_path, width, height)
56
+
57
+ return nil unless success
58
+
59
+ # Check if thumbnail is larger than original
60
+ if File.size(cached_path) > File.size(source_path)
61
+ Jekyll.logger.warn "AutoThumbnails:",
62
+ "Thumbnail larger than original (#{File.size(cached_path)} > #{File.size(source_path)}), " \
63
+ "deleting #{cached_path}"
64
+ FileUtils.rm_f(cached_path)
65
+ return nil
66
+ end
67
+
68
+ cached_path
69
+ end
70
+
71
+ # Build thumbnail filename
72
+ #
73
+ # @param basename [String] image basename (no extension)
74
+ # @param digest [String] 6-char MD5 digest
75
+ # @param width [Integer, nil] width
76
+ # @param height [Integer, nil] height
77
+ # @param ext [String] file extension
78
+ # @return [String] thumbnail filename
79
+ def build_thumbnail_filename(basename, digest, width, height, ext)
80
+ width_str = width || ""
81
+ height_str = height || ""
82
+ "#{basename}_thumb-#{digest}-#{width_str}x#{height_str}#{ext}"
83
+ end
84
+
85
+ private
86
+
87
+ # Generate thumbnail using ImageMagick
88
+ #
89
+ # @param source_path [String] source image path
90
+ # @param dest_path [String] destination thumbnail path
91
+ # @param width [Integer, nil] target width
92
+ # @param height [Integer, nil] target height
93
+ # @return [Boolean] true if successful
94
+ def shell_generate(source_path, dest_path, width, height)
95
+ geometry = build_geometry(width, height)
96
+ ext = File.extname(source_path)
97
+
98
+ # Build command array (no shell interpretation)
99
+ cmd = ["convert", source_path, "-resize", geometry]
100
+
101
+ # Add quality for lossy formats
102
+ if quality_needed?(ext)
103
+ cmd << "-quality"
104
+ cmd << @config.quality.to_s
105
+ end
106
+
107
+ cmd << dest_path
108
+
109
+ # Call system with array (bypasses shell)
110
+ system(*cmd)
111
+ end
112
+
113
+ # Build ImageMagick geometry string
114
+ #
115
+ # @param width [Integer, nil] target width
116
+ # @param height [Integer, nil] target height
117
+ # @return [String] geometry string (e.g., "400x300>")
118
+ def build_geometry(width, height)
119
+ width_str = width || ""
120
+ height_str = height || ""
121
+ "#{width_str}x#{height_str}>"
122
+ end
123
+
124
+ # Check if quality parameter needed for image format
125
+ #
126
+ # @param ext [String] file extension
127
+ # @return [Boolean] true if quality parameter should be used
128
+ def quality_needed?(ext)
129
+ %w[.jpg .jpeg].include?(ext.downcase)
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JekyllAutoThumbnails
4
+ # Jekyll hook integration
5
+ module Hooks
6
+ # Initialize optimization system
7
+ #
8
+ # @param site [Jekyll::Site] Jekyll site
9
+ def self.initialize_system(site)
10
+ config = Configuration.new(site)
11
+ return unless config.enabled?
12
+
13
+ site.data["auto_thumbnails_config"] = config
14
+ site.data["auto_thumbnails_registry"] = Registry.new
15
+ site.data["auto_thumbnails_generator"] = Generator.new(config, site.source)
16
+
17
+ Jekyll.logger.info "AutoThumbnails:", "System initialized"
18
+ end
19
+
20
+ # Process site - scan, generate, replace
21
+ #
22
+ # @param site [Jekyll::Site] Jekyll site
23
+ def self.process_site(site)
24
+ config = site.data["auto_thumbnails_config"]
25
+ return unless config&.enabled?
26
+
27
+ registry = site.data["auto_thumbnails_registry"]
28
+ generator = site.data["auto_thumbnails_generator"]
29
+
30
+ # Check ImageMagick
31
+ unless generator.imagemagick_available?
32
+ Jekyll.logger.warn "AutoThumbnails:", "ImageMagick not found - skipping"
33
+ return
34
+ end
35
+
36
+ # Scan all documents and pages
37
+ (site.documents + site.pages).each do |doc|
38
+ next unless doc.output
39
+
40
+ Scanner.scan_html(doc.output, registry, config, site.source)
41
+ end
42
+
43
+ Jekyll.logger.info "AutoThumbnails:", "Found #{registry.entries.size} images to optimize"
44
+
45
+ # Generate thumbnails
46
+ url_map = {}
47
+ registry.entries.each do |url, requirements|
48
+ cached_path = generator.generate(url, requirements[:width], requirements[:height])
49
+
50
+ if cached_path
51
+ # Build thumbnail URL (use forward slashes for URLs, not File.join)
52
+ thumb_filename = File.basename(cached_path)
53
+ url_dir = File.dirname(url)
54
+ # Ensure URL uses forward slashes (cross-platform URLs)
55
+ thumb_url = if url_dir == "."
56
+ "/#{thumb_filename}"
57
+ else
58
+ "#{url_dir}/#{thumb_filename}"
59
+ end
60
+ url_map[url] = thumb_url
61
+ else
62
+ Jekyll.logger.warn "AutoThumbnails:", "Failed to generate thumbnail for #{url}"
63
+ end
64
+ end
65
+
66
+ # Store url_map for post_write hook
67
+ site.data["auto_thumbnails_url_map"] = url_map
68
+
69
+ # Replace URLs in HTML
70
+ (site.documents + site.pages).each do |doc|
71
+ next unless doc.output
72
+
73
+ doc.output = replace_urls(doc.output, url_map)
74
+ end
75
+
76
+ Jekyll.logger.info "AutoThumbnails:", "Generated #{url_map.size} thumbnails"
77
+ end
78
+
79
+ # Copy thumbnails from cache to _site
80
+ #
81
+ # @param site [Jekyll::Site] Jekyll site
82
+ def self.copy_thumbnails(site)
83
+ config = site.data["auto_thumbnails_config"]
84
+ return unless config&.enabled?
85
+
86
+ url_map = site.data["auto_thumbnails_url_map"]
87
+ return unless url_map && !url_map.empty?
88
+
89
+ Jekyll.logger.info "AutoThumbnails:", "Copying #{url_map.size} thumbnails to _site"
90
+
91
+ url_map.each_value do |thumb_url|
92
+ thumb_filename = File.basename(thumb_url)
93
+ cached_path = File.join(config.cache_dir, thumb_filename)
94
+
95
+ # Build destination path in _site preserving directory structure
96
+ dest_path = File.join(site.dest, thumb_url.sub(%r{^/}, ""))
97
+ dest_dir = File.dirname(dest_path)
98
+
99
+ FileUtils.mkdir_p(dest_dir)
100
+ FileUtils.cp(cached_path, dest_path)
101
+ end
102
+
103
+ Jekyll.logger.info "AutoThumbnails:", "All thumbnails copied"
104
+ end
105
+
106
+ # Replace image URLs in HTML
107
+ #
108
+ # @param html [String] HTML content
109
+ # @param url_map [Hash] original URL => thumbnail URL
110
+ # @return [String] modified HTML
111
+ def self.replace_urls(html, url_map)
112
+ # Return early if no replacements needed
113
+ return html if url_map.empty?
114
+
115
+ doc = Nokogiri::HTML(html)
116
+
117
+ doc.css("article img").each do |img|
118
+ src = img["src"]
119
+ next unless src
120
+
121
+ # Find thumbnail URL for this image
122
+ thumb_url = url_map[src]
123
+ img["src"] = thumb_url if thumb_url
124
+ end
125
+
126
+ # Serialize with encoding declaration to match Jekyll output
127
+ doc.to_html
128
+ end
129
+
130
+ private_class_method :replace_urls
131
+ end
132
+ end
133
+
134
+ # Register Jekyll hooks
135
+ Jekyll::Hooks.register :site, :post_read do |site|
136
+ JekyllAutoThumbnails::Hooks.initialize_system(site)
137
+ end
138
+
139
+ Jekyll::Hooks.register :site, :post_render do |site|
140
+ JekyllAutoThumbnails::Hooks.process_site(site)
141
+ end
142
+
143
+ Jekyll::Hooks.register :site, :post_write do |site|
144
+ JekyllAutoThumbnails::Hooks.copy_thumbnails(site)
145
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JekyllAutoThumbnails
4
+ # Image requirement registry
5
+ #
6
+ # Tracks images needing thumbnails and their required dimensions.
7
+ # Handles duplicate registrations by keeping the largest dimensions.
8
+ class Registry
9
+ # Initialize empty registry
10
+ def initialize
11
+ @entries = {}
12
+ end
13
+
14
+ # Register an image with required dimensions
15
+ #
16
+ # @param url [String] image URL
17
+ # @param width [Integer, nil] required width
18
+ # @param height [Integer, nil] required height
19
+ def register(url, width, height)
20
+ existing = @entries[url]
21
+
22
+ @entries[url] = if existing
23
+ # Update to max dimensions
24
+ {
25
+ width: [existing[:width], width].compact.max,
26
+ height: [existing[:height], height].compact.max
27
+ }
28
+ else
29
+ { width: width, height: height }
30
+ end
31
+ end
32
+
33
+ # Check if image is registered
34
+ #
35
+ # @param url [String] image URL
36
+ # @return [Boolean] true if registered
37
+ def registered?(url)
38
+ @entries.key?(url)
39
+ end
40
+
41
+ # Get requirements for image
42
+ #
43
+ # @param url [String] image URL
44
+ # @return [Hash, nil] {width:, height:} or nil if not registered
45
+ def requirements_for(url)
46
+ @entries[url]&.dup
47
+ end
48
+
49
+ # Get all registered entries
50
+ #
51
+ # @return [Hash] url => {width:, height:}
52
+ def entries
53
+ @entries.dup
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+ require "open3"
5
+
6
+ module JekyllAutoThumbnails
7
+ # HTML scanning for images
8
+ module Scanner
9
+ # Scan HTML for images needing optimization
10
+ #
11
+ # @param html [String] HTML content
12
+ # @param registry [Registry] image registry
13
+ # @param config [Configuration] configuration
14
+ # @param site_source [String] Jekyll site source (optional, for unsized image checking)
15
+ def self.scan_html(html, registry, config, site_source = nil)
16
+ doc = Nokogiri::HTML(html)
17
+
18
+ # Only process images in <article> tags
19
+ doc.css("article img").each do |img|
20
+ src = img["src"]
21
+ next unless src
22
+ next if UrlResolver.external?(src)
23
+
24
+ # Check for explicit dimensions
25
+ width = parse_dimension(img["width"])
26
+ height = parse_dimension(img["height"])
27
+
28
+ if width || height
29
+ # Explicitly sized - calculate missing dimension if needed
30
+ if site_source && (width.nil? || height.nil?)
31
+ Jekyll.logger.debug "AutoThumbnails:", "Calculating dimensions for #{src} (#{width}x#{height})"
32
+ width, height = calculate_dimensions(src, width, height, site_source)
33
+ Jekyll.logger.debug "AutoThumbnails:", "Calculated: #{width}x#{height}"
34
+ end
35
+
36
+ # Skip if dimensions match original (no thumbnail needed)
37
+ if site_source && dimensions_match_original?(src, width, height, site_source)
38
+ Jekyll.logger.debug "AutoThumbnails:", "Skipping #{src} - dimensions match original"
39
+ next
40
+ end
41
+
42
+ Jekyll.logger.debug "AutoThumbnails:", "Registering #{src} at #{width}x#{height}"
43
+ registry.register(src, width, height)
44
+ elsif site_source && (config.max_width || config.max_height)
45
+ # Unsized but max config exists - check actual dimensions
46
+ check_and_register_oversized(src, registry, config, site_source)
47
+ end
48
+ end
49
+ end
50
+
51
+ # Check if image exceeds max dimensions and register if so
52
+ #
53
+ # @param url [String] image URL
54
+ # @param registry [Registry] image registry
55
+ # @param config [Configuration] configuration
56
+ # @param site_source [String] site source directory
57
+ def self.check_and_register_oversized(url, registry, config, site_source)
58
+ file_path = UrlResolver.to_filesystem_path(url, site_source)
59
+ return unless file_path && File.exist?(file_path)
60
+
61
+ actual_width, actual_height = image_dimensions(file_path)
62
+ return unless actual_width && actual_height
63
+
64
+ # Check if exceeds max dimensions
65
+ exceeds_width = config.max_width && actual_width > config.max_width
66
+ exceeds_height = config.max_height && actual_height > config.max_height
67
+
68
+ return unless exceeds_width || exceeds_height
69
+
70
+ # Register with max dimensions (preserving aspect ratio logic in Generator)
71
+ registry.register(url, config.max_width, config.max_height)
72
+ end
73
+
74
+ # Get image dimensions from file
75
+ #
76
+ # @param file_path [String] path to image file
77
+ # @return [Array<Integer, Integer>, nil] [width, height] or nil
78
+ def self.image_dimensions(file_path)
79
+ # Use ImageMagick identify command (shell-free, cross-platform)
80
+ # Use [0] to get only first frame (important for animated GIFs)
81
+ output, status = Open3.capture2e("identify", "-format", "%wx%h", "#{file_path}[0]")
82
+ return nil unless status.success? && !output.strip.empty?
83
+
84
+ width, height = output.strip.split("x").map(&:to_i)
85
+ [width, height]
86
+ rescue StandardError
87
+ nil
88
+ end
89
+
90
+ # Calculate missing dimension based on aspect ratio
91
+ #
92
+ # @param url [String] image URL
93
+ # @param width [Integer, nil] specified width
94
+ # @param height [Integer, nil] specified height
95
+ # @param site_source [String] site source directory
96
+ # @return [Array<Integer, Integer>] [width, height] with calculated dimension
97
+ def self.calculate_dimensions(url, width, height, site_source)
98
+ file_path = UrlResolver.to_filesystem_path(url, site_source)
99
+ return [width, height] unless file_path && File.exist?(file_path)
100
+
101
+ actual_width, actual_height = image_dimensions(file_path)
102
+ return [width, height] unless actual_width && actual_height
103
+
104
+ # Calculate missing dimension preserving aspect ratio
105
+ if width && !height
106
+ # Width specified, calculate height
107
+ aspect_ratio = actual_height.to_f / actual_width
108
+ height = (width * aspect_ratio).round
109
+ elsif height && !width
110
+ # Height specified, calculate width
111
+ aspect_ratio = actual_width.to_f / actual_height
112
+ width = (height * aspect_ratio).round
113
+ end
114
+
115
+ [width, height]
116
+ end
117
+
118
+ # Parse dimension attribute (width or height)
119
+ #
120
+ # @param value [String, nil] attribute value
121
+ # @return [Integer, nil] parsed integer or nil
122
+ def self.parse_dimension(value)
123
+ return nil if value.nil? || value.empty?
124
+
125
+ # Strip non-numeric characters (e.g., "300px" -> 300)
126
+ numeric = value.to_s.gsub(/[^\d]/, "")
127
+ return nil if numeric.empty?
128
+
129
+ numeric.to_i
130
+ end
131
+
132
+ # Check if requested dimensions match original dimensions
133
+ #
134
+ # @param url [String] image URL
135
+ # @param width [Integer] requested width
136
+ # @param height [Integer] requested height
137
+ # @param site_source [String] site source directory
138
+ # @return [Boolean] true if dimensions match original
139
+ def self.dimensions_match_original?(url, width, height, site_source)
140
+ file_path = UrlResolver.to_filesystem_path(url, site_source)
141
+ return false unless file_path && File.exist?(file_path)
142
+
143
+ actual_width, actual_height = image_dimensions(file_path)
144
+ return false unless actual_width && actual_height
145
+
146
+ width == actual_width && height == actual_height
147
+ end
148
+
149
+ private_class_method :check_and_register_oversized, :calculate_dimensions, :parse_dimension,
150
+ :dimensions_match_original?
151
+ end
152
+ end