0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-07 13:19:19 +08:00
discourse/spec/support/image_orientation_helpers.rb
Alan Guo Xiang Tan 9e2a10b427
DEV: Add image-processing regression coverage (#42117)
Previously, important image-processing behavior lacked precise
regression coverage, making image-library upgrades difficult to assess
safely.

This change adds coverage without changing production behavior:

- Exercises public upload and image-model outcomes for SVG sanitization,
format detection and conversion, transparency, animation metadata, EXIF
orientation normalization, malformed input rejection, crop positioning,
tiny-image resizing, dominant-color caching, and image-quality
selection.
- Keeps normal LetterAvatar RSpec coverage portable by generating one
representative PNG and checking its format and requested dimensions.
- Adds a separate Linux-only `script/letter_avatar_pixel_diff` workflow
that compares A–Z renders with committed baselines and generates an HTML
expected/actual/highlighted-diff report. Use `--update-baselines` when
intentionally accepting rendering changes.
- Uses deterministic fixtures with regeneration guidance beside the
relevant specs.
2026-07-30 15:28:07 +08:00

54 lines
1.7 KiB
Ruby
Vendored

# frozen_string_literal: true
require "chunky_png"
module ImageOrientationHelpers
private
def with_jpeg_orientation(source_path:, orientation:)
exif =
"Exif\0\0".b + "II*\0".b + [8].pack("V") + [1].pack("v") + [0x0112].pack("v") +
[3].pack("v") + [1].pack("V") + [orientation].pack("v") + "\0\0".b + [0].pack("V")
marker = "\xFF\xE1".b + [exif.bytesize + 2].pack("n") + exif
source = File.binread(source_path)
oriented_file = Tempfile.new(["oriented-#{orientation}", ".jpg"])
oriented_file.binmode
oriented_file.write(source.byteslice(0, 2) + marker + source.byteslice(2..))
oriented_file.rewind
yield oriented_file
ensure
oriented_file&.close!
end
def stored_color_grid(upload:, rows:, columns:, palette:)
stored_path = Discourse.store.path_for(upload)
Dir.mktmpdir do |directory|
png_path = File.join(directory, "stored.png")
ImageMagick.magick(stored_path, png_path, read: [stored_path], write: [directory])
stored_image = ChunkyPNG::Image.from_file(png_path)
Array.new(rows) do |row_index|
Array.new(columns) do |column_index|
x = (column_index * stored_image.width + stored_image.width / 2) / columns
y = (row_index * stored_image.height + stored_image.height / 2) / rows
nearest_palette_color(pixel: stored_image[x, y], palette: palette)
end
end
end
end
def nearest_palette_color(pixel:, palette:)
actual_rgb = [ChunkyPNG::Color.r(pixel), ChunkyPNG::Color.g(pixel), ChunkyPNG::Color.b(pixel)]
palette
.min_by do |_name, expected_rgb|
expected_rgb
.zip(actual_rgb)
.sum { |expected_channel, actual_channel| (expected_channel - actual_channel)**2 }
end
.first
end
end