Why adaptive conversion
A useful AVIF conversion script should not run one quality setting over every file. Photos, screenshots, diagrams, and transparent PNGs fail in different ways.
The target is not the smallest possible output. The target is the smallest output that preserves the intended visual quality for that asset type. That means the script needs to inspect each file, try candidate settings, and reject outputs that are too degraded.
Ruby is a reasonable orchestration language for this because the expensive work belongs to tools such as avifenc and ImageMagick. Ruby can manage files, metadata, process execution, retries, and reporting.
Metadata first
Before encoding, collect width, height, format, alpha channel, color profile, and file size. These facts decide the starting strategy. A photographic JPEG and a transparent UI PNG should not enter the same preset path.
Metadata also protects against bad surprises. Very large images may need downscaling policy. Animated assets may be out of scope. Files with transparency need settings that preserve alpha correctly.
The script should print what it believes about each input before it mutates anything. Silent conversion is convenient until one file class was misdetected.
Quality search
A fixed quality value is a guess. A better script tries a small ordered set of candidates, such as a few qcolor values, and keeps the smallest file that passes the quality gate.
The search does not need to be mathematically perfect. It needs to be bounded and predictable. For each candidate, encode to a temporary path, measure the output size, compare quality, and retain the best acceptable result.
For important assets, the script should keep enough logs to explain the choice: input size, candidate settings, similarity score, output size, and final decision.
Similarity checks
SSIM or another similarity metric can catch obvious damage, but it is not a substitute for human inspection. Metrics can miss text sharpness, small UI artifacts, banding, or changes that are visually annoying in a specific context.
Use thresholds as guardrails. A photo may tolerate different artifacts than a screenshot with small text. A logo may need stricter handling than a background image.
The safest script supports a dry run, a report-only mode, and a way to quarantine failures for manual review. Automation should reduce the inspection set, not pretend inspection is obsolete.
Parallelism and safety
Ractors can parallelize independent work, but encoders are already CPU-heavy. The right concurrency level is bounded by cores, thermals, memory, and how much other work the machine should remain able to do.
Each worker should operate on independent files and write to temporary paths. Replacing originals should be a final, explicit step after validation. Keeping originals until the batch is verified is cheap insurance.
The script should also be idempotent. If it is interrupted, rerunning it should skip completed good outputs, retry incomplete files, and never confuse a partial temporary file with a finished asset.
When to keep originals
For public-site assets, originals should usually remain available outside the generated output directory. AVIF is a delivery format, not necessarily the archival format. A later browser-support change, encoder improvement, or visual complaint should not require recovering the source from a lossy derivative.
The script can still be aggressive in generated directories. It can skip files that are already smaller, keep the original when AVIF fails the similarity check, and emit a manifest that explains every decision. That makes the batch reviewable instead of mysterious.
A script skeleton
The core script can be small if each step has one job: inspect, encode candidates, compare, and decide. The encoder remains external; Ruby coordinates the workflow.
def encode_candidate(input, output, quality:)
system(
"avifenc",
"--min", quality.to_s,
"--max", quality.to_s,
input,
output,
exception: true
)
end
def best_candidate(input, qualities: [36, 42, 48, 54])
qualities.filter_map do |quality|
output = "#{input}.q#{quality}.avif"
encode_candidate(input, output, quality: quality)
[quality, output, File.size(output)] if visually_ok?(input, output)
end.min_by { |_quality, _path, size| size }
end
The missing pieces are where most production caution lives: temporary directories, cleanup, SSIM thresholds by asset type, manifest logging, and a dry-run mode. Ractors can parallelize calls to this routine, but the concurrency should be capped because each encoder process is already CPU-intensive.
Failure handling
Image conversion scripts need conservative failure behavior. If one candidate encode fails, the script should keep the original, record the failure, and continue with the batch. It should not leave a half-written AVIF at the final path or delete the source before validation has passed.
The manifest is the safety net. For each input, record the detected metadata, chosen settings, output size, similarity score, and final decision. That makes the run auditable and lets a later script skip completed files without trusting filename conventions alone.
The script should also keep temporary files outside the final asset path. Atomic rename after validation is a small detail, but it prevents half-written outputs from being served or mistaken for successful conversions.
Sources
- libavif repository and avifenc. https://github.com/AOMediaCodec/libavif
- ImageMagick compare documentation. https://imagemagick.org/script/compare.php
- Ruby documentation, Ractor. https://docs.ruby-lang.org/en/master/Ractor.html