LibGfx+LibWeb: Move EXIF orientation to rendering
|
@ -20,6 +20,26 @@ struct BackingStore {
|
|||
size_t size_in_bytes { 0 };
|
||||
};
|
||||
|
||||
IntRect Bitmap::rect() const
|
||||
{
|
||||
return { {}, size() };
|
||||
}
|
||||
|
||||
IntSize Bitmap::size() const
|
||||
{
|
||||
return exif_oriented_size(m_size, m_exif_orientation);
|
||||
}
|
||||
|
||||
int Bitmap::width() const
|
||||
{
|
||||
return size().width();
|
||||
}
|
||||
|
||||
int Bitmap::height() const
|
||||
{
|
||||
return size().height();
|
||||
}
|
||||
|
||||
size_t Bitmap::minimum_pitch(size_t width, BitmapFormat format)
|
||||
{
|
||||
size_t element_size;
|
||||
|
@ -49,19 +69,19 @@ static bool size_would_overflow(BitmapFormat format, IntSize size)
|
|||
return Checked<size_t>::multiplication_would_overflow(pitch, size.height());
|
||||
}
|
||||
|
||||
ErrorOr<NonnullRefPtr<Bitmap>> Bitmap::create(BitmapFormat format, IntSize size)
|
||||
ErrorOr<NonnullRefPtr<Bitmap>> Bitmap::create(BitmapFormat format, IntSize size, ExifOrientation orientation)
|
||||
{
|
||||
// For backwards compatibility, premultiplied alpha is assumed
|
||||
return create(format, AlphaType::Premultiplied, size);
|
||||
return create(format, AlphaType::Premultiplied, size, orientation);
|
||||
}
|
||||
|
||||
ErrorOr<NonnullRefPtr<Bitmap>> Bitmap::create(BitmapFormat format, AlphaType alpha_type, IntSize size)
|
||||
ErrorOr<NonnullRefPtr<Bitmap>> Bitmap::create(BitmapFormat format, AlphaType alpha_type, IntSize size, ExifOrientation orientation)
|
||||
{
|
||||
auto backing_store = TRY(Bitmap::allocate_backing_store(format, size));
|
||||
return AK::adopt_nonnull_ref_or_enomem(new (nothrow) Bitmap(format, alpha_type, size, backing_store));
|
||||
return AK::adopt_nonnull_ref_or_enomem(new (nothrow) Bitmap(format, alpha_type, size, backing_store, orientation));
|
||||
}
|
||||
|
||||
ErrorOr<NonnullRefPtr<Bitmap>> Bitmap::create_shareable(BitmapFormat format, AlphaType alpha_type, IntSize size)
|
||||
ErrorOr<NonnullRefPtr<Bitmap>> Bitmap::create_shareable(BitmapFormat format, AlphaType alpha_type, IntSize size, ExifOrientation orientation)
|
||||
{
|
||||
if (size_would_overflow(format, size))
|
||||
return Error::from_string_literal("Gfx::Bitmap::create_shareable size overflow");
|
||||
|
@ -70,16 +90,17 @@ ErrorOr<NonnullRefPtr<Bitmap>> Bitmap::create_shareable(BitmapFormat format, Alp
|
|||
auto const data_size = size_in_bytes(pitch, size.height());
|
||||
|
||||
auto buffer = TRY(Core::AnonymousBuffer::create_with_size(round_up_to_power_of_two(data_size, PAGE_SIZE)));
|
||||
auto bitmap = TRY(Bitmap::create_with_anonymous_buffer(format, alpha_type, buffer, size));
|
||||
auto bitmap = TRY(Bitmap::create_with_anonymous_buffer(format, alpha_type, buffer, size, orientation));
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
Bitmap::Bitmap(BitmapFormat format, AlphaType alpha_type, IntSize size, BackingStore const& backing_store)
|
||||
Bitmap::Bitmap(BitmapFormat format, AlphaType alpha_type, IntSize size, BackingStore const& backing_store, ExifOrientation orientation)
|
||||
: m_size(size)
|
||||
, m_data(backing_store.data)
|
||||
, m_pitch(backing_store.pitch)
|
||||
, m_format(format)
|
||||
, m_alpha_type(alpha_type)
|
||||
, m_exif_orientation(orientation)
|
||||
{
|
||||
VERIFY(!m_size.is_empty());
|
||||
VERIFY(!size_would_overflow(format, size));
|
||||
|
@ -90,41 +111,43 @@ Bitmap::Bitmap(BitmapFormat format, AlphaType alpha_type, IntSize size, BackingS
|
|||
};
|
||||
}
|
||||
|
||||
ErrorOr<NonnullRefPtr<Bitmap>> Bitmap::create_wrapper(BitmapFormat format, AlphaType alpha_type, IntSize size, size_t pitch, void* data, Function<void()>&& destruction_callback)
|
||||
ErrorOr<NonnullRefPtr<Bitmap>> Bitmap::create_wrapper(BitmapFormat format, AlphaType alpha_type, IntSize size, size_t pitch, void* data, Function<void()>&& destruction_callback, ExifOrientation orientation)
|
||||
{
|
||||
if (size_would_overflow(format, size))
|
||||
return Error::from_string_literal("Gfx::Bitmap::create_wrapper size overflow");
|
||||
return adopt_ref(*new Bitmap(format, alpha_type, size, pitch, data, move(destruction_callback)));
|
||||
return adopt_ref(*new Bitmap(format, alpha_type, size, pitch, data, move(destruction_callback), orientation));
|
||||
}
|
||||
|
||||
Bitmap::Bitmap(BitmapFormat format, AlphaType alpha_type, IntSize size, size_t pitch, void* data, Function<void()>&& destruction_callback)
|
||||
Bitmap::Bitmap(BitmapFormat format, AlphaType alpha_type, IntSize size, size_t pitch, void* data, Function<void()>&& destruction_callback, ExifOrientation orientation)
|
||||
: m_size(size)
|
||||
, m_data(data)
|
||||
, m_pitch(pitch)
|
||||
, m_format(format)
|
||||
, m_alpha_type(alpha_type)
|
||||
, m_destruction_callback(move(destruction_callback))
|
||||
, m_exif_orientation(orientation)
|
||||
{
|
||||
VERIFY(pitch >= minimum_pitch(size.width(), format));
|
||||
VERIFY(!size_would_overflow(format, size));
|
||||
// FIXME: assert that `data` is actually long enough!
|
||||
}
|
||||
|
||||
ErrorOr<NonnullRefPtr<Bitmap>> Bitmap::create_with_anonymous_buffer(BitmapFormat format, AlphaType alpha_type, Core::AnonymousBuffer buffer, IntSize size)
|
||||
ErrorOr<NonnullRefPtr<Bitmap>> Bitmap::create_with_anonymous_buffer(BitmapFormat format, AlphaType alpha_type, Core::AnonymousBuffer buffer, IntSize size, ExifOrientation orientation)
|
||||
{
|
||||
if (size_would_overflow(format, size))
|
||||
return Error::from_string_literal("Gfx::Bitmap::create_with_anonymous_buffer size overflow");
|
||||
|
||||
return adopt_nonnull_ref_or_enomem(new (nothrow) Bitmap(format, alpha_type, move(buffer), size));
|
||||
return adopt_nonnull_ref_or_enomem(new (nothrow) Bitmap(format, alpha_type, move(buffer), size, orientation));
|
||||
}
|
||||
|
||||
Bitmap::Bitmap(BitmapFormat format, AlphaType alpha_type, Core::AnonymousBuffer buffer, IntSize size)
|
||||
Bitmap::Bitmap(BitmapFormat format, AlphaType alpha_type, Core::AnonymousBuffer buffer, IntSize size, ExifOrientation orientation)
|
||||
: m_size(size)
|
||||
, m_data(buffer.data<void>())
|
||||
, m_pitch(minimum_pitch(size.width(), format))
|
||||
, m_format(format)
|
||||
, m_alpha_type(alpha_type)
|
||||
, m_buffer(move(buffer))
|
||||
, m_exif_orientation(orientation)
|
||||
{
|
||||
VERIFY(!size_would_overflow(format, size));
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
#include <LibCore/AnonymousBuffer.h>
|
||||
#include <LibGfx/Color.h>
|
||||
#include <LibGfx/Forward.h>
|
||||
#include <LibGfx/ImageOrientation.h>
|
||||
#include <LibGfx/Rect.h>
|
||||
|
||||
namespace Gfx {
|
||||
|
@ -64,11 +65,11 @@ struct BackingStore;
|
|||
|
||||
class Bitmap : public RefCounted<Bitmap> {
|
||||
public:
|
||||
[[nodiscard]] static ErrorOr<NonnullRefPtr<Bitmap>> create(BitmapFormat, IntSize);
|
||||
[[nodiscard]] static ErrorOr<NonnullRefPtr<Bitmap>> create(BitmapFormat, AlphaType, IntSize);
|
||||
[[nodiscard]] static ErrorOr<NonnullRefPtr<Bitmap>> create_shareable(BitmapFormat, AlphaType, IntSize);
|
||||
[[nodiscard]] static ErrorOr<NonnullRefPtr<Bitmap>> create_wrapper(BitmapFormat, AlphaType, IntSize, size_t pitch, void*, Function<void()>&& destruction_callback = {});
|
||||
[[nodiscard]] static ErrorOr<NonnullRefPtr<Bitmap>> create_with_anonymous_buffer(BitmapFormat, AlphaType, Core::AnonymousBuffer, IntSize);
|
||||
[[nodiscard]] static ErrorOr<NonnullRefPtr<Bitmap>> create(BitmapFormat, IntSize, ExifOrientation = ExifOrientation::Default);
|
||||
[[nodiscard]] static ErrorOr<NonnullRefPtr<Bitmap>> create(BitmapFormat, AlphaType, IntSize, ExifOrientation = ExifOrientation::Default);
|
||||
[[nodiscard]] static ErrorOr<NonnullRefPtr<Bitmap>> create_shareable(BitmapFormat, AlphaType, IntSize, ExifOrientation = ExifOrientation::Default);
|
||||
[[nodiscard]] static ErrorOr<NonnullRefPtr<Bitmap>> create_wrapper(BitmapFormat, AlphaType, IntSize, size_t pitch, void*, Function<void()>&& destruction_callback = {}, ExifOrientation = ExifOrientation::Default);
|
||||
[[nodiscard]] static ErrorOr<NonnullRefPtr<Bitmap>> create_with_anonymous_buffer(BitmapFormat, AlphaType, Core::AnonymousBuffer, IntSize, ExifOrientation = ExifOrientation::Default);
|
||||
|
||||
ErrorOr<NonnullRefPtr<Gfx::Bitmap>> clone() const;
|
||||
|
||||
|
@ -102,10 +103,11 @@ public:
|
|||
[[nodiscard]] ARGB32 const* end() const;
|
||||
[[nodiscard]] size_t data_size() const;
|
||||
|
||||
[[nodiscard]] IntRect rect() const { return { {}, m_size }; }
|
||||
[[nodiscard]] IntSize size() const { return m_size; }
|
||||
[[nodiscard]] int width() const { return m_size.width(); }
|
||||
[[nodiscard]] int height() const { return m_size.height(); }
|
||||
[[nodiscard]] IntRect rect() const;
|
||||
[[nodiscard]] IntSize size() const;
|
||||
[[nodiscard]] int width() const;
|
||||
[[nodiscard]] int height() const;
|
||||
[[nodiscard]] ExifOrientation exif_orientation() const { return m_exif_orientation; }
|
||||
|
||||
[[nodiscard]] size_t pitch() const { return m_pitch; }
|
||||
|
||||
|
@ -148,9 +150,9 @@ public:
|
|||
void set_alpha_type_destructive(AlphaType);
|
||||
|
||||
private:
|
||||
Bitmap(BitmapFormat, AlphaType, IntSize, BackingStore const&);
|
||||
Bitmap(BitmapFormat, AlphaType, IntSize, size_t pitch, void*, Function<void()>&& destruction_callback);
|
||||
Bitmap(BitmapFormat, AlphaType, Core::AnonymousBuffer, IntSize);
|
||||
Bitmap(BitmapFormat, AlphaType, IntSize, BackingStore const&, ExifOrientation);
|
||||
Bitmap(BitmapFormat, AlphaType, IntSize, size_t pitch, void*, Function<void()>&& destruction_callback, ExifOrientation);
|
||||
Bitmap(BitmapFormat, AlphaType, Core::AnonymousBuffer, IntSize, ExifOrientation);
|
||||
|
||||
static ErrorOr<BackingStore> allocate_backing_store(BitmapFormat format, IntSize size);
|
||||
|
||||
|
@ -161,6 +163,7 @@ private:
|
|||
AlphaType m_alpha_type { AlphaType::Premultiplied };
|
||||
Core::AnonymousBuffer m_buffer;
|
||||
Function<void()> m_destruction_callback;
|
||||
ExifOrientation m_exif_orientation;
|
||||
};
|
||||
|
||||
ALWAYS_INLINE u8* Bitmap::unchecked_scanline_u8(int y)
|
||||
|
|
|
@ -16,7 +16,7 @@ namespace Gfx {
|
|||
|
||||
static BitmapMetadata get_metadata(Bitmap const& bitmap)
|
||||
{
|
||||
return BitmapMetadata { .format = bitmap.format(), .alpha_type = bitmap.alpha_type(), .size = bitmap.size(), .size_in_bytes = bitmap.size_in_bytes() };
|
||||
return BitmapMetadata { .format = bitmap.format(), .alpha_type = bitmap.alpha_type(), .size = bitmap.size(), .exif_orientation = bitmap.exif_orientation(), .size_in_bytes = bitmap.size_in_bytes() };
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ ErrorOr<void> encode(Encoder& encoder, Gfx::BitmapMetadata const& metadata)
|
|||
TRY(encoder.encode(static_cast<u32>(metadata.alpha_type)));
|
||||
TRY(encoder.encode(metadata.size_in_bytes));
|
||||
TRY(encoder.encode(metadata.size));
|
||||
TRY(encoder.encode(static_cast<u32>(metadata.exif_orientation)));
|
||||
|
||||
return {};
|
||||
}
|
||||
|
@ -49,8 +50,12 @@ ErrorOr<Gfx::BitmapMetadata> decode(Decoder& decoder)
|
|||
|
||||
auto size_in_bytes = TRY(decoder.decode<size_t>());
|
||||
auto size = TRY(decoder.decode<Gfx::IntSize>());
|
||||
auto raw_exif_orientation = TRY(decoder.decode<u32>());
|
||||
if (!Gfx::is_valid_exif_orientation(raw_exif_orientation))
|
||||
return Error::from_string_literal("IPC: Invalid Gfx::BitmapSequence exif orientation");
|
||||
auto exif_orientation = static_cast<Gfx::ExifOrientation>(raw_exif_orientation);
|
||||
|
||||
return Gfx::BitmapMetadata { format, alpha_type, size, size_in_bytes };
|
||||
return Gfx::BitmapMetadata { format, alpha_type, size, exif_orientation, size_in_bytes };
|
||||
}
|
||||
|
||||
template<>
|
||||
|
@ -135,7 +140,7 @@ ErrorOr<Gfx::BitmapSequence> decode(Decoder& decoder)
|
|||
|
||||
bytes_read += size_in_bytes;
|
||||
|
||||
bitmap = TRY(Gfx::Bitmap::create_with_anonymous_buffer(metadata.format, metadata.alpha_type, move(buffer), metadata.size));
|
||||
bitmap = TRY(Gfx::Bitmap::create_with_anonymous_buffer(metadata.format, metadata.alpha_type, move(buffer), metadata.size, metadata.exif_orientation));
|
||||
}
|
||||
|
||||
bitmaps.append(bitmap);
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
#include <AK/RefPtr.h>
|
||||
#include <LibGfx/Bitmap.h>
|
||||
#include <LibGfx/ImageOrientation.h>
|
||||
#include <LibGfx/Size.h>
|
||||
#include <LibIPC/Forward.h>
|
||||
|
||||
|
@ -22,6 +23,7 @@ struct BitmapMetadata {
|
|||
Gfx::BitmapFormat format;
|
||||
Gfx::AlphaType alpha_type;
|
||||
Gfx::IntSize size;
|
||||
Gfx::ExifOrientation exif_orientation;
|
||||
size_t size_in_bytes;
|
||||
};
|
||||
|
||||
|
|
|
@ -58,6 +58,7 @@ set(SOURCES
|
|||
Triangle.cpp
|
||||
VectorGraphic.cpp
|
||||
SkiaBackendContext.cpp
|
||||
ImageOrientation.cpp
|
||||
)
|
||||
|
||||
set(SWIFT_EXCLUDE_HEADERS
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
#include <LibGfx/ImageFormats/PNGLoader.h>
|
||||
#include <LibGfx/ImageFormats/TIFFLoader.h>
|
||||
#include <LibGfx/ImageFormats/TIFFMetadata.h>
|
||||
#include <LibGfx/ImageOrientation.h>
|
||||
#include <LibGfx/ImmutableBitmap.h>
|
||||
#include <LibGfx/Painter.h>
|
||||
#include <png.h>
|
||||
|
@ -35,7 +36,6 @@ struct PNGLoadingContext {
|
|||
OwnPtr<ExifMetadata> exif_metadata;
|
||||
|
||||
ErrorOr<size_t> read_frames(png_structp, png_infop);
|
||||
ErrorOr<void> apply_exif_orientation();
|
||||
|
||||
ErrorOr<void> read_all_frames()
|
||||
{
|
||||
|
@ -48,8 +48,6 @@ struct PNGLoadingContext {
|
|||
|
||||
frame_count = TRY(read_frames(png_ptr, info_ptr));
|
||||
|
||||
if (exif_metadata)
|
||||
TRY(apply_exif_orientation());
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
@ -221,36 +219,13 @@ ErrorOr<void> PNGImageDecoderPlugin::initialize()
|
|||
return {};
|
||||
}
|
||||
|
||||
ErrorOr<void> PNGLoadingContext::apply_exif_orientation()
|
||||
{
|
||||
auto orientation = exif_metadata->orientation().value_or(TIFF::Orientation::Default);
|
||||
if (orientation == TIFF::Orientation::Default)
|
||||
return {};
|
||||
|
||||
for (auto& img_frame_descriptor : frame_descriptors) {
|
||||
auto& img = img_frame_descriptor.image;
|
||||
auto oriented_bmp = TRY(ExifOrientedBitmap::create(orientation, img->size(), img->format()));
|
||||
|
||||
for (int y = 0; y < img->size().height(); ++y) {
|
||||
for (int x = 0; x < img->size().width(); ++x) {
|
||||
auto pixel = img->get_pixel(x, y);
|
||||
oriented_bmp.set_pixel(x, y, pixel.value());
|
||||
}
|
||||
}
|
||||
|
||||
img_frame_descriptor.image = oriented_bmp.bitmap();
|
||||
}
|
||||
|
||||
size = ExifOrientedBitmap::oriented_size(size, orientation);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
ErrorOr<size_t> PNGLoadingContext::read_frames(png_structp png_ptr, png_infop info_ptr)
|
||||
{
|
||||
auto exif_orientation = exif_metadata ? static_cast<ExifOrientation>(exif_metadata->orientation().value()) : ExifOrientation::Default;
|
||||
|
||||
Vector<u8*> row_pointers;
|
||||
auto decode_frame = [&](IntSize frame_size) -> ErrorOr<NonnullRefPtr<Bitmap>> {
|
||||
auto frame_bitmap = TRY(Bitmap::create(BitmapFormat::BGRA8888, AlphaType::Unpremultiplied, frame_size));
|
||||
auto frame_bitmap = TRY(Bitmap::create(BitmapFormat::BGRA8888, AlphaType::Unpremultiplied, frame_size, exif_orientation));
|
||||
|
||||
row_pointers.resize_and_keep_capacity(frame_size.height());
|
||||
for (auto i = 0; i < frame_size.height(); ++i)
|
||||
|
@ -259,7 +234,7 @@ ErrorOr<size_t> PNGLoadingContext::read_frames(png_structp png_ptr, png_infop in
|
|||
png_read_image(png_ptr, row_pointers.data());
|
||||
return frame_bitmap;
|
||||
};
|
||||
|
||||
|
||||
if (png_get_acTL(png_ptr, info_ptr, &frame_count, &loop_count)) {
|
||||
// acTL chunk present: This is an APNG.
|
||||
png_set_acTL(png_ptr, info_ptr, frame_count, loop_count);
|
||||
|
|
28
Libraries/LibGfx/ImageOrientation.cpp
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright (c) 2025, Tuur Martens <tuurmartens4@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibGfx/ImageOrientation.h>
|
||||
|
||||
namespace Gfx {
|
||||
|
||||
bool is_valid_exif_orientation(u32 orientation)
|
||||
{
|
||||
switch (static_cast<Gfx::ExifOrientation>(orientation)) {
|
||||
case Gfx::ExifOrientation::Default:
|
||||
case Gfx::ExifOrientation::FlipHorizontally:
|
||||
case Gfx::ExifOrientation::Rotate180:
|
||||
case Gfx::ExifOrientation::FlipVertically:
|
||||
case Gfx::ExifOrientation::Rotate90ClockwiseThenFlipHorizontally:
|
||||
case Gfx::ExifOrientation::Rotate90Clockwise:
|
||||
case Gfx::ExifOrientation::FlipHorizontallyThenRotate90Clockwise:
|
||||
case Gfx::ExifOrientation::Rotate90CounterClockwise:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
101
Libraries/LibGfx/ImageOrientation.h
Normal file
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* Copyright (c) 2025, Tuur Martens <tuurmartens4@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Types.h>
|
||||
#include <LibGfx/AffineTransform.h>
|
||||
#include <LibGfx/Rect.h>
|
||||
#include <cmath>
|
||||
|
||||
namespace Gfx {
|
||||
|
||||
enum class ExifOrientation : u8 {
|
||||
Default = 1,
|
||||
FlipHorizontally = 2,
|
||||
Rotate180 = 3,
|
||||
FlipVertically = 4,
|
||||
Rotate90ClockwiseThenFlipHorizontally = 5,
|
||||
Rotate90Clockwise = 6,
|
||||
FlipHorizontallyThenRotate90Clockwise = 7,
|
||||
Rotate90CounterClockwise = 8,
|
||||
};
|
||||
|
||||
[[nodiscard]]
|
||||
bool is_valid_exif_orientation(u32 orientation);
|
||||
|
||||
template<typename T>
|
||||
[[nodiscard]]
|
||||
Gfx::AffineTransform compute_exif_orientation_matrix(Gfx::ExifOrientation orientation, Gfx::Rect<T> const& dst_rect)
|
||||
{
|
||||
Gfx::AffineTransform matrix;
|
||||
|
||||
switch (orientation) {
|
||||
case Gfx::ExifOrientation::Default:
|
||||
return matrix;
|
||||
case Gfx::ExifOrientation::FlipHorizontally:
|
||||
matrix.set_translation(dst_rect.width() / 2, 0);
|
||||
matrix.set_scale(-1, 1);
|
||||
matrix.translate(-dst_rect.width() / 2.f, 0);
|
||||
break;
|
||||
case Gfx::ExifOrientation::Rotate180:
|
||||
matrix.set_translation(dst_rect.width(), dst_rect.height());
|
||||
matrix.rotate_radians(M_PI);
|
||||
break;
|
||||
case Gfx::ExifOrientation::FlipVertically:
|
||||
matrix.set_translation(0, dst_rect.height() / 2);
|
||||
matrix.set_scale(1, -1);
|
||||
matrix.translate(0, -dst_rect.height() / 2);
|
||||
break;
|
||||
case Gfx::ExifOrientation::Rotate90ClockwiseThenFlipHorizontally:
|
||||
matrix.set_translation(dst_rect.height(), 0);
|
||||
matrix.rotate_radians(-M_PI / 2.);
|
||||
matrix.translate(0, -dst_rect.height());
|
||||
matrix.scale(-1, 1);
|
||||
break;
|
||||
case Gfx::ExifOrientation::Rotate90Clockwise:
|
||||
matrix.set_translation(dst_rect.width(), 0);
|
||||
matrix.rotate_radians(M_PI / 2.);
|
||||
break;
|
||||
case Gfx::ExifOrientation::FlipHorizontallyThenRotate90Clockwise:
|
||||
// We translate by the dst_rect.height(), which will be the new dst_rect.width() of the image.
|
||||
matrix.set_translation(dst_rect.width(), 0);
|
||||
matrix.rotate_radians(M_PI / 2.);
|
||||
// We translate by the old dst_rect.height() to move the image back to the origin.
|
||||
matrix.translate(dst_rect.height(), 0);
|
||||
matrix.scale(-1, 1);
|
||||
break;
|
||||
case Gfx::ExifOrientation::Rotate90CounterClockwise:
|
||||
matrix.translate(0, dst_rect.height());
|
||||
matrix.rotate_radians(-M_PI / 2);
|
||||
break;
|
||||
default:
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
return matrix;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
[[nodiscard]]
|
||||
static Size<T> exif_oriented_size(Size<T> const& size, Gfx::ExifOrientation orientation)
|
||||
{
|
||||
switch (orientation) {
|
||||
case Gfx::ExifOrientation::Default:
|
||||
case Gfx::ExifOrientation::FlipHorizontally:
|
||||
case Gfx::ExifOrientation::Rotate180:
|
||||
case Gfx::ExifOrientation::FlipVertically:
|
||||
return size;
|
||||
case Gfx::ExifOrientation::Rotate90ClockwiseThenFlipHorizontally:
|
||||
case Gfx::ExifOrientation::Rotate90Clockwise:
|
||||
case Gfx::ExifOrientation::FlipHorizontallyThenRotate90Clockwise:
|
||||
case Gfx::ExifOrientation::Rotate90CounterClockwise:
|
||||
return { size.height(), size.width() };
|
||||
}
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
}
|
|
@ -23,12 +23,22 @@ struct ImmutableBitmapImpl {
|
|||
|
||||
int ImmutableBitmap::width() const
|
||||
{
|
||||
return m_impl->sk_image->width();
|
||||
auto const* bitmap = m_impl->source.get_pointer<NonnullRefPtr<Gfx::Bitmap>>();
|
||||
|
||||
if (!bitmap)
|
||||
return m_impl->sk_image->width();
|
||||
|
||||
return (*bitmap)->width();
|
||||
}
|
||||
|
||||
int ImmutableBitmap::height() const
|
||||
{
|
||||
return m_impl->sk_image->height();
|
||||
auto const* bitmap = m_impl->source.get_pointer<NonnullRefPtr<Gfx::Bitmap>>();
|
||||
|
||||
if (!bitmap)
|
||||
return m_impl->sk_image->height();
|
||||
|
||||
return (*bitmap)->height();
|
||||
}
|
||||
|
||||
IntRect ImmutableBitmap::rect() const
|
||||
|
@ -57,6 +67,14 @@ RefPtr<Gfx::Bitmap const> ImmutableBitmap::bitmap() const
|
|||
return m_impl->source.get<NonnullRefPtr<Gfx::Bitmap>>();
|
||||
}
|
||||
|
||||
ExifOrientation ImmutableBitmap::get_exif_orientation() const
|
||||
{
|
||||
if (auto const* bitmap = m_impl->source.get_pointer<NonnullRefPtr<Gfx::Bitmap>>())
|
||||
return (*bitmap)->exif_orientation();
|
||||
|
||||
return ExifOrientation::Default;
|
||||
}
|
||||
|
||||
Color ImmutableBitmap::get_pixel(int x, int y) const
|
||||
{
|
||||
// FIXME: Implement for PaintingSurface
|
||||
|
|
|
@ -41,6 +41,9 @@ public:
|
|||
|
||||
RefPtr<Bitmap const> bitmap() const;
|
||||
|
||||
[[nodiscard]]
|
||||
ExifOrientation get_exif_orientation() const;
|
||||
|
||||
private:
|
||||
NonnullOwnPtr<ImmutableBitmapImpl> m_impl;
|
||||
|
||||
|
|
|
@ -142,6 +142,21 @@ void PainterSkia::fill_rect(Gfx::FloatRect const& rect, Color color)
|
|||
});
|
||||
}
|
||||
|
||||
static SkMatrix to_skia_matrix(Gfx::AffineTransform const& affine_transform)
|
||||
{
|
||||
SkScalar affine[6];
|
||||
affine[0] = affine_transform.a();
|
||||
affine[1] = affine_transform.b();
|
||||
affine[2] = affine_transform.c();
|
||||
affine[3] = affine_transform.d();
|
||||
affine[4] = affine_transform.e();
|
||||
affine[5] = affine_transform.f();
|
||||
|
||||
SkMatrix matrix;
|
||||
matrix.setAffine(affine);
|
||||
return matrix;
|
||||
}
|
||||
|
||||
void PainterSkia::draw_bitmap(Gfx::FloatRect const& dst_rect, Gfx::ImmutableBitmap const& src_bitmap, Gfx::IntRect const& src_rect, Gfx::ScalingMode scaling_mode, ReadonlySpan<Gfx::Filter> filters, float global_alpha, Gfx::CompositingAndBlendingOperator compositing_and_blending_operator)
|
||||
{
|
||||
SkPaint paint;
|
||||
|
@ -150,13 +165,24 @@ void PainterSkia::draw_bitmap(Gfx::FloatRect const& dst_rect, Gfx::ImmutableBitm
|
|||
paint.setBlender(to_skia_blender(compositing_and_blending_operator));
|
||||
|
||||
impl().with_canvas([&](auto& canvas) {
|
||||
auto oriented_dst_rect = dst_rect;
|
||||
|
||||
auto const transformation_matrix = compute_exif_orientation_matrix(src_bitmap.get_exif_orientation(), oriented_dst_rect);
|
||||
auto const skia_transformation_matrix = to_skia_matrix(transformation_matrix);
|
||||
|
||||
canvas.save();
|
||||
canvas.translate(oriented_dst_rect.x(), oriented_dst_rect.y());
|
||||
canvas.concat(skia_transformation_matrix);
|
||||
canvas.translate(-oriented_dst_rect.x(), -oriented_dst_rect.y());
|
||||
|
||||
canvas.drawImageRect(
|
||||
src_bitmap.sk_image(),
|
||||
to_skia_rect(src_rect),
|
||||
to_skia_rect(dst_rect),
|
||||
to_skia_rect(oriented_dst_rect),
|
||||
to_skia_sampling_options(scaling_mode),
|
||||
&paint,
|
||||
SkCanvas::kStrict_SrcRectConstraint);
|
||||
canvas.restore();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -161,6 +161,13 @@ WebIDL::ExceptionOr<void> CanvasRenderingContext2D::draw_image_internal(CanvasIm
|
|||
// When the source rectangle is outside the source image, the source rectangle must be clipped
|
||||
// to the source image and the destination rectangle must be clipped in the same proportion.
|
||||
auto clipped_source = source_rect.intersected(bitmap->rect().to_type<float>());
|
||||
|
||||
if (bitmap->bitmap()) {
|
||||
auto const exif_orientation = bitmap->get_exif_orientation();
|
||||
auto const oriented_size = Gfx::exif_oriented_size(destination_rect.size(), exif_orientation);
|
||||
destination_rect.set_size(oriented_size);
|
||||
}
|
||||
|
||||
auto clipped_destination = destination_rect;
|
||||
if (clipped_source != source_rect) {
|
||||
clipped_destination.set_width(clipped_destination.width() * (clipped_source.width() / source_rect.width()));
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
#include <AK/NonnullRefPtr.h>
|
||||
#include <AK/Utf8View.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <LibGfx/AffineTransform.h>
|
||||
#include <LibGfx/Color.h>
|
||||
#include <LibGfx/CompositingAndBlendingOperator.h>
|
||||
#include <LibGfx/Forward.h>
|
||||
|
@ -74,6 +75,8 @@ struct DrawScaledImmutableBitmap {
|
|||
Gfx::IntRect clip_rect;
|
||||
NonnullRefPtr<Gfx::ImmutableBitmap const> bitmap;
|
||||
Gfx::ScalingMode scaling_mode;
|
||||
Gfx::AffineTransform transformation_matrix;
|
||||
bool rect_correction_necessary;
|
||||
|
||||
[[nodiscard]] Gfx::IntRect bounding_rect() const { return clip_rect; }
|
||||
void translate_by(Gfx::IntPoint const& offset)
|
||||
|
@ -91,9 +94,11 @@ struct DrawRepeatedImmutableBitmap {
|
|||
|
||||
Gfx::IntRect dst_rect;
|
||||
Gfx::IntRect clip_rect;
|
||||
Gfx::IntSize src_size;
|
||||
NonnullRefPtr<Gfx::ImmutableBitmap const> bitmap;
|
||||
Gfx::ScalingMode scaling_mode;
|
||||
Repeat repeat;
|
||||
Gfx::AffineTransform transformation_matrix;
|
||||
|
||||
void translate_by(Gfx::IntPoint const& offset) { dst_rect.translate_by(offset); }
|
||||
};
|
||||
|
|
|
@ -53,21 +53,6 @@ static SkRRect to_skia_rrect(auto const& rect, CornerRadii const& corner_radii)
|
|||
return rrect;
|
||||
}
|
||||
|
||||
static SkMatrix to_skia_matrix(Gfx::AffineTransform const& affine_transform)
|
||||
{
|
||||
SkScalar affine[6];
|
||||
affine[0] = affine_transform.a();
|
||||
affine[1] = affine_transform.b();
|
||||
affine[2] = affine_transform.c();
|
||||
affine[3] = affine_transform.d();
|
||||
affine[4] = affine_transform.e();
|
||||
affine[5] = affine_transform.f();
|
||||
|
||||
SkMatrix matrix;
|
||||
matrix.setAffine(affine);
|
||||
return matrix;
|
||||
}
|
||||
|
||||
void DisplayListPlayerSkia::flush()
|
||||
{
|
||||
if (m_context)
|
||||
|
@ -133,15 +118,49 @@ void DisplayListPlayerSkia::draw_painting_surface(DrawPaintingSurface const& com
|
|||
canvas.drawImageRect(image, src_rect, dst_rect, to_skia_sampling_options(command.scaling_mode), &paint, SkCanvas::kStrict_SrcRectConstraint);
|
||||
}
|
||||
|
||||
static SkMatrix to_skia_matrix(Gfx::AffineTransform const& affine_transform)
|
||||
{
|
||||
SkScalar affine[6];
|
||||
affine[0] = affine_transform.a();
|
||||
affine[1] = affine_transform.b();
|
||||
affine[2] = affine_transform.c();
|
||||
affine[3] = affine_transform.d();
|
||||
affine[4] = affine_transform.e();
|
||||
affine[5] = affine_transform.f();
|
||||
|
||||
SkMatrix matrix;
|
||||
matrix.setAffine(affine);
|
||||
return matrix;
|
||||
}
|
||||
|
||||
void DisplayListPlayerSkia::draw_scaled_immutable_bitmap(DrawScaledImmutableBitmap const& command)
|
||||
{
|
||||
auto dst_rect = to_skia_rect(command.dst_rect);
|
||||
// Our dst_rect is already correctly rotated, but since we rotate our canvas, our dst_rect now applies to a rotated canvas.
|
||||
// This means our once correct rect now needs to be rotated 90deg *again* for it to be correct.
|
||||
// This is equivalent to swapping the width and height of the rect.
|
||||
// Of course, we don't *always* rotate, so only perform this correction if we are rotating.
|
||||
auto cmd_dst_rect = command.dst_rect;
|
||||
if (command.rect_correction_necessary) {
|
||||
cmd_dst_rect.set_size(cmd_dst_rect.height(), cmd_dst_rect.width());
|
||||
}
|
||||
|
||||
auto dst_rect = to_skia_rect(cmd_dst_rect);
|
||||
auto clip_rect = to_skia_rect(command.clip_rect);
|
||||
|
||||
auto& canvas = surface().canvas();
|
||||
SkPaint paint;
|
||||
auto skia_transformation_matrix { to_skia_matrix(command.transformation_matrix) };
|
||||
|
||||
canvas.save();
|
||||
// We clip first so we don't transform the clip rect.
|
||||
canvas.clipRect(clip_rect);
|
||||
|
||||
canvas.translate(dst_rect.x(), dst_rect.y());
|
||||
canvas.concat(skia_transformation_matrix);
|
||||
canvas.translate(-dst_rect.x(), -dst_rect.y());
|
||||
|
||||
SkPaint const paint;
|
||||
canvas.drawImageRect(command.bitmap->sk_image(), dst_rect, to_skia_sampling_options(command.scaling_mode), &paint);
|
||||
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
|
@ -149,9 +168,13 @@ void DisplayListPlayerSkia::draw_repeated_immutable_bitmap(DrawRepeatedImmutable
|
|||
{
|
||||
SkMatrix matrix;
|
||||
auto dst_rect = command.dst_rect.to_type<float>();
|
||||
auto src_size = command.bitmap->size().to_type<float>();
|
||||
auto src_size = command.src_size.to_type<float>();
|
||||
matrix.setScale(dst_rect.width() / src_size.width(), dst_rect.height() / src_size.height());
|
||||
|
||||
auto const skia_transformation_matrix { to_skia_matrix(command.transformation_matrix) };
|
||||
matrix.postConcat(skia_transformation_matrix);
|
||||
matrix.postTranslate(dst_rect.x(), dst_rect.y());
|
||||
|
||||
auto sampling_options = to_skia_sampling_options(command.scaling_mode);
|
||||
|
||||
auto tile_mode_x = command.repeat.x ? SkTileMode::kRepeat : SkTileMode::kDecal;
|
||||
|
|
|
@ -194,26 +194,57 @@ void DisplayListRecorder::draw_painting_surface(Gfx::IntRect const& dst_rect, No
|
|||
});
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
static bool is_rect_correction_necessary(Gfx::ExifOrientation exif_orientation)
|
||||
{
|
||||
switch (exif_orientation) {
|
||||
case Gfx::ExifOrientation::Rotate90Clockwise:
|
||||
case Gfx::ExifOrientation::Rotate90CounterClockwise:
|
||||
case Gfx::ExifOrientation::FlipHorizontallyThenRotate90Clockwise:
|
||||
case Gfx::ExifOrientation::Rotate90ClockwiseThenFlipHorizontally:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void DisplayListRecorder::draw_scaled_immutable_bitmap(Gfx::IntRect const& dst_rect, Gfx::IntRect const& clip_rect, Gfx::ImmutableBitmap const& bitmap, Gfx::ScalingMode scaling_mode)
|
||||
{
|
||||
if (dst_rect.is_empty())
|
||||
return;
|
||||
|
||||
auto const exif_orientation = bitmap.get_exif_orientation();
|
||||
auto const rect_correction_necessary = is_rect_correction_necessary(exif_orientation);
|
||||
|
||||
auto const transformation_matrix = compute_exif_orientation_matrix(exif_orientation, dst_rect);
|
||||
|
||||
append(DrawScaledImmutableBitmap {
|
||||
.dst_rect = dst_rect,
|
||||
.clip_rect = clip_rect,
|
||||
.bitmap = bitmap,
|
||||
.scaling_mode = scaling_mode,
|
||||
.transformation_matrix = transformation_matrix,
|
||||
.rect_correction_necessary = rect_correction_necessary,
|
||||
});
|
||||
}
|
||||
|
||||
void DisplayListRecorder::draw_repeated_immutable_bitmap(Gfx::IntRect dst_rect, Gfx::IntRect clip_rect, NonnullRefPtr<Gfx::ImmutableBitmap const> bitmap, Gfx::ScalingMode scaling_mode, DrawRepeatedImmutableBitmap::Repeat repeat)
|
||||
{
|
||||
if (dst_rect.is_empty())
|
||||
return;
|
||||
|
||||
auto const exif_orientation = bitmap->get_exif_orientation();
|
||||
auto const transformation_matrix = compute_exif_orientation_matrix(exif_orientation, dst_rect);
|
||||
auto const size = bitmap->size();
|
||||
|
||||
append(DrawRepeatedImmutableBitmap {
|
||||
.dst_rect = dst_rect,
|
||||
.clip_rect = clip_rect,
|
||||
.src_size = size,
|
||||
.bitmap = move(bitmap),
|
||||
.scaling_mode = scaling_mode,
|
||||
.repeat = repeat,
|
||||
.transformation_matrix = transformation_matrix,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -352,21 +352,6 @@ TEST_CASE(test_png)
|
|||
TRY_OR_FAIL(expect_single_frame(*plugin_decoder));
|
||||
}
|
||||
|
||||
TEST_CASE(test_exif)
|
||||
{
|
||||
auto file = TRY_OR_FAIL(Core::MappedFile::map(TEST_INPUT("png/exif.png"sv)));
|
||||
EXPECT(Gfx::PNGImageDecoderPlugin::sniff(file->bytes()));
|
||||
auto plugin_decoder = TRY_OR_FAIL(Gfx::PNGImageDecoderPlugin::create(file->bytes()));
|
||||
|
||||
auto frame = TRY_OR_FAIL(expect_single_frame_of_size(*plugin_decoder, { 200, 100 }));
|
||||
EXPECT(plugin_decoder->metadata().has_value());
|
||||
auto const& exif_metadata = static_cast<Gfx::ExifMetadata const&>(plugin_decoder->metadata().value());
|
||||
EXPECT_EQ(*exif_metadata.orientation(), Gfx::TIFF::Orientation::Rotate90Clockwise);
|
||||
|
||||
EXPECT_EQ(frame.image->get_pixel(65, 70), Gfx::Color(0, 255, 0));
|
||||
EXPECT_EQ(frame.image->get_pixel(190, 10), Gfx::Color(255, 0, 0));
|
||||
}
|
||||
|
||||
TEST_CASE(test_png_malformed_frame)
|
||||
{
|
||||
Array test_inputs = {
|
||||
|
|
After Width: | Height: | Size: 870 B |
After Width: | Height: | Size: 870 B |
After Width: | Height: | Size: 870 B |
After Width: | Height: | Size: 870 B |
After Width: | Height: | Size: 870 B |
After Width: | Height: | Size: 870 B |
After Width: | Height: | Size: 870 B |
After Width: | Height: | Size: 870 B |
After Width: | Height: | Size: 870 B |
|
@ -0,0 +1,11 @@
|
|||
<!DOCTYPE html>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: white;
|
||||
}
|
||||
</style>
|
||||
<img src="../images/exif-orientation-png.png">
|
BIN
Tests/LibWeb/Screenshot/images/exif-orientation-png.png
Normal file
After Width: | Height: | Size: 40 KiB |
62
Tests/LibWeb/Screenshot/input/exif-orientation-png.html
Normal file
|
@ -0,0 +1,62 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>CSS Images Module Level 3: image-orientation: from-image</title>
|
||||
<link rel="author" title="Stephen Chenney" href="mailto:schenney@chromium.org">
|
||||
<link rel="help" href="https://drafts.csswg.org/css-images-3/#propdef-image-orientation">
|
||||
<meta name="fuzzy" content="maxDifference=0-100; totalPixels=0-236">
|
||||
<link rel="match" href="../expected/exif-orientation-png-ref.html" />
|
||||
<style>
|
||||
body {
|
||||
overflow: hidden;
|
||||
image-orientation: from-image;
|
||||
}
|
||||
div {
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
vertical-align: top;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p>The images should rotate respecting their EXIF orientation because
|
||||
image-orientation: from-image is specified.</p>
|
||||
<div>
|
||||
<img src="../data/exif-orientation-png/exif-orientation-1-ul.png"/>
|
||||
<br>Normal
|
||||
</div>
|
||||
<div>
|
||||
<img src="../data/exif-orientation-png/exif-orientation-2-ur.png"/>
|
||||
<br>Flipped horizontally
|
||||
</div>
|
||||
<div>
|
||||
<img src="../data/exif-orientation-png/exif-orientation-3-lr.png"/>
|
||||
<br>Rotated 180°
|
||||
</div>
|
||||
<div>
|
||||
<img src="../data/exif-orientation-png/exif-orientation-4-lol.png"/>
|
||||
<br>Flipped vertically
|
||||
</div>
|
||||
<div>
|
||||
<img src="../data/exif-orientation-png/exif-orientation-5-lu.png"/>
|
||||
<br>Rotated 90° CCW and flipped vertically
|
||||
</div>
|
||||
<div>
|
||||
<img src="../data/exif-orientation-png/exif-orientation-6-ru.png"/>
|
||||
<br>Rotated 90° CW
|
||||
</div>
|
||||
<div>
|
||||
<img src="../data/exif-orientation-png/exif-orientation-7-rl.png"/>
|
||||
<br>Rotated 90° CW and flipped vertically
|
||||
</div>
|
||||
<div>
|
||||
<img src="../data/exif-orientation-png/exif-orientation-8-llo.png"/>
|
||||
<br>Rotated 90° CCW
|
||||
</div>
|
||||
<div>
|
||||
<img src="../data/exif-orientation-png/exif-orientation-9-u.png"/>
|
||||
<br>Undefined (invalid value)
|
||||
</div>
|
||||
</body>
|
||||
</html>
|