Files
e8ebb84a7e Replace bild imaging library with boxes-ltd/imaging (#36261) (#36606)
* Add benchmarks for image transformation functions

Covers GeneratePreview, GenerateThumbnail, GenerateMiniPreviewImage,
FillCenter, and MakeImageUpright — all functions that delegate to the
external imaging library. Baseline before/after the library swap.

* Replace anthonynsimon/bild with boxes-ltd/imaging

Reverts the imaging library swap from PR #29657 (disintegration/imaging →
anthonynsimon/bild) and replaces it with github.com/boxes-ltd/imaging, the
maintained fork of disintegration/imaging with the same API.

- emoji.go: restore direct use of imaging.Fit with Lanczos filter
- orientation.go: restore FlipH/FlipV/Rotate90/180/270/Transpose/Transverse
- preview.go: restore imaging.Resize calls with Lanczos filter
- utils.go: remove bild-based Resize/Fit/CropCenter helpers; restore the
  original FillCenter using imaging.Fill
- Remove tests and test images added for the bild-specific helpers
- Regenerate orientation test expected images (2,3,6,7,8) and GIF thumbnail
  to match boxes-ltd/imaging output

* gofmt imaging package files

* Add tests and fixtures for the boxes-ltd/imaging-backed functions

Cover MakeImageUpright across all 8 EXIF orientations, GeneratePreview,
GenerateMiniPreviewImage, and FillCenter against fixtures regenerated
under the new library.

* Contain boxes-ltd/imaging behind the local imaging wrapper

Add a Fit wrapper alongside FillCenter and route emoji.go through the
local package so boxes-ltd/imaging is only imported from
channels/app/imaging.

* Add TestFit to cover the local Fit wrapper

Dimensional checks for the Fit wrapper used by emoji.go, mirroring
TestGenerateThumbnail. Pixel correctness is covered by the upstream
boxes-ltd/imaging tests; this guards against wrapper-level mistakes
(transposed args, wrong filter).

* Address CodeRabbit nits in TestFillCenter and TestFit

Decode a fresh source image per TestFillCenter subtest so cases stay
isolated even if the wrapper ever mutates input. Rename the TestFit
"smaller than bounds (clone)" case to "no resize when smaller than
bounds" since the assertion only checks dimensions, not clone semantics.

* Revert per-subtest decode in TestFillCenter

imaging.Fill never mutates its input — it always returns a fresh
*image.NRGBA — so re-decoding the source for every subtest was
unnecessary work. Decode once, share across subtests.

* Compare decoded pixels instead of raw PNG/JPEG bytes in tests

image/png isn't byte-stable across Go versions, so comparing
re-encoded byte streams against checked-in fixtures is brittle to
toolchain bumps and re-encoding. Decode both sides and compare RGBA
pixels via a shared requireSameImage helper. Fixtures stay on disk
unchanged.

Covers TestFillCenter, TestGeneratePreview, TestGenerateMiniPreviewImage,
and TestMakeImageUpright. TestFillImageTransparency is left alone since
its byte-comparison pattern predates this branch.

* Report total diff rate when requireSameImage fails

Walk every pixel before failing instead of stopping at the first
mismatch. The failure message now includes how many pixels differ, the
percentage of the image that's off, and the first divergence.

---------

(cherry picked from commit b868fbf8fa)

Co-authored-by: Jesse Hallam <[email protected]>

Co-authored-by: Hanzo Dev <[email protected]>
2026-05-19 19:15:09 +00:00

160 lines
3.9 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package imaging
import (
"errors"
"fmt"
"image"
"io"
"strings"
"github.com/bep/imagemeta"
"github.com/boxes-ltd/imaging"
)
const (
/*
EXIF Image Orientations
1 2 3 4 5 6 7 8
888888 888888 88 88 8888888888 88 88 8888888888
88 88 88 88 88 88 88 88 88 88 88 88
8888 8888 8888 8888 88 8888888888 8888888888 88
88 88 88 88
88 88 888888 888888
*/
Upright = iota + 1
UprightMirrored
UpsideDown
UpsideDownMirrored
RotatedCWMirrored
RotatedCCW
RotatedCCWMirrored
RotatedCW
)
var errStopDecoding = fmt.Errorf("stop decoding")
// MakeImageUpright changes the orientation of the given image.
func MakeImageUpright(img image.Image, orientation int) image.Image {
switch orientation {
case UprightMirrored:
return imaging.FlipH(img)
case UpsideDown:
return imaging.Rotate180(img)
case UpsideDownMirrored:
return imaging.FlipV(img)
case RotatedCWMirrored:
return imaging.Transpose(img)
case RotatedCCW:
return imaging.Rotate270(img)
case RotatedCCWMirrored:
return imaging.Transverse(img)
case RotatedCW:
return imaging.Rotate90(img)
default:
return img
}
}
type fwSeeker struct {
r io.Reader
pos int64
}
func (f *fwSeeker) Read(p []byte) (int, error) {
n, err := f.r.Read(p)
if err != nil {
return n, err
}
f.pos += int64(n)
return n, nil
}
func (f *fwSeeker) Seek(offset int64, whence int) (int64, error) {
isForwardSeek := (whence == io.SeekStart && offset >= f.pos) ||
(whence == io.SeekCurrent && offset >= 0)
// We only support seeking forward.
if !isForwardSeek {
return 0, fmt.Errorf("seeking backwards is not supported")
}
toRead := offset
if whence == io.SeekStart {
toRead -= f.pos
}
// Seeking forward means we can simply discard the data.
n, err := io.CopyN(io.Discard, f.r, toRead)
if err != nil {
return n, fmt.Errorf("failed to seek: %w", err)
}
f.pos += n
return f.pos, nil
}
// GetImageOrientation reads the input data and returns the EXIF encoded
// image orientation. Supported formats are JPEG, PNG, TIFF, and WebP.
// Passing an io.ReadSeeker is preferable as we can't guarantee a plain
// io.Reader will work for all formats (e.g. TIFF requires backwards seeking).
func GetImageOrientation(input io.Reader, format string) (int, error) {
orientation := Upright
// Strip the "image/" prefix from the format in case it's a MIME type.
format, _ = strings.CutPrefix(format, "image/")
var imgFormat imagemeta.ImageFormat
switch format {
case "jpeg":
imgFormat = imagemeta.JPEG
case "png":
imgFormat = imagemeta.PNG
case "tiff":
imgFormat = imagemeta.TIFF
case "webp":
imgFormat = imagemeta.WebP
default:
// We don't support EXIF on any other format.
return orientation, fmt.Errorf("unsupported image format: %s", format)
}
var rs io.ReadSeeker
if r, ok := input.(io.ReadSeeker); ok {
rs = r
} else {
rs = &fwSeeker{r: input}
}
opts := imagemeta.Options{
R: rs,
HandleTag: func(tag imagemeta.TagInfo) error {
if tag.Tag == "Orientation" {
if o, ok := tag.Value.(uint16); ok {
orientation = int(o)
// Stop decoding after we've found the orientation tag]
// since it's the only one we care about.
return errStopDecoding
}
}
return nil
},
ShouldHandleTag: func(tag imagemeta.TagInfo) bool {
// We only care about the orientation tag.
return tag.Tag == "Orientation"
},
Sources: imagemeta.EXIF, // We only care about EXIF data.
ImageFormat: imgFormat,
}
if err := imagemeta.Decode(opts); err != nil && !errors.Is(err, errStopDecoding) {
return Upright, fmt.Errorf("failed to decode exif data: %w", err)
}
return orientation, nil
}