Class: Abqari::ImagePipeline
- Inherits:
-
Object
- Object
- Abqari::ImagePipeline
- Defined in:
- lib/abqari/image_pipeline.rb,
lib/abqari/image_pipeline.rb
Overview
Backends — strategy pattern. Each implements thumbnail and
dimensions; ImagePipeline picks one in detect_backend.
Defined Under Namespace
Classes: ShellBackend, VipsBackend
Constant Summary collapse
- IMAGE_EXTENSIONS =
Raster sources the pipeline handles. SVG is excluded — vector, nothing to thumbnail.
%w[.jpg .jpeg .png .gif .webp .avif].freeze
- DEFAULTS =
{ 'optimise' => true, 'processor' => 'auto', # auto | vips-native | vips | imagemagick # AVIF first so browsers pick it ahead of WebP — AVIF is 20–50% # smaller at equal quality and has universal modern-browser # support as of 2026 (Safari 16.4+, Chrome/Edge 85+, Firefox # 113+). Older browsers fall through to WebP, then the original. 'formats' => %w[avif webp], 'widths' => [400, 800, 1200], # Per-format quality. AVIF's quality scale is perceptually # different from JPEG/WebP — 50 produces files smaller than WebP # at 80 with comparable quality. Applying one number across all # formats (the legacy behaviour) made AVIF files LARGER than # WebP, defeating the format's purpose. 'quality' => { 'avif' => 50, 'webp' => 80, 'original' => 85 }, 'sizes' => '(min-width: 768px) 50vw, 100vw', 'source_dir' => 'app/assets/images', # Strip EXIF, GPS, embedded thumbnails, ICC profiles. Photographers # who need to preserve metadata (portfolio sites that surface # camera info) can flip this off. 'preserve_metadata' => false }.freeze
- HASH_LENGTH =
10
Class Method Summary collapse
Instance Method Summary collapse
- #backend ⇒ Object
- #enabled? ⇒ Boolean
-
#initialize(site) ⇒ ImagePipeline
constructor
A new instance of ImagePipeline.
-
#picture_tag(source, alt: '', sizes: nil, widths: nil) ⇒ Object
picture_tag — emits a
<picture>block for an asset image. -
#picture_tag_for_url(url, alt:, sizes: nil, widths: nil, attrs: {}) ⇒ Object
Emit a
<picture>block for a URL that's been registered with the pipeline (asset or bundle). -
#post_process_html(html, page: nil) ⇒ Object
Rewrite
<img>tags whosesrcwe know how to optimise. -
#process ⇒ Object
Phase entry point — called from Site#build BEFORE render.
-
#source_path_for(url) ⇒ Object
Resolve a URL to its on-disk source path, or nil if we don't recognise it.
Constructor Details
#initialize(site) ⇒ ImagePipeline
Returns a new instance of ImagePipeline.
77 78 79 80 81 82 83 84 85 86 87 |
# File 'lib/abqari/image_pipeline.rb', line 77 def initialize(site) @site = site @config = merged_config(site.config['images']) @backend = nil @variants_cache = {} @cache_mutex = Mutex.new @registry = {} @registry_mutex = Mutex.new @url_to_bundle_dir = nil @config['formats'] = prune_unsupported_formats(@config['formats']) end |
Class Method Details
.format_capabilities ⇒ Object
298 299 300 |
# File 'lib/abqari/image_pipeline.rb', line 298 def format_capabilities @format_capabilities ||= {} end |
.warned_formats ⇒ Object
302 303 304 |
# File 'lib/abqari/image_pipeline.rb', line 302 def warned_formats @warned_formats ||= Set.new end |
Instance Method Details
#backend ⇒ Object
93 94 95 96 97 |
# File 'lib/abqari/image_pipeline.rb', line 93 def backend @backend = detect_backend if @backend.nil? && !@backend_resolved @backend_resolved = true @backend end |
#enabled? ⇒ Boolean
89 90 91 |
# File 'lib/abqari/image_pipeline.rb', line 89 def enabled? @config['optimise'] == true && backend end |
#picture_tag(source, alt: '', sizes: nil, widths: nil) ⇒ Object
picture_tag — emits a <picture> block for an asset image. The
source argument is resolved under app/assets/images/ (the
legacy ERB helper convention). For bundle images, prefer the
automatic rewrite in post_process_html or the
bundle_picture_tag helper.
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
# File 'lib/abqari/image_pipeline.rb', line 124 def picture_tag(source, alt: '', sizes: nil, widths: nil) sizes ||= @config['sizes'] requested_widths = (widths || @config['widths']).sort source_path = absolute_source(source) unless File.exist?(source_path) # Source doesn't exist locally — emit a literal <img> with the # source value as-is (allows authors to use full URLs). return %(<img src="#{escape_attr(source)}" alt="#{escape_attr(alt)}" loading="lazy" decoding="async">) end unless enabled? copy_passthrough(source_path, '/assets/images/') dims = dimension_attrs(source_path) url = "/assets/images/#{File.basename(source_path)}" return %(<img src="#{escape_attr(url)}" alt="#{escape_attr(alt)}"#{dims} loading="lazy" decoding="async">) end url_prefix = '/assets/images/' target_dir = File.join(@site.output_dir, 'assets', 'images') register_source("#{url_prefix}#{File.basename(source_path)}", source_path, target_dir, url_prefix) render_picture("#{url_prefix}#{File.basename(source_path)}", alt, sizes, requested_widths) end |
#picture_tag_for_url(url, alt:, sizes: nil, widths: nil, attrs: {}) ⇒ Object
Emit a <picture> block for a URL that's been registered with
the pipeline (asset or bundle). Returns nil if the URL isn't
registered — callers fall back to whatever they were going to
do otherwise (a plain <img>, the legacy sibling-lookup, etc.).
Used by bundle_picture_tag in render_context to opt explicit
layout-level uses into the pipeline.
155 156 157 158 159 160 161 162 |
# File 'lib/abqari/image_pipeline.rb', line 155 def picture_tag_for_url(url, alt:, sizes: nil, widths: nil, attrs: {}) return nil unless enabled? return nil unless source_path_for(canonical_url(url)) sizes ||= @config['sizes'] widths = (widths || @config['widths']).sort render_picture(canonical_url(url), alt, sizes, widths, extra_attrs: attrs) end |
#post_process_html(html, page: nil) ⇒ Object
Rewrite <img> tags whose src we know how to optimise. Asset
URLs and bundle URLs are both routed through the registry — any
<img src=...> that maps to a registered source gets replaced
with a <picture> element. Unknown images pass through
unchanged.
page: is accepted for symmetry with other post-processors but
the registry lookup is URL-based (we already have a complete
map by Site#build time) so the page itself isn't needed here.
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 |
# File 'lib/abqari/image_pipeline.rb', line 173 def post_process_html(html, page: nil) return html unless enabled? html.gsub(/<img\s+([^>]*?)>/m) do |match| attrs = ::Regexp.last_match(1) src = attrs[/src="([^"]+)"/, 1] next match unless src registered_url = canonical_url(src) next match unless source_path_for(registered_url) alt = attrs[/alt="([^"]*)"/, 1] || '' render_picture(registered_url, alt, @config['sizes'], @config['widths'].sort) end end |
#process ⇒ Object
Phase entry point — called from Site#build BEFORE render. Copies
app/assets/images/ originals through (always — needed for the
passthrough case), then if optimisation is on, walks every known
image source and registers it in the URL → variants map so
post_process_html can rewrite <img> tags during render.
104 105 106 107 108 109 110 |
# File 'lib/abqari/image_pipeline.rb', line 104 def process copy_originals return unless enabled? build_url_to_bundle_dir_map register_known_sources end |
#source_path_for(url) ⇒ Object
Resolve a URL to its on-disk source path, or nil if we don't
recognise it. Used by post-processing to figure out which <img src=...> tags to rewrite.
115 116 117 |
# File 'lib/abqari/image_pipeline.rb', line 115 def source_path_for(url) @registry_mutex.synchronize { @registry[url]&.dig(:source_path) } end |