diff options
Diffstat (limited to 'Userland/Libraries/LibGfx')
83 files changed, 16870 insertions, 0 deletions
diff --git a/Userland/Libraries/LibGfx/AffineTransform.cpp b/Userland/Libraries/LibGfx/AffineTransform.cpp new file mode 100644 index 0000000000..29236b83f0 --- /dev/null +++ b/Userland/Libraries/LibGfx/AffineTransform.cpp @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <AK/LogStream.h> +#include <AK/Optional.h> +#include <LibGfx/AffineTransform.h> +#include <LibGfx/Rect.h> + +namespace Gfx { + +bool AffineTransform::is_identity() const +{ + return m_values[0] == 1 && m_values[1] == 0 && m_values[2] == 0 && m_values[3] == 1 && m_values[4] == 0 && m_values[5] == 0; +} + +static float hypotenuse(float x, float y) +{ + // FIXME: This won't handle overflow :( + return sqrt(x * x + y * y); +} + +float AffineTransform::x_scale() const +{ + return hypotenuse(m_values[0], m_values[1]); +} + +float AffineTransform::y_scale() const +{ + return hypotenuse(m_values[2], m_values[3]); +} + +AffineTransform& AffineTransform::scale(float sx, float sy) +{ + m_values[0] *= sx; + m_values[1] *= sx; + m_values[2] *= sy; + m_values[3] *= sy; + return *this; +} + +AffineTransform& AffineTransform::translate(float tx, float ty) +{ + m_values[4] += tx * m_values[0] + ty * m_values[2]; + m_values[5] += tx * m_values[1] + ty * m_values[3]; + return *this; +} + +AffineTransform& AffineTransform::multiply(const AffineTransform& other) +{ + AffineTransform result; + result.m_values[0] = other.a() * a() + other.b() * c(); + result.m_values[1] = other.a() * b() + other.b() * d(); + result.m_values[2] = other.c() * a() + other.d() * c(); + result.m_values[3] = other.c() * b() + other.d() * d(); + result.m_values[4] = other.e() * a() + other.f() * c() + e(); + result.m_values[5] = other.e() * b() + other.f() * d() + f(); + *this = result; + return *this; +} + +AffineTransform& AffineTransform::rotate_radians(float radians) +{ + float sin_angle = sinf(radians); + float cos_angle = cosf(radians); + AffineTransform rotation(cos_angle, sin_angle, -sin_angle, cos_angle, 0, 0); + multiply(rotation); + return *this; +} + +void AffineTransform::map(float unmapped_x, float unmapped_y, float& mapped_x, float& mapped_y) const +{ + mapped_x = (m_values[0] * unmapped_x + m_values[2] * unmapped_y + m_values[4]); + mapped_y = (m_values[1] * unmapped_x + m_values[3] * unmapped_y + m_values[5]); +} + +template<> +IntPoint AffineTransform::map(const IntPoint& point) const +{ + float mapped_x; + float mapped_y; + map(point.x(), point.y(), mapped_x, mapped_y); + return { roundf(mapped_x), roundf(mapped_y) }; +} + +template<> +FloatPoint AffineTransform::map(const FloatPoint& point) const +{ + float mapped_x; + float mapped_y; + map(point.x(), point.y(), mapped_x, mapped_y); + return { mapped_x, mapped_y }; +} + +template<> +IntSize AffineTransform::map(const IntSize& size) const +{ + return { roundf(size.width() * x_scale()), roundf(size.height() * y_scale()) }; +} + +template<> +FloatSize AffineTransform::map(const FloatSize& size) const +{ + return { size.width() * x_scale(), size.height() * y_scale() }; +} + +template<typename T> +static T smallest_of(T p1, T p2, T p3, T p4) +{ + return min(min(p1, p2), min(p3, p4)); +} + +template<typename T> +static T largest_of(T p1, T p2, T p3, T p4) +{ + return max(max(p1, p2), max(p3, p4)); +} + +template<> +FloatRect AffineTransform::map(const FloatRect& rect) const +{ + FloatPoint p1 = map(rect.top_left()); + FloatPoint p2 = map(rect.top_right().translated(1, 0)); + FloatPoint p3 = map(rect.bottom_right().translated(1, 1)); + FloatPoint p4 = map(rect.bottom_left().translated(0, 1)); + float left = smallest_of(p1.x(), p2.x(), p3.x(), p4.x()); + float top = smallest_of(p1.y(), p2.y(), p3.y(), p4.y()); + float right = largest_of(p1.x(), p2.x(), p3.x(), p4.x()); + float bottom = largest_of(p1.y(), p2.y(), p3.y(), p4.y()); + return { left, top, right - left, bottom - top }; +} + +template<> +IntRect AffineTransform::map(const IntRect& rect) const +{ + return enclosing_int_rect(map(FloatRect(rect))); +} + +const LogStream& operator<<(const LogStream& stream, const AffineTransform& value) +{ + if (value.is_identity()) + return stream << "{ Identity }"; + + return stream << "{ " + << value.a() << ", " + << value.b() << ", " + << value.c() << ", " + << value.d() << ", " + << value.e() << ", " + << value.f() << " }"; +} + +} diff --git a/Userland/Libraries/LibGfx/AffineTransform.h b/Userland/Libraries/LibGfx/AffineTransform.h new file mode 100644 index 0000000000..41334eb138 --- /dev/null +++ b/Userland/Libraries/LibGfx/AffineTransform.h @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Forward.h> +#include <AK/LogStream.h> +#include <LibGfx/Forward.h> + +namespace Gfx { + +class AffineTransform { +public: + AffineTransform() + : m_values { 1, 0, 0, 1, 0, 0 } + { + } + + AffineTransform(float a, float b, float c, float d, float e, float f) + : m_values { a, b, c, d, e, f } + { + } + + bool is_identity() const; + + void map(float unmapped_x, float unmapped_y, float& mapped_x, float& mapped_y) const; + + template<typename T> + Point<T> map(const Point<T>&) const; + + template<typename T> + Size<T> map(const Size<T>&) const; + + template<typename T> + Rect<T> map(const Rect<T>&) const; + + float a() const { return m_values[0]; } + float b() const { return m_values[1]; } + float c() const { return m_values[2]; } + float d() const { return m_values[3]; } + float e() const { return m_values[4]; } + float f() const { return m_values[5]; } + + float x_scale() const; + float y_scale() const; + + AffineTransform& scale(float sx, float sy); + AffineTransform& translate(float tx, float ty); + AffineTransform& rotate_radians(float); + AffineTransform& multiply(const AffineTransform&); + +private: + float m_values[6] { 0 }; +}; + +const LogStream& operator<<(const LogStream&, const AffineTransform&); + +} diff --git a/Userland/Libraries/LibGfx/BMPLoader.cpp b/Userland/Libraries/LibGfx/BMPLoader.cpp new file mode 100644 index 0000000000..2b178f0257 --- /dev/null +++ b/Userland/Libraries/LibGfx/BMPLoader.cpp @@ -0,0 +1,1400 @@ +/* + * Copyright (c) 2020, Matthew Olsson <matthewcolsson@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <AK/Function.h> +#include <AK/LexicalPath.h> +#include <AK/MappedFile.h> +#include <LibGfx/BMPLoader.h> + +#ifndef BMP_DEBUG +# define BMP_DEBUG 0 +#endif + +#define IF_BMP_DEBUG(x) \ + if (BMP_DEBUG) \ + x + +namespace Gfx { + +const u8 bmp_header_size = 14; +const u32 color_palette_limit = 1024; + +// Compression flags +struct Compression { + enum : u32 { + RGB = 0, + RLE8, + RLE4, + BITFIELDS, + RLE24, // doubles as JPEG for V4+, but that is unsupported + PNG, + ALPHABITFIELDS, + CMYK = 11, + CMYKRLE8, + CMYKRLE4, + }; +}; + +struct DIBCore { + // u16 for BITMAPHEADERCORE, but i32 for everything else. If the dib type is + // BITMAPHEADERCORE, this is range checked. + i32 width; + i32 height; + u16 bpp; +}; + +struct DIBInfo { + u32 compression { Compression::RGB }; + u32 image_size { 0 }; + i32 horizontal_resolution { 0 }; + i32 vertical_resolution { 0 }; + u32 number_of_palette_colors { 0 }; + u32 number_of_important_palette_colors { number_of_palette_colors }; + + // Introduced in the BITMAPV2INFOHEADER and would ideally be stored in the + // DIBV2 struct, however with a compression value of BI_BITFIELDS or + // BI_ALPHABITFIELDS, these can be specified with the Info header. + Vector<u32> masks; + Vector<i8> mask_shifts; + Vector<u8> mask_sizes; +}; + +struct DIBOSV2 { + u16 recording; + u16 halftoning; + u16 size1; + u16 size2; +}; + +template<typename T> +struct Endpoint { + T x; + T y; + T z; +}; + +struct DIBV4 { + u32 color_space { 0 }; + Endpoint<i32> red_endpoint { 0, 0, 0 }; + Endpoint<i32> green_endpoint { 0, 0, 0 }; + Endpoint<i32> blue_endpoint { 0, 0, 0 }; + Endpoint<u32> gamma_endpoint { 0, 0, 0 }; +}; + +struct DIBV5 { + u32 intent { 0 }; + u32 profile_data { 0 }; + u32 profile_size { 0 }; +}; + +struct DIB { + DIBCore core; + DIBInfo info; + DIBOSV2 osv2; + DIBV4 v4; + DIBV5 v5; +}; + +enum class DIBType { + Core = 0, + OSV2Short, + OSV2, + Info, + V2, + V3, + V4, + V5 +}; + +struct BMPLoadingContext { + enum class State { + NotDecoded = 0, + HeaderDecoded, + DIBDecoded, + ColorTableDecoded, + PixelDataDecoded, + Error, + }; + State state { State::NotDecoded }; + + const u8* file_bytes { nullptr }; + size_t file_size { 0 }; + u32 data_offset { 0 }; + + DIB dib; + DIBType dib_type; + + Vector<u32> color_table; + RefPtr<Gfx::Bitmap> bitmap; + + u32 dib_size() const + { + switch (dib_type) { + case DIBType::Core: + return 12; + case DIBType::OSV2Short: + return 16; + case DIBType::OSV2: + return 64; + case DIBType::Info: + return 40; + case DIBType::V2: + return 52; + case DIBType::V3: + return 56; + case DIBType::V4: + return 108; + case DIBType::V5: + return 124; + } + + ASSERT_NOT_REACHED(); + } +}; + +static RefPtr<Bitmap> load_bmp_impl(const u8*, size_t); + +RefPtr<Gfx::Bitmap> load_bmp(const StringView& path) +{ + auto file_or_error = MappedFile::map(path); + if (file_or_error.is_error()) + return nullptr; + auto bitmap = load_bmp_impl((const u8*)file_or_error.value()->data(), file_or_error.value()->size()); + if (bitmap) + bitmap->set_mmap_name(String::formatted("Gfx::Bitmap [{}] - Decoded BMP: {}", bitmap->size(), LexicalPath::canonicalized_path(path))); + return bitmap; +} + +RefPtr<Gfx::Bitmap> load_bmp_from_memory(const u8* data, size_t length) +{ + auto bitmap = load_bmp_impl(data, length); + if (bitmap) + bitmap->set_mmap_name(String::formatted("Gfx::Bitmap [{}] - Decoded BMP: <memory>", bitmap->size())); + return bitmap; +} + +static const LogStream& operator<<(const LogStream& out, Endpoint<i32> ep) +{ + return out << "(" << ep.x << ", " << ep.y << ", " << ep.z << ")"; +} + +static const LogStream& operator<<(const LogStream& out, Endpoint<u32> ep) +{ + return out << "(" << ep.x << ", " << ep.y << ", " << ep.z << ")"; +} + +class Streamer { +public: + Streamer(const u8* data, size_t size) + : m_data_ptr(data) + , m_size_remaining(size) + { + } + + u8 read_u8() + { + ASSERT(m_size_remaining >= 1); + m_size_remaining--; + return *(m_data_ptr++); + } + + u16 read_u16() + { + return read_u8() | (read_u8() << 8); + } + + u32 read_u24() + { + return read_u8() | (read_u8() << 8) | (read_u8() << 16); + } + + i32 read_i32() + { + return static_cast<i32>(read_u16() | (read_u16() << 16)); + } + + u32 read_u32() + { + return read_u16() | (read_u16() << 16); + } + + void drop_bytes(u8 num_bytes) + { + ASSERT(m_size_remaining >= num_bytes); + m_size_remaining -= num_bytes; + m_data_ptr += num_bytes; + } + + bool at_end() const { return !m_size_remaining; } + + bool has_u8() const { return m_size_remaining >= 1; } + bool has_u16() const { return m_size_remaining >= 2; } + bool has_u24() const { return m_size_remaining >= 3; } + bool has_u32() const { return m_size_remaining >= 4; } + + size_t remaining() const { return m_size_remaining; } + +private: + const u8* m_data_ptr { nullptr }; + size_t m_size_remaining { 0 }; +}; + +// Lookup table for distributing all possible 2-bit numbers evenly into 8-bit numbers +static u8 scaling_factors_2bit[4] = { + 0x00, + 0x55, + 0xaa, + 0xff, +}; + +// Lookup table for distributing all possible 3-bit numbers evenly into 8-bit numbers +static u8 scaling_factors_3bit[8] = { + 0x00, + 0x24, + 0x48, + 0x6d, + 0x91, + 0xb6, + 0xdb, + 0xff, +}; + +static u8 scale_masked_8bit_number(u8 number, u8 bits_set) +{ + // If there are more than 4 bit set, an easy way to scale the number is to + // just copy the most significant bits into the least significant bits + if (bits_set >= 4) + return number | (number >> bits_set); + if (!bits_set) + return 0; + if (bits_set == 1) + return number ? 0xff : 0; + if (bits_set == 2) + return scaling_factors_2bit[number >> 6]; + return scaling_factors_3bit[number >> 5]; +} + +static u8 get_scaled_color(u32 data, u8 mask_size, i8 mask_shift) +{ + // A negative mask_shift indicates we actually need to left shift + // the result in order to get out a valid 8-bit color (for example, the blue + // value in an RGB555 encoding is XXXBBBBB, which needs to be shifted to the + // left by 3, hence it would have a "mask_shift" value of -3). + if (mask_shift < 0) + return scale_masked_8bit_number(data << -mask_shift, mask_size); + return scale_masked_8bit_number(data >> mask_shift, mask_size); +} + +// Scales an 8-bit number with "mask_size" bits set (and "8 - mask_size" bits +// ignored). This function scales the number appropriately over the entire +// 256 value color spectrum. +// Note that a much simpler scaling can be done by simple bit shifting. If you +// just ignore the bottom 8-mask_size bits, then you get *close*. However, +// consider, as an example, a 5 bit number (so the bottom 3 bits are ignored). +// The purest white you could get is 0xf8, which is 248 in RGB-land. We need +// to scale the values in order to reach the proper value of 255. +static u32 int_to_scaled_rgb(BMPLoadingContext& context, u32 data) +{ + IF_BMP_DEBUG(dbg() << "DIB info sizes before access: #masks=" << context.dib.info.masks.size() << ", #mask_sizes=" << context.dib.info.mask_sizes.size() << ", #mask_shifts=" << context.dib.info.mask_shifts.size()); + + u8 r = get_scaled_color(data & context.dib.info.masks[0], context.dib.info.mask_sizes[0], context.dib.info.mask_shifts[0]); + u8 g = get_scaled_color(data & context.dib.info.masks[1], context.dib.info.mask_sizes[1], context.dib.info.mask_shifts[1]); + u8 b = get_scaled_color(data & context.dib.info.masks[2], context.dib.info.mask_sizes[2], context.dib.info.mask_shifts[2]); + u32 color = (r << 16) | (g << 8) | b; + + if (context.dib.info.masks.size() == 4) { + // The bitmap has an alpha mask + u8 a = get_scaled_color(data & context.dib.info.masks[3], context.dib.info.mask_sizes[3], context.dib.info.mask_shifts[3]); + color |= (a << 24); + } else { + color |= 0xff000000; + } + + return color; +} + +static void populate_dib_mask_info_if_needed(BMPLoadingContext& context) +{ + if (context.dib.info.masks.is_empty()) + return; + + // Mask shift is the number of right shifts needed to align the MSb of the + // mask to the MSb of the LSB. Note that this can be a negative number. + // Mask size is the number of set bits in the mask. This is required for + // color scaling (for example, ensuring that a 4-bit color value spans the + // entire 256 value color spectrum. + auto& masks = context.dib.info.masks; + auto& mask_shifts = context.dib.info.mask_shifts; + auto& mask_sizes = context.dib.info.mask_sizes; + + if (!mask_shifts.is_empty() && !mask_sizes.is_empty()) + return; + + ASSERT(mask_shifts.is_empty() && mask_sizes.is_empty()); + + mask_shifts.ensure_capacity(masks.size()); + mask_sizes.ensure_capacity(masks.size()); + + for (size_t i = 0; i < masks.size(); ++i) { + u32 mask = masks[i]; + if (!mask) { + mask_shifts.append(0); + mask_sizes.append(0); + continue; + } + int trailing_zeros = count_trailing_zeroes_32(mask); + int size = count_trailing_zeroes_32(~(mask >> trailing_zeros)); + mask_shifts.append(trailing_zeros - 8); + mask_sizes.append(size); + } +} + +static bool check_for_invalid_bitmask_combinations(BMPLoadingContext& context) +{ + auto& bpp = context.dib.core.bpp; + auto& compression = context.dib.info.compression; + + if (compression == Compression::ALPHABITFIELDS && context.dib_type != DIBType::Info) + return false; + + switch (context.dib_type) { + case DIBType::Core: + if (bpp == 2 || bpp == 16 || bpp == 32) + return false; + break; + case DIBType::Info: + switch (compression) { + case Compression::BITFIELDS: + case Compression::ALPHABITFIELDS: + if (bpp != 16 && bpp != 32) + return false; + break; + case Compression::RGB: + break; + case Compression::RLE8: + if (bpp > 8) + return false; + break; + case Compression::RLE4: + // TODO: This is a guess + if (bpp > 4) + return false; + break; + default: + // Other compressions are not officially supported. + // Technically, we could even drop ALPHABITFIELDS. + return false; + } + break; + case DIBType::OSV2Short: + case DIBType::OSV2: + case DIBType::V2: + case DIBType::V3: + case DIBType::V4: + case DIBType::V5: + if (compression == Compression::BITFIELDS && bpp != 16 && bpp != 32) + return false; + break; + } + + return true; +} + +static bool set_dib_bitmasks(BMPLoadingContext& context, Streamer& streamer) +{ + if (!check_for_invalid_bitmask_combinations(context)) + return false; + + auto& bpp = context.dib.core.bpp; + if (bpp <= 8 || bpp == 24) + return true; + + auto& compression = context.dib.info.compression; + auto& type = context.dib_type; + + if (type > DIBType::OSV2 && bpp == 16 && compression == Compression::RGB) { + context.dib.info.masks.append({ 0x7c00, 0x03e0, 0x001f }); + context.dib.info.mask_shifts.append({ 7, 2, -3 }); + context.dib.info.mask_sizes.append({ 5, 5, 5 }); + } else if (type == DIBType::Info && (compression == Compression::BITFIELDS || compression == Compression::ALPHABITFIELDS)) { + // Consume the extra BITFIELDS bytes + auto number_of_mask_fields = compression == Compression::ALPHABITFIELDS ? 4 : 3; + + for (auto i = 0; i < number_of_mask_fields; i++) { + if (!streamer.has_u32()) + return false; + context.dib.info.masks.append(streamer.read_u32()); + } + } + + populate_dib_mask_info_if_needed(context); + return true; +} + +static bool decode_bmp_header(BMPLoadingContext& context) +{ + if (context.state == BMPLoadingContext::State::Error) + return false; + + if (context.state >= BMPLoadingContext::State::HeaderDecoded) + return true; + + if (!context.file_bytes || context.file_size < bmp_header_size) { + IF_BMP_DEBUG(dbg() << "Missing BMP header"); + context.state = BMPLoadingContext::State::Error; + return false; + } + + Streamer streamer(context.file_bytes, bmp_header_size); + + u16 header = streamer.read_u16(); + if (header != 0x4d42) { + IF_BMP_DEBUG(dbgprintf("BMP has invalid magic header number: %04x\n", header)); + context.state = BMPLoadingContext::State::Error; + return false; + } + + // The reported size of the file in the header is actually not important + // for decoding the file. Some specifications say that this value should + // be the size of the header instead, so we just rely on the known file + // size, instead of a possibly-correct-but-also-possibly-incorrect reported + // value of the file size. + streamer.drop_bytes(4); + + // Ignore reserved bytes + streamer.drop_bytes(4); + context.data_offset = streamer.read_u32(); + + IF_BMP_DEBUG(dbg() << "BMP file size: " << context.file_size); + IF_BMP_DEBUG(dbg() << "BMP data offset: " << context.data_offset); + + if (context.data_offset >= context.file_size) { + IF_BMP_DEBUG(dbg() << "BMP data offset is beyond file end?!"); + return false; + } + + context.state = BMPLoadingContext::State::HeaderDecoded; + return true; +} + +static bool decode_bmp_core_dib(BMPLoadingContext& context, Streamer& streamer) +{ + auto& core = context.dib.core; + + // The width and height are u16 fields in the actual BITMAPCOREHEADER format. + if (context.dib_type == DIBType::Core) { + core.width = streamer.read_u16(); + core.height = streamer.read_u16(); + } else { + core.width = streamer.read_i32(); + core.height = streamer.read_i32(); + } + + if (core.width < 0) { + IF_BMP_DEBUG(dbg() << "BMP has a negative width: " << core.width); + return false; + } + + if (static_cast<size_t>(core.width) > maximum_width_for_decoded_images || static_cast<size_t>(abs(core.height)) > maximum_height_for_decoded_images) { + dbgln("This BMP is too large for comfort: {}x{}", core.width, abs(core.height)); + return false; + } + + auto color_planes = streamer.read_u16(); + if (color_planes != 1) { + IF_BMP_DEBUG(dbg() << "BMP has an invalid number of color planes: " << color_planes); + return false; + } + + core.bpp = streamer.read_u16(); + switch (core.bpp) { + case 1: + case 2: + case 4: + case 8: + case 16: + case 24: + case 32: + break; + default: + IF_BMP_DEBUG(dbg() << "BMP has an invalid bpp: " << core.bpp); + context.state = BMPLoadingContext::State::Error; + return false; + } + + IF_BMP_DEBUG(dbg() << "BMP width: " << core.width); + IF_BMP_DEBUG(dbg() << "BMP height: " << core.height); + IF_BMP_DEBUG(dbg() << "BMP bits_per_pixel: " << core.bpp); + + return true; +} + +ALWAYS_INLINE static bool is_supported_compression_format(BMPLoadingContext& context, u32 compression) +{ + return compression == Compression::RGB || compression == Compression::BITFIELDS + || compression == Compression::ALPHABITFIELDS || compression == Compression::RLE8 + || compression == Compression::RLE4 || (compression == Compression::RLE24 && context.dib_type <= DIBType::OSV2); +} + +static bool decode_bmp_osv2_dib(BMPLoadingContext& context, Streamer& streamer, bool short_variant = false) +{ + auto& core = context.dib.core; + + core.width = streamer.read_u32(); + core.height = streamer.read_u32(); + + if (core.width < 0) { + IF_BMP_DEBUG(dbg() << "BMP has a negative width: " << core.width); + return false; + } + + auto color_planes = streamer.read_u16(); + if (color_planes != 1) { + IF_BMP_DEBUG(dbg() << "BMP has an invalid number of color planes: " << color_planes); + return false; + } + + core.bpp = streamer.read_u16(); + switch (core.bpp) { + case 1: + case 2: + case 4: + case 8: + case 24: + break; + default: + // OS/2 didn't expect 16- or 32-bpp to be popular. + IF_BMP_DEBUG(dbg() << "BMP has an invalid bpp: " << core.bpp); + context.state = BMPLoadingContext::State::Error; + return false; + } + + IF_BMP_DEBUG(dbg() << "BMP width: " << core.width); + IF_BMP_DEBUG(dbg() << "BMP height: " << core.height); + IF_BMP_DEBUG(dbg() << "BMP bpp: " << core.bpp); + + if (short_variant) + return true; + + auto& info = context.dib.info; + auto& osv2 = context.dib.osv2; + + info.compression = streamer.read_u32(); + info.image_size = streamer.read_u32(); + info.horizontal_resolution = streamer.read_u32(); + info.vertical_resolution = streamer.read_u32(); + info.number_of_palette_colors = streamer.read_u32(); + info.number_of_important_palette_colors = streamer.read_u32(); + + if (!is_supported_compression_format(context, info.compression)) { + IF_BMP_DEBUG(dbg() << "BMP has unsupported compression value: " << info.compression); + return false; + } + + if (info.number_of_palette_colors > color_palette_limit || info.number_of_important_palette_colors > color_palette_limit) { + IF_BMP_DEBUG(dbg() << "BMP header indicates too many palette colors: " << info.number_of_palette_colors); + return false; + } + + // Units (2) + reserved (2) + streamer.drop_bytes(4); + + osv2.recording = streamer.read_u16(); + osv2.halftoning = streamer.read_u16(); + osv2.size1 = streamer.read_u32(); + osv2.size2 = streamer.read_u32(); + + // ColorEncoding (4) + Identifier (4) + streamer.drop_bytes(8); + + IF_BMP_DEBUG(dbg() << "BMP compression: " << info.compression); + IF_BMP_DEBUG(dbg() << "BMP image size: " << info.image_size); + IF_BMP_DEBUG(dbg() << "BMP horizontal res: " << info.horizontal_resolution); + IF_BMP_DEBUG(dbg() << "BMP vertical res: " << info.vertical_resolution); + IF_BMP_DEBUG(dbg() << "BMP colors: " << info.number_of_palette_colors); + IF_BMP_DEBUG(dbg() << "BMP important colors: " << info.number_of_important_palette_colors); + + return true; +} + +static bool decode_bmp_info_dib(BMPLoadingContext& context, Streamer& streamer) +{ + if (!decode_bmp_core_dib(context, streamer)) + return false; + + auto& info = context.dib.info; + + auto compression = streamer.read_u32(); + info.compression = compression; + if (!is_supported_compression_format(context, compression)) { + IF_BMP_DEBUG(dbg() << "BMP has unsupported compression value: " << compression); + return false; + } + + info.image_size = streamer.read_u32(); + info.horizontal_resolution = streamer.read_i32(); + info.vertical_resolution = streamer.read_i32(); + info.number_of_palette_colors = streamer.read_u32(); + info.number_of_important_palette_colors = streamer.read_u32(); + + if (info.number_of_palette_colors > color_palette_limit || info.number_of_important_palette_colors > color_palette_limit) { + IF_BMP_DEBUG(dbg() << "BMP header indicates too many palette colors: " << info.number_of_palette_colors); + return false; + } + + if (info.number_of_important_palette_colors == 0) + info.number_of_important_palette_colors = info.number_of_palette_colors; + + IF_BMP_DEBUG(dbg() << "BMP compression: " << info.compression); + IF_BMP_DEBUG(dbg() << "BMP image size: " << info.image_size); + IF_BMP_DEBUG(dbg() << "BMP horizontal resolution: " << info.horizontal_resolution); + IF_BMP_DEBUG(dbg() << "BMP vertical resolution: " << info.vertical_resolution); + IF_BMP_DEBUG(dbg() << "BMP palette colors: " << info.number_of_palette_colors); + IF_BMP_DEBUG(dbg() << "BMP important palette colors: " << info.number_of_important_palette_colors); + + return true; +} + +static bool decode_bmp_v2_dib(BMPLoadingContext& context, Streamer& streamer) +{ + if (!decode_bmp_info_dib(context, streamer)) + return false; + + context.dib.info.masks.append(streamer.read_u32()); + context.dib.info.masks.append(streamer.read_u32()); + context.dib.info.masks.append(streamer.read_u32()); + + IF_BMP_DEBUG(dbgprintf("BMP red mask: %08x\n", context.dib.info.masks[0])); + IF_BMP_DEBUG(dbgprintf("BMP green mask: %08x\n", context.dib.info.masks[1])); + IF_BMP_DEBUG(dbgprintf("BMP blue mask: %08x\n", context.dib.info.masks[2])); + + return true; +} + +static bool decode_bmp_v3_dib(BMPLoadingContext& context, Streamer& streamer) +{ + if (!decode_bmp_v2_dib(context, streamer)) + return false; + + // There is zero documentation about when alpha masks actually get applied. + // Well, there's some, but it's not even close to comprehensive. So, this is + // in no way based off of any spec, it's simply based off of the BMP test + // suite results. + if (context.dib.info.compression == Compression::ALPHABITFIELDS) { + context.dib.info.masks.append(streamer.read_u32()); + IF_BMP_DEBUG(dbgprintf("BMP alpha mask: %08x\n", context.dib.info.masks[3])); + } else if (context.dib_size() >= 56 && context.dib.core.bpp >= 16) { + auto mask = streamer.read_u32(); + if ((context.dib.core.bpp == 32 && mask != 0) || context.dib.core.bpp == 16) { + context.dib.info.masks.append(mask); + IF_BMP_DEBUG(dbgprintf("BMP alpha mask: %08x\n", mask)); + } + } else { + streamer.drop_bytes(4); + } + + return true; +} + +static bool decode_bmp_v4_dib(BMPLoadingContext& context, Streamer& streamer) +{ + if (!decode_bmp_v3_dib(context, streamer)) + return false; + + auto& v4 = context.dib.v4; + v4.color_space = streamer.read_u32(); + v4.red_endpoint = { streamer.read_i32(), streamer.read_i32(), streamer.read_i32() }; + v4.green_endpoint = { streamer.read_i32(), streamer.read_i32(), streamer.read_i32() }; + v4.blue_endpoint = { streamer.read_i32(), streamer.read_i32(), streamer.read_i32() }; + v4.gamma_endpoint = { streamer.read_u32(), streamer.read_u32(), streamer.read_u32() }; + + IF_BMP_DEBUG(dbg() << "BMP color space: " << v4.color_space); + IF_BMP_DEBUG(dbg() << "BMP red endpoint: " << v4.red_endpoint); + IF_BMP_DEBUG(dbg() << "BMP green endpoint: " << v4.green_endpoint); + IF_BMP_DEBUG(dbg() << "BMP blue endpoint: " << v4.blue_endpoint); + IF_BMP_DEBUG(dbg() << "BMP gamma endpoint: " << v4.gamma_endpoint); + + return true; +} + +static bool decode_bmp_v5_dib(BMPLoadingContext& context, Streamer& streamer) +{ + if (!decode_bmp_v4_dib(context, streamer)) + return false; + + auto& v5 = context.dib.v5; + v5.intent = streamer.read_u32(); + v5.profile_data = streamer.read_u32(); + v5.profile_size = streamer.read_u32(); + + IF_BMP_DEBUG(dbg() << "BMP intent: " << v5.intent); + IF_BMP_DEBUG(dbg() << "BMP profile data: " << v5.profile_data); + IF_BMP_DEBUG(dbg() << "BMP profile size: " << v5.profile_size); + + return true; +} + +static bool decode_bmp_dib(BMPLoadingContext& context) +{ + if (context.state == BMPLoadingContext::State::Error) + return false; + + if (context.state >= BMPLoadingContext::State::DIBDecoded) + return true; + + if (context.state < BMPLoadingContext::State::HeaderDecoded && !decode_bmp_header(context)) + return false; + + if (context.file_size < bmp_header_size + 4) + return false; + + Streamer streamer(context.file_bytes + bmp_header_size, 4); + u32 dib_size = streamer.read_u32(); + + if (context.file_size < bmp_header_size + dib_size) + return false; + if (context.data_offset < bmp_header_size + dib_size) { + IF_BMP_DEBUG(dbg() << "Shenanigans! BMP pixel data and header usually don't overlap."); + return false; + } + + streamer = Streamer(context.file_bytes + bmp_header_size + 4, context.data_offset - bmp_header_size - 4); + + IF_BMP_DEBUG(dbg() << "BMP dib size: " << dib_size); + + bool error = false; + + if (dib_size == 12) { + context.dib_type = DIBType::Core; + if (!decode_bmp_core_dib(context, streamer)) + error = true; + } else if (dib_size == 64) { + context.dib_type = DIBType::OSV2; + if (!decode_bmp_osv2_dib(context, streamer)) + error = true; + } else if (dib_size == 16) { + context.dib_type = DIBType::OSV2Short; + if (!decode_bmp_osv2_dib(context, streamer, true)) + error = true; + } else if (dib_size == 40) { + context.dib_type = DIBType::Info; + if (!decode_bmp_info_dib(context, streamer)) + error = true; + } else if (dib_size == 52) { + context.dib_type = DIBType::V2; + if (!decode_bmp_v2_dib(context, streamer)) + error = true; + } else if (dib_size == 56) { + context.dib_type = DIBType::V3; + if (!decode_bmp_v3_dib(context, streamer)) + error = true; + } else if (dib_size == 108) { + context.dib_type = DIBType::V4; + if (!decode_bmp_v4_dib(context, streamer)) + error = true; + } else if (dib_size == 124) { + context.dib_type = DIBType::V5; + if (!decode_bmp_v5_dib(context, streamer)) + error = true; + } else { + IF_BMP_DEBUG(dbg() << "Unsupported BMP DIB size: " << dib_size); + error = true; + } + + switch (context.dib.info.compression) { + case Compression::RGB: + case Compression::RLE8: + case Compression::RLE4: + case Compression::BITFIELDS: + case Compression::RLE24: + case Compression::PNG: + case Compression::ALPHABITFIELDS: + case Compression::CMYK: + case Compression::CMYKRLE8: + case Compression::CMYKRLE4: + break; + default: + error = true; + } + + if (!error && !set_dib_bitmasks(context, streamer)) + error = true; + + if (error) { + IF_BMP_DEBUG(dbg() << "BMP has an invalid DIB"); + context.state = BMPLoadingContext::State::Error; + return false; + } + + context.state = BMPLoadingContext::State::DIBDecoded; + + return true; +} + +static bool decode_bmp_color_table(BMPLoadingContext& context) +{ + if (context.state == BMPLoadingContext::State::Error) + return false; + + if (context.state < BMPLoadingContext::State::DIBDecoded && !decode_bmp_dib(context)) + return false; + + if (context.state >= BMPLoadingContext::State::ColorTableDecoded) + return true; + + if (context.dib.core.bpp > 8) { + context.state = BMPLoadingContext::State::ColorTableDecoded; + return true; + } + + auto bytes_per_color = context.dib_type == DIBType::Core ? 3 : 4; + u32 max_colors = 1 << context.dib.core.bpp; + ASSERT(context.data_offset >= bmp_header_size + context.dib_size()); + auto size_of_color_table = context.data_offset - bmp_header_size - context.dib_size(); + + if (context.dib_type <= DIBType::OSV2) { + // Partial color tables are not supported, so the space of the color + // table must be at least enough for the maximum amount of colors + if (size_of_color_table < 3 * max_colors) { + // This is against the spec, but most viewers process it anyways + IF_BMP_DEBUG(dbg() << "BMP with CORE header does not have enough colors. Has: " << size_of_color_table << ", expected: " << (3 * max_colors)); + } + } + + Streamer streamer(context.file_bytes + bmp_header_size + context.dib_size(), size_of_color_table); + for (u32 i = 0; !streamer.at_end() && i < max_colors; ++i) { + if (bytes_per_color == 4) { + if (!streamer.has_u32()) + return false; + context.color_table.append(streamer.read_u32()); + } else { + if (!streamer.has_u24()) + return false; + context.color_table.append(streamer.read_u24()); + } + } + + context.state = BMPLoadingContext::State::ColorTableDecoded; + return true; +} + +struct RLEState { + enum : u8 { + PixelCount = 0, + PixelValue, + Meta, // Represents just consuming a null byte, which indicates something special + }; +}; + +static bool uncompress_bmp_rle_data(BMPLoadingContext& context, ByteBuffer& buffer) +{ + // RLE-compressed images cannot be stored top-down + if (context.dib.core.height < 0) { + IF_BMP_DEBUG(dbg() << "BMP is top-down and RLE compressed"); + context.state = BMPLoadingContext::State::Error; + return false; + } + + Streamer streamer(context.file_bytes + context.data_offset, context.file_size - context.data_offset); + + auto compression = context.dib.info.compression; + + u32 total_rows = static_cast<u32>(context.dib.core.height); + u32 total_columns = round_up_to_power_of_two(static_cast<u32>(context.dib.core.width), 4); + u32 column = 0; + u32 row = 0; + auto currently_consuming = RLEState::PixelCount; + i16 pixel_count = 0; + + // ByteBuffer asserts that allocating the memory never fails. + // FIXME: ByteBuffer should return either RefPtr<> or Optional<>. + // Decoding the RLE data on-the-fly might actually be faster, and avoids this topic entirely. + u32 buffer_size; + if (compression == Compression::RLE24) { + buffer_size = total_rows * round_up_to_power_of_two(total_columns, 4) * 4; + } else { + buffer_size = total_rows * round_up_to_power_of_two(total_columns, 4); + } + if (buffer_size > 300 * MiB) { + IF_BMP_DEBUG(dbg() << "Suspiciously large amount of RLE data"); + return false; + } + buffer = ByteBuffer::create_zeroed(buffer_size); + + // Avoid as many if statements as possible by pulling out + // compression-dependent actions into separate lambdas + Function<u32()> get_buffer_index; + Function<bool(u32, bool)> set_byte; + Function<Optional<u32>()> read_byte; + + if (compression == Compression::RLE8) { + get_buffer_index = [&]() -> u32 { return row * total_columns + column; }; + } else if (compression == Compression::RLE4) { + get_buffer_index = [&]() -> u32 { return (row * total_columns + column) / 2; }; + } else { + get_buffer_index = [&]() -> u32 { return (row * total_columns + column) * 3; }; + } + + if (compression == Compression::RLE8) { + set_byte = [&](u32 color, bool) -> bool { + if (column >= total_columns) { + column = 0; + row++; + } + auto index = get_buffer_index(); + if (index >= buffer.size()) { + IF_BMP_DEBUG(dbg() << "BMP has badly-formatted RLE data"); + return false; + } + buffer[index] = color; + column++; + return true; + }; + } else if (compression == Compression::RLE24) { + set_byte = [&](u32 color, bool) -> bool { + if (column >= total_columns) { + column = 0; + row++; + } + auto index = get_buffer_index(); + if (index + 3 >= buffer.size()) { + IF_BMP_DEBUG(dbg() << "BMP has badly-formatted RLE data"); + return false; + } + ((u32&)buffer[index]) = color; + column++; + return true; + }; + } else { + set_byte = [&](u32 byte, bool rle4_set_second_nibble) -> bool { + if (column >= total_columns) { + column = 0; + row++; + } + + u32 index = get_buffer_index(); + if (index >= buffer.size() || (rle4_set_second_nibble && index + 1 >= buffer.size())) { + IF_BMP_DEBUG(dbg() << "BMP has badly-formatted RLE data"); + return false; + } + + if (column % 2) { + buffer[index] |= byte >> 4; + if (rle4_set_second_nibble) { + buffer[index + 1] |= byte << 4; + column++; + } + } else { + if (rle4_set_second_nibble) { + buffer[index] = byte; + column++; + } else { + buffer[index] |= byte & 0xf0; + } + } + + column++; + return true; + }; + } + + if (compression == Compression::RLE24) { + read_byte = [&]() -> Optional<u32> { + if (!streamer.has_u24()) { + IF_BMP_DEBUG(dbg() << "BMP has badly-formatted RLE data"); + return {}; + } + return streamer.read_u24(); + }; + } else { + read_byte = [&]() -> Optional<u32> { + if (!streamer.has_u8()) { + IF_BMP_DEBUG(dbg() << "BMP has badly-formatted RLE data"); + return {}; + } + return streamer.read_u8(); + }; + } + + while (true) { + u32 byte; + + switch (currently_consuming) { + case RLEState::PixelCount: + if (!streamer.has_u8()) + return false; + byte = streamer.read_u8(); + if (!byte) { + currently_consuming = RLEState::Meta; + } else { + pixel_count = byte; + currently_consuming = RLEState::PixelValue; + } + break; + case RLEState::PixelValue: { + auto result = read_byte(); + if (!result.has_value()) + return false; + byte = result.value(); + for (u16 i = 0; i < pixel_count; ++i) { + if (compression != Compression::RLE4) { + if (!set_byte(byte, true)) + return false; + } else { + if (!set_byte(byte, i != pixel_count - 1)) + return false; + i++; + } + } + + currently_consuming = RLEState::PixelCount; + break; + } + case RLEState::Meta: + if (!streamer.has_u8()) + return false; + byte = streamer.read_u8(); + if (!byte) { + column = 0; + row++; + currently_consuming = RLEState::PixelCount; + continue; + } + if (byte == 1) + return true; + if (byte == 2) { + if (!streamer.has_u8()) + return false; + u8 offset_x = streamer.read_u8(); + if (!streamer.has_u8()) + return false; + u8 offset_y = streamer.read_u8(); + column += offset_x; + if (column >= total_columns) { + column -= total_columns; + row++; + } + row += offset_y; + currently_consuming = RLEState::PixelCount; + continue; + } + + // Consume literal bytes + pixel_count = byte; + i16 i = byte; + + while (i >= 1) { + auto result = read_byte(); + if (!result.has_value()) + return false; + byte = result.value(); + if (!set_byte(byte, i != 1)) + return false; + i--; + if (compression == Compression::RLE4) + i--; + } + + // Optionally consume a padding byte + if (compression != Compression::RLE4) { + if (pixel_count % 2) { + if (!streamer.has_u8()) + return false; + byte = streamer.read_u8(); + } + } else { + if (((pixel_count + 1) / 2) % 2) { + if (!streamer.has_u8()) + return false; + byte = streamer.read_u8(); + } + } + currently_consuming = RLEState::PixelCount; + break; + } + } + + ASSERT_NOT_REACHED(); +} + +static bool decode_bmp_pixel_data(BMPLoadingContext& context) +{ + if (context.state == BMPLoadingContext::State::Error) + return false; + + if (context.state <= BMPLoadingContext::State::ColorTableDecoded && !decode_bmp_color_table(context)) + return false; + + const u16 bits_per_pixel = context.dib.core.bpp; + + BitmapFormat format = [&]() -> BitmapFormat { + switch (bits_per_pixel) { + case 1: + return BitmapFormat::Indexed1; + case 2: + return BitmapFormat::Indexed2; + case 4: + return BitmapFormat::Indexed4; + case 8: + return BitmapFormat::Indexed8; + case 16: + if (context.dib.info.masks.size() == 4) + return BitmapFormat::RGBA32; + return BitmapFormat::RGB32; + case 24: + return BitmapFormat::RGB32; + case 32: + return BitmapFormat::RGBA32; + default: + return BitmapFormat::Invalid; + } + }(); + + if (format == BitmapFormat::Invalid) { + IF_BMP_DEBUG(dbg() << "BMP has invalid bpp of " << bits_per_pixel); + context.state = BMPLoadingContext::State::Error; + return false; + } + + const u32 width = abs(context.dib.core.width); + const u32 height = abs(context.dib.core.height); + context.bitmap = Bitmap::create_purgeable(format, { static_cast<int>(width), static_cast<int>(height) }); + if (!context.bitmap) { + IF_BMP_DEBUG(dbg() << "BMP appears to have overly large dimensions"); + return false; + } + + ByteBuffer rle_buffer; + ReadonlyBytes bytes { context.file_bytes + context.data_offset, context.file_size - context.data_offset }; + + if (context.dib.info.compression == Compression::RLE4 || context.dib.info.compression == Compression::RLE8 + || context.dib.info.compression == Compression::RLE24) { + if (!uncompress_bmp_rle_data(context, rle_buffer)) + return false; + bytes = rle_buffer.bytes(); + } + + Streamer streamer(bytes.data(), bytes.size()); + + auto process_row = [&](u32 row) -> bool { + u32 space_remaining_before_consuming_row = streamer.remaining(); + + for (u32 column = 0; column < width;) { + switch (bits_per_pixel) { + case 1: { + if (!streamer.has_u8()) + return false; + u8 byte = streamer.read_u8(); + u8 mask = 8; + while (column < width && mask > 0) { + mask -= 1; + context.bitmap->scanline_u8(row)[column++] = (byte >> mask) & 0x1; + } + break; + } + case 2: { + if (!streamer.has_u8()) + return false; + u8 byte = streamer.read_u8(); + u8 mask = 8; + while (column < width && mask > 0) { + mask -= 2; + context.bitmap->scanline_u8(row)[column++] = (byte >> mask) & 0x3; + } + break; + } + case 4: { + if (!streamer.has_u8()) + return false; + u8 byte = streamer.read_u8(); + context.bitmap->scanline_u8(row)[column++] = (byte >> 4) & 0xf; + if (column < width) + context.bitmap->scanline_u8(row)[column++] = byte & 0xf; + break; + } + case 8: + if (!streamer.has_u8()) + return false; + context.bitmap->scanline_u8(row)[column++] = streamer.read_u8(); + break; + case 16: { + if (!streamer.has_u16()) + return false; + context.bitmap->scanline(row)[column++] = int_to_scaled_rgb(context, streamer.read_u16()); + break; + } + case 24: { + if (!streamer.has_u24()) + return false; + context.bitmap->scanline(row)[column++] = streamer.read_u24(); + break; + } + case 32: + if (!streamer.has_u32()) + return false; + if (context.dib.info.masks.is_empty()) { + context.bitmap->scanline(row)[column++] = streamer.read_u32() | 0xff000000; + } else { + context.bitmap->scanline(row)[column++] = int_to_scaled_rgb(context, streamer.read_u32()); + } + break; + } + } + + auto consumed = space_remaining_before_consuming_row - streamer.remaining(); + + // Calculate padding + u8 bytes_to_drop = [consumed]() -> u8 { + switch (consumed % 4) { + case 0: + return 0; + case 1: + return 3; + case 2: + return 2; + case 3: + return 1; + } + ASSERT_NOT_REACHED(); + }(); + if (streamer.remaining() < bytes_to_drop) + return false; + streamer.drop_bytes(bytes_to_drop); + + return true; + }; + + if (context.dib.core.height < 0) { + // BMP is stored top-down + for (u32 row = 0; row < height; ++row) { + if (!process_row(row)) + return false; + } + } else { + for (i32 row = height - 1; row >= 0; --row) { + if (!process_row(row)) + return false; + } + } + + for (size_t i = 0; i < context.color_table.size(); ++i) + context.bitmap->set_palette_color(i, Color::from_rgb(context.color_table[i])); + + context.state = BMPLoadingContext::State::PixelDataDecoded; + + return true; +} + +static RefPtr<Bitmap> load_bmp_impl(const u8* data, size_t data_size) +{ + BMPLoadingContext context; + context.file_bytes = data; + context.file_size = data_size; + + // Forces a decode of the header, dib, and color table as well + if (!decode_bmp_pixel_data(context)) { + context.state = BMPLoadingContext::State::Error; + return nullptr; + } + + return context.bitmap; +} + +BMPImageDecoderPlugin::BMPImageDecoderPlugin(const u8* data, size_t data_size) +{ + m_context = make<BMPLoadingContext>(); + m_context->file_bytes = data; + m_context->file_size = data_size; +} + +BMPImageDecoderPlugin::~BMPImageDecoderPlugin() +{ +} + +IntSize BMPImageDecoderPlugin::size() +{ + if (m_context->state == BMPLoadingContext::State::Error) + return {}; + + if (m_context->state < BMPLoadingContext::State::DIBDecoded && !decode_bmp_dib(*m_context)) + return {}; + + return { m_context->dib.core.width, abs(m_context->dib.core.height) }; +} + +RefPtr<Gfx::Bitmap> BMPImageDecoderPlugin::bitmap() +{ + if (m_context->state == BMPLoadingContext::State::Error) + return nullptr; + + if (m_context->state < BMPLoadingContext::State::PixelDataDecoded && !decode_bmp_pixel_data(*m_context)) + return nullptr; + + ASSERT(m_context->bitmap); + return m_context->bitmap; +} + +void BMPImageDecoderPlugin::set_volatile() +{ + if (m_context->bitmap) + m_context->bitmap->set_volatile(); +} + +bool BMPImageDecoderPlugin::set_nonvolatile() +{ + if (!m_context->bitmap) + return false; + return m_context->bitmap->set_nonvolatile(); +} + +bool BMPImageDecoderPlugin::sniff() +{ + return decode_bmp_header(*m_context); +} + +bool BMPImageDecoderPlugin::is_animated() +{ + return false; +} + +size_t BMPImageDecoderPlugin::loop_count() +{ + return 0; +} + +size_t BMPImageDecoderPlugin::frame_count() +{ + return 1; +} + +ImageFrameDescriptor BMPImageDecoderPlugin::frame(size_t i) +{ + if (i > 0) + return { bitmap(), 0 }; + return {}; +} + +} diff --git a/Userland/Libraries/LibGfx/BMPLoader.h b/Userland/Libraries/LibGfx/BMPLoader.h new file mode 100644 index 0000000000..92f54e6b5f --- /dev/null +++ b/Userland/Libraries/LibGfx/BMPLoader.h @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2020, Matthew Olsson <matthewcolsson@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <LibGfx/Bitmap.h> +#include <LibGfx/ImageDecoder.h> + +namespace Gfx { + +RefPtr<Gfx::Bitmap> load_bmp(const StringView& path); +RefPtr<Gfx::Bitmap> load_bmp_from_memory(const u8*, size_t); + +struct BMPLoadingContext; + +class BMPImageDecoderPlugin final : public ImageDecoderPlugin { +public: + virtual ~BMPImageDecoderPlugin() override; + BMPImageDecoderPlugin(const u8*, size_t); + + virtual IntSize size() override; + virtual RefPtr<Gfx::Bitmap> bitmap() override; + virtual void set_volatile() override; + [[nodiscard]] virtual bool set_nonvolatile() override; + virtual bool sniff() override; + virtual bool is_animated() override; + virtual size_t loop_count() override; + virtual size_t frame_count() override; + virtual ImageFrameDescriptor frame(size_t i) override; + +private: + OwnPtr<BMPLoadingContext> m_context; +}; + +} diff --git a/Userland/Libraries/LibGfx/BMPWriter.cpp b/Userland/Libraries/LibGfx/BMPWriter.cpp new file mode 100644 index 0000000000..8ae2b00a8b --- /dev/null +++ b/Userland/Libraries/LibGfx/BMPWriter.cpp @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2020, Ben Jilks <benjyjilks@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <AK/Vector.h> +#include <LibGfx/BMPWriter.h> +#include <LibGfx/Bitmap.h> +#include <cstring> + +namespace Gfx { + +constexpr int bytes_per_pixel = 3; + +#define FILE_HEADER_SIZE 14 +#define IMAGE_INFORMATION_SIZE 40 +#define PIXEL_DATA_OFFSET FILE_HEADER_SIZE + IMAGE_INFORMATION_SIZE + +class Streamer { +public: + Streamer(u8* data) + : m_data(data) + { + } + + void write_u8(u8 i) + { + *(m_data++) = i; + } + + void write_u16(u16 i) + { + *(m_data++) = i & 0xFF; + *(m_data++) = (i >> 8) & 0xFF; + } + + void write_u32(u32 i) + { + write_u16(i & 0xFFFF); + write_u16((i >> 16) & 0xFFFF); + } + + void write_i32(i32 i) + { + write_u32(static_cast<u32>(i)); + } + +private: + u8* m_data; +}; + +static ByteBuffer write_pixel_data(const RefPtr<Bitmap> bitmap, int pixel_row_data_size) +{ + int image_size = pixel_row_data_size * bitmap->height(); + auto buffer = ByteBuffer::create_uninitialized(image_size); + + int current_row = 0; + for (int y = bitmap->height() - 1; y >= 0; --y) { + auto* row = buffer.data() + (pixel_row_data_size * current_row++); + for (int x = 0; x < bitmap->width(); x++) { + auto pixel = bitmap->get_pixel(x, y); + row[x * bytes_per_pixel + 0] = pixel.blue(); + row[x * bytes_per_pixel + 1] = pixel.green(); + row[x * bytes_per_pixel + 2] = pixel.red(); + } + } + + return buffer; +} + +static ByteBuffer compress_pixel_data(const ByteBuffer& pixel_data, BMPWriter::Compression compression) +{ + switch (compression) { + case BMPWriter::Compression::RGB: + return pixel_data; + } + + ASSERT_NOT_REACHED(); +} + +ByteBuffer BMPWriter::dump(const RefPtr<Bitmap> bitmap) +{ + int pixel_row_data_size = (bytes_per_pixel * 8 * bitmap->width() + 31) / 32 * 4; + int image_size = pixel_row_data_size * bitmap->height(); + auto buffer = ByteBuffer::create_uninitialized(PIXEL_DATA_OFFSET); + + auto pixel_data = write_pixel_data(bitmap, pixel_row_data_size); + pixel_data = compress_pixel_data(pixel_data, m_compression); + + int file_size = PIXEL_DATA_OFFSET + pixel_data.size(); + Streamer streamer(buffer.data()); + streamer.write_u8('B'); + streamer.write_u8('M'); + streamer.write_u32(file_size); + streamer.write_u32(0); + streamer.write_u32(PIXEL_DATA_OFFSET); + + streamer.write_u32(IMAGE_INFORMATION_SIZE); // Header size + streamer.write_i32(bitmap->width()); // ImageWidth + streamer.write_i32(bitmap->height()); // ImageHeight + streamer.write_u16(1); // Planes + streamer.write_u16(bytes_per_pixel * 8); // BitsPerPixel + streamer.write_u32((u32)m_compression); // Compression + streamer.write_u32(image_size); // ImageSize + streamer.write_i32(0); // XpixelsPerMeter + streamer.write_i32(0); // YpixelsPerMeter + streamer.write_u32(0); // TotalColors + streamer.write_u32(0); // ImportantColors + + buffer.append(pixel_data.data(), pixel_data.size()); + return buffer; +} + +} diff --git a/Userland/Libraries/LibGfx/BMPWriter.h b/Userland/Libraries/LibGfx/BMPWriter.h new file mode 100644 index 0000000000..d881ab5f9d --- /dev/null +++ b/Userland/Libraries/LibGfx/BMPWriter.h @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2020, Ben Jilks <benjyjilks@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/ByteBuffer.h> + +namespace Gfx { + +class Bitmap; + +class BMPWriter { +public: + BMPWriter() = default; + + ByteBuffer dump(const RefPtr<Bitmap>); + + enum class Compression : u32 { + RGB = 0, + }; + + inline void set_compression(Compression compression) { m_compression = compression; } + +private: + Compression m_compression { Compression::RGB }; +}; + +} diff --git a/Userland/Libraries/LibGfx/Bitmap.cpp b/Userland/Libraries/LibGfx/Bitmap.cpp new file mode 100644 index 0000000000..7c031f59be --- /dev/null +++ b/Userland/Libraries/LibGfx/Bitmap.cpp @@ -0,0 +1,495 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <AK/Checked.h> +#include <AK/Memory.h> +#include <AK/MemoryStream.h> +#include <AK/Optional.h> +#include <AK/SharedBuffer.h> +#include <AK/String.h> +#include <LibGfx/BMPLoader.h> +#include <LibGfx/Bitmap.h> +#include <LibGfx/GIFLoader.h> +#include <LibGfx/ICOLoader.h> +#include <LibGfx/JPGLoader.h> +#include <LibGfx/PBMLoader.h> +#include <LibGfx/PGMLoader.h> +#include <LibGfx/PNGLoader.h> +#include <LibGfx/PPMLoader.h> +#include <LibGfx/ShareableBitmap.h> +#include <fcntl.h> +#include <stdio.h> +#include <sys/mman.h> + +namespace Gfx { + +struct BackingStore { + void* data { nullptr }; + size_t pitch { 0 }; + size_t size_in_bytes { 0 }; +}; + +size_t Bitmap::minimum_pitch(size_t width, BitmapFormat format) +{ + size_t element_size; + switch (determine_storage_format(format)) { + case StorageFormat::Indexed8: + element_size = 1; + break; + case StorageFormat::RGB32: + case StorageFormat::RGBA32: + element_size = 4; + break; + default: + ASSERT_NOT_REACHED(); + } + + return width * element_size; +} + +static bool size_would_overflow(BitmapFormat format, const IntSize& size) +{ + if (size.width() < 0 || size.height() < 0) + return true; + // This check is a bit arbitrary, but should protect us from most shenanigans: + if (size.width() >= 32768 || size.height() >= 32768) + return true; + // In contrast, this check is absolutely necessary: + size_t pitch = Bitmap::minimum_pitch(size.width(), format); + return Checked<size_t>::multiplication_would_overflow(pitch, size.height()); +} + +RefPtr<Bitmap> Bitmap::create(BitmapFormat format, const IntSize& size) +{ + auto backing_store = Bitmap::allocate_backing_store(format, size, Purgeable::No); + if (!backing_store.has_value()) + return nullptr; + return adopt(*new Bitmap(format, size, Purgeable::No, backing_store.value())); +} + +RefPtr<Bitmap> Bitmap::create_purgeable(BitmapFormat format, const IntSize& size) +{ + auto backing_store = Bitmap::allocate_backing_store(format, size, Purgeable::Yes); + if (!backing_store.has_value()) + return nullptr; + return adopt(*new Bitmap(format, size, Purgeable::Yes, backing_store.value())); +} + +RefPtr<Bitmap> Bitmap::create_shareable(BitmapFormat format, const IntSize& size) +{ + if (size_would_overflow(format, size)) + return nullptr; + + const auto pitch = minimum_pitch(size.width(), format); + const auto data_size = size_in_bytes(pitch, size.height()); + auto shared_buffer = SharedBuffer::create_with_size(data_size); + if (!shared_buffer) + return nullptr; + return adopt(*new Bitmap(format, shared_buffer.release_nonnull(), size, Vector<RGBA32>())); +} + +Bitmap::Bitmap(BitmapFormat format, const IntSize& size, Purgeable purgeable, const BackingStore& backing_store) + : m_size(size) + , m_data(backing_store.data) + , m_pitch(backing_store.pitch) + , m_format(format) + , m_purgeable(purgeable == Purgeable::Yes) +{ + ASSERT(!m_size.is_empty()); + ASSERT(!size_would_overflow(format, size)); + ASSERT(m_data); + ASSERT(backing_store.size_in_bytes == size_in_bytes()); + allocate_palette_from_format(format, {}); + m_needs_munmap = true; +} + +RefPtr<Bitmap> Bitmap::create_wrapper(BitmapFormat format, const IntSize& size, size_t pitch, void* data) +{ + if (size_would_overflow(format, size)) + return nullptr; + return adopt(*new Bitmap(format, size, pitch, data)); +} + +RefPtr<Bitmap> Bitmap::load_from_file(const StringView& path) +{ +#define __ENUMERATE_IMAGE_FORMAT(Name, Ext) \ + if (path.ends_with(Ext, CaseSensitivity::CaseInsensitive)) \ + return load_##Name(path); + ENUMERATE_IMAGE_FORMATS +#undef __ENUMERATE_IMAGE_FORMAT + + return nullptr; +} + +Bitmap::Bitmap(BitmapFormat format, const IntSize& size, size_t pitch, void* data) + : m_size(size) + , m_data(data) + , m_pitch(pitch) + , m_format(format) +{ + ASSERT(pitch >= minimum_pitch(size.width(), format)); + ASSERT(!size_would_overflow(format, size)); + // FIXME: assert that `data` is actually long enough! + + allocate_palette_from_format(format, {}); +} + +RefPtr<Bitmap> Bitmap::create_with_shared_buffer(BitmapFormat format, NonnullRefPtr<SharedBuffer>&& shared_buffer, const IntSize& size) +{ + return create_with_shared_buffer(format, move(shared_buffer), size, {}); +} + +static bool check_size(const IntSize& size, BitmapFormat format, unsigned actual_size) +{ + + // FIXME: Code duplication of size_in_bytes() and m_pitch + unsigned expected_size_min = Bitmap::minimum_pitch(size.width(), format) * size.height(); + unsigned expected_size_max = round_up_to_power_of_two(expected_size_min, PAGE_SIZE); + if (expected_size_min > actual_size || actual_size > expected_size_max) { + // Getting here is most likely an error. + dbgln("Constructing a shared bitmap for format {} and size {}, which demands {} bytes, which rounds up to at most {}.", + static_cast<int>(format), + size, + expected_size_min, + expected_size_max); + + dbgln("However, we were given {} bytes, which is outside this range?! Refusing cowardly.", actual_size); + return false; + } + return true; +} + +RefPtr<Bitmap> Bitmap::create_with_shared_buffer(BitmapFormat format, NonnullRefPtr<SharedBuffer>&& shared_buffer, const IntSize& size, const Vector<RGBA32>& palette) +{ + if (size_would_overflow(format, size)) + return nullptr; + + if (!check_size(size, format, shared_buffer->size())) + return {}; + + return adopt(*new Bitmap(format, move(shared_buffer), size, palette)); +} + +/// Read a bitmap as described by: +/// - actual size +/// - width +/// - height +/// - format +/// - palette count +/// - palette data (= palette count * RGBA32) +/// - image data (= actual size * u8) +RefPtr<Bitmap> Bitmap::create_from_serialized_byte_buffer(ByteBuffer&& buffer) +{ + InputMemoryStream stream { buffer }; + unsigned actual_size; + unsigned width; + unsigned height; + BitmapFormat format; + unsigned palette_size; + Vector<RGBA32> palette; + + auto read = [&]<typename T>(T& value) { + if (stream.read({ &value, sizeof(T) }) != sizeof(T)) + return false; + return true; + }; + + if (!read(actual_size) || !read(width) || !read(height) || !read(format) || !read(palette_size)) + return nullptr; + + if (format > BitmapFormat::RGBA32 || format < BitmapFormat::Indexed1) + return nullptr; + + if (!check_size({ width, height }, format, actual_size)) + return {}; + + palette.ensure_capacity(palette_size); + for (size_t i = 0; i < palette_size; ++i) { + if (!read(palette[i])) + return {}; + } + + if (stream.remaining() < actual_size) + return {}; + + auto data = stream.bytes().slice(stream.offset(), actual_size); + + auto bitmap = Bitmap::create(format, { width, height }); + if (!bitmap) + return {}; + + bitmap->m_palette = new RGBA32[palette_size]; + memcpy(bitmap->m_palette, palette.data(), palette_size * sizeof(RGBA32)); + + data.copy_to({ bitmap->scanline(0), bitmap->size_in_bytes() }); + + return bitmap; +} + +ByteBuffer Bitmap::serialize_to_byte_buffer() const +{ + auto buffer = ByteBuffer::create_uninitialized(4 * sizeof(unsigned) + sizeof(BitmapFormat) + sizeof(RGBA32) * palette_size(m_format) + size_in_bytes()); + OutputMemoryStream stream { buffer }; + + auto write = [&]<typename T>(T value) { + if (stream.write({ &value, sizeof(T) }) != sizeof(T)) + return false; + return true; + }; + + auto palette = palette_to_vector(); + + if (!write(size_in_bytes()) || !write((unsigned)size().width()) || !write((unsigned)size().height()) || !write(m_format) || !write((unsigned)palette.size())) + return {}; + + for (auto& p : palette) { + if (!write(p)) + return {}; + } + + auto size = size_in_bytes(); + ASSERT(stream.remaining() == size); + if (stream.write({ scanline(0), size }) != size) + return {}; + + return buffer; +} + +Bitmap::Bitmap(BitmapFormat format, NonnullRefPtr<SharedBuffer>&& shared_buffer, const IntSize& size, const Vector<RGBA32>& palette) + : m_size(size) + , m_data(shared_buffer->data<void>()) + , m_pitch(minimum_pitch(size.width(), format)) + , m_format(format) + , m_shared_buffer(move(shared_buffer)) +{ + ASSERT(!is_indexed() || !palette.is_empty()); + ASSERT(!size_would_overflow(format, size)); + ASSERT(size_in_bytes() <= static_cast<size_t>(m_shared_buffer->size())); + + if (is_indexed(m_format)) + allocate_palette_from_format(m_format, palette); +} + +RefPtr<Gfx::Bitmap> Bitmap::clone() const +{ + RefPtr<Gfx::Bitmap> new_bitmap {}; + if (m_purgeable) { + new_bitmap = Bitmap::create_purgeable(format(), size()); + } else { + new_bitmap = Bitmap::create(format(), size()); + } + + if (!new_bitmap) { + return nullptr; + } + + ASSERT(size_in_bytes() == new_bitmap->size_in_bytes()); + memcpy(new_bitmap->scanline(0), scanline(0), size_in_bytes()); + + return new_bitmap; +} + +RefPtr<Gfx::Bitmap> Bitmap::rotated(Gfx::RotationDirection rotation_direction) const +{ + auto w = this->width(); + auto h = this->height(); + + auto new_bitmap = Gfx::Bitmap::create(this->format(), { h, w }); + if (!new_bitmap) + return nullptr; + + for (int i = 0; i < w; i++) { + for (int j = 0; j < h; j++) { + Color color; + if (rotation_direction == Gfx::RotationDirection::Left) + color = this->get_pixel(w - i - 1, j); + else + color = this->get_pixel(i, h - j - 1); + + new_bitmap->set_pixel(j, i, color); + } + } + + return new_bitmap; +} + +RefPtr<Gfx::Bitmap> Bitmap::flipped(Gfx::Orientation orientation) const +{ + auto w = this->width(); + auto h = this->height(); + + auto new_bitmap = Gfx::Bitmap::create(this->format(), { w, h }); + if (!new_bitmap) + return nullptr; + + for (int i = 0; i < w; i++) { + for (int j = 0; j < h; j++) { + Color color = this->get_pixel(i, j); + if (orientation == Orientation::Vertical) + new_bitmap->set_pixel(i, h - j - 1, color); + else + new_bitmap->set_pixel(w - i - 1, j, color); + } + } + + return new_bitmap; +} + +RefPtr<Bitmap> Bitmap::to_bitmap_backed_by_shared_buffer() const +{ + if (m_shared_buffer) + return *this; + auto buffer = SharedBuffer::create_with_size(size_in_bytes()); + if (!buffer) + return nullptr; + auto bitmap = Bitmap::create_with_shared_buffer(m_format, *buffer, m_size, palette_to_vector()); + if (!bitmap) + return nullptr; + memcpy(buffer->data<void>(), scanline(0), size_in_bytes()); + return bitmap; +} + +Bitmap::~Bitmap() +{ + if (m_needs_munmap) { + int rc = munmap(m_data, size_in_bytes()); + ASSERT(rc == 0); + } + m_data = nullptr; + delete[] m_palette; +} + +void Bitmap::set_mmap_name([[maybe_unused]] const StringView& name) +{ + ASSERT(m_needs_munmap); +#ifdef __serenity__ + ::set_mmap_name(m_data, size_in_bytes(), name.to_string().characters()); +#endif +} + +void Bitmap::fill(Color color) +{ + ASSERT(!is_indexed(m_format)); + for (int y = 0; y < height(); ++y) { + auto* scanline = this->scanline(y); + fast_u32_fill(scanline, color.value(), width()); + } +} + +void Bitmap::set_volatile() +{ + ASSERT(m_purgeable); + if (m_volatile) + return; +#ifdef __serenity__ + int rc = madvise(m_data, size_in_bytes(), MADV_SET_VOLATILE); + if (rc < 0) { + perror("madvise(MADV_SET_VOLATILE)"); + ASSERT_NOT_REACHED(); + } +#endif + m_volatile = true; +} + +[[nodiscard]] bool Bitmap::set_nonvolatile() +{ + ASSERT(m_purgeable); + if (!m_volatile) + return true; +#ifdef __serenity__ + int rc = madvise(m_data, size_in_bytes(), MADV_SET_NONVOLATILE); + if (rc < 0) { + perror("madvise(MADV_SET_NONVOLATILE)"); + ASSERT_NOT_REACHED(); + } +#else + int rc = 0; +#endif + m_volatile = false; + return rc == 0; +} + +int Bitmap::shbuf_id() const +{ + return m_shared_buffer ? m_shared_buffer->shbuf_id() : -1; +} + +ShareableBitmap Bitmap::to_shareable_bitmap(pid_t peer_pid) const +{ + auto bitmap = to_bitmap_backed_by_shared_buffer(); + if (!bitmap) + return {}; + if (peer_pid > 0) + bitmap->shared_buffer()->share_with(peer_pid); + return ShareableBitmap(*bitmap); +} + +Optional<BackingStore> Bitmap::allocate_backing_store(BitmapFormat format, const IntSize& size, [[maybe_unused]] Purgeable purgeable) +{ + if (size_would_overflow(format, size)) + return {}; + + const auto pitch = minimum_pitch(size.width(), format); + const auto data_size_in_bytes = size_in_bytes(pitch, size.height()); + + int map_flags = MAP_ANONYMOUS | MAP_PRIVATE; + if (purgeable == Purgeable::Yes) + map_flags |= MAP_NORESERVE; +#ifdef __serenity__ + void* data = mmap_with_name(nullptr, data_size_in_bytes, PROT_READ | PROT_WRITE, map_flags, 0, 0, String::format("GraphicsBitmap [%dx%d]", size.width(), size.height()).characters()); +#else + void* data = mmap(nullptr, data_size_in_bytes, PROT_READ | PROT_WRITE, map_flags, 0, 0); +#endif + if (data == MAP_FAILED) { + perror("mmap"); + return {}; + } + return { { data, pitch, data_size_in_bytes } }; +} + +void Bitmap::allocate_palette_from_format(BitmapFormat format, const Vector<RGBA32>& source_palette) +{ + size_t size = palette_size(format); + if (size == 0) + return; + m_palette = new RGBA32[size]; + if (!source_palette.is_empty()) { + ASSERT(source_palette.size() == size); + memcpy(m_palette, source_palette.data(), size * sizeof(RGBA32)); + } +} + +Vector<RGBA32> Bitmap::palette_to_vector() const +{ + Vector<RGBA32> vector; + auto size = palette_size(m_format); + vector.ensure_capacity(size); + for (size_t i = 0; i < size; ++i) + vector.unchecked_append(palette_color(i).value()); + return vector; +} + +} diff --git a/Userland/Libraries/LibGfx/Bitmap.h b/Userland/Libraries/LibGfx/Bitmap.h new file mode 100644 index 0000000000..4d0b236f57 --- /dev/null +++ b/Userland/Libraries/LibGfx/Bitmap.h @@ -0,0 +1,336 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Forward.h> +#include <AK/RefCounted.h> +#include <AK/RefPtr.h> +#include <LibGfx/Color.h> +#include <LibGfx/Forward.h> +#include <LibGfx/Rect.h> + +#define ENUMERATE_IMAGE_FORMATS \ + __ENUMERATE_IMAGE_FORMAT(pbm, ".pbm") \ + __ENUMERATE_IMAGE_FORMAT(pgm, ".pgm") \ + __ENUMERATE_IMAGE_FORMAT(png, ".png") \ + __ENUMERATE_IMAGE_FORMAT(ppm, ".ppm") \ + __ENUMERATE_IMAGE_FORMAT(gif, ".gif") \ + __ENUMERATE_IMAGE_FORMAT(bmp, ".bmp") \ + __ENUMERATE_IMAGE_FORMAT(ico, ".ico") \ + __ENUMERATE_IMAGE_FORMAT(jpg, ".jpg") \ + __ENUMERATE_IMAGE_FORMAT(jpg, ".jpeg") + +namespace Gfx { + +enum class BitmapFormat { + Invalid, + Indexed1, + Indexed2, + Indexed4, + Indexed8, + RGB32, + RGBA32, +}; + +enum class StorageFormat { + Indexed8, + RGB32, + RGBA32, +}; + +static StorageFormat determine_storage_format(BitmapFormat format) +{ + switch (format) { + case BitmapFormat::RGB32: + return StorageFormat::RGB32; + case BitmapFormat::RGBA32: + return StorageFormat::RGBA32; + case BitmapFormat::Indexed1: + case BitmapFormat::Indexed2: + case BitmapFormat::Indexed4: + case BitmapFormat::Indexed8: + return StorageFormat::Indexed8; + default: + ASSERT_NOT_REACHED(); + } +} + +struct BackingStore; + +enum RotationDirection { + Left, + Right +}; + +class Bitmap : public RefCounted<Bitmap> { +public: + static RefPtr<Bitmap> create(BitmapFormat, const IntSize&); + static RefPtr<Bitmap> create_shareable(BitmapFormat, const IntSize&); + static RefPtr<Bitmap> create_purgeable(BitmapFormat, const IntSize&); + static RefPtr<Bitmap> create_wrapper(BitmapFormat, const IntSize&, size_t pitch, void*); + static RefPtr<Bitmap> load_from_file(const StringView& path); + static RefPtr<Bitmap> create_with_shared_buffer(BitmapFormat, NonnullRefPtr<SharedBuffer>&&, const IntSize&); + static RefPtr<Bitmap> create_with_shared_buffer(BitmapFormat, NonnullRefPtr<SharedBuffer>&&, const IntSize&, const Vector<RGBA32>& palette); + static RefPtr<Bitmap> create_from_serialized_byte_buffer(ByteBuffer&& buffer); + static bool is_path_a_supported_image_format(const StringView& path) + { +#define __ENUMERATE_IMAGE_FORMAT(Name, Ext) \ + if (path.ends_with(Ext, CaseSensitivity::CaseInsensitive)) \ + return true; + ENUMERATE_IMAGE_FORMATS +#undef __ENUMERATE_IMAGE_FORMAT + + return false; + } + + RefPtr<Gfx::Bitmap> clone() const; + + RefPtr<Gfx::Bitmap> rotated(Gfx::RotationDirection) const; + RefPtr<Gfx::Bitmap> flipped(Gfx::Orientation) const; + RefPtr<Bitmap> to_bitmap_backed_by_shared_buffer() const; + ByteBuffer serialize_to_byte_buffer() const; + + ShareableBitmap to_shareable_bitmap(pid_t peer_pid = -1) const; + + ~Bitmap(); + + u8* scanline_u8(int y); + const u8* scanline_u8(int y) const; + RGBA32* scanline(int y); + const RGBA32* scanline(int y) const; + + IntRect rect() const { return { {}, m_size }; } + IntSize size() const { return m_size; } + int width() const { return m_size.width(); } + int height() const { return m_size.height(); } + size_t pitch() const { return m_pitch; } + int shbuf_id() const; + + SharedBuffer* shared_buffer() { return m_shared_buffer.ptr(); } + const SharedBuffer* shared_buffer() const { return m_shared_buffer.ptr(); } + + ALWAYS_INLINE bool is_indexed() const + { + return is_indexed(m_format); + } + + ALWAYS_INLINE static bool is_indexed(BitmapFormat format) + { + return format == BitmapFormat::Indexed8 || format == BitmapFormat::Indexed4 + || format == BitmapFormat::Indexed2 || format == BitmapFormat::Indexed1; + } + + static size_t palette_size(BitmapFormat format) + { + switch (format) { + case BitmapFormat::Indexed1: + return 2; + case BitmapFormat::Indexed2: + return 4; + case BitmapFormat::Indexed4: + return 16; + case BitmapFormat::Indexed8: + return 256; + default: + return 0; + } + } + + Vector<RGBA32> palette_to_vector() const; + + static unsigned bpp_for_format(BitmapFormat format) + { + switch (format) { + case BitmapFormat::Indexed1: + return 1; + case BitmapFormat::Indexed2: + return 2; + case BitmapFormat::Indexed4: + return 4; + case BitmapFormat::Indexed8: + return 8; + case BitmapFormat::RGB32: + case BitmapFormat::RGBA32: + return 32; + default: + ASSERT_NOT_REACHED(); + case BitmapFormat::Invalid: + return 0; + } + } + + static size_t minimum_pitch(size_t width, BitmapFormat); + + unsigned bpp() const + { + return bpp_for_format(m_format); + } + + void fill(Color); + + bool has_alpha_channel() const { return m_format == BitmapFormat::RGBA32; } + BitmapFormat format() const { return m_format; } + + void set_mmap_name(const StringView&); + + static constexpr size_t size_in_bytes(size_t pitch, int height) { return pitch * height; } + size_t size_in_bytes() const { return size_in_bytes(m_pitch, height()); } + + Color palette_color(u8 index) const { return Color::from_rgba(m_palette[index]); } + void set_palette_color(u8 index, Color color) { m_palette[index] = color.value(); } + + template<StorageFormat> + Color get_pixel(int x, int y) const; + Color get_pixel(int x, int y) const; + Color get_pixel(const IntPoint& position) const + { + return get_pixel(position.x(), position.y()); + } + + template<StorageFormat> + void set_pixel(int x, int y, Color); + void set_pixel(int x, int y, Color); + void set_pixel(const IntPoint& position, Color color) + { + set_pixel(position.x(), position.y(), color); + } + + bool is_purgeable() const { return m_purgeable; } + bool is_volatile() const { return m_volatile; } + void set_volatile(); + [[nodiscard]] bool set_nonvolatile(); + +private: + enum class Purgeable { + No, + Yes + }; + Bitmap(BitmapFormat, const IntSize&, Purgeable, const BackingStore&); + Bitmap(BitmapFormat, const IntSize&, size_t pitch, void*); + Bitmap(BitmapFormat, NonnullRefPtr<SharedBuffer>&&, const IntSize&, const Vector<RGBA32>& palette); + + static Optional<BackingStore> allocate_backing_store(BitmapFormat, const IntSize&, Purgeable); + + void allocate_palette_from_format(BitmapFormat, const Vector<RGBA32>& source_palette); + + IntSize m_size; + void* m_data { nullptr }; + RGBA32* m_palette { nullptr }; + size_t m_pitch { 0 }; + BitmapFormat m_format { BitmapFormat::Invalid }; + bool m_needs_munmap { false }; + bool m_purgeable { false }; + bool m_volatile { false }; + RefPtr<SharedBuffer> m_shared_buffer; +}; + +inline u8* Bitmap::scanline_u8(int y) +{ + ASSERT(y >= 0 && y < height()); + return reinterpret_cast<u8*>(m_data) + (y * m_pitch); +} + +inline const u8* Bitmap::scanline_u8(int y) const +{ + ASSERT(y >= 0 && y < height()); + return reinterpret_cast<const u8*>(m_data) + (y * m_pitch); +} + +inline RGBA32* Bitmap::scanline(int y) +{ + return reinterpret_cast<RGBA32*>(scanline_u8(y)); +} + +inline const RGBA32* Bitmap::scanline(int y) const +{ + return reinterpret_cast<const RGBA32*>(scanline_u8(y)); +} + +template<> +inline Color Bitmap::get_pixel<StorageFormat::RGB32>(int x, int y) const +{ + ASSERT(x >= 0 && x < width()); + return Color::from_rgb(scanline(y)[x]); +} + +template<> +inline Color Bitmap::get_pixel<StorageFormat::RGBA32>(int x, int y) const +{ + ASSERT(x >= 0 && x < width()); + return Color::from_rgba(scanline(y)[x]); +} + +template<> +inline Color Bitmap::get_pixel<StorageFormat::Indexed8>(int x, int y) const +{ + ASSERT(x >= 0 && x < width()); + return Color::from_rgb(m_palette[scanline_u8(y)[x]]); +} + +inline Color Bitmap::get_pixel(int x, int y) const +{ + switch (determine_storage_format(m_format)) { + case StorageFormat::RGB32: + return get_pixel<StorageFormat::RGB32>(x, y); + case StorageFormat::RGBA32: + return get_pixel<StorageFormat::RGBA32>(x, y); + case StorageFormat::Indexed8: + return get_pixel<StorageFormat::Indexed8>(x, y); + default: + ASSERT_NOT_REACHED(); + } +} + +template<> +inline void Bitmap::set_pixel<StorageFormat::RGB32>(int x, int y, Color color) +{ + ASSERT(x >= 0 && x < width()); + scanline(y)[x] = color.value(); +} +template<> +inline void Bitmap::set_pixel<StorageFormat::RGBA32>(int x, int y, Color color) +{ + ASSERT(x >= 0 && x < width()); + scanline(y)[x] = color.value(); // drop alpha +} +inline void Bitmap::set_pixel(int x, int y, Color color) +{ + switch (determine_storage_format(m_format)) { + case StorageFormat::RGB32: + set_pixel<StorageFormat::RGB32>(x, y, color); + break; + case StorageFormat::RGBA32: + set_pixel<StorageFormat::RGBA32>(x, y, color); + break; + case StorageFormat::Indexed8: + ASSERT_NOT_REACHED(); + default: + ASSERT_NOT_REACHED(); + } +} + +} diff --git a/Userland/Libraries/LibGfx/BitmapFont.cpp b/Userland/Libraries/LibGfx/BitmapFont.cpp new file mode 100644 index 0000000000..e5e24cbde5 --- /dev/null +++ b/Userland/Libraries/LibGfx/BitmapFont.cpp @@ -0,0 +1,324 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "BitmapFont.h" +#include "Bitmap.h" +#include "Emoji.h" +#include <AK/StdLibExtras.h> +#include <AK/StringBuilder.h> +#include <AK/Utf32View.h> +#include <AK/Utf8View.h> +#include <AK/Vector.h> +#include <AK/kmalloc.h> +#include <LibCore/FileStream.h> +#include <LibGfx/FontDatabase.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/mman.h> +#include <unistd.h> + +namespace Gfx { + +struct [[gnu::packed]] FontFileHeader { + char magic[4]; + u8 glyph_width; + u8 glyph_height; + u8 type; + u8 is_variable_width; + u8 glyph_spacing; + u8 baseline; + u8 mean_line; + u8 presentation_size; + u16 weight; + char name[32]; + char family[32]; +}; + +NonnullRefPtr<Font> BitmapFont::clone() const +{ + size_t bytes_per_glyph = sizeof(u32) * glyph_height(); + auto* new_rows = static_cast<unsigned*>(malloc(bytes_per_glyph * m_glyph_count)); + memcpy(new_rows, m_rows, bytes_per_glyph * m_glyph_count); + auto* new_widths = static_cast<u8*>(malloc(m_glyph_count)); + if (m_glyph_widths) + memcpy(new_widths, m_glyph_widths, m_glyph_count); + else + memset(new_widths, m_glyph_width, m_glyph_count); + return adopt(*new BitmapFont(m_name, m_family, new_rows, new_widths, m_fixed_width, m_glyph_width, m_glyph_height, m_glyph_spacing, m_type, m_baseline, m_mean_line, m_presentation_size, m_weight, true)); +} + +NonnullRefPtr<BitmapFont> BitmapFont::create(u8 glyph_height, u8 glyph_width, bool fixed, FontTypes type) +{ + size_t bytes_per_glyph = sizeof(u32) * glyph_height; + size_t count = glyph_count_by_type(type); + auto* new_rows = static_cast<unsigned*>(malloc(bytes_per_glyph * count)); + memset(new_rows, 0, bytes_per_glyph * count); + auto* new_widths = static_cast<u8*>(malloc(count)); + memset(new_widths, glyph_width, count); + return adopt(*new BitmapFont("Untitled", "Untitled", new_rows, new_widths, fixed, glyph_width, glyph_height, 1, type, 0, 0, 0, 400, true)); +} + +BitmapFont::BitmapFont(String name, String family, unsigned* rows, u8* widths, bool is_fixed_width, u8 glyph_width, u8 glyph_height, u8 glyph_spacing, FontTypes type, u8 baseline, u8 mean_line, u8 presentation_size, u16 weight, bool owns_arrays) + : m_name(name) + , m_family(family) + , m_type(type) + , m_rows(rows) + , m_glyph_widths(widths) + , m_glyph_width(glyph_width) + , m_glyph_height(glyph_height) + , m_min_glyph_width(glyph_width) + , m_max_glyph_width(glyph_width) + , m_glyph_spacing(glyph_spacing) + , m_baseline(baseline) + , m_mean_line(mean_line) + , m_presentation_size(presentation_size) + , m_weight(weight) + , m_fixed_width(is_fixed_width) + , m_owns_arrays(owns_arrays) +{ + update_x_height(); + + m_glyph_count = glyph_count_by_type(m_type); + + if (!m_fixed_width) { + u8 maximum = 0; + u8 minimum = 255; + for (size_t i = 0; i < m_glyph_count; ++i) { + minimum = min(minimum, m_glyph_widths[i]); + maximum = max(maximum, m_glyph_widths[i]); + } + m_min_glyph_width = minimum; + m_max_glyph_width = maximum; + } +} + +BitmapFont::~BitmapFont() +{ + if (m_owns_arrays) { + free(m_glyph_widths); + free(m_rows); + } +} + +RefPtr<BitmapFont> BitmapFont::load_from_memory(const u8* data) +{ + auto& header = *reinterpret_cast<const FontFileHeader*>(data); + if (memcmp(header.magic, "!Fnt", 4)) { + dbgln("header.magic != '!Fnt', instead it's '{:c}{:c}{:c}{:c}'", header.magic[0], header.magic[1], header.magic[2], header.magic[3]); + return nullptr; + } + if (header.name[sizeof(header.name) - 1] != '\0') { + dbgln("Font name not fully null-terminated"); + return nullptr; + } + + if (header.family[sizeof(header.family) - 1] != '\0') { + dbgln("Font family not fully null-terminated"); + return nullptr; + } + + FontTypes type; + if (header.type == 0) + type = FontTypes::Default; + else if (header.type == 1) + type = FontTypes::LatinExtendedA; + else + ASSERT_NOT_REACHED(); + + size_t count = glyph_count_by_type(type); + size_t bytes_per_glyph = sizeof(unsigned) * header.glyph_height; + + auto* rows = const_cast<unsigned*>((const unsigned*)(data + sizeof(FontFileHeader))); + u8* widths = nullptr; + if (header.is_variable_width) + widths = (u8*)(rows) + count * bytes_per_glyph; + return adopt(*new BitmapFont(String(header.name), String(header.family), rows, widths, !header.is_variable_width, header.glyph_width, header.glyph_height, header.glyph_spacing, type, header.baseline, header.mean_line, header.presentation_size, header.weight)); +} + +size_t BitmapFont::glyph_count_by_type(FontTypes type) +{ + if (type == FontTypes::Default) + return 256; + + if (type == FontTypes::LatinExtendedA) + return 384; + + dbgln("Unknown font type: {}", (int)type); + ASSERT_NOT_REACHED(); +} + +RefPtr<BitmapFont> BitmapFont::load_from_file(const StringView& path) +{ + auto file_or_error = MappedFile::map(path); + if (file_or_error.is_error()) + return nullptr; + + auto font = load_from_memory((const u8*)file_or_error.value()->data()); + if (!font) + return nullptr; + + font->m_mapped_file = file_or_error.release_value(); + return font; +} + +bool BitmapFont::write_to_file(const StringView& path) +{ + FontFileHeader header; + memset(&header, 0, sizeof(FontFileHeader)); + memcpy(header.magic, "!Fnt", 4); + header.glyph_width = m_glyph_width; + header.glyph_height = m_glyph_height; + header.type = m_type; + header.baseline = m_baseline; + header.mean_line = m_mean_line; + header.is_variable_width = !m_fixed_width; + header.glyph_spacing = m_glyph_spacing; + header.presentation_size = m_presentation_size; + header.weight = m_weight; + memcpy(header.name, m_name.characters(), min(m_name.length(), sizeof(header.name) - 1)); + memcpy(header.family, m_family.characters(), min(m_family.length(), sizeof(header.family) - 1)); + + size_t bytes_per_glyph = sizeof(unsigned) * m_glyph_height; + size_t count = glyph_count_by_type(m_type); + + auto stream_result = Core::OutputFileStream::open_buffered(path); + if (stream_result.is_error()) + return false; + auto& stream = stream_result.value(); + + stream << ReadonlyBytes { &header, sizeof(header) }; + stream << ReadonlyBytes { m_rows, count * bytes_per_glyph }; + stream << ReadonlyBytes { m_glyph_widths, count }; + + stream.flush(); + if (stream.handle_any_error()) + return false; + + return true; +} + +GlyphBitmap BitmapFont::glyph_bitmap(u32 code_point) const +{ + return GlyphBitmap(&m_rows[code_point * m_glyph_height], { glyph_width(code_point), m_glyph_height }); +} + +int BitmapFont::glyph_or_emoji_width(u32 code_point) const +{ + if (code_point < m_glyph_count) + return glyph_width(code_point); + + if (m_fixed_width) + return m_glyph_width; + + auto* emoji = Emoji::emoji_for_code_point(code_point); + if (emoji == nullptr) + return glyph_width('?'); + return emoji->size().width(); +} + +int BitmapFont::width(const StringView& string) const +{ + Utf8View utf8 { string }; + return width(utf8); +} + +int BitmapFont::width(const Utf8View& utf8) const +{ + bool first = true; + int width = 0; + + for (u32 code_point : utf8) { + if (!first) + width += glyph_spacing(); + first = false; + width += glyph_or_emoji_width(code_point); + } + + return width; +} + +int BitmapFont::width(const Utf32View& view) const +{ + if (view.length() == 0) + return 0; + int width = (view.length() - 1) * glyph_spacing(); + for (size_t i = 0; i < view.length(); ++i) + width += glyph_or_emoji_width(view.code_points()[i]); + return width; +} + +void BitmapFont::set_type(FontTypes type) +{ + if (type == m_type) + return; + + if (type == FontTypes::Default) + return; + + size_t new_glyph_count = glyph_count_by_type(type); + if (new_glyph_count <= m_glyph_count) { + m_glyph_count = new_glyph_count; + return; + } + + int item_count_to_copy = min(m_glyph_count, new_glyph_count); + + size_t bytes_per_glyph = sizeof(u32) * glyph_height(); + + auto* new_rows = static_cast<unsigned*>(kmalloc(bytes_per_glyph * new_glyph_count)); + memset(new_rows, (unsigned)0, bytes_per_glyph * new_glyph_count); + memcpy(new_rows, m_rows, bytes_per_glyph * item_count_to_copy); + + auto* new_widths = static_cast<u8*>(kmalloc(new_glyph_count)); + memset(new_widths, (u8)0, new_glyph_count); + memcpy(new_widths, m_glyph_widths, item_count_to_copy); + + kfree(m_rows); + kfree(m_glyph_widths); + + m_type = type; + m_glyph_count = new_glyph_count; + m_rows = new_rows; + m_glyph_widths = new_widths; +} + +String BitmapFont::qualified_name() const +{ + return String::formatted("{} {} {}", family(), presentation_size(), weight()); +} + +const Font& BitmapFont::bold_variant() const +{ + if (m_bold_variant) + return *m_bold_variant; + m_bold_variant = Gfx::FontDatabase::the().get(m_family, m_presentation_size, 700); + if (!m_bold_variant) + m_bold_variant = this; + return *m_bold_variant; +} + +} diff --git a/Userland/Libraries/LibGfx/BitmapFont.h b/Userland/Libraries/LibGfx/BitmapFont.h new file mode 100644 index 0000000000..c3ca9f4a1d --- /dev/null +++ b/Userland/Libraries/LibGfx/BitmapFont.h @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/MappedFile.h> +#include <AK/RefCounted.h> +#include <AK/RefPtr.h> +#include <AK/String.h> +#include <AK/Types.h> +#include <LibGfx/Font.h> +#include <LibGfx/Size.h> + +namespace Gfx { + +enum FontTypes { + Default = 0, + LatinExtendedA = 1 +}; + +class BitmapFont : public Font { +public: + NonnullRefPtr<Font> clone() const; + static NonnullRefPtr<BitmapFont> create(u8 glyph_height, u8 glyph_width, bool fixed, FontTypes type); + + static RefPtr<BitmapFont> load_from_file(const StringView& path); + bool write_to_file(const StringView& path); + + ~BitmapFont(); + + u8 presentation_size() const { return m_presentation_size; } + void set_presentation_size(u8 size) { m_presentation_size = size; } + + u16 weight() const { return m_weight; } + void set_weight(u16 weight) { m_weight = weight; } + + GlyphBitmap glyph_bitmap(u32 code_point) const; + + u8 glyph_width(size_t ch) const { return m_fixed_width ? m_glyph_width : m_glyph_widths[ch]; } + int glyph_or_emoji_width(u32 code_point) const; + u8 glyph_height() const { return m_glyph_height; } + int x_height() const { return m_x_height; } + + u8 min_glyph_width() const { return m_min_glyph_width; } + u8 max_glyph_width() const { return m_max_glyph_width; } + u8 glyph_fixed_width() const { return m_glyph_width; } + + u8 baseline() const { return m_baseline; } + void set_baseline(u8 baseline) + { + m_baseline = baseline; + update_x_height(); + } + + u8 mean_line() const { return m_mean_line; } + void set_mean_line(u8 mean_line) + { + m_mean_line = mean_line; + update_x_height(); + } + + int width(const StringView&) const; + int width(const Utf8View&) const; + int width(const Utf32View&) const; + + const String& name() const { return m_name; } + void set_name(String name) { m_name = move(name); } + + bool is_fixed_width() const { return m_fixed_width; } + void set_fixed_width(bool b) { m_fixed_width = b; } + + u8 glyph_spacing() const { return m_glyph_spacing; } + void set_glyph_spacing(u8 spacing) { m_glyph_spacing = spacing; } + + void set_glyph_width(size_t ch, u8 width) + { + ASSERT(m_glyph_widths); + m_glyph_widths[ch] = width; + } + + int glyph_count() const { return m_glyph_count; } + + FontTypes type() { return m_type; } + void set_type(FontTypes type); + + const String& family() const { return m_family; } + void set_family(String family) { m_family = move(family); } + + String qualified_name() const; + + const Font& bold_variant() const; + +private: + BitmapFont(String name, String family, unsigned* rows, u8* widths, bool is_fixed_width, u8 glyph_width, u8 glyph_height, u8 glyph_spacing, FontTypes type, u8 baseline, u8 mean_line, u8 presentation_size, u16 weight, bool owns_arrays = false); + + static RefPtr<BitmapFont> load_from_memory(const u8*); + static size_t glyph_count_by_type(FontTypes type); + + void update_x_height() { m_x_height = m_baseline - m_mean_line; }; + + String m_name; + String m_family; + FontTypes m_type; + size_t m_glyph_count { 256 }; + + unsigned* m_rows { nullptr }; + u8* m_glyph_widths { nullptr }; + RefPtr<MappedFile> m_mapped_file; + + u8 m_glyph_width { 0 }; + u8 m_glyph_height { 0 }; + u8 m_x_height { 0 }; + u8 m_min_glyph_width { 0 }; + u8 m_max_glyph_width { 0 }; + u8 m_glyph_spacing { 0 }; + u8 m_baseline { 0 }; + u8 m_mean_line { 0 }; + u8 m_presentation_size { 0 }; + u16 m_weight { 0 }; + + bool m_fixed_width { false }; + bool m_owns_arrays { false }; + + mutable RefPtr<Gfx::Font> m_bold_variant; +}; + +} diff --git a/Userland/Libraries/LibGfx/CMakeLists.txt b/Userland/Libraries/LibGfx/CMakeLists.txt new file mode 100644 index 0000000000..2a6d9750db --- /dev/null +++ b/Userland/Libraries/LibGfx/CMakeLists.txt @@ -0,0 +1,37 @@ +set(SOURCES + AffineTransform.cpp + Bitmap.cpp + BitmapFont.cpp + BMPLoader.cpp + BMPWriter.cpp + CharacterBitmap.cpp + ClassicStylePainter.cpp + ClassicWindowTheme.cpp + Color.cpp + DisjointRectSet.cpp + Emoji.cpp + Font.cpp + FontDatabase.cpp + GIFLoader.cpp + ICOLoader.cpp + ImageDecoder.cpp + JPGLoader.cpp + Painter.cpp + Palette.cpp + Path.cpp + PBMLoader.cpp + PGMLoader.cpp + PNGLoader.cpp + PPMLoader.cpp + Point.cpp + Rect.cpp + ShareableBitmap.cpp + Size.cpp + StylePainter.cpp + SystemTheme.cpp + Triangle.cpp + WindowTheme.cpp +) + +serenity_lib(LibGfx gfx) +target_link_libraries(LibGfx LibM LibCore) diff --git a/Userland/Libraries/LibGfx/CharacterBitmap.cpp b/Userland/Libraries/LibGfx/CharacterBitmap.cpp new file mode 100644 index 0000000000..d51ff1d080 --- /dev/null +++ b/Userland/Libraries/LibGfx/CharacterBitmap.cpp @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "CharacterBitmap.h" + +namespace Gfx { + +CharacterBitmap::CharacterBitmap(const char* ascii_data, unsigned width, unsigned height) + : m_bits(ascii_data) + , m_size(width, height) +{ +} + +CharacterBitmap::~CharacterBitmap() +{ +} + +NonnullRefPtr<CharacterBitmap> CharacterBitmap::create_from_ascii(const char* asciiData, unsigned width, unsigned height) +{ + return adopt(*new CharacterBitmap(asciiData, width, height)); +} + +} diff --git a/Userland/Libraries/LibGfx/CharacterBitmap.h b/Userland/Libraries/LibGfx/CharacterBitmap.h new file mode 100644 index 0000000000..9780383570 --- /dev/null +++ b/Userland/Libraries/LibGfx/CharacterBitmap.h @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "Size.h" +#include <AK/RefCounted.h> +#include <AK/RefPtr.h> + +namespace Gfx { + +class CharacterBitmap : public RefCounted<CharacterBitmap> { +public: + static NonnullRefPtr<CharacterBitmap> create_from_ascii(const char* asciiData, unsigned width, unsigned height); + ~CharacterBitmap(); + + bool bit_at(unsigned x, unsigned y) const { return m_bits[y * width() + x] == '#'; } + const char* bits() const { return m_bits; } + + IntSize size() const { return m_size; } + unsigned width() const { return m_size.width(); } + unsigned height() const { return m_size.height(); } + +private: + CharacterBitmap(const char* b, unsigned w, unsigned h); + + const char* m_bits { nullptr }; + IntSize m_size; +}; + +} diff --git a/Userland/Libraries/LibGfx/ClassicStylePainter.cpp b/Userland/Libraries/LibGfx/ClassicStylePainter.cpp new file mode 100644 index 0000000000..95c9e9ffb6 --- /dev/null +++ b/Userland/Libraries/LibGfx/ClassicStylePainter.cpp @@ -0,0 +1,405 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * Copyright (c) 2020 Sarah Taube <metalflakecobaltpaint@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <AK/StringView.h> +#include <LibGfx/Bitmap.h> +#include <LibGfx/CharacterBitmap.h> +#include <LibGfx/ClassicStylePainter.h> +#include <LibGfx/Painter.h> +#include <LibGfx/Palette.h> + +namespace Gfx { + +void ClassicStylePainter::paint_tab_button(Painter& painter, const IntRect& rect, const Palette& palette, bool active, bool hovered, bool enabled, bool top) +{ + Color base_color = palette.button(); + Color highlight_color2 = palette.threed_highlight(); + Color shadow_color1 = palette.threed_shadow1(); + Color shadow_color2 = palette.threed_shadow2(); + + if (hovered && enabled && !active) + base_color = palette.hover_highlight(); + + PainterStateSaver saver(painter); + painter.translate(rect.location()); + + if (top) { + // Base + painter.fill_rect({ 1, 1, rect.width() - 2, rect.height() - 1 }, base_color); + + // Top line + painter.draw_line({ 2, 0 }, { rect.width() - 3, 0 }, highlight_color2); + + // Left side + painter.draw_line({ 0, 2 }, { 0, rect.height() - 1 }, highlight_color2); + painter.set_pixel({ 1, 1 }, highlight_color2); + + // Right side + + IntPoint top_right_outer { rect.width() - 1, 2 }; + IntPoint bottom_right_outer { rect.width() - 1, rect.height() - 1 }; + painter.draw_line(top_right_outer, bottom_right_outer, shadow_color2); + + IntPoint top_right_inner { rect.width() - 2, 2 }; + IntPoint bottom_right_inner { rect.width() - 2, rect.height() - 1 }; + painter.draw_line(top_right_inner, bottom_right_inner, shadow_color1); + + painter.set_pixel(rect.width() - 2, 1, shadow_color2); + } else { + // Base + painter.fill_rect({ 0, 0, rect.width() - 1, rect.height() }, base_color); + + // Bottom line + painter.draw_line({ 2, rect.height() - 1 }, { rect.width() - 3, rect.height() - 1 }, shadow_color2); + + // Left side + painter.draw_line({ 0, 0 }, { 0, rect.height() - 3 }, highlight_color2); + painter.set_pixel({ 1, rect.height() - 2 }, highlight_color2); + + // Right side + IntPoint top_right_outer { rect.width() - 1, 0 }; + IntPoint bottom_right_outer { rect.width() - 1, rect.height() - 3 }; + painter.draw_line(top_right_outer, bottom_right_outer, shadow_color2); + + IntPoint top_right_inner { rect.width() - 2, 0 }; + IntPoint bottom_right_inner { rect.width() - 2, rect.height() - 3 }; + painter.draw_line(top_right_inner, bottom_right_inner, shadow_color1); + + painter.set_pixel(rect.width() - 2, rect.height() - 2, shadow_color2); + } +} + +static void paint_button_new(Painter& painter, const IntRect& a_rect, const Palette& palette, bool pressed, bool checked, bool hovered, bool enabled, bool focused) +{ + Color button_color = palette.button(); + Color highlight_color = palette.threed_highlight(); + Color shadow_color1 = palette.threed_shadow1(); + Color shadow_color2 = palette.threed_shadow2(); + + if (checked && enabled) { + if (hovered) + button_color = palette.hover_highlight(); + else + button_color = palette.button(); + } else if (hovered && enabled) + button_color = palette.hover_highlight(); + + PainterStateSaver saver(painter); + + auto rect = a_rect; + if (focused) { + painter.draw_rect(a_rect, palette.threed_shadow2()); + rect.shrink(2, 2); + } + + painter.translate(rect.location()); + + if (pressed || checked) { + // Base + Gfx::IntRect base_rect { 1, 1, rect.width() - 2, rect.height() - 2 }; + + if (checked && !pressed) + painter.fill_rect_with_dither_pattern(base_rect, palette.button().lightened(1.3f), palette.button()); + else + painter.fill_rect(base_rect, button_color); + + // Top shadow + painter.draw_line({ 0, 0 }, { rect.width() - 2, 0 }, shadow_color2); + painter.draw_line({ 0, 0 }, { 0, rect.height() - 2 }, shadow_color2); + + // Sunken shadow + painter.draw_line({ 1, 1 }, { rect.width() - 3, 1 }, shadow_color1); + painter.draw_line({ 1, 2 }, { 1, rect.height() - 3 }, shadow_color1); + + // Outer highlight + painter.draw_line({ 0, rect.height() - 1 }, { rect.width() - 1, rect.height() - 1 }, highlight_color); + painter.draw_line({ rect.width() - 1, 0 }, { rect.width() - 1, rect.height() - 2 }, highlight_color); + + // Inner highlight + painter.draw_line({ 1, rect.height() - 2 }, { rect.width() - 2, rect.height() - 2 }, palette.button()); + painter.draw_line({ rect.width() - 2, 1 }, { rect.width() - 2, rect.height() - 3 }, palette.button()); + } else { + // Base + painter.fill_rect({ 0, 0, rect.width(), rect.height() }, button_color); + + // Top highlight + painter.draw_line({ 1, 1 }, { rect.width() - 3, 1 }, highlight_color); + painter.draw_line({ 1, 1 }, { 1, rect.height() - 3 }, highlight_color); + + // Outer shadow + painter.draw_line({ 0, rect.height() - 1 }, { rect.width() - 1, rect.height() - 1 }, shadow_color2); + painter.draw_line({ rect.width() - 1, 0 }, { rect.width() - 1, rect.height() - 2 }, shadow_color2); + + // Inner shadow + painter.draw_line({ 1, rect.height() - 2 }, { rect.width() - 2, rect.height() - 2 }, shadow_color1); + painter.draw_line({ rect.width() - 2, 1 }, { rect.width() - 2, rect.height() - 3 }, shadow_color1); + } +} + +void ClassicStylePainter::paint_button(Painter& painter, const IntRect& rect, const Palette& palette, ButtonStyle button_style, bool pressed, bool hovered, bool checked, bool enabled, bool focused) +{ + if (button_style == ButtonStyle::Normal) + return paint_button_new(painter, rect, palette, pressed, checked, hovered, enabled, focused); + + if (button_style == ButtonStyle::CoolBar && !enabled) + return; + + Color button_color = palette.button(); + Color highlight_color = palette.threed_highlight(); + Color shadow_color = palette.threed_shadow1(); + + PainterStateSaver saver(painter); + painter.translate(rect.location()); + + if (pressed || checked) { + // Base + IntRect base_rect { 1, 1, rect.width() - 2, rect.height() - 2 }; + if (checked && !pressed) { + painter.fill_rect_with_dither_pattern(base_rect, palette.button().lightened(1.3f), palette.button()); + } else { + painter.fill_rect(base_rect, button_color); + } + + // Sunken shadow + painter.draw_line({ 1, 1 }, { rect.width() - 2, 1 }, shadow_color); + painter.draw_line({ 1, 2 }, { 1, rect.height() - 2 }, shadow_color); + + // Bottom highlight + painter.draw_line({ rect.width() - 2, 1 }, { rect.width() - 2, rect.height() - 3 }, highlight_color); + painter.draw_line({ 1, rect.height() - 2 }, { rect.width() - 2, rect.height() - 2 }, highlight_color); + } else if (button_style == ButtonStyle::CoolBar && hovered) { + // Base + painter.fill_rect({ 1, 1, rect.width() - 2, rect.height() - 2 }, button_color); + + // Top highlight + painter.draw_line({ 1, 1 }, { rect.width() - 2, 1 }, highlight_color); + painter.draw_line({ 1, 2 }, { 1, rect.height() - 2 }, highlight_color); + + // Bottom shadow + painter.draw_line({ rect.width() - 2, 1 }, { rect.width() - 2, rect.height() - 3 }, shadow_color); + painter.draw_line({ 1, rect.height() - 2 }, { rect.width() - 2, rect.height() - 2 }, shadow_color); + } +} + +void ClassicStylePainter::paint_surface(Painter& painter, const IntRect& rect, const Palette& palette, bool paint_vertical_lines, bool paint_top_line) +{ + painter.fill_rect({ rect.x(), rect.y() + 1, rect.width(), rect.height() - 2 }, palette.button()); + painter.draw_line(rect.top_left(), rect.top_right(), paint_top_line ? palette.threed_highlight() : palette.button()); + painter.draw_line(rect.bottom_left(), rect.bottom_right(), palette.threed_shadow1()); + if (paint_vertical_lines) { + painter.draw_line(rect.top_left().translated(0, 1), rect.bottom_left().translated(0, -1), palette.threed_highlight()); + painter.draw_line(rect.top_right(), rect.bottom_right().translated(0, -1), palette.threed_shadow1()); + } +} + +void ClassicStylePainter::paint_frame(Painter& painter, const IntRect& rect, const Palette& palette, FrameShape shape, FrameShadow shadow, int thickness, bool skip_vertical_lines) +{ + Color top_left_color; + Color bottom_right_color; + Color dark_shade = palette.threed_shadow1(); + Color light_shade = palette.threed_highlight(); + + if (shape == FrameShape::Container && thickness >= 2) { + if (shadow == FrameShadow::Raised) { + dark_shade = palette.threed_shadow2(); + } + } + + if (shadow == FrameShadow::Raised) { + top_left_color = light_shade; + bottom_right_color = dark_shade; + } else if (shadow == FrameShadow::Sunken) { + top_left_color = dark_shade; + bottom_right_color = light_shade; + } else if (shadow == FrameShadow::Plain) { + top_left_color = dark_shade; + bottom_right_color = dark_shade; + } + + if (thickness >= 1) { + painter.draw_line(rect.top_left(), rect.top_right(), top_left_color); + painter.draw_line(rect.bottom_left(), rect.bottom_right(), bottom_right_color); + + if (shape != FrameShape::Panel || !skip_vertical_lines) { + painter.draw_line(rect.top_left().translated(0, 1), rect.bottom_left().translated(0, -1), top_left_color); + painter.draw_line(rect.top_right(), rect.bottom_right().translated(0, -1), bottom_right_color); + } + } + + if (shape == FrameShape::Container && thickness >= 2) { + Color top_left_color; + Color bottom_right_color; + Color dark_shade = palette.threed_shadow2(); + Color light_shade = palette.button(); + if (shadow == FrameShadow::Raised) { + dark_shade = palette.threed_shadow1(); + top_left_color = light_shade; + bottom_right_color = dark_shade; + } else if (shadow == FrameShadow::Sunken) { + top_left_color = dark_shade; + bottom_right_color = light_shade; + } else if (shadow == FrameShadow::Plain) { + top_left_color = dark_shade; + bottom_right_color = dark_shade; + } + IntRect inner_container_frame_rect = rect.shrunken(2, 2); + painter.draw_line(inner_container_frame_rect.top_left(), inner_container_frame_rect.top_right(), top_left_color); + painter.draw_line(inner_container_frame_rect.bottom_left(), inner_container_frame_rect.bottom_right(), bottom_right_color); + painter.draw_line(inner_container_frame_rect.top_left().translated(0, 1), inner_container_frame_rect.bottom_left().translated(0, -1), top_left_color); + painter.draw_line(inner_container_frame_rect.top_right(), inner_container_frame_rect.bottom_right().translated(0, -1), bottom_right_color); + } + + if (shape == FrameShape::Box && thickness >= 2) { + swap(top_left_color, bottom_right_color); + IntRect inner_rect = rect.shrunken(2, 2); + painter.draw_line(inner_rect.top_left(), inner_rect.top_right(), top_left_color); + painter.draw_line(inner_rect.bottom_left(), inner_rect.bottom_right(), bottom_right_color); + painter.draw_line(inner_rect.top_left().translated(0, 1), inner_rect.bottom_left().translated(0, -1), top_left_color); + painter.draw_line(inner_rect.top_right(), inner_rect.bottom_right().translated(0, -1), bottom_right_color); + } +} + +void ClassicStylePainter::paint_window_frame(Painter& painter, const IntRect& rect, const Palette& palette) +{ + Color base_color = palette.button(); + Color dark_shade = palette.threed_shadow2(); + Color mid_shade = palette.threed_shadow1(); + Color light_shade = palette.threed_highlight(); + + painter.draw_line(rect.top_left(), rect.top_right(), base_color); + painter.draw_line(rect.top_left().translated(0, 1), rect.bottom_left(), base_color); + painter.draw_line(rect.top_left().translated(1, 1), rect.top_right().translated(-1, 1), light_shade); + painter.draw_line(rect.top_left().translated(1, 1), rect.bottom_left().translated(1, -1), light_shade); + painter.draw_line(rect.top_left().translated(2, 2), rect.top_right().translated(-2, 2), base_color); + painter.draw_line(rect.top_left().translated(2, 2), rect.bottom_left().translated(2, -2), base_color); + painter.draw_line(rect.top_left().translated(3, 3), rect.top_right().translated(-3, 3), base_color); + painter.draw_line(rect.top_left().translated(3, 3), rect.bottom_left().translated(3, -3), base_color); + + painter.draw_line(rect.top_right(), rect.bottom_right(), dark_shade); + painter.draw_line(rect.top_right().translated(-1, 1), rect.bottom_right().translated(-1, -1), mid_shade); + painter.draw_line(rect.top_right().translated(-2, 2), rect.bottom_right().translated(-2, -2), base_color); + painter.draw_line(rect.top_right().translated(-3, 3), rect.bottom_right().translated(-3, -3), base_color); + painter.draw_line(rect.bottom_left(), rect.bottom_right(), dark_shade); + painter.draw_line(rect.bottom_left().translated(1, -1), rect.bottom_right().translated(-1, -1), mid_shade); + painter.draw_line(rect.bottom_left().translated(2, -2), rect.bottom_right().translated(-2, -2), base_color); + painter.draw_line(rect.bottom_left().translated(3, -3), rect.bottom_right().translated(-3, -3), base_color); +} + +void ClassicStylePainter::paint_progress_bar(Painter& painter, const IntRect& rect, const Palette& palette, int min, int max, int value, const StringView& text) +{ + // First we fill the entire widget with the gradient. This incurs a bit of + // overdraw but ensures a consistent look throughout the progression. + Color start_color = palette.active_window_border1(); + Color end_color = palette.active_window_border2(); + painter.fill_rect_with_gradient(rect, start_color, end_color); + + if (!text.is_null()) { + painter.draw_text(rect.translated(1, 1), text, TextAlignment::Center, palette.base_text()); + painter.draw_text(rect, text, TextAlignment::Center, palette.base_text().inverted()); + } + + float range_size = max - min; + float progress = (value - min) / range_size; + + // Then we carve out a hole in the remaining part of the widget. + // We draw the text a third time, clipped and inverse, for sharp contrast. + float progress_width = progress * rect.width(); + IntRect hole_rect { (int)progress_width, 0, (int)(rect.width() - progress_width), rect.height() }; + hole_rect.move_by(rect.location()); + hole_rect.set_right_without_resize(rect.right()); + PainterStateSaver saver(painter); + painter.fill_rect(hole_rect, palette.base()); + + painter.add_clip_rect(hole_rect); + if (!text.is_null()) + painter.draw_text(rect.translated(0, 0), text, TextAlignment::Center, palette.base_text()); +} + +static RefPtr<Gfx::Bitmap> s_unfilled_circle_bitmap; +static RefPtr<Gfx::Bitmap> s_filled_circle_bitmap; +static RefPtr<Gfx::Bitmap> s_changing_filled_circle_bitmap; +static RefPtr<Gfx::Bitmap> s_changing_unfilled_circle_bitmap; + +static const Gfx::Bitmap& circle_bitmap(bool checked, bool changing) +{ + if (changing) + return checked ? *s_changing_filled_circle_bitmap : *s_changing_unfilled_circle_bitmap; + return checked ? *s_filled_circle_bitmap : *s_unfilled_circle_bitmap; +} + +void ClassicStylePainter::paint_radio_button(Painter& painter, const IntRect& rect, const Palette&, bool is_checked, bool is_being_pressed) +{ + if (!s_unfilled_circle_bitmap) { + s_unfilled_circle_bitmap = Bitmap::load_from_file("/res/icons/serenity/unfilled-radio-circle.png"); + s_filled_circle_bitmap = Bitmap::load_from_file("/res/icons/serenity/filled-radio-circle.png"); + s_changing_filled_circle_bitmap = Bitmap::load_from_file("/res/icons/serenity/changing-filled-radio-circle.png"); + s_changing_unfilled_circle_bitmap = Bitmap::load_from_file("/res/icons/serenity/changing-unfilled-radio-circle.png"); + } + + auto& bitmap = circle_bitmap(is_checked, is_being_pressed); + painter.blit(rect.location(), bitmap, bitmap.rect()); +} + +static const char* s_checked_bitmap_data = { + " " + " # " + " ## " + " ### " + " ## ### " + " ##### " + " ### " + " # " + " " +}; + +static Gfx::CharacterBitmap* s_checked_bitmap; +static const int s_checked_bitmap_width = 9; +static const int s_checked_bitmap_height = 9; + +void ClassicStylePainter::paint_check_box(Painter& painter, const IntRect& rect, const Palette& palette, bool is_enabled, bool is_checked, bool is_being_pressed) +{ + painter.fill_rect(rect, is_enabled ? palette.base() : palette.window()); + paint_frame(painter, rect, palette, Gfx::FrameShape::Container, Gfx::FrameShadow::Sunken, 2); + + if (is_being_pressed) { + // FIXME: This color should not be hard-coded. + painter.draw_rect(rect.shrunken(4, 4), Color::MidGray); + } + + if (is_checked) { + if (!s_checked_bitmap) + s_checked_bitmap = &Gfx::CharacterBitmap::create_from_ascii(s_checked_bitmap_data, s_checked_bitmap_width, s_checked_bitmap_height).leak_ref(); + painter.draw_bitmap(rect.shrunken(4, 4).location(), *s_checked_bitmap, is_enabled ? palette.base_text() : palette.threed_shadow1()); + } +} + +void ClassicStylePainter::paint_transparency_grid(Painter& painter, const IntRect& rect, const Palette& palette) +{ + painter.fill_rect_with_checkerboard(rect, { 8, 8 }, palette.base().darkened(0.9), palette.base()); +} + +} diff --git a/Userland/Libraries/LibGfx/ClassicStylePainter.h b/Userland/Libraries/LibGfx/ClassicStylePainter.h new file mode 100644 index 0000000000..de5302ecb5 --- /dev/null +++ b/Userland/Libraries/LibGfx/ClassicStylePainter.h @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * Copyright (c) 2020 Sarah Taube <metalflakecobaltpaint@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Forward.h> +#include <LibGfx/Forward.h> +#include <LibGfx/StylePainter.h> + +namespace Gfx { + +class ClassicStylePainter : public BaseStylePainter { +public: + void paint_button(Painter&, const IntRect&, const Palette&, ButtonStyle, bool pressed, bool hovered = false, bool checked = false, bool enabled = true, bool focused = false) override; + void paint_tab_button(Painter&, const IntRect&, const Palette&, bool active, bool hovered, bool enabled, bool top) override; + void paint_surface(Painter&, const IntRect&, const Palette&, bool paint_vertical_lines = true, bool paint_top_line = true) override; + void paint_frame(Painter&, const IntRect&, const Palette&, FrameShape, FrameShadow, int thickness, bool skip_vertical_lines = false) override; + void paint_window_frame(Painter&, const IntRect&, const Palette&) override; + void paint_progress_bar(Painter&, const IntRect&, const Palette&, int min, int max, int value, const StringView& text) override; + void paint_radio_button(Painter&, const IntRect&, const Palette&, bool is_checked, bool is_being_pressed) override; + void paint_check_box(Painter&, const IntRect&, const Palette&, bool is_enabled, bool is_checked, bool is_being_pressed) override; + void paint_transparency_grid(Painter&, const IntRect&, const Palette&) override; +}; + +} diff --git a/Userland/Libraries/LibGfx/ClassicWindowTheme.cpp b/Userland/Libraries/LibGfx/ClassicWindowTheme.cpp new file mode 100644 index 0000000000..5476bec441 --- /dev/null +++ b/Userland/Libraries/LibGfx/ClassicWindowTheme.cpp @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <LibGfx/Bitmap.h> +#include <LibGfx/ClassicWindowTheme.h> +#include <LibGfx/Font.h> +#include <LibGfx/FontDatabase.h> +#include <LibGfx/Painter.h> +#include <LibGfx/Palette.h> +#include <LibGfx/StylePainter.h> + +namespace Gfx { + +ClassicWindowTheme::ClassicWindowTheme() +{ +} + +ClassicWindowTheme::~ClassicWindowTheme() +{ +} + +Gfx::IntRect ClassicWindowTheme::title_bar_icon_rect(WindowType window_type, const IntRect& window_rect, const Palette& palette) const +{ + auto titlebar_rect = title_bar_rect(window_type, window_rect, palette); + Gfx::IntRect icon_rect { + titlebar_rect.x() + 2, + titlebar_rect.y(), + 16, + 16, + }; + icon_rect.center_vertically_within(titlebar_rect); + icon_rect.move_by(0, 1); + return icon_rect; +} + +Gfx::IntRect ClassicWindowTheme::title_bar_text_rect(WindowType window_type, const IntRect& window_rect, const Palette& palette) const +{ + auto titlebar_rect = title_bar_rect(window_type, window_rect, palette); + auto titlebar_icon_rect = title_bar_icon_rect(window_type, window_rect, palette); + return { + titlebar_rect.x() + 3 + titlebar_icon_rect.width() + 2, + titlebar_rect.y(), + titlebar_rect.width() - 5 - titlebar_icon_rect.width() - 2, + titlebar_rect.height() + }; +} + +void ClassicWindowTheme::paint_normal_frame(Painter& painter, WindowState window_state, const IntRect& window_rect, const StringView& title_text, const Bitmap& icon, const Palette& palette, const IntRect& leftmost_button_rect) const +{ + auto frame_rect = frame_rect_for_window(WindowType::Normal, window_rect, palette); + frame_rect.set_location({ 0, 0 }); + Gfx::StylePainter::paint_window_frame(painter, frame_rect, palette); + + auto& title_font = FontDatabase::default_bold_font(); + + auto titlebar_rect = title_bar_rect(WindowType::Normal, window_rect, palette); + auto titlebar_icon_rect = title_bar_icon_rect(WindowType::Normal, window_rect, palette); + auto titlebar_inner_rect = title_bar_text_rect(WindowType::Normal, window_rect, palette); + auto titlebar_title_rect = titlebar_inner_rect; + titlebar_title_rect.set_width(FontDatabase::default_bold_font().width(title_text)); + + auto [title_color, border_color, border_color2, stripes_color, shadow_color] = compute_frame_colors(window_state, palette); + + painter.draw_line(titlebar_rect.bottom_left().translated(0, 1), titlebar_rect.bottom_right().translated(0, 1), palette.button()); + painter.draw_line(titlebar_rect.bottom_left().translated(0, 2), titlebar_rect.bottom_right().translated(0, 2), palette.button()); + + painter.fill_rect_with_gradient(titlebar_rect, border_color, border_color2); + + int stripe_left = titlebar_title_rect.right() + 5; + int stripe_right = leftmost_button_rect.left() - 3; + if (stripe_left && stripe_right && stripe_left < stripe_right) { + for (int i = 2; i <= titlebar_inner_rect.height() - 2; i += 2) { + painter.draw_line({ stripe_left, titlebar_inner_rect.y() + i }, { stripe_right, titlebar_inner_rect.y() + i }, stripes_color); + } + } + + auto clipped_title_rect = titlebar_title_rect; + clipped_title_rect.set_width(stripe_right - clipped_title_rect.x()); + if (!clipped_title_rect.is_empty()) { + painter.draw_text(clipped_title_rect.translated(1, 2), title_text, title_font, Gfx::TextAlignment::CenterLeft, shadow_color, Gfx::TextElision::Right); + // FIXME: The translated(0, 1) wouldn't be necessary if we could center text based on its baseline. + painter.draw_text(clipped_title_rect.translated(0, 1), title_text, title_font, Gfx::TextAlignment::CenterLeft, title_color, Gfx::TextElision::Right); + } + + painter.blit(titlebar_icon_rect.location(), icon, icon.rect()); +} + +IntRect ClassicWindowTheme::title_bar_rect(WindowType window_type, const IntRect& window_rect, const Palette& palette) const +{ + auto& title_font = FontDatabase::default_bold_font(); + auto window_titlebar_height = title_bar_height(palette); + // FIXME: The top of the titlebar doesn't get redrawn properly if this padding is different + int total_vertical_padding = title_font.glyph_height() - 1; + + if (window_type == WindowType::Notification) + return { window_rect.width() + 3, total_vertical_padding / 2 - 1, window_titlebar_height, window_rect.height() }; + return { 4, 4, window_rect.width(), window_titlebar_height }; +} + +ClassicWindowTheme::FrameColors ClassicWindowTheme::compute_frame_colors(WindowState state, const Palette& palette) const +{ + switch (state) { + case WindowState::Highlighted: + return { palette.highlight_window_title(), palette.highlight_window_border1(), palette.highlight_window_border2(), palette.highlight_window_title_stripes(), palette.highlight_window_title_shadow() }; + case WindowState::Moving: + return { palette.moving_window_title(), palette.moving_window_border1(), palette.moving_window_border2(), palette.moving_window_title_stripes(), palette.moving_window_title_shadow() }; + case WindowState::Active: + return { palette.active_window_title(), palette.active_window_border1(), palette.active_window_border2(), palette.active_window_title_stripes(), palette.active_window_title_shadow() }; + case WindowState::Inactive: + return { palette.inactive_window_title(), palette.inactive_window_border1(), palette.inactive_window_border2(), palette.inactive_window_title_stripes(), palette.inactive_window_title_shadow() }; + default: + ASSERT_NOT_REACHED(); + } +} + +void ClassicWindowTheme::paint_notification_frame(Painter& painter, const IntRect& window_rect, const Palette& palette, const IntRect& close_button_rect) const +{ + auto frame_rect = frame_rect_for_window(WindowType::Notification, window_rect, palette); + frame_rect.set_location({ 0, 0 }); + Gfx::StylePainter::paint_window_frame(painter, frame_rect, palette); + + auto titlebar_rect = title_bar_rect(WindowType::Notification, window_rect, palette); + painter.fill_rect_with_gradient(Gfx::Orientation::Vertical, titlebar_rect, palette.active_window_border1(), palette.active_window_border2()); + + int stripe_top = close_button_rect.bottom() + 4; + int stripe_bottom = window_rect.height() - 3; + if (stripe_top && stripe_bottom && stripe_top < stripe_bottom) { + for (int i = 2; i <= palette.window_title_height() - 2; i += 2) { + painter.draw_line({ titlebar_rect.x() + i, stripe_top }, { titlebar_rect.x() + i, stripe_bottom }, palette.active_window_title_stripes()); + } + } +} + +IntRect ClassicWindowTheme::frame_rect_for_window(WindowType window_type, const IntRect& window_rect, const Gfx::Palette& palette) const +{ + auto window_titlebar_height = title_bar_height(palette); + + switch (window_type) { + case WindowType::Normal: + return { + window_rect.x() - 4, + window_rect.y() - window_titlebar_height - 6, + window_rect.width() + 8, + window_rect.height() + 10 + window_titlebar_height + }; + case WindowType::Notification: + return { + window_rect.x() - 3, + window_rect.y() - 3, + window_rect.width() + 6 + window_titlebar_height, + window_rect.height() + 6 + }; + default: + return window_rect; + } +} + +Vector<IntRect> ClassicWindowTheme::layout_buttons(WindowType window_type, const IntRect& window_rect, const Palette& palette, size_t buttons) const +{ + int window_button_width = palette.window_title_button_width(); + int window_button_height = palette.window_title_button_height(); + int pos; + Vector<IntRect> button_rects; + if (window_type == WindowType::Notification) + pos = title_bar_rect(window_type, window_rect, palette).top() + 2; + else + pos = title_bar_text_rect(window_type, window_rect, palette).right() + 1; + + for (size_t i = 0; i < buttons; i++) { + if (window_type == WindowType::Notification) { + // The button height & width have to be equal or it leaks out of its area + Gfx::IntRect rect { 0, pos, window_button_height, window_button_height }; + rect.center_horizontally_within(title_bar_rect(window_type, window_rect, palette)); + button_rects.append(rect); + pos += window_button_height; + } else { + pos -= window_button_width; + Gfx::IntRect rect { pos, 0, window_button_width, window_button_height }; + rect.center_vertically_within(title_bar_text_rect(window_type, window_rect, palette)); + button_rects.append(rect); + } + } + return button_rects; +} + +int ClassicWindowTheme::title_bar_height(const Palette& palette) const +{ + auto& title_font = FontDatabase::default_bold_font(); + return max(palette.window_title_height(), title_font.glyph_height() + 8); +} + +} diff --git a/Userland/Libraries/LibGfx/ClassicWindowTheme.h b/Userland/Libraries/LibGfx/ClassicWindowTheme.h new file mode 100644 index 0000000000..b6ae32e891 --- /dev/null +++ b/Userland/Libraries/LibGfx/ClassicWindowTheme.h @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <LibGfx/Color.h> +#include <LibGfx/WindowTheme.h> + +namespace Gfx { + +class ClassicWindowTheme final : public WindowTheme { +public: + ClassicWindowTheme(); + virtual ~ClassicWindowTheme() override; + + virtual void paint_normal_frame(Painter&, WindowState, const IntRect& window_rect, const StringView& title, const Bitmap& icon, const Palette&, const IntRect& leftmost_button_rect) const override; + virtual void paint_notification_frame(Painter&, const IntRect& window_rect, const Palette&, const IntRect& close_button_rect) const override; + + virtual int title_bar_height(const Palette&) const override; + virtual IntRect title_bar_rect(WindowType, const IntRect& window_rect, const Palette&) const override; + virtual IntRect title_bar_icon_rect(WindowType, const IntRect& window_rect, const Palette&) const override; + virtual IntRect title_bar_text_rect(WindowType, const IntRect& window_rect, const Palette&) const override; + + virtual IntRect frame_rect_for_window(WindowType, const IntRect& window_rect, const Palette&) const override; + + virtual Vector<IntRect> layout_buttons(WindowType, const IntRect& window_rect, const Palette&, size_t buttons) const override; + +private: + struct FrameColors { + Color title_color; + Color border_color; + Color border_color2; + Color title_stripes_color; + Color title_shadow_color; + }; + + FrameColors compute_frame_colors(WindowState, const Palette&) const; +}; + +} diff --git a/Userland/Libraries/LibGfx/Color.cpp b/Userland/Libraries/LibGfx/Color.cpp new file mode 100644 index 0000000000..f8a0f244f4 --- /dev/null +++ b/Userland/Libraries/LibGfx/Color.cpp @@ -0,0 +1,440 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <AK/Assertions.h> +#include <AK/Optional.h> +#include <AK/String.h> +#include <AK/Vector.h> +#include <LibGfx/Color.h> +#include <LibGfx/SystemTheme.h> +#include <LibIPC/Decoder.h> +#include <LibIPC/Encoder.h> +#include <ctype.h> +#include <stdio.h> +#include <stdlib.h> + +namespace Gfx { + +Color::Color(NamedColor named) +{ + if (named == Transparent) { + m_value = 0; + return; + } + + struct { + u8 r; + u8 g; + u8 b; + } rgb; + + switch (named) { + case Black: + rgb = { 0, 0, 0 }; + break; + case White: + rgb = { 255, 255, 255 }; + break; + case Red: + rgb = { 255, 0, 0 }; + break; + case Green: + rgb = { 0, 255, 0 }; + break; + case Cyan: + rgb = { 0, 255, 255 }; + break; + case DarkCyan: + rgb = { 0, 127, 127 }; + break; + case MidCyan: + rgb = { 0, 192, 192 }; + break; + case Blue: + rgb = { 0, 0, 255 }; + break; + case Yellow: + rgb = { 255, 255, 0 }; + break; + case Magenta: + rgb = { 255, 0, 255 }; + break; + case DarkGray: + rgb = { 64, 64, 64 }; + break; + case MidGray: + rgb = { 127, 127, 127 }; + break; + case LightGray: + rgb = { 192, 192, 192 }; + break; + case MidGreen: + rgb = { 0, 192, 0 }; + break; + case MidBlue: + rgb = { 0, 0, 192 }; + break; + case MidRed: + rgb = { 192, 0, 0 }; + break; + case MidMagenta: + rgb = { 192, 0, 192 }; + break; + case DarkGreen: + rgb = { 0, 128, 0 }; + break; + case DarkBlue: + rgb = { 0, 0, 128 }; + break; + case DarkRed: + rgb = { 128, 0, 0 }; + break; + case WarmGray: + rgb = { 212, 208, 200 }; + break; + default: + ASSERT_NOT_REACHED(); + break; + } + + m_value = 0xff000000 | (rgb.r << 16) | (rgb.g << 8) | rgb.b; +} + +String Color::to_string() const +{ + return String::format("#%02x%02x%02x%02x", red(), green(), blue(), alpha()); +} + +String Color::to_string_without_alpha() const +{ + return String::format("#%02x%02x%02x", red(), green(), blue()); +} + +static Optional<Color> parse_rgb_color(const StringView& string) +{ + ASSERT(string.starts_with("rgb(")); + ASSERT(string.ends_with(")")); + + auto substring = string.substring_view(4, string.length() - 5); + auto parts = substring.split_view(','); + + if (parts.size() != 3) + return {}; + + auto r = parts[0].to_uint().value_or(256); + auto g = parts[1].to_uint().value_or(256); + auto b = parts[2].to_uint().value_or(256); + + if (r > 255 || g > 255 || b > 255) + return {}; + + return Color(r, g, b); +} + +static Optional<Color> parse_rgba_color(const StringView& string) +{ + ASSERT(string.starts_with("rgba(")); + ASSERT(string.ends_with(")")); + + auto substring = string.substring_view(5, string.length() - 6); + auto parts = substring.split_view(','); + + if (parts.size() != 4) + return {}; + + auto r = parts[0].to_int().value_or(256); + auto g = parts[1].to_int().value_or(256); + auto b = parts[2].to_int().value_or(256); + + double alpha = strtod(parts[3].to_string().characters(), nullptr); + unsigned a = alpha * 255; + + if (r > 255 || g > 255 || b > 255 || a > 255) + return {}; + + return Color(r, g, b, a); +} + +Optional<Color> Color::from_string(const StringView& string) +{ + if (string.is_empty()) + return {}; + + struct ColorAndWebName { + constexpr ColorAndWebName(RGBA32 c, const char* n) + : color(c) + , name(n) + { + } + RGBA32 color; + StringView name; + }; + + constexpr ColorAndWebName web_colors[] = { + // CSS Level 1 + { 0x000000, "black" }, + { 0xc0c0c0, "silver" }, + { 0x808080, "gray" }, + { 0xffffff, "white" }, + { 0x800000, "maroon" }, + { 0xff0000, "red" }, + { 0x800080, "purple" }, + { 0xff00ff, "fuchsia" }, + { 0x008000, "green" }, + { 0x00ff00, "lime" }, + { 0x808000, "olive" }, + { 0xffff00, "yellow" }, + { 0x000080, "navy" }, + { 0x0000ff, "blue" }, + { 0x008080, "teal" }, + { 0x00ffff, "aqua" }, + // CSS Level 2 (Revision 1) + { 0xffa500, "orange" }, + // CSS Color Module Level 3 + { 0xf0f8ff, "aliceblue" }, + { 0xfaebd7, "antiquewhite" }, + { 0x7fffd4, "aquamarine" }, + { 0xf0ffff, "azure" }, + { 0xf5f5dc, "beige" }, + { 0xffe4c4, "bisque" }, + { 0xffebcd, "blanchedalmond" }, + { 0x8a2be2, "blueviolet" }, + { 0xa52a2a, "brown" }, + { 0xdeb887, "burlywood" }, + { 0x5f9ea0, "cadetblue" }, + { 0x7fff00, "chartreuse" }, + { 0xd2691e, "chocolate" }, + { 0xff7f50, "coral" }, + { 0x6495ed, "cornflowerblue" }, + { 0xfff8dc, "cornsilk" }, + { 0xdc143c, "crimson" }, + { 0x00ffff, "cyan" }, + { 0x00008b, "darkblue" }, + { 0x008b8b, "darkcyan" }, + { 0xb8860b, "darkgoldenrod" }, + { 0xa9a9a9, "darkgray" }, + { 0x006400, "darkgreen" }, + { 0xa9a9a9, "darkgrey" }, + { 0xbdb76b, "darkkhaki" }, + { 0x8b008b, "darkmagenta" }, + { 0x556b2f, "darkolivegreen" }, + { 0xff8c00, "darkorange" }, + { 0x9932cc, "darkorchid" }, + { 0x8b0000, "darkred" }, + { 0xe9967a, "darksalmon" }, + { 0x8fbc8f, "darkseagreen" }, + { 0x483d8b, "darkslateblue" }, + { 0x2f4f4f, "darkslategray" }, + { 0x2f4f4f, "darkslategrey" }, + { 0x00ced1, "darkturquoise" }, + { 0x9400d3, "darkviolet" }, + { 0xff1493, "deeppink" }, + { 0x00bfff, "deepskyblue" }, + { 0x696969, "dimgray" }, + { 0x696969, "dimgrey" }, + { 0x1e90ff, "dodgerblue" }, + { 0xb22222, "firebrick" }, + { 0xfffaf0, "floralwhite" }, + { 0x228b22, "forestgreen" }, + { 0xdcdcdc, "gainsboro" }, + { 0xf8f8ff, "ghostwhite" }, + { 0xffd700, "gold" }, + { 0xdaa520, "goldenrod" }, + { 0xadff2f, "greenyellow" }, + { 0x808080, "grey" }, + { 0xf0fff0, "honeydew" }, + { 0xff69b4, "hotpink" }, + { 0xcd5c5c, "indianred" }, + { 0x4b0082, "indigo" }, + { 0xfffff0, "ivory" }, + { 0xf0e68c, "khaki" }, + { 0xe6e6fa, "lavender" }, + { 0xfff0f5, "lavenderblush" }, + { 0x7cfc00, "lawngreen" }, + { 0xfffacd, "lemonchiffon" }, + { 0xadd8e6, "lightblue" }, + { 0xf08080, "lightcoral" }, + { 0xe0ffff, "lightcyan" }, + { 0xfafad2, "lightgoldenrodyellow" }, + { 0xd3d3d3, "lightgray" }, + { 0x90ee90, "lightgreen" }, + { 0xd3d3d3, "lightgrey" }, + { 0xffb6c1, "lightpink" }, + { 0xffa07a, "lightsalmon" }, + { 0x20b2aa, "lightseagreen" }, + { 0x87cefa, "lightskyblue" }, + { 0x778899, "lightslategray" }, + { 0x778899, "lightslategrey" }, + { 0xb0c4de, "lightsteelblue" }, + { 0xffffe0, "lightyellow" }, + { 0x32cd32, "limegreen" }, + { 0xfaf0e6, "linen" }, + { 0xff00ff, "magenta" }, + { 0x66cdaa, "mediumaquamarine" }, + { 0x0000cd, "mediumblue" }, + { 0xba55d3, "mediumorchid" }, + { 0x9370db, "mediumpurple" }, + { 0x3cb371, "mediumseagreen" }, + { 0x7b68ee, "mediumslateblue" }, + { 0x00fa9a, "mediumspringgreen" }, + { 0x48d1cc, "mediumturquoise" }, + { 0xc71585, "mediumvioletred" }, + { 0x191970, "midnightblue" }, + { 0xf5fffa, "mintcream" }, + { 0xffe4e1, "mistyrose" }, + { 0xffe4b5, "moccasin" }, + { 0xffdead, "navajowhite" }, + { 0xfdf5e6, "oldlace" }, + { 0x6b8e23, "olivedrab" }, + { 0xff4500, "orangered" }, + { 0xda70d6, "orchid" }, + { 0xeee8aa, "palegoldenrod" }, + { 0x98fb98, "palegreen" }, + { 0xafeeee, "paleturquoise" }, + { 0xdb7093, "palevioletred" }, + { 0xffefd5, "papayawhip" }, + { 0xffdab9, "peachpuff" }, + { 0xcd853f, "peru" }, + { 0xffc0cb, "pink" }, + { 0xdda0dd, "plum" }, + { 0xb0e0e6, "powderblue" }, + { 0xbc8f8f, "rosybrown" }, + { 0x4169e1, "royalblue" }, + { 0x8b4513, "saddlebrown" }, + { 0xfa8072, "salmon" }, + { 0xf4a460, "sandybrown" }, + { 0x2e8b57, "seagreen" }, + { 0xfff5ee, "seashell" }, + { 0xa0522d, "sienna" }, + { 0x87ceeb, "skyblue" }, + { 0x6a5acd, "slateblue" }, + { 0x708090, "slategray" }, + { 0x708090, "slategrey" }, + { 0xfffafa, "snow" }, + { 0x00ff7f, "springgreen" }, + { 0x4682b4, "steelblue" }, + { 0xd2b48c, "tan" }, + { 0xd8bfd8, "thistle" }, + { 0xff6347, "tomato" }, + { 0x40e0d0, "turquoise" }, + { 0xee82ee, "violet" }, + { 0xf5deb3, "wheat" }, + { 0xf5f5f5, "whitesmoke" }, + { 0x9acd32, "yellowgreen" }, + // CSS Color Module Level 4 + { 0x663399, "rebeccapurple" }, + // (Fallback) + { 0x000000, nullptr } + }; + + for (size_t i = 0; !web_colors[i].name.is_null(); ++i) { + if (string == web_colors[i].name) + return Color::from_rgb(web_colors[i].color); + } + + if (string.starts_with("rgb(") && string.ends_with(")")) + return parse_rgb_color(string); + + if (string.starts_with("rgba(") && string.ends_with(")")) + return parse_rgba_color(string); + + if (string[0] != '#') + return {}; + + auto hex_nibble_to_u8 = [](char nibble) -> Optional<u8> { + if (!isxdigit(nibble)) + return {}; + if (nibble >= '0' && nibble <= '9') + return nibble - '0'; + return 10 + (tolower(nibble) - 'a'); + }; + + if (string.length() == 4) { + Optional<u8> r = hex_nibble_to_u8(string[1]); + Optional<u8> g = hex_nibble_to_u8(string[2]); + Optional<u8> b = hex_nibble_to_u8(string[3]); + if (!r.has_value() || !g.has_value() || !b.has_value()) + return {}; + return Color(r.value() * 17, g.value() * 17, b.value() * 17); + } + + if (string.length() == 5) { + Optional<u8> r = hex_nibble_to_u8(string[1]); + Optional<u8> g = hex_nibble_to_u8(string[2]); + Optional<u8> b = hex_nibble_to_u8(string[3]); + Optional<u8> a = hex_nibble_to_u8(string[4]); + if (!r.has_value() || !g.has_value() || !b.has_value() || !a.has_value()) + return {}; + return Color(r.value() * 17, g.value() * 17, b.value() * 17, a.value() * 17); + } + + if (string.length() != 7 && string.length() != 9) + return {}; + + auto to_hex = [&](char c1, char c2) -> Optional<u8> { + auto nib1 = hex_nibble_to_u8(c1); + auto nib2 = hex_nibble_to_u8(c2); + if (!nib1.has_value() || !nib2.has_value()) + return {}; + return nib1.value() << 4 | nib2.value(); + }; + + Optional<u8> r = to_hex(string[1], string[2]); + Optional<u8> g = to_hex(string[3], string[4]); + Optional<u8> b = to_hex(string[5], string[6]); + Optional<u8> a = string.length() == 9 ? to_hex(string[7], string[8]) : Optional<u8>(255); + + if (!r.has_value() || !g.has_value() || !b.has_value() || !a.has_value()) + return {}; + + return Color(r.value(), g.value(), b.value(), a.value()); +} + +const LogStream& operator<<(const LogStream& stream, Color value) +{ + return stream << value.to_string(); +} +} + +bool IPC::encode(IPC::Encoder& encoder, const Color& color) +{ + encoder << color.value(); + return true; +} + +bool IPC::decode(IPC::Decoder& decoder, Color& color) +{ + u32 rgba = 0; + if (!decoder.decode(rgba)) + return false; + color = Color::from_rgba(rgba); + return true; +} + +void AK::Formatter<Gfx::Color>::format(FormatBuilder& builder, const Gfx::Color& value) +{ + Formatter<StringView>::format(builder, value.to_string()); +} diff --git a/Userland/Libraries/LibGfx/Color.h b/Userland/Libraries/LibGfx/Color.h new file mode 100644 index 0000000000..e30cacba6c --- /dev/null +++ b/Userland/Libraries/LibGfx/Color.h @@ -0,0 +1,333 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Assertions.h> +#include <AK/Format.h> +#include <AK/Forward.h> +#include <AK/SIMD.h> +#include <AK/StdLibExtras.h> +#include <LibIPC/Forward.h> + +namespace Gfx { + +enum class ColorRole; +typedef u32 RGBA32; + +constexpr u32 make_rgb(u8 r, u8 g, u8 b) +{ + return ((r << 16) | (g << 8) | b); +} + +struct HSV { + double hue { 0 }; + double saturation { 0 }; + double value { 0 }; +}; + +class Color { +public: + enum NamedColor { + Transparent, + Black, + White, + Red, + Green, + Cyan, + Blue, + Yellow, + Magenta, + DarkGray, + MidGray, + LightGray, + WarmGray, + DarkCyan, + DarkGreen, + DarkBlue, + DarkRed, + MidCyan, + MidGreen, + MidRed, + MidBlue, + MidMagenta, + }; + + constexpr Color() { } + Color(NamedColor); + constexpr Color(u8 r, u8 g, u8 b) + : m_value(0xff000000 | (r << 16) | (g << 8) | b) + { + } + constexpr Color(u8 r, u8 g, u8 b, u8 a) + : m_value((a << 24) | (r << 16) | (g << 8) | b) + { + } + + static constexpr Color from_rgb(unsigned rgb) { return Color(rgb | 0xff000000); } + static constexpr Color from_rgba(unsigned rgba) { return Color(rgba); } + + constexpr u8 red() const { return (m_value >> 16) & 0xff; } + constexpr u8 green() const { return (m_value >> 8) & 0xff; } + constexpr u8 blue() const { return m_value & 0xff; } + constexpr u8 alpha() const { return (m_value >> 24) & 0xff; } + + void set_alpha(u8 value) + { + m_value &= 0x00ffffff; + m_value |= value << 24; + } + + constexpr void set_red(u8 value) + { + m_value &= 0xff00ffff; + m_value |= value << 16; + } + + constexpr void set_green(u8 value) + { + m_value &= 0xffff00ff; + m_value |= value << 8; + } + + constexpr void set_blue(u8 value) + { + m_value &= 0xffffff00; + m_value |= value; + } + + Color with_alpha(u8 alpha) const + { + return Color((m_value & 0x00ffffff) | alpha << 24); + } + + Color blend(Color source) const + { + if (!alpha() || source.alpha() == 255) + return source; + + if (!source.alpha()) + return *this; + +#ifdef __SSE__ + using AK::SIMD::i32x4; + + const i32x4 color = { + red(), + green(), + blue() + }; + const i32x4 source_color = { + source.red(), + source.green(), + source.blue() + }; + + const int d = 255 * (alpha() + source.alpha()) - alpha() * source.alpha(); + const i32x4 out = (color * alpha() * (255 - source.alpha()) + 255 * source.alpha() * source_color) / d; + return Color(out[0], out[1], out[2], d / 255); +#else + int d = 255 * (alpha() + source.alpha()) - alpha() * source.alpha(); + u8 r = (red() * alpha() * (255 - source.alpha()) + 255 * source.alpha() * source.red()) / d; + u8 g = (green() * alpha() * (255 - source.alpha()) + 255 * source.alpha() * source.green()) / d; + u8 b = (blue() * alpha() * (255 - source.alpha()) + 255 * source.alpha() * source.blue()) / d; + u8 a = d / 255; + return Color(r, g, b, a); +#endif + } + + Color to_grayscale() const + { + int gray = (red() + green() + blue()) / 3; + return Color(gray, gray, gray, alpha()); + } + + Color darkened(float amount = 0.5f) const + { + return Color(red() * amount, green() * amount, blue() * amount, alpha()); + } + + Color lightened(float amount = 1.2f) const + { + return Color(min(255, (int)((float)red() * amount)), min(255, (int)((float)green() * amount)), min(255, (int)((float)blue() * amount)), alpha()); + } + + Color inverted() const + { + return Color(~red(), ~green(), ~blue(), alpha()); + } + + Color xored(const Color& other) const + { + return Color(((other.m_value ^ m_value) & 0x00ffffff) | (m_value & 0xff000000)); + } + + RGBA32 value() const { return m_value; } + + bool operator==(const Color& other) const + { + return m_value == other.m_value; + } + + bool operator!=(const Color& other) const + { + return m_value != other.m_value; + } + + String to_string() const; + String to_string_without_alpha() const; + static Optional<Color> from_string(const StringView&); + + HSV to_hsv() const + { + HSV hsv; + double r = static_cast<double>(red()) / 255.0; + double g = static_cast<double>(green()) / 255.0; + double b = static_cast<double>(blue()) / 255.0; + double max = AK::max(AK::max(r, g), b); + double min = AK::min(AK::min(r, g), b); + double chroma = max - min; + + if (!chroma) + hsv.hue = 0.0; + else if (max == r) + hsv.hue = (60.0 * ((g - b) / chroma)) + 360.0; + else if (max == g) + hsv.hue = (60.0 * ((b - r) / chroma)) + 120.0; + else + hsv.hue = (60.0 * ((r - g) / chroma)) + 240.0; + + if (hsv.hue >= 360.0) + hsv.hue -= 360.0; + + if (!max) + hsv.saturation = 0; + else + hsv.saturation = chroma / max; + + hsv.value = max; + + ASSERT(hsv.hue >= 0.0 && hsv.hue < 360.0); + ASSERT(hsv.saturation >= 0.0 && hsv.saturation <= 1.0); + ASSERT(hsv.value >= 0.0 && hsv.value <= 1.0); + + return hsv; + } + + static Color from_hsv(double hue, double saturation, double value) + { + return from_hsv({ hue, saturation, value }); + } + + static Color from_hsv(const HSV& hsv) + { + ASSERT(hsv.hue >= 0.0 && hsv.hue < 360.0); + ASSERT(hsv.saturation >= 0.0 && hsv.saturation <= 1.0); + ASSERT(hsv.value >= 0.0 && hsv.value <= 1.0); + + double hue = hsv.hue; + double saturation = hsv.saturation; + double value = hsv.value; + + int high = static_cast<int>(hue / 60.0) % 6; + double f = (hue / 60.0) - high; + double c1 = value * (1.0 - saturation); + double c2 = value * (1.0 - saturation * f); + double c3 = value * (1.0 - saturation * (1.0 - f)); + + double r = 0; + double g = 0; + double b = 0; + + switch (high) { + case 0: + r = value; + g = c3; + b = c1; + break; + case 1: + r = c2; + g = value; + b = c1; + break; + case 2: + r = c1; + g = value; + b = c3; + break; + case 3: + r = c1; + g = c2; + b = value; + break; + case 4: + r = c3; + g = c1; + b = value; + break; + case 5: + r = value; + g = c1; + b = c2; + break; + } + + u8 out_r = (u8)(r * 255); + u8 out_g = (u8)(g * 255); + u8 out_b = (u8)(b * 255); + return Color(out_r, out_g, out_b); + } + +private: + constexpr explicit Color(RGBA32 rgba) + : m_value(rgba) + { + } + + RGBA32 m_value { 0 }; +}; + +const LogStream& operator<<(const LogStream&, Color); + +} + +using Gfx::Color; + +namespace AK { + +template<> +struct Formatter<Gfx::Color> : public Formatter<StringView> { + void format(FormatBuilder& builder, const Gfx::Color& value); +}; + +} + +namespace IPC { + +bool encode(Encoder&, const Gfx::Color&); +bool decode(Decoder&, Gfx::Color&); + +} diff --git a/Userland/Libraries/LibGfx/DisjointRectSet.cpp b/Userland/Libraries/LibGfx/DisjointRectSet.cpp new file mode 100644 index 0000000000..1ec3d95d90 --- /dev/null +++ b/Userland/Libraries/LibGfx/DisjointRectSet.cpp @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <LibGfx/DisjointRectSet.h> + +namespace Gfx { + +bool DisjointRectSet::add_no_shatter(const IntRect& new_rect) +{ + if (new_rect.is_empty()) + return false; + for (auto& rect : m_rects) { + if (rect.contains(new_rect)) + return false; + } + + m_rects.append(new_rect); + return true; +} + +void DisjointRectSet::shatter() +{ + Vector<IntRect, 32> output; + output.ensure_capacity(m_rects.size()); + bool pass_had_intersections = false; + do { + pass_had_intersections = false; + output.clear_with_capacity(); + for (size_t i = 0; i < m_rects.size(); ++i) { + auto& r1 = m_rects[i]; + for (size_t j = 0; j < m_rects.size(); ++j) { + if (i == j) + continue; + auto& r2 = m_rects[j]; + if (!r1.intersects(r2)) + continue; + pass_had_intersections = true; + auto pieces = r1.shatter(r2); + for (auto& piece : pieces) + output.append(piece); + m_rects.remove(i); + for (; i < m_rects.size(); ++i) + output.append(m_rects[i]); + goto next_pass; + } + output.append(r1); + } + next_pass: + swap(output, m_rects); + } while (pass_had_intersections); +} + +void DisjointRectSet::move_by(int dx, int dy) +{ + for (auto& r : m_rects) + r.move_by(dx, dy); +} + +bool DisjointRectSet::contains(const IntRect& rect) const +{ + if (is_empty() || rect.is_empty()) + return false; + + // TODO: This could use some optimization + DisjointRectSet remainder(rect); + for (auto& r : m_rects) { + auto shards = remainder.shatter(r); + if (shards.is_empty()) + return true; + remainder = move(shards); + } + return false; +} + +bool DisjointRectSet::intersects(const IntRect& rect) const +{ + for (auto& r : m_rects) { + if (r.intersects(rect)) + return true; + } + return false; +} + +bool DisjointRectSet::intersects(const DisjointRectSet& rects) const +{ + if (this == &rects) + return true; + + for (auto& r : m_rects) { + for (auto& r2 : rects.m_rects) { + if (r.intersects(r2)) + return true; + } + } + return false; +} + +DisjointRectSet DisjointRectSet::intersected(const IntRect& rect) const +{ + DisjointRectSet intersected_rects; + intersected_rects.m_rects.ensure_capacity(m_rects.capacity()); + for (auto& r : m_rects) { + auto intersected_rect = r.intersected(rect); + if (!intersected_rect.is_empty()) + intersected_rects.m_rects.append(intersected_rect); + } + // Since there should be no overlaps, we don't need to call shatter() + return intersected_rects; +} + +DisjointRectSet DisjointRectSet::intersected(const DisjointRectSet& rects) const +{ + if (&rects == this) + return clone(); + if (is_empty() || rects.is_empty()) + return {}; + + DisjointRectSet intersected_rects; + intersected_rects.m_rects.ensure_capacity(m_rects.capacity()); + for (auto& r : m_rects) { + for (auto& r2 : rects.m_rects) { + auto intersected_rect = r.intersected(r2); + if (!intersected_rect.is_empty()) + intersected_rects.m_rects.append(intersected_rect); + } + } + // Since there should be no overlaps, we don't need to call shatter() + return intersected_rects; +} + +DisjointRectSet DisjointRectSet::shatter(const IntRect& hammer) const +{ + if (hammer.is_empty()) + return clone(); + + DisjointRectSet shards; + for (auto& rect : m_rects) { + for (auto& shard : rect.shatter(hammer)) + shards.add_no_shatter(shard); + } + // Since there should be no overlaps, we don't need to call shatter() + return shards; +} + +DisjointRectSet DisjointRectSet::shatter(const DisjointRectSet& hammer) const +{ + if (this == &hammer) + return {}; + if (hammer.is_empty() || !intersects(hammer)) + return clone(); + + // TODO: This could use some optimization + DisjointRectSet shards = shatter(hammer.m_rects[0]); + auto rects_count = hammer.m_rects.size(); + for (size_t i = 1; i < rects_count && !shards.is_empty(); i++) { + if (hammer.m_rects[i].intersects(shards.m_rects)) { + auto shattered = shards.shatter(hammer.m_rects[i]); + shards = move(shattered); + } + } + // Since there should be no overlaps, we don't need to call shatter() + return shards; +} + +} diff --git a/Userland/Libraries/LibGfx/DisjointRectSet.h b/Userland/Libraries/LibGfx/DisjointRectSet.h new file mode 100644 index 0000000000..a54ce44ce3 --- /dev/null +++ b/Userland/Libraries/LibGfx/DisjointRectSet.h @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Vector.h> +#include <LibGfx/Point.h> +#include <LibGfx/Rect.h> + +namespace Gfx { + +class DisjointRectSet { +public: + DisjointRectSet(const DisjointRectSet&) = delete; + DisjointRectSet& operator=(const DisjointRectSet&) = delete; + + DisjointRectSet() { } + ~DisjointRectSet() { } + + DisjointRectSet(const IntRect& rect) + { + m_rects.append(rect); + } + + DisjointRectSet(DisjointRectSet&&) = default; + DisjointRectSet& operator=(DisjointRectSet&&) = default; + + DisjointRectSet clone() const + { + DisjointRectSet rects; + rects.m_rects = m_rects; + return rects; + } + + void move_by(int dx, int dy); + void move_by(const IntPoint& delta) + { + move_by(delta.x(), delta.y()); + } + + void add(const IntRect& rect) + { + if (add_no_shatter(rect) && m_rects.size() > 1) + shatter(); + } + + template<typename Container> + void add_many(const Container& rects) + { + bool added = false; + for (const auto& rect : rects) { + if (add_no_shatter(rect)) + added = true; + } + if (added && m_rects.size() > 1) + shatter(); + } + + void add(const DisjointRectSet& rect_set) + { + if (this == &rect_set) + return; + if (m_rects.is_empty()) { + m_rects = rect_set.m_rects; + } else { + add_many(rect_set.rects()); + } + } + + DisjointRectSet shatter(const IntRect&) const; + DisjointRectSet shatter(const DisjointRectSet& hammer) const; + + bool contains(const IntRect&) const; + bool intersects(const IntRect&) const; + bool intersects(const DisjointRectSet&) const; + DisjointRectSet intersected(const IntRect&) const; + DisjointRectSet intersected(const DisjointRectSet&) const; + + template<typename Function> + IterationDecision for_each_intersected(const IntRect& rect, Function f) const + { + if (is_empty() || rect.is_empty()) + return IterationDecision::Continue; + for (auto& r : m_rects) { + auto intersected_rect = r.intersected(rect); + if (intersected_rect.is_empty()) + continue; + IterationDecision decision = f(intersected_rect); + if (decision != IterationDecision::Continue) + return decision; + } + return IterationDecision::Continue; + } + + template<typename Function> + IterationDecision for_each_intersected(const DisjointRectSet& rects, Function f) const + { + if (is_empty() || rects.is_empty()) + return IterationDecision::Continue; + if (this == &rects) { + for (auto& r : m_rects) { + IterationDecision decision = f(r); + if (decision != IterationDecision::Continue) + return decision; + } + } else { + for (auto& r : m_rects) { + for (auto& r2 : rects.m_rects) { + auto intersected_rect = r.intersected(r2); + if (intersected_rect.is_empty()) + continue; + IterationDecision decision = f(intersected_rect); + if (decision != IterationDecision::Continue) + return decision; + } + } + } + return IterationDecision::Continue; + } + + bool is_empty() const { return m_rects.is_empty(); } + size_t size() const { return m_rects.size(); } + + void clear() { m_rects.clear(); } + void clear_with_capacity() { m_rects.clear_with_capacity(); } + const Vector<IntRect, 32>& rects() const { return m_rects; } + Vector<IntRect, 32> take_rects() { return move(m_rects); } + +private: + bool add_no_shatter(const IntRect&); + void shatter(); + + Vector<IntRect, 32> m_rects; +}; + +} diff --git a/Userland/Libraries/LibGfx/Emoji.cpp b/Userland/Libraries/LibGfx/Emoji.cpp new file mode 100644 index 0000000000..5965f452fb --- /dev/null +++ b/Userland/Libraries/LibGfx/Emoji.cpp @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <AK/HashMap.h> +#include <AK/String.h> +#include <LibGfx/Bitmap.h> +#include <LibGfx/Emoji.h> + +namespace Gfx { + +static HashMap<u32, RefPtr<Gfx::Bitmap>> s_emojis; + +const Bitmap* Emoji::emoji_for_code_point(u32 code_point) +{ + auto it = s_emojis.find(code_point); + if (it != s_emojis.end()) + return (*it).value.ptr(); + + String path = String::format("/res/emoji/U+%X.png", code_point); + + auto bitmap = Bitmap::load_from_file(path); + if (!bitmap) { + s_emojis.set(code_point, nullptr); + return nullptr; + } + + s_emojis.set(code_point, bitmap); + return bitmap.ptr(); +} + +} diff --git a/Userland/Libraries/LibGfx/Emoji.h b/Userland/Libraries/LibGfx/Emoji.h new file mode 100644 index 0000000000..232927732e --- /dev/null +++ b/Userland/Libraries/LibGfx/Emoji.h @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Types.h> + +namespace Gfx { + +class Bitmap; + +class Emoji { +public: + static const Gfx::Bitmap* emoji_for_code_point(u32 code_point); +}; + +} diff --git a/Userland/Libraries/LibGfx/Filters/BoxBlurFilter.h b/Userland/Libraries/LibGfx/Filters/BoxBlurFilter.h new file mode 100644 index 0000000000..8be43befe1 --- /dev/null +++ b/Userland/Libraries/LibGfx/Filters/BoxBlurFilter.h @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "GenericConvolutionFilter.h" + +namespace Gfx { + +template<size_t N> +class BoxBlurFilter : public GenericConvolutionFilter<N> { +public: + BoxBlurFilter() { } + virtual ~BoxBlurFilter() { } + + virtual const char* class_name() const override { return "BoxBlurFilter"; } +}; + +} diff --git a/Userland/Libraries/LibGfx/Filters/Filter.h b/Userland/Libraries/LibGfx/Filters/Filter.h new file mode 100644 index 0000000000..e0ea811d4a --- /dev/null +++ b/Userland/Libraries/LibGfx/Filters/Filter.h @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2020, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <LibGfx/Bitmap.h> +#include <LibGfx/Rect.h> + +namespace Gfx { + +class Filter { +public: + class Parameters { + public: + virtual bool is_generic_convolution_filter() const { return false; } + + virtual ~Parameters() { } + }; + virtual ~Filter() { } + + virtual const char* class_name() const = 0; + + virtual void apply(Bitmap&, const IntRect&, const Bitmap&, const IntRect&, const Parameters&) = 0; + +protected: + Filter() { } +}; + +} diff --git a/Userland/Libraries/LibGfx/Filters/GenericConvolutionFilter.h b/Userland/Libraries/LibGfx/Filters/GenericConvolutionFilter.h new file mode 100644 index 0000000000..416f84321a --- /dev/null +++ b/Userland/Libraries/LibGfx/Filters/GenericConvolutionFilter.h @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2020, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "Filter.h" +#include <LibGfx/Matrix.h> +#include <LibGfx/Matrix4x4.h> + +namespace Gfx { + +template<size_t N, typename T> +inline static constexpr void normalize(Matrix<N, T>& matrix) +{ + auto sum = 0.0f; + for (size_t i = 0; i < matrix.Size; ++i) { + for (size_t j = 0; j < matrix.Size; ++j) { + sum += matrix.elements()[i][j]; + } + } + for (size_t i = 0; i < matrix.Size; ++i) { + for (size_t j = 0; j < matrix.Size; ++j) { + matrix.elements()[i][j] /= sum; + } + } +} + +template<size_t N> +class GenericConvolutionFilter : public Filter { +public: + class Parameters : public Filter::Parameters { + public: + Parameters(const Gfx::Matrix<N, float>& kernel, bool should_wrap = false) + : m_kernel(kernel) + , m_should_wrap(should_wrap) + + { + } + + const Gfx::Matrix<N, float>& kernel() const { return m_kernel; } + Gfx::Matrix<N, float>& kernel() { return m_kernel; } + bool should_wrap() const { return m_should_wrap; } + + private: + virtual bool is_generic_convolution_filter() const override { return true; } + Gfx::Matrix<N, float> m_kernel; + bool m_should_wrap { false }; + }; + + class ApplyCache { + template<size_t> + friend class GenericConvolutionFilter; + + private: + RefPtr<Gfx::Bitmap> m_target; + }; + + GenericConvolutionFilter() { } + virtual ~GenericConvolutionFilter() { } + + virtual const char* class_name() const override { return "GenericConvolutionFilter"; } + + virtual void apply(Bitmap& target_bitmap, const IntRect& target_rect, const Bitmap& source_bitmap, const IntRect& source_rect, const Filter::Parameters& parameters) override + { + ASSERT(parameters.is_generic_convolution_filter()); + auto& gcf_params = static_cast<const GenericConvolutionFilter::Parameters&>(parameters); + + ApplyCache apply_cache; + apply(target_bitmap, target_rect, source_bitmap, source_rect, gcf_params, apply_cache); + } + + void apply(Bitmap& target, IntRect target_rect, const Bitmap& source, const IntRect& source_rect, const GenericConvolutionFilter::Parameters& parameters, ApplyCache& apply_cache) + { + // The target area (where the filter is applied) must be entirely + // contained by the source area. source_rect should be describing + // the pixels that can be accessed to apply this filter, while + // target_rect should describe the area where to apply the filter on. + ASSERT(source_rect.contains(target_rect)); + ASSERT(source.size().contains(target.size())); + ASSERT(target.rect().contains(target_rect)); + ASSERT(source.rect().contains(source_rect)); + + // If source is different from target, it should still be describing + // essentially the same bitmap. But it allows us to modify target + // without a temporary bitmap. This is important if this filter + // is applied on multiple areas of the same bitmap, at which point + // we would need to be able to access unmodified pixels if the + // areas are (almost) adjacent. + int source_delta_x = target_rect.x() - source_rect.x(); + int source_delta_y = target_rect.y() - source_rect.y(); + if (&target == &source && (!apply_cache.m_target || !apply_cache.m_target->size().contains(source_rect.size()))) { + // TODO: We probably don't need the entire source_rect, we could inflate + // the target_rect appropriately + apply_cache.m_target = Gfx::Bitmap::create(source.format(), source_rect.size()); + target_rect.move_by(-target_rect.location()); + } + + Bitmap* render_target_bitmap = (&target != &source) ? &target : apply_cache.m_target.ptr(); + + // FIXME: Help! I am naive! + constexpr static ssize_t offset = N / 2; + for (auto i_ = 0; i_ < target_rect.width(); ++i_) { + ssize_t i = i_ + target_rect.x(); + for (auto j_ = 0; j_ < target_rect.height(); ++j_) { + ssize_t j = j_ + target_rect.y(); + FloatVector3 value(0, 0, 0); + for (auto k = 0l; k < (ssize_t)N; ++k) { + auto ki = i + k - offset; + if (ki < source_rect.x() || ki > source_rect.right()) { + if (parameters.should_wrap()) + ki = (ki + source.size().width()) % source.size().width(); // TODO: fix up using source_rect + else + continue; + } + + for (auto l = 0l; l < (ssize_t)N; ++l) { + auto lj = j + l - offset; + if (lj < source_rect.y() || lj > source_rect.bottom()) { + if (parameters.should_wrap()) + lj = (lj + source.size().height()) % source.size().height(); // TODO: fix up using source_rect + else + continue; + } + + auto pixel = source.get_pixel(ki, lj); + FloatVector3 pixel_value(pixel.red(), pixel.green(), pixel.blue()); + + value = value + pixel_value * parameters.kernel().elements()[k][l]; + } + } + + value.clamp(0, 255); + render_target_bitmap->set_pixel(i, j, Color(value.x(), value.y(), value.z(), source.get_pixel(i + source_delta_x, j + source_delta_y).alpha())); + } + } + + if (render_target_bitmap != &target) { + // FIXME: Substitute for some sort of faster "blit" method. + for (auto i_ = 0; i_ < target_rect.width(); ++i_) { + auto i = i_ + target_rect.x(); + for (auto j_ = 0; j_ < target_rect.height(); ++j_) { + auto j = j_ + target_rect.y(); + target.set_pixel(i, j, render_target_bitmap->get_pixel(i_, j_)); + } + } + } + } +}; + +} diff --git a/Userland/Libraries/LibGfx/Filters/LaplacianFilter.h b/Userland/Libraries/LibGfx/Filters/LaplacianFilter.h new file mode 100644 index 0000000000..fe1c247582 --- /dev/null +++ b/Userland/Libraries/LibGfx/Filters/LaplacianFilter.h @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "GenericConvolutionFilter.h" + +namespace Gfx { + +class LaplacianFilter : public GenericConvolutionFilter<3> { +public: + LaplacianFilter() { } + virtual ~LaplacianFilter() { } + + virtual const char* class_name() const override { return "LaplacianFilter"; } +}; + +} diff --git a/Userland/Libraries/LibGfx/Filters/SharpenFilter.h b/Userland/Libraries/LibGfx/Filters/SharpenFilter.h new file mode 100644 index 0000000000..25456aa691 --- /dev/null +++ b/Userland/Libraries/LibGfx/Filters/SharpenFilter.h @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "GenericConvolutionFilter.h" + +namespace Gfx { + +class SharpenFilter : public GenericConvolutionFilter<3> { +public: + SharpenFilter() { } + virtual ~SharpenFilter() { } + + virtual const char* class_name() const override { return "SharpenFilter"; } +}; + +} diff --git a/Userland/Libraries/LibGfx/Filters/SpatialGaussianBlurFilter.h b/Userland/Libraries/LibGfx/Filters/SpatialGaussianBlurFilter.h new file mode 100644 index 0000000000..8bf8d414ca --- /dev/null +++ b/Userland/Libraries/LibGfx/Filters/SpatialGaussianBlurFilter.h @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2020, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "GenericConvolutionFilter.h" +#include <AK/StdLibExtras.h> + +namespace Gfx { + +template<size_t N, typename = typename AK::EnableIf<N % 2 == 1>::Type> +class SpatialGaussianBlurFilter : public GenericConvolutionFilter<N> { +public: + SpatialGaussianBlurFilter() { } + virtual ~SpatialGaussianBlurFilter() { } + + virtual const char* class_name() const override { return "SpatialGaussianBlurFilter"; } +}; + +} diff --git a/Userland/Libraries/LibGfx/Font.cpp b/Userland/Libraries/LibGfx/Font.cpp new file mode 100644 index 0000000000..ba0849b21b --- /dev/null +++ b/Userland/Libraries/LibGfx/Font.cpp @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020, Stephan Unverwerth <s.unverwerth@gmx.de> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <LibGfx/BitmapFont.h> +#include <LibGfx/Font.h> + +namespace Gfx { + +RefPtr<Font> Font::load_from_file(const StringView& path) +{ + if (path.ends_with(".font")) { + return BitmapFont::load_from_file(path); + } + return {}; +} + +} diff --git a/Userland/Libraries/LibGfx/Font.h b/Userland/Libraries/LibGfx/Font.h new file mode 100644 index 0000000000..2e74798725 --- /dev/null +++ b/Userland/Libraries/LibGfx/Font.h @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2020, Stephan Unverwerth <s.unverwerth@gmx.de> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/MappedFile.h> +#include <AK/RefCounted.h> +#include <AK/RefPtr.h> +#include <AK/String.h> +#include <AK/Types.h> +#include <LibGfx/Size.h> + +namespace Gfx { + +// FIXME: Make a MutableGlyphBitmap buddy class for FontEditor instead? +class GlyphBitmap { + friend class BitmapFont; + +public: + const unsigned* rows() const { return m_rows; } + unsigned row(unsigned index) const { return m_rows[index]; } + + bool bit_at(int x, int y) const { return row(y) & (1 << x); } + void set_bit_at(int x, int y, bool b) + { + auto& mutable_row = const_cast<unsigned*>(m_rows)[y]; + if (b) + mutable_row |= 1 << x; + else + mutable_row &= ~(1 << x); + } + + IntSize size() const { return m_size; } + int width() const { return m_size.width(); } + int height() const { return m_size.height(); } + +private: + GlyphBitmap(const unsigned* rows, IntSize size) + : m_rows(rows) + , m_size(size) + { + } + + const unsigned* m_rows { nullptr }; + IntSize m_size; +}; + +class Font : public RefCounted<Font> { +public: + static RefPtr<Font> load_from_file(const StringView& path); + + virtual NonnullRefPtr<Font> clone() const = 0; + virtual ~Font() {}; + + virtual u8 presentation_size() const = 0; + + virtual u16 weight() const = 0; + virtual GlyphBitmap glyph_bitmap(u32 code_point) const = 0; + + virtual u8 glyph_width(size_t ch) const = 0; + virtual int glyph_or_emoji_width(u32 code_point) const = 0; + virtual u8 glyph_height() const = 0; + virtual int x_height() const = 0; + + virtual u8 min_glyph_width() const = 0; + virtual u8 max_glyph_width() const = 0; + virtual u8 glyph_fixed_width() const = 0; + + virtual u8 baseline() const = 0; + virtual u8 mean_line() const = 0; + + virtual int width(const StringView&) const = 0; + virtual int width(const Utf8View&) const = 0; + virtual int width(const Utf32View&) const = 0; + + virtual const String& name() const = 0; + + virtual bool is_fixed_width() const = 0; + + virtual u8 glyph_spacing() const = 0; + + virtual int glyph_count() const = 0; + + virtual const String& family() const = 0; + + virtual String qualified_name() const = 0; + + virtual const Font& bold_variant() const = 0; +}; + +} diff --git a/Userland/Libraries/LibGfx/FontDatabase.cpp b/Userland/Libraries/LibGfx/FontDatabase.cpp new file mode 100644 index 0000000000..4fe466ece9 --- /dev/null +++ b/Userland/Libraries/LibGfx/FontDatabase.cpp @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <AK/NonnullRefPtrVector.h> +#include <AK/QuickSort.h> +#include <LibCore/DirIterator.h> +#include <LibGfx/Font.h> +#include <LibGfx/FontDatabase.h> +#include <dirent.h> +#include <stdio.h> +#include <stdlib.h> + +namespace Gfx { + +static FontDatabase* s_the; + +FontDatabase& FontDatabase::the() +{ + if (!s_the) + s_the = new FontDatabase; + return *s_the; +} + +Font& FontDatabase::default_font() +{ + static Font* font; + if (!font) { + font = FontDatabase::the().get_by_name("Katica 10 400"); + ASSERT(font); + } + return *font; +} + +Font& FontDatabase::default_fixed_width_font() +{ + static Font* font; + if (!font) { + font = FontDatabase::the().get_by_name("Csilla 10 400"); + ASSERT(font); + } + return *font; +} + +Font& FontDatabase::default_bold_fixed_width_font() +{ + static Font* font; + if (!font) { + font = FontDatabase::the().get_by_name("Csilla 10 700"); + ASSERT(font); + } + return *font; +} + +Font& FontDatabase::default_bold_font() +{ + static Font* font; + if (!font) { + font = FontDatabase::the().get_by_name("Katica 10 700"); + ASSERT(font); + } + return *font; +} + +struct FontDatabase::Private { + HashMap<String, RefPtr<Gfx::Font>> full_name_to_font_map; +}; + +FontDatabase::FontDatabase() + : m_private(make<Private>()) +{ + Core::DirIterator di("/res/fonts", Core::DirIterator::SkipDots); + if (di.has_error()) { + fprintf(stderr, "DirIterator: %s\n", di.error_string()); + exit(1); + } + while (di.has_next()) { + String name = di.next_path(); + if (!name.ends_with(".font")) + continue; + + auto path = String::format("/res/fonts/%s", name.characters()); + if (auto font = Gfx::Font::load_from_file(path)) { + m_private->full_name_to_font_map.set(font->qualified_name(), font); + } + } +} + +FontDatabase::~FontDatabase() +{ +} + +void FontDatabase::for_each_font(Function<void(const Gfx::Font&)> callback) +{ + Vector<RefPtr<Gfx::Font>> fonts; + fonts.ensure_capacity(m_private->full_name_to_font_map.size()); + for (auto& it : m_private->full_name_to_font_map) + fonts.append(it.value); + quick_sort(fonts, [](auto& a, auto& b) { return a->qualified_name() < b->qualified_name(); }); + for (auto& font : fonts) + callback(*font); +} + +void FontDatabase::for_each_fixed_width_font(Function<void(const Gfx::Font&)> callback) +{ + Vector<RefPtr<Gfx::Font>> fonts; + fonts.ensure_capacity(m_private->full_name_to_font_map.size()); + for (auto& it : m_private->full_name_to_font_map) { + if (it.value->is_fixed_width()) + fonts.append(it.value); + } + quick_sort(fonts, [](auto& a, auto& b) { return a->qualified_name() < b->qualified_name(); }); + for (auto& font : fonts) + callback(*font); +} + +RefPtr<Gfx::Font> FontDatabase::get_by_name(const StringView& name) +{ + auto it = m_private->full_name_to_font_map.find(name); + if (it == m_private->full_name_to_font_map.end()) { + dbgln("Font lookup failed: '{}'", name); + return nullptr; + } + return it->value; +} + +RefPtr<Gfx::Font> FontDatabase::get(const String& family, unsigned size, unsigned weight) +{ + for (auto& it : m_private->full_name_to_font_map) { + auto& font = *it.value; + if (font.family() == family && font.presentation_size() == size && font.weight() == weight) + return font; + } + return nullptr; +} + +} diff --git a/Userland/Libraries/LibGfx/FontDatabase.h b/Userland/Libraries/LibGfx/FontDatabase.h new file mode 100644 index 0000000000..e1c21a20a3 --- /dev/null +++ b/Userland/Libraries/LibGfx/FontDatabase.h @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Function.h> +#include <AK/HashMap.h> +#include <AK/String.h> +#include <LibGfx/Forward.h> + +namespace Gfx { + +class FontDatabase { +public: + static FontDatabase& the(); + + static Font& default_font(); + static Font& default_bold_font(); + + static Font& default_fixed_width_font(); + static Font& default_bold_fixed_width_font(); + + RefPtr<Gfx::Font> get(const String& family, unsigned size, unsigned weight); + RefPtr<Gfx::Font> get_by_name(const StringView&); + void for_each_font(Function<void(const Gfx::Font&)>); + void for_each_fixed_width_font(Function<void(const Gfx::Font&)>); + +private: + FontDatabase(); + ~FontDatabase(); + + struct Private; + OwnPtr<Private> m_private; +}; + +} diff --git a/Userland/Libraries/LibGfx/Forward.h b/Userland/Libraries/LibGfx/Forward.h new file mode 100644 index 0000000000..7d7fc2f482 --- /dev/null +++ b/Userland/Libraries/LibGfx/Forward.h @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +namespace Gfx { + +class Bitmap; +class CharacterBitmap; +class Color; +class DisjointRectSet; +class Emoji; +class Font; +class GlyphBitmap; +class ImageDecoder; +class Painter; +class Palette; +class PaletteImpl; +class Path; +class ShareableBitmap; +class StylePainter; +struct SystemTheme; +class Triangle; + +template<typename T> +class Point; + +template<typename T> +class Size; + +template<typename T> +class Rect; + +using IntRect = Rect<int>; +using FloatRect = Rect<float>; + +using IntPoint = Point<int>; +using FloatPoint = Point<float>; + +using IntSize = Size<int>; +using FloatSize = Size<float>; + +enum class BitmapFormat; +enum class ColorRole; +enum class TextAlignment; + +} + +using Gfx::Color; diff --git a/Userland/Libraries/LibGfx/GIFLoader.cpp b/Userland/Libraries/LibGfx/GIFLoader.cpp new file mode 100644 index 0000000000..d29c5969e3 --- /dev/null +++ b/Userland/Libraries/LibGfx/GIFLoader.cpp @@ -0,0 +1,765 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <AK/Array.h> +#include <AK/ByteBuffer.h> +#include <AK/LexicalPath.h> +#include <AK/MappedFile.h> +#include <AK/MemoryStream.h> +#include <AK/NonnullOwnPtrVector.h> +#include <LibGfx/GIFLoader.h> +#include <LibGfx/Painter.h> +#include <math.h> +#include <stdio.h> +#include <string.h> + +//#define GIF_DEBUG + +namespace Gfx { + +// Row strides and offsets for each interlace pass. +static const int INTERLACE_ROW_STRIDES[] = { 8, 8, 4, 2 }; +static const int INTERLACE_ROW_OFFSETS[] = { 0, 4, 2, 1 }; + +struct ImageDescriptor { + u16 x { 0 }; + u16 y { 0 }; + u16 width { 0 }; + u16 height { 0 }; + bool use_global_color_map { true }; + bool interlaced { false }; + Color color_map[256]; + u8 lzw_min_code_size { 0 }; + Vector<u8> lzw_encoded_bytes; + + // Fields from optional graphic control extension block + enum DisposalMethod : u8 { + None = 0, + InPlace = 1, + RestoreBackground = 2, + RestorePrevious = 3, + }; + DisposalMethod disposal_method { None }; + u8 transparency_index { 0 }; + u16 duration { 0 }; + bool transparent { false }; + bool user_input { false }; + + const IntRect rect() const + { + return { this->x, this->y, this->width, this->height }; + } +}; + +struct LogicalScreen { + u16 width; + u16 height; + Color color_map[256]; +}; + +struct GIFLoadingContext { + enum State { + NotDecoded = 0, + FrameDescriptorsLoaded, + FrameComplete, + }; + State state { NotDecoded }; + enum ErrorState { + NoError = 0, + FailedToDecodeAllFrames, + FailedToDecodeAnyFrame, + FailedToLoadFrameDescriptors, + }; + ErrorState error_state { NoError }; + const u8* data { nullptr }; + size_t data_size { 0 }; + LogicalScreen logical_screen {}; + u8 background_color_index { 0 }; + NonnullOwnPtrVector<ImageDescriptor> images {}; + size_t loops { 1 }; + RefPtr<Gfx::Bitmap> frame_buffer; + size_t current_frame { 0 }; + RefPtr<Gfx::Bitmap> prev_frame_buffer; +}; + +RefPtr<Gfx::Bitmap> load_gif(const StringView& path) +{ + auto file_or_error = MappedFile::map(path); + if (file_or_error.is_error()) + return nullptr; + GIFImageDecoderPlugin gif_decoder((const u8*)file_or_error.value()->data(), file_or_error.value()->size()); + auto bitmap = gif_decoder.bitmap(); + if (bitmap) + bitmap->set_mmap_name(String::formatted("Gfx::Bitmap [{}] - Decoded GIF: {}", bitmap->size(), LexicalPath::canonicalized_path(path))); + return bitmap; +} + +RefPtr<Gfx::Bitmap> load_gif_from_memory(const u8* data, size_t length) +{ + GIFImageDecoderPlugin gif_decoder(data, length); + auto bitmap = gif_decoder.bitmap(); + if (bitmap) + bitmap->set_mmap_name(String::formatted("Gfx::Bitmap [{}] - Decoded GIF: <memory>", bitmap->size())); + return bitmap; +} + +enum class GIFFormat { + GIF87a, + GIF89a, +}; + +static Optional<GIFFormat> decode_gif_header(InputMemoryStream& stream) +{ + static const char valid_header_87[] = "GIF87a"; + static const char valid_header_89[] = "GIF89a"; + + Array<u8, 6> header; + stream >> header; + + if (stream.handle_any_error()) + return {}; + + if (header.span() == ReadonlyBytes { valid_header_87, 6 }) + return GIFFormat::GIF87a; + if (header.span() == ReadonlyBytes { valid_header_89, 6 }) + return GIFFormat::GIF89a; + + return {}; +} + +class LZWDecoder { +private: + static constexpr int max_code_size = 12; + +public: + explicit LZWDecoder(const Vector<u8>& lzw_bytes, u8 min_code_size) + : m_lzw_bytes(lzw_bytes) + , m_code_size(min_code_size) + , m_original_code_size(min_code_size) + , m_table_capacity(pow(2, min_code_size)) + { + init_code_table(); + } + + u16 add_control_code() + { + const u16 control_code = m_code_table.size(); + m_code_table.append(Vector<u8> {}); + m_original_code_table.append(Vector<u8> {}); + if (m_code_table.size() >= m_table_capacity && m_code_size < max_code_size) { + + ++m_code_size; + ++m_original_code_size; + m_table_capacity *= 2; + } + return control_code; + } + + void reset() + { + m_code_table.clear(); + m_code_table.append(m_original_code_table); + m_code_size = m_original_code_size; + m_table_capacity = pow(2, m_code_size); + m_output.clear(); + } + + Optional<u16> next_code() + { + size_t current_byte_index = m_current_bit_index / 8; + if (current_byte_index >= m_lzw_bytes.size()) { + return {}; + } + + // Extract the code bits using a 32-bit mask to cover the possibility that if + // the current code size > 9 bits then the code can span 3 bytes. + u8 current_bit_offset = m_current_bit_index % 8; + u32 mask = (u32)(m_table_capacity - 1) << current_bit_offset; + + // Make a padded copy of the final bytes in the data to ensure we don't read past the end. + if (current_byte_index + sizeof(mask) > m_lzw_bytes.size()) { + u8 padded_last_bytes[sizeof(mask)] = { 0 }; + for (int i = 0; current_byte_index + i < m_lzw_bytes.size(); ++i) { + padded_last_bytes[i] = m_lzw_bytes[current_byte_index + i]; + } + const u32* addr = (const u32*)&padded_last_bytes; + m_current_code = (*addr & mask) >> current_bit_offset; + } else { + const u32* addr = (const u32*)&m_lzw_bytes.at(current_byte_index); + m_current_code = (*addr & mask) >> current_bit_offset; + } + + if (m_current_code > m_code_table.size()) { +#ifdef GIF_DEBUG + dbg() << "Corrupted LZW stream, invalid code: " << m_current_code << " at bit index: " + << m_current_bit_index << ", code table size: " << m_code_table.size(); +#endif + return {}; + } else if (m_current_code == m_code_table.size() && m_output.is_empty()) { +#ifdef GIF_DEBUG + dbg() << "Corrupted LZW stream, valid new code but output buffer is empty: " << m_current_code + << " at bit index: " << m_current_bit_index << ", code table size: " << m_code_table.size(); +#endif + return {}; + } + + m_current_bit_index += m_code_size; + + return m_current_code; + } + + Vector<u8>& get_output() + { + ASSERT(m_current_code <= m_code_table.size()); + if (m_current_code < m_code_table.size()) { + Vector<u8> new_entry = m_output; + m_output = m_code_table.at(m_current_code); + new_entry.append(m_output[0]); + extend_code_table(new_entry); + } else if (m_current_code == m_code_table.size()) { + ASSERT(!m_output.is_empty()); + m_output.append(m_output[0]); + extend_code_table(m_output); + } + return m_output; + } + +private: + void init_code_table() + { + m_code_table.clear(); + for (u16 i = 0; i < m_table_capacity; ++i) { + m_code_table.append({ (u8)i }); + } + m_original_code_table = m_code_table; + } + + void extend_code_table(const Vector<u8>& entry) + { + if (entry.size() > 1 && m_code_table.size() < 4096) { + m_code_table.append(entry); + if (m_code_table.size() >= m_table_capacity && m_code_size < max_code_size) { + ++m_code_size; + m_table_capacity *= 2; + } + } + } + + const Vector<u8>& m_lzw_bytes; + + int m_current_bit_index { 0 }; + + Vector<Vector<u8>> m_code_table {}; + Vector<Vector<u8>> m_original_code_table {}; + + u8 m_code_size { 0 }; + u8 m_original_code_size { 0 }; + + u32 m_table_capacity { 0 }; + + u16 m_current_code { 0 }; + Vector<u8> m_output {}; +}; + +static void copy_frame_buffer(Bitmap& dest, const Bitmap& src) +{ + ASSERT(dest.size_in_bytes() == src.size_in_bytes()); + memcpy(dest.scanline(0), src.scanline(0), dest.size_in_bytes()); +} + +static bool decode_frame(GIFLoadingContext& context, size_t frame_index) +{ + if (frame_index >= context.images.size()) { + return false; + } + + if (context.state >= GIFLoadingContext::State::FrameComplete && frame_index == context.current_frame) { + return true; + } + + size_t start_frame = context.current_frame + 1; + if (context.state < GIFLoadingContext::State::FrameComplete) { + start_frame = 0; + context.frame_buffer = Bitmap::create_purgeable(BitmapFormat::RGBA32, { context.logical_screen.width, context.logical_screen.height }); + if (!context.frame_buffer) + return false; + context.prev_frame_buffer = Bitmap::create_purgeable(BitmapFormat::RGBA32, { context.logical_screen.width, context.logical_screen.height }); + if (!context.prev_frame_buffer) + return false; + } else if (frame_index < context.current_frame) { + start_frame = 0; + } + + for (size_t i = start_frame; i <= frame_index; ++i) { + auto& image = context.images.at(i); + + const auto previous_image_disposal_method = i > 0 ? context.images.at(i - 1).disposal_method : ImageDescriptor::DisposalMethod::None; + + if (i == 0) { + context.frame_buffer->fill(Color::Transparent); + } else if (i > 0 && image.disposal_method == ImageDescriptor::DisposalMethod::RestorePrevious + && previous_image_disposal_method != ImageDescriptor::DisposalMethod::RestorePrevious) { + // This marks the start of a run of frames that once disposed should be restored to the + // previous underlying image contents. Therefore we make a copy of the current frame + // buffer so that it can be restored later. + copy_frame_buffer(*context.prev_frame_buffer, *context.frame_buffer); + } + + if (previous_image_disposal_method == ImageDescriptor::DisposalMethod::RestoreBackground) { + // Note: RestoreBackground could be interpreted either as restoring the underlying + // background of the entire image (e.g. container element's background-color), or the + // background color of the GIF itself. It appears that all major browsers and most other + // GIF decoders adhere to the former interpretation, therefore we will do the same by + // clearing the entire frame buffer to transparent. + Painter painter(*context.frame_buffer); + painter.clear_rect(context.images.at(i - 1).rect(), Color::Transparent); + } else if (i > 0 && previous_image_disposal_method == ImageDescriptor::DisposalMethod::RestorePrevious) { + // Previous frame indicated that once disposed, it should be restored to *its* previous + // underlying image contents, therefore we restore the saved previous frame buffer. + copy_frame_buffer(*context.frame_buffer, *context.prev_frame_buffer); + } + + LZWDecoder decoder(image.lzw_encoded_bytes, image.lzw_min_code_size); + + // Add GIF-specific control codes + const int clear_code = decoder.add_control_code(); + const int end_of_information_code = decoder.add_control_code(); + + const auto& color_map = image.use_global_color_map ? context.logical_screen.color_map : image.color_map; + + int pixel_index = 0; + int row = 0; + int interlace_pass = 0; + while (true) { + Optional<u16> code = decoder.next_code(); + if (!code.has_value()) { +#ifdef GIF_DEBUG + dbgln("Unexpectedly reached end of gif frame data"); +#endif + return false; + } + + if (code.value() == clear_code) { + decoder.reset(); + continue; + } + if (code.value() == end_of_information_code) + break; + if (!image.width) + continue; + + auto colors = decoder.get_output(); + for (const auto& color : colors) { + auto c = color_map[color]; + + int x = pixel_index % image.width + image.x; + int y = row + image.y; + + if (context.frame_buffer->rect().contains(x, y) && (!image.transparent || color != image.transparency_index)) { + context.frame_buffer->set_pixel(x, y, c); + } + + ++pixel_index; + if (pixel_index % image.width == 0) { + if (image.interlaced) { + if (row + INTERLACE_ROW_STRIDES[interlace_pass] >= image.height) { + ++interlace_pass; + // FIXME: We could probably figure this out earlier and fail before doing a bunch of work. + if (interlace_pass >= 4) + return false; + row = INTERLACE_ROW_OFFSETS[interlace_pass]; + } else { + row += INTERLACE_ROW_STRIDES[interlace_pass]; + } + } else { + ++row; + } + } + } + } + + context.current_frame = i; + context.state = GIFLoadingContext::State::FrameComplete; + } + + return true; +} + +static bool load_gif_frame_descriptors(GIFLoadingContext& context) +{ + if (context.data_size < 32) + return false; + + InputMemoryStream stream { { context.data, context.data_size } }; + + Optional<GIFFormat> format = decode_gif_header(stream); + if (!format.has_value()) { + return false; + } + + LittleEndian<u16> value; + + stream >> value; + context.logical_screen.width = value; + + stream >> value; + context.logical_screen.height = value; + + if (stream.handle_any_error()) + return false; + + if (context.logical_screen.width > maximum_width_for_decoded_images || context.logical_screen.height > maximum_height_for_decoded_images) { + dbgln("This GIF is too large for comfort: {}x{}", context.logical_screen.width, context.logical_screen.height); + return false; + } + + u8 gcm_info = 0; + stream >> gcm_info; + + if (stream.handle_any_error()) + return false; + + stream >> context.background_color_index; + if (stream.handle_any_error()) + return false; + + u8 pixel_aspect_ratio = 0; + stream >> pixel_aspect_ratio; + if (stream.handle_any_error()) + return false; + + u8 bits_per_pixel = (gcm_info & 7) + 1; + int color_map_entry_count = 1; + for (int i = 0; i < bits_per_pixel; ++i) + color_map_entry_count *= 2; + + for (int i = 0; i < color_map_entry_count; ++i) { + u8 r = 0; + u8 g = 0; + u8 b = 0; + stream >> r >> g >> b; + context.logical_screen.color_map[i] = { r, g, b }; + } + + if (stream.handle_any_error()) + return false; + + NonnullOwnPtr<ImageDescriptor> current_image = make<ImageDescriptor>(); + for (;;) { + u8 sentinel = 0; + stream >> sentinel; + + if (stream.handle_any_error()) + return false; + + if (sentinel == '!') { + u8 extension_type = 0; + stream >> extension_type; + if (stream.handle_any_error()) + return false; + + u8 sub_block_length = 0; + + Vector<u8> sub_block {}; + for (;;) { + stream >> sub_block_length; + + if (stream.handle_any_error()) + return false; + + if (sub_block_length == 0) + break; + + u8 dummy = 0; + for (u16 i = 0; i < sub_block_length; ++i) { + stream >> dummy; + sub_block.append(dummy); + } + + if (stream.handle_any_error()) + return false; + } + + if (extension_type == 0xF9) { + if (sub_block.size() != 4) { +#ifdef GIF_DEBUG + dbgln("Unexpected graphic control size"); +#endif + continue; + } + + u8 disposal_method = (sub_block[0] & 0x1C) >> 2; + current_image->disposal_method = (ImageDescriptor::DisposalMethod)disposal_method; + + u8 user_input = (sub_block[0] & 0x2) >> 1; + current_image->user_input = user_input == 1; + + u8 transparent = sub_block[0] & 1; + current_image->transparent = transparent == 1; + + u16 duration = sub_block[1] + ((u16)sub_block[2] >> 8); + current_image->duration = duration; + + current_image->transparency_index = sub_block[3]; + } + + if (extension_type == 0xFF) { + if (sub_block.size() != 14) { +#ifdef GIF_DEBUG + dbg() << "Unexpected application extension size: " << sub_block.size(); +#endif + continue; + } + + if (sub_block[11] != 1) { +#ifdef GIF_DEBUG + dbgln("Unexpected application extension format"); +#endif + continue; + } + + u16 loops = sub_block[12] + (sub_block[13] << 8); + context.loops = loops; + } + + continue; + } + + if (sentinel == ',') { + context.images.append(move(current_image)); + auto& image = context.images.last(); + + LittleEndian<u16> tmp; + + u8 packed_fields { 0 }; + + stream >> tmp; + image.x = tmp; + + stream >> tmp; + image.y = tmp; + + stream >> tmp; + image.width = tmp; + + stream >> tmp; + image.height = tmp; + + stream >> packed_fields; + if (stream.handle_any_error()) + return false; + + image.use_global_color_map = !(packed_fields & 0x80); + image.interlaced = (packed_fields & 0x40) != 0; + + if (!image.use_global_color_map) { + size_t local_color_table_size = pow(2, (packed_fields & 7) + 1); + + for (size_t i = 0; i < local_color_table_size; ++i) { + u8 r = 0; + u8 g = 0; + u8 b = 0; + stream >> r >> g >> b; + image.color_map[i] = { r, g, b }; + } + } + + stream >> image.lzw_min_code_size; + if (stream.handle_any_error()) + return false; + + u8 lzw_encoded_bytes_expected = 0; + + for (;;) { + stream >> lzw_encoded_bytes_expected; + + if (stream.handle_any_error()) + return false; + + if (lzw_encoded_bytes_expected == 0) + break; + + Array<u8, 256> buffer; + stream >> buffer.span().trim(lzw_encoded_bytes_expected); + + if (stream.handle_any_error()) + return false; + + for (int i = 0; i < lzw_encoded_bytes_expected; ++i) { + image.lzw_encoded_bytes.append(buffer[i]); + } + } + + current_image = make<ImageDescriptor>(); + continue; + } + + if (sentinel == ';') { + break; + } + + return false; + } + + context.state = GIFLoadingContext::State::FrameDescriptorsLoaded; + return true; +} + +GIFImageDecoderPlugin::GIFImageDecoderPlugin(const u8* data, size_t size) +{ + m_context = make<GIFLoadingContext>(); + m_context->data = data; + m_context->data_size = size; +} + +GIFImageDecoderPlugin::~GIFImageDecoderPlugin() { } + +IntSize GIFImageDecoderPlugin::size() +{ + if (m_context->error_state == GIFLoadingContext::ErrorState::FailedToLoadFrameDescriptors) { + return {}; + } + + if (m_context->state < GIFLoadingContext::State::FrameDescriptorsLoaded) { + if (!load_gif_frame_descriptors(*m_context)) { + m_context->error_state = GIFLoadingContext::ErrorState::FailedToLoadFrameDescriptors; + return {}; + } + } + + return { m_context->logical_screen.width, m_context->logical_screen.height }; +} + +RefPtr<Gfx::Bitmap> GIFImageDecoderPlugin::bitmap() +{ + if (m_context->state < GIFLoadingContext::State::FrameComplete) { + return frame(0).image; + } + return m_context->frame_buffer; +} + +void GIFImageDecoderPlugin::set_volatile() +{ + if (m_context->frame_buffer) { + m_context->frame_buffer->set_volatile(); + } +} + +bool GIFImageDecoderPlugin::set_nonvolatile() +{ + if (!m_context->frame_buffer) { + return true; + } + return m_context->frame_buffer->set_nonvolatile(); +} + +bool GIFImageDecoderPlugin::sniff() +{ + InputMemoryStream stream { { m_context->data, m_context->data_size } }; + return decode_gif_header(stream).has_value(); +} + +bool GIFImageDecoderPlugin::is_animated() +{ + if (m_context->error_state != GIFLoadingContext::ErrorState::NoError) { + return false; + } + + if (m_context->state < GIFLoadingContext::State::FrameDescriptorsLoaded) { + if (!load_gif_frame_descriptors(*m_context)) { + m_context->error_state = GIFLoadingContext::ErrorState::FailedToLoadFrameDescriptors; + return false; + } + } + + return m_context->images.size() > 1; +} + +size_t GIFImageDecoderPlugin::loop_count() +{ + if (m_context->error_state != GIFLoadingContext::ErrorState::NoError) { + return 0; + } + + if (m_context->state < GIFLoadingContext::State::FrameDescriptorsLoaded) { + if (!load_gif_frame_descriptors(*m_context)) { + m_context->error_state = GIFLoadingContext::ErrorState::FailedToLoadFrameDescriptors; + return 0; + } + } + + return m_context->loops; +} + +size_t GIFImageDecoderPlugin::frame_count() +{ + if (m_context->error_state != GIFLoadingContext::ErrorState::NoError) { + return 1; + } + + if (m_context->state < GIFLoadingContext::State::FrameDescriptorsLoaded) { + if (!load_gif_frame_descriptors(*m_context)) { + m_context->error_state = GIFLoadingContext::ErrorState::FailedToLoadFrameDescriptors; + return 1; + } + } + + return m_context->images.size(); +} + +ImageFrameDescriptor GIFImageDecoderPlugin::frame(size_t i) +{ + if (m_context->error_state >= GIFLoadingContext::ErrorState::FailedToDecodeAnyFrame) { + return {}; + } + + if (m_context->state < GIFLoadingContext::State::FrameDescriptorsLoaded) { + if (!load_gif_frame_descriptors(*m_context)) { + m_context->error_state = GIFLoadingContext::ErrorState::FailedToLoadFrameDescriptors; + return {}; + } + } + + if (m_context->error_state == GIFLoadingContext::ErrorState::NoError && !decode_frame(*m_context, i)) { + if (m_context->state < GIFLoadingContext::State::FrameComplete || !decode_frame(*m_context, 0)) { + m_context->error_state = GIFLoadingContext::ErrorState::FailedToDecodeAnyFrame; + return {}; + } + m_context->error_state = GIFLoadingContext::ErrorState::FailedToDecodeAllFrames; + } + + ImageFrameDescriptor frame {}; + frame.image = m_context->frame_buffer->clone(); + frame.duration = m_context->images.at(i).duration * 10; + + if (frame.duration <= 10) { + frame.duration = 100; + } + + return frame; +} + +} diff --git a/Userland/Libraries/LibGfx/GIFLoader.h b/Userland/Libraries/LibGfx/GIFLoader.h new file mode 100644 index 0000000000..24be735381 --- /dev/null +++ b/Userland/Libraries/LibGfx/GIFLoader.h @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <LibGfx/Bitmap.h> +#include <LibGfx/ImageDecoder.h> + +namespace Gfx { + +RefPtr<Gfx::Bitmap> load_gif(const StringView& path); +RefPtr<Gfx::Bitmap> load_gif_from_memory(const u8*, size_t); + +struct GIFLoadingContext; + +class GIFImageDecoderPlugin final : public ImageDecoderPlugin { +public: + virtual ~GIFImageDecoderPlugin() override; + GIFImageDecoderPlugin(const u8*, size_t); + + virtual IntSize size() override; + virtual RefPtr<Gfx::Bitmap> bitmap() override; + virtual void set_volatile() override; + [[nodiscard]] virtual bool set_nonvolatile() override; + virtual bool sniff() override; + virtual bool is_animated() override; + virtual size_t loop_count() override; + virtual size_t frame_count() override; + virtual ImageFrameDescriptor frame(size_t i) override; + +private: + OwnPtr<GIFLoadingContext> m_context; +}; + +} diff --git a/Userland/Libraries/LibGfx/Gamma.h b/Userland/Libraries/LibGfx/Gamma.h new file mode 100644 index 0000000000..f6be93b8fe --- /dev/null +++ b/Userland/Libraries/LibGfx/Gamma.h @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2020, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "Color.h" +#include <math.h> +#include <xmmintrin.h> + +#include <AK/SIMD.h> + +#define GAMMA 2.2 + +// Most computer graphics are stored in the sRGB color space, which stores something close to +// the square root of the display intensity of each color channel. This is problematic for most +// operations that we want to perform on colors, since they typically assume that color scales +// linearly (e.g. rgb(127, 0, 0) is half as bright as rgb(255, 0, 0)). This causes incorrect +// results that look more gray than they should, to fix this we have to convert colors to the linear +// color space before performing these operations, then convert back before displaying. +// +// Conversion between linear and sRGB spaces are somewhat expensive to do on the CPU, so we instead +// interpret sRGB colors as gamma2.2 colors, which are close enough in most cases to be indistinguishable. +// Gamma 2.2 colors follow the simple rule of `display_intensity = pow(stored_intensity, 2.2)`. +// This module implements some fast color space transforms between the gamma2.2 and linear color spaces, plus +// some common primitive operations like blending. +// +// For a more in-depth overview of how gamma-adjustment works, check out: +// https://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/ + +namespace Gfx { + +using AK::SIMD::f32x4; + +#ifndef NO_FPU + +# ifdef __SSE__ + +// Transform f32x4 from gamma2.2 space to linear space +// Assumes x is in range [0, 1] +// FIXME: Remove this hack once clang-11 is available as the default in Github Actions. +// This is apparently sometime mid-December. https://github.com/actions/virtual-environments/issues/2130 +# if !defined(__clang__) || __clang_major__ >= 11 +constexpr f32x4 gamma_to_linear4(f32x4 x) +# else +inline f32x4 gamma_to_linear4(f32x4 x) +# endif +{ + return (0.8f + 0.2f * x) * x * x; +} + +// Transform f32x4 from linear space to gamma2.2 space +// Assumes x is in range [0, 1] +inline f32x4 linear_to_gamma4(f32x4 x) +{ + // Source for approximation: https://mimosa-pudica.net/fast-gamma/ + constexpr float a = 0.00279491f; + constexpr float b = 1.15907984f; + float c = (b / sqrt(1 + a)) - 1; + return ((b * __builtin_ia32_rsqrtps(x + a)) - c) * x; +} + +// Linearize v1 and v2, lerp them by mix factor, then convert back. +// The output is entirely v1 when mix = 0 and entirely v2 when mix = 1 +inline f32x4 gamma_accurate_lerp4(f32x4 v1, f32x4 v2, float mix) +{ + return linear_to_gamma4(gamma_to_linear4(v1) * (1 - mix) + gamma_to_linear4(v2) * mix); +} + +# endif + +// Transform scalar from gamma2.2 space to linear space +// Assumes x is in range [0, 1] +constexpr float gamma_to_linear(float x) +{ + return (0.8 + 0.2 * x) * x * x; +} + +// Transform scalar from linear space to gamma2.2 space +// Assumes x is in range [0, 1] +inline float linear_to_gamma(float x) +{ + // Source for approximation: https://mimosa-pudica.net/fast-gamma/ + constexpr float a = 0.00279491; + constexpr float b = 1.15907984; + float c = (b / sqrt(1 + a)) - 1; + return ((b / __builtin_sqrt(x + a)) - c) * x; +} + +// Linearize v1 and v2, lerp them by mix factor, then convert back. +// The output is entirely v1 when mix = 0 and entirely v2 when mix = 1 +inline float gamma_accurate_lerp(float v1, float v2, float mix) +{ + return linear_to_gamma(gamma_to_linear(v1) * (1 - mix) + gamma_to_linear(v2) * mix); +} + +// Convert a and b to linear space, blend them by mix factor, then convert back. +// The output is entirely a when mix = 0 and entirely b when mix = 1 +inline Color gamma_accurate_blend(Color a, Color b, float mix) +{ +# ifdef __SSE__ + f32x4 ac = { + (float)a.red(), + (float)a.green(), + (float)a.blue(), + }; + f32x4 bc = { + (float)b.red(), + (float)b.green(), + (float)b.blue(), + }; + f32x4 out = 255.f * gamma_accurate_lerp4(ac * (1.f / 255.f), bc * (1.f / 255.f), mix); + return Color(out[0], out[1], out[2]); +# else + return { + static_cast<u8>(255. * gamma_accurate_lerp(a.red() / 255., b.red() / 255., mix)), + static_cast<u8>(255. * gamma_accurate_lerp(a.green() / 255., b.green() / 255., mix)), + static_cast<u8>(255. * gamma_accurate_lerp(a.blue() / 255., b.blue() / 255., mix)), + }; +# endif +} + +#endif + +} diff --git a/Userland/Libraries/LibGfx/ICOLoader.cpp b/Userland/Libraries/LibGfx/ICOLoader.cpp new file mode 100644 index 0000000000..031f4e657e --- /dev/null +++ b/Userland/Libraries/LibGfx/ICOLoader.cpp @@ -0,0 +1,438 @@ +/* + * Copyright (c) 2020, Paul Roukema <roukemap@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <AK/ByteBuffer.h> +#include <AK/LexicalPath.h> +#include <AK/MappedFile.h> +#include <AK/MemoryStream.h> +#include <AK/NonnullOwnPtrVector.h> +#include <AK/Types.h> +#include <LibGfx/ICOLoader.h> +#include <LibGfx/PNGLoader.h> +#include <math.h> +#include <stdio.h> +#include <string.h> + +//#define ICO_DEBUG + +namespace Gfx { + +// FIXME: This is in little-endian order. Maybe need a NetworkOrdered<T> equivalent eventually. +struct ICONDIR { + u16 must_be_0 = 0; + u16 must_be_1 = 0; + u16 image_count = 0; +}; +static_assert(sizeof(ICONDIR) == 6); + +struct ICONDIRENTRY { + u8 width; + u8 height; + u8 color_count; + u8 reserved_0; + u16 planes; + u16 bits_per_pixel; + u32 size; + u32 offset; +}; +static_assert(sizeof(ICONDIRENTRY) == 16); + +struct [[gnu::packed]] BMPFILEHEADER { + u8 signature[2]; + u32 size; + u16 reserved1; + u16 reserved2; + u32 offset; +}; +static_assert(sizeof(BMPFILEHEADER) == 14); + +struct BITMAPINFOHEADER { + u32 size; + i32 width; + i32 height; + u16 planes; + u16 bpp; + u32 compression; + u32 size_image; + u32 vres; + u32 hres; + u32 palette_size; + u32 important_colors; +}; +static_assert(sizeof(BITMAPINFOHEADER) == 40); + +struct [[gnu::packed]] BMP_ARGB { + u8 b; + u8 g; + u8 r; + u8 a; +}; +static_assert(sizeof(BMP_ARGB) == 4); + +struct ImageDescriptor { + u16 width; + u16 height; + size_t offset; + size_t size; + RefPtr<Gfx::Bitmap> bitmap; +}; + +struct ICOLoadingContext { + enum State { + NotDecoded = 0, + Error, + DirectoryDecoded, + BitmapDecoded + }; + State state { NotDecoded }; + const u8* data { nullptr }; + size_t data_size { 0 }; + Vector<ImageDescriptor> images; + size_t largest_index; +}; + +RefPtr<Gfx::Bitmap> load_ico(const StringView& path) +{ + auto file_or_error = MappedFile::map(path); + if (file_or_error.is_error()) + return nullptr; + ICOImageDecoderPlugin decoder((const u8*)file_or_error.value()->data(), file_or_error.value()->size()); + auto bitmap = decoder.bitmap(); + if (bitmap) + bitmap->set_mmap_name(String::formatted("Gfx::Bitmap [{}] - Decoded ICO: {}", bitmap->size(), LexicalPath::canonicalized_path(path))); + return bitmap; +} + +RefPtr<Gfx::Bitmap> load_ico_from_memory(const u8* data, size_t length) +{ + ICOImageDecoderPlugin decoder(data, length); + auto bitmap = decoder.bitmap(); + if (bitmap) + bitmap->set_mmap_name(String::formatted("Gfx::Bitmap [{}] - Decoded ICO: <memory>", bitmap->size())); + return bitmap; +} + +static Optional<size_t> decode_ico_header(InputMemoryStream& stream) +{ + ICONDIR header; + stream >> Bytes { &header, sizeof(header) }; + if (stream.handle_any_error()) + return {}; + + if (header.must_be_0 != 0 || header.must_be_1 != 1) + return {}; + return { header.image_count }; +} + +static Optional<ImageDescriptor> decode_ico_direntry(InputMemoryStream& stream) +{ + ICONDIRENTRY entry; + stream >> Bytes { &entry, sizeof(entry) }; + if (stream.handle_any_error()) + return {}; + + ImageDescriptor desc = { entry.width, entry.height, entry.offset, entry.size, nullptr }; + if (desc.width == 0) + desc.width = 256; + if (desc.height == 0) + desc.height = 256; + + return { desc }; +} + +static size_t find_largest_image(const ICOLoadingContext& context) +{ + size_t max_area = 0; + size_t index = 0; + size_t largest_index = 0; + for (const auto& desc : context.images) { + if (desc.width * desc.height > max_area) { + max_area = desc.width * desc.height; + largest_index = index; + } + ++index; + } + return largest_index; +} + +static bool load_ico_directory(ICOLoadingContext& context) +{ + InputMemoryStream stream { { context.data, context.data_size } }; + + auto image_count = decode_ico_header(stream); + if (!image_count.has_value() || image_count.value() == 0) { + return false; + } + + for (size_t i = 0; i < image_count.value(); ++i) { + auto maybe_desc = decode_ico_direntry(stream); + if (!maybe_desc.has_value()) { +#ifdef ICO_DEBUG + printf("load_ico_directory: error loading entry: %lu\n", i); +#endif + return false; + } + + auto& desc = maybe_desc.value(); + if (desc.offset + desc.size < desc.offset // detect integer overflow + || (desc.offset + desc.size) > context.data_size) { +#ifdef ICO_DEBUG + printf("load_ico_directory: offset: %lu size: %lu doesn't fit in ICO size: %lu\n", + desc.offset, desc.size, context.data_size); +#endif + return false; + } +#ifdef ICO_DEBUG + printf("load_ico_directory: index %zu width: %u height: %u offset: %lu size: %lu\n", + i, desc.width, desc.height, desc.offset, desc.size); +#endif + context.images.append(desc); + } + context.largest_index = find_largest_image(context); + context.state = ICOLoadingContext::State::DirectoryDecoded; + return true; +} + +static bool load_ico_bmp(ICOLoadingContext& context, ImageDescriptor& desc) +{ + BITMAPINFOHEADER info; + if (desc.size < sizeof(info)) + return false; + + memcpy(&info, context.data + desc.offset, sizeof(info)); + if (info.size != sizeof(info)) { +#ifdef ICO_DEBUG + printf("load_ico_bmp: info size: %u, expected: %lu\n", info.size, sizeof(info)); +#endif + return false; + } + + if (info.width < 0) { +#ifdef ICO_DEBUG + printf("load_ico_bmp: width %d < 0\n", info.width); +#endif + return false; + } + bool topdown = false; + if (info.height < 0) { + topdown = true; + info.height = -info.height; + } + + if (info.planes != 1) { +#ifdef ICO_DEBUG + printf("load_ico_bmp: planes: %d != 1", info.planes); +#endif + return false; + } + + if (info.bpp != 32) { +#ifdef ICO_DEBUG + printf("load_ico_bmp: unsupported bpp: %u\n", info.bpp); +#endif + return false; + } + +#ifdef ICO_DEBUG + printf("load_ico_bmp: width: %d height: %d direction: %s bpp: %d size_image: %u\n", + info.width, info.height, topdown ? "TopDown" : "BottomUp", info.bpp, info.size_image); +#endif + + if (info.compression != 0 || info.palette_size != 0 || info.important_colors != 0) { +#ifdef ICO_DEBUG + printf("load_ico_bmp: following fields must be 0: compression: %u palette_size: %u important_colors: %u\n", + info.compression, info.palette_size, info.important_colors); +#endif + return false; + } + + if (info.width != desc.width || info.height != 2 * desc.height) { +#ifdef ICO_DEBUG + printf("load_ico_bmp: size mismatch: ico %dx%d, bmp %dx%d\n", + desc.width, desc.height, info.width, info.height); +#endif + return false; + } + + // Mask is 1bpp, and each row must be 4-byte aligned + size_t mask_row_len = align_up_to(align_up_to(desc.width, 8) / 8, 4); + size_t required_len = desc.height * (desc.width * sizeof(BMP_ARGB) + mask_row_len); + size_t available_len = desc.size - sizeof(info); + if (required_len > available_len) { +#ifdef ICO_DEBUG + printf("load_ico_bmp: required_len: %lu > available_len: %lu\n", + required_len, available_len); +#endif + return false; + } + + desc.bitmap = Bitmap::create_purgeable(BitmapFormat::RGBA32, { desc.width, desc.height }); + if (!desc.bitmap) + return false; + Bitmap& bitmap = *desc.bitmap; + const u8* image_base = context.data + desc.offset + sizeof(info); + const BMP_ARGB* data_base = (const BMP_ARGB*)image_base; + const u8* mask_base = image_base + desc.width * desc.height * sizeof(BMP_ARGB); + for (int y = 0; y < desc.height; y++) { + const u8* row_mask = mask_base + mask_row_len * y; + const BMP_ARGB* row_data = data_base + desc.width * y; + for (int x = 0; x < desc.width; x++) { + u8 mask = !!(row_mask[x / 8] & (0x80 >> (x % 8))); + BMP_ARGB data = row_data[x]; + bitmap.set_pixel(x, topdown ? y : desc.height - y - 1, + Color(data.r, data.g, data.b, mask ? 0 : data.a)); + } + } + return true; +} + +static bool load_ico_bitmap(ICOLoadingContext& context, Optional<size_t> index) +{ + if (context.state < ICOLoadingContext::State::DirectoryDecoded) { + if (!load_ico_directory(context)) { + context.state = ICOLoadingContext::State::Error; + return false; + } + context.state = ICOLoadingContext::State::DirectoryDecoded; + } + size_t real_index = context.largest_index; + if (index.has_value()) + real_index = index.value(); + if (real_index >= context.images.size()) { + return false; + } + + ImageDescriptor& desc = context.images[real_index]; + + PNGImageDecoderPlugin png_decoder(context.data + desc.offset, desc.size); + if (png_decoder.sniff()) { + desc.bitmap = png_decoder.bitmap(); + if (!desc.bitmap) { +#ifdef ICO_DEBUG + printf("load_ico_bitmap: failed to load PNG encoded image index: %lu\n", real_index); +#endif + return false; + } + return true; + } else { + if (!load_ico_bmp(context, desc)) { +#ifdef ICO_DEBUG + printf("load_ico_bitmap: failed to load BMP encoded image index: %lu\n", real_index); +#endif + return false; + } + return true; + } +} + +ICOImageDecoderPlugin::ICOImageDecoderPlugin(const u8* data, size_t size) +{ + m_context = make<ICOLoadingContext>(); + m_context->data = data; + m_context->data_size = size; +} + +ICOImageDecoderPlugin::~ICOImageDecoderPlugin() { } + +IntSize ICOImageDecoderPlugin::size() +{ + if (m_context->state == ICOLoadingContext::State::Error) { + return {}; + } + + if (m_context->state < ICOLoadingContext::State::DirectoryDecoded) { + if (!load_ico_directory(*m_context)) { + m_context->state = ICOLoadingContext::State::Error; + return {}; + } + m_context->state = ICOLoadingContext::State::DirectoryDecoded; + } + + return { m_context->images[m_context->largest_index].width, m_context->images[m_context->largest_index].height }; +} + +RefPtr<Gfx::Bitmap> ICOImageDecoderPlugin::bitmap() +{ + if (m_context->state == ICOLoadingContext::State::Error) + return nullptr; + + if (m_context->state < ICOLoadingContext::State::BitmapDecoded) { + // NOTE: This forces the chunk decoding to happen. + bool success = load_ico_bitmap(*m_context, {}); + if (!success) { + m_context->state = ICOLoadingContext::State::Error; + return nullptr; + } + m_context->state = ICOLoadingContext::State::BitmapDecoded; + } + + ASSERT(m_context->images[m_context->largest_index].bitmap); + return m_context->images[m_context->largest_index].bitmap; +} + +void ICOImageDecoderPlugin::set_volatile() +{ + if (m_context->images[0].bitmap) + m_context->images[0].bitmap->set_volatile(); +} + +bool ICOImageDecoderPlugin::set_nonvolatile() +{ + if (!m_context->images[0].bitmap) + return false; + return m_context->images[0].bitmap->set_nonvolatile(); +} + +bool ICOImageDecoderPlugin::sniff() +{ + InputMemoryStream stream { { m_context->data, m_context->data_size } }; + return decode_ico_header(stream).has_value(); +} + +bool ICOImageDecoderPlugin::is_animated() +{ + return false; +} + +size_t ICOImageDecoderPlugin::loop_count() +{ + return 0; +} + +size_t ICOImageDecoderPlugin::frame_count() +{ + return 1; +} + +ImageFrameDescriptor ICOImageDecoderPlugin::frame(size_t i) +{ + if (i > 0) { + return { bitmap(), 0 }; + } + return {}; +} + +} diff --git a/Userland/Libraries/LibGfx/ICOLoader.h b/Userland/Libraries/LibGfx/ICOLoader.h new file mode 100644 index 0000000000..12c40b3398 --- /dev/null +++ b/Userland/Libraries/LibGfx/ICOLoader.h @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2020, Paul Roukema <roukemap@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <LibGfx/Bitmap.h> +#include <LibGfx/ImageDecoder.h> + +namespace Gfx { + +RefPtr<Gfx::Bitmap> load_ico(const StringView& path); +RefPtr<Gfx::Bitmap> load_ico_from_memory(const u8*, size_t); + +struct ICOLoadingContext; + +class ICOImageDecoderPlugin final : public ImageDecoderPlugin { +public: + virtual ~ICOImageDecoderPlugin() override; + ICOImageDecoderPlugin(const u8*, size_t); + + virtual IntSize size() override; + virtual RefPtr<Gfx::Bitmap> bitmap() override; + virtual void set_volatile() override; + [[nodiscard]] virtual bool set_nonvolatile() override; + virtual bool sniff() override; + virtual bool is_animated() override; + virtual size_t loop_count() override; + virtual size_t frame_count() override; + virtual ImageFrameDescriptor frame(size_t i) override; + +private: + OwnPtr<ICOLoadingContext> m_context; +}; + +} diff --git a/Userland/Libraries/LibGfx/ImageDecoder.cpp b/Userland/Libraries/LibGfx/ImageDecoder.cpp new file mode 100644 index 0000000000..21c4a72a59 --- /dev/null +++ b/Userland/Libraries/LibGfx/ImageDecoder.cpp @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <LibGfx/BMPLoader.h> +#include <LibGfx/GIFLoader.h> +#include <LibGfx/ICOLoader.h> +#include <LibGfx/ImageDecoder.h> +#include <LibGfx/JPGLoader.h> +#include <LibGfx/PBMLoader.h> +#include <LibGfx/PGMLoader.h> +#include <LibGfx/PNGLoader.h> +#include <LibGfx/PPMLoader.h> + +namespace Gfx { + +ImageDecoder::ImageDecoder(const u8* data, size_t size) +{ + m_plugin = make<PNGImageDecoderPlugin>(data, size); + if (m_plugin->sniff()) + return; + + m_plugin = make<GIFImageDecoderPlugin>(data, size); + if (m_plugin->sniff()) + return; + + m_plugin = make<BMPImageDecoderPlugin>(data, size); + if (m_plugin->sniff()) + return; + + m_plugin = make<PBMImageDecoderPlugin>(data, size); + if (m_plugin->sniff()) + return; + + m_plugin = make<PGMImageDecoderPlugin>(data, size); + if (m_plugin->sniff()) + return; + + m_plugin = make<PPMImageDecoderPlugin>(data, size); + if (m_plugin->sniff()) + return; + + m_plugin = make<ICOImageDecoderPlugin>(data, size); + if (m_plugin->sniff()) + return; + + m_plugin = make<JPGImageDecoderPlugin>(data, size); + if (m_plugin->sniff()) + return; + + m_plugin = nullptr; +} + +ImageDecoder::~ImageDecoder() +{ +} + +RefPtr<Gfx::Bitmap> ImageDecoder::bitmap() const +{ + if (!m_plugin) + return nullptr; + return m_plugin->bitmap(); +} + +} diff --git a/Userland/Libraries/LibGfx/ImageDecoder.h b/Userland/Libraries/LibGfx/ImageDecoder.h new file mode 100644 index 0000000000..a054f524a9 --- /dev/null +++ b/Userland/Libraries/LibGfx/ImageDecoder.h @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/ByteBuffer.h> +#include <AK/OwnPtr.h> +#include <AK/RefCounted.h> +#include <AK/RefPtr.h> +#include <LibGfx/Size.h> + +namespace Gfx { + +class Bitmap; + +static constexpr size_t maximum_width_for_decoded_images = 16384; +static constexpr size_t maximum_height_for_decoded_images = 16384; + +struct ImageFrameDescriptor { + RefPtr<Bitmap> image; + int duration { 0 }; +}; + +class ImageDecoderPlugin { +public: + virtual ~ImageDecoderPlugin() { } + + virtual IntSize size() = 0; + virtual RefPtr<Gfx::Bitmap> bitmap() = 0; + + virtual void set_volatile() = 0; + [[nodiscard]] virtual bool set_nonvolatile() = 0; + + virtual bool sniff() = 0; + + virtual bool is_animated() = 0; + virtual size_t loop_count() = 0; + virtual size_t frame_count() = 0; + virtual ImageFrameDescriptor frame(size_t i) = 0; + +protected: + ImageDecoderPlugin() { } +}; + +class ImageDecoder : public RefCounted<ImageDecoder> { +public: + static NonnullRefPtr<ImageDecoder> create(const u8* data, size_t size) { return adopt(*new ImageDecoder(data, size)); } + static NonnullRefPtr<ImageDecoder> create(const ByteBuffer& data) { return adopt(*new ImageDecoder(data.data(), data.size())); } + ~ImageDecoder(); + + bool is_valid() const { return m_plugin; } + + IntSize size() const { return m_plugin ? m_plugin->size() : IntSize(); } + int width() const { return size().width(); } + int height() const { return size().height(); } + RefPtr<Gfx::Bitmap> bitmap() const; + void set_volatile() + { + if (m_plugin) + m_plugin->set_volatile(); + } + [[nodiscard]] bool set_nonvolatile() { return m_plugin ? m_plugin->set_nonvolatile() : false; } + bool sniff() const { return m_plugin ? m_plugin->sniff() : false; } + bool is_animated() const { return m_plugin ? m_plugin->is_animated() : false; } + size_t loop_count() const { return m_plugin ? m_plugin->loop_count() : 0; } + size_t frame_count() const { return m_plugin ? m_plugin->frame_count() : 0; } + ImageFrameDescriptor frame(size_t i) const { return m_plugin ? m_plugin->frame(i) : ImageFrameDescriptor(); } + +private: + ImageDecoder(const u8*, size_t); + + mutable OwnPtr<ImageDecoderPlugin> m_plugin; +}; + +} diff --git a/Userland/Libraries/LibGfx/JPGLoader.cpp b/Userland/Libraries/LibGfx/JPGLoader.cpp new file mode 100644 index 0000000000..5b521804f0 --- /dev/null +++ b/Userland/Libraries/LibGfx/JPGLoader.cpp @@ -0,0 +1,1420 @@ +/* + * Copyright (c) 2020, The SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <AK/Bitmap.h> +#include <AK/ByteBuffer.h> +#include <AK/HashMap.h> +#include <AK/LexicalPath.h> +#include <AK/MappedFile.h> +#include <AK/MemoryStream.h> +#include <AK/String.h> +#include <AK/Vector.h> +#include <LibGfx/Bitmap.h> +#include <LibGfx/JPGLoader.h> +#include <math.h> + +//#define JPG_DEBUG + +#define JPG_INVALID 0X0000 + +#define JPG_APPN0 0XFFE0 +#define JPG_APPN1 0XFFE1 +#define JPG_APPN2 0XFFE2 +#define JPG_APPN3 0XFFE3 +#define JPG_APPN4 0XFFE4 +#define JPG_APPN5 0XFFE5 +#define JPG_APPN6 0XFFE6 +#define JPG_APPN7 0XFFE7 +#define JPG_APPN8 0XFFE8 +#define JPG_APPN9 0XFFE9 +#define JPG_APPNA 0XFFEA +#define JPG_APPNB 0XFFEB +#define JPG_APPNC 0XFFEC +#define JPG_APPND 0XFFED +#define JPG_APPNE 0xFFEE +#define JPG_APPNF 0xFFEF + +#define JPG_RESERVED1 0xFFF1 +#define JPG_RESERVED2 0xFFF2 +#define JPG_RESERVED3 0xFFF3 +#define JPG_RESERVED4 0xFFF4 +#define JPG_RESERVED5 0xFFF5 +#define JPG_RESERVED6 0xFFF6 +#define JPG_RESERVED7 0xFFF7 +#define JPG_RESERVED8 0xFFF8 +#define JPG_RESERVED9 0xFFF9 +#define JPG_RESERVEDA 0xFFFA +#define JPG_RESERVEDB 0xFFFB +#define JPG_RESERVEDC 0xFFFC +#define JPG_RESERVEDD 0xFFFD + +#define JPG_RST0 0xFFD0 +#define JPG_RST1 0xFFD1 +#define JPG_RST2 0xFFD2 +#define JPG_RST3 0xFFD3 +#define JPG_RST4 0xFFD4 +#define JPG_RST5 0xFFD5 +#define JPG_RST6 0xFFD6 +#define JPG_RST7 0xFFD7 + +#define JPG_DHP 0xFFDE +#define JPG_EXP 0xFFDF + +#define JPG_DHT 0XFFC4 +#define JPG_DQT 0XFFDB +#define JPG_EOI 0xFFD9 +#define JPG_RST 0XFFDD +#define JPG_SOF0 0XFFC0 +#define JPG_SOF2 0xFFC2 +#define JPG_SOI 0XFFD8 +#define JPG_SOS 0XFFDA +#define JPG_COM 0xFFFE + +namespace Gfx { + +constexpr static u8 zigzag_map[64] { + 0, 1, 8, 16, 9, 2, 3, 10, + 17, 24, 32, 25, 18, 11, 4, 5, + 12, 19, 26, 33, 40, 48, 41, 34, + 27, 20, 13, 6, 7, 14, 21, 28, + 35, 42, 49, 56, 57, 50, 43, 36, + 29, 22, 15, 23, 30, 37, 44, 51, + 58, 59, 52, 45, 38, 31, 39, 46, + 53, 60, 61, 54, 47, 55, 62, 63 +}; + +using Marker = u16; + +/** + * MCU means group of data units that are coded together. A data unit is an 8x8 + * block of component data. In interleaved scans, number of non-interleaved data + * units of a component C is Ch * Cv, where Ch and Cv represent the horizontal & + * vertical subsampling factors of the component, respectively. A MacroBlock is + * an 8x8 block of RGB values before encoding, and 8x8 block of YCbCr values when + * we're done decoding the huffman stream. + */ +struct Macroblock { + union { + i32 y[64] = { 0 }; + i32 r[64]; + }; + + union { + i32 cb[64] = { 0 }; + i32 g[64]; + }; + + union { + i32 cr[64] = { 0 }; + i32 b[64]; + }; +}; + +struct MacroblockMeta { + u32 total { 0 }; + u32 padded_total { 0 }; + u32 hcount { 0 }; + u32 vcount { 0 }; + u32 hpadded_count { 0 }; + u32 vpadded_count { 0 }; +}; + +struct ComponentSpec { + u8 serial_id { 255 }; // In the interval [0, 3). + u8 id { 0 }; + u8 hsample_factor { 1 }; // Horizontal sampling factor. + u8 vsample_factor { 1 }; // Vertical sampling factor. + u8 ac_destination_id { 0 }; + u8 dc_destination_id { 0 }; + u8 qtable_id { 0 }; // Quantization table id. +}; + +struct StartOfFrame { + + // Of these, only the first 3 are in mainstream use, and refers to SOF0-2. + enum class FrameType { + Baseline_DCT = 0, + Extended_Sequential_DCT = 1, + Progressive_DCT = 2, + Sequential_Lossless = 3, + Differential_Sequential_DCT = 5, + Differential_Progressive_DCT = 6, + Differential_Sequential_Lossless = 7, + Extended_Sequential_DCT_Arithmetic = 9, + Progressive_DCT_Arithmetic = 10, + Sequential_Lossless_Arithmetic = 11, + Differential_Sequential_DCT_Arithmetic = 13, + Differential_Progressive_DCT_Arithmetic = 14, + Differential_Sequential_Lossless_Arithmetic = 15, + }; + + FrameType type { FrameType::Baseline_DCT }; + u8 precision { 0 }; + u16 height { 0 }; + u16 width { 0 }; +}; + +struct HuffmanTableSpec { + u8 type { 0 }; + u8 destination_id { 0 }; + u8 code_counts[16] = { 0 }; + Vector<u8> symbols; + Vector<u16> codes; +}; + +struct HuffmanStreamState { + Vector<u8> stream; + u8 bit_offset { 0 }; + size_t byte_offset { 0 }; +}; + +struct JPGLoadingContext { + enum State { + NotDecoded = 0, + Error, + FrameDecoded, + BitmapDecoded + }; + + State state { State::NotDecoded }; + const u8* data { nullptr }; + size_t data_size { 0 }; + u32 luma_table[64] = { 0 }; + u32 chroma_table[64] = { 0 }; + StartOfFrame frame; + u8 hsample_factor { 0 }; + u8 vsample_factor { 0 }; + u8 component_count { 0 }; + HashMap<u8, ComponentSpec> components; + RefPtr<Gfx::Bitmap> bitmap; + u16 dc_reset_interval { 0 }; + HashMap<u8, HuffmanTableSpec> dc_tables; + HashMap<u8, HuffmanTableSpec> ac_tables; + HuffmanStreamState huffman_stream; + i32 previous_dc_values[3] = { 0 }; + MacroblockMeta mblock_meta; +}; + +static void generate_huffman_codes(HuffmanTableSpec& table) +{ + unsigned code = 0; + for (auto number_of_codes : table.code_counts) { + for (int i = 0; i < number_of_codes; i++) + table.codes.append(code++); + code <<= 1; + } +} + +static Optional<size_t> read_huffman_bits(HuffmanStreamState& hstream, size_t count = 1) +{ + if (count > (8 * sizeof(size_t))) { +#ifdef JPG_DEBUG + dbg() << String::format("Can't read %zu bits at once!", count); +#endif + return {}; + } + size_t value = 0; + while (count--) { + if (hstream.byte_offset >= hstream.stream.size()) { +#ifdef JPG_DEBUG + dbg() << String::format("Huffman stream exhausted. This could be an error!"); +#endif + return {}; + } + u8 current_byte = hstream.stream[hstream.byte_offset]; + u8 current_bit = 1u & (u32)(current_byte >> (7 - hstream.bit_offset)); // MSB first. + hstream.bit_offset++; + value = (value << 1) | (size_t)current_bit; + if (hstream.bit_offset == 8) { + hstream.byte_offset++; + hstream.bit_offset = 0; + } + } + return value; +} + +static Optional<u8> get_next_symbol(HuffmanStreamState& hstream, const HuffmanTableSpec& table) +{ + unsigned code = 0; + size_t code_cursor = 0; + for (int i = 0; i < 16; i++) { // Codes can't be longer than 16 bits. + auto result = read_huffman_bits(hstream); + if (!result.has_value()) + return {}; + code = (code << 1) | (i32)result.release_value(); + for (int j = 0; j < table.code_counts[i]; j++) { + if (code == table.codes[code_cursor]) + return table.symbols[code_cursor]; + code_cursor++; + } + } + +#ifdef JPG_DEBUG + dbgln("If you're seeing this...the jpeg decoder needs to support more kinds of JPEGs!"); +#endif + return {}; +} + +/** + * Build the macroblocks possible by reading single (MCU) subsampled pair of CbCr. + * Depending on the sampling factors, we may not see triples of y, cb, cr in that + * order. If sample factors differ from one, we'll read more than one block of y- + * coefficients before we get to read a cb-cr block. + + * In the function below, `hcursor` and `vcursor` denote the location of the block + * we're building in the macroblock matrix. `vfactor_i` and `hfactor_i` are cursors + * that iterate over the vertical and horizontal subsampling factors, respectively. + * When we finish one iteration of the innermost loop, we'll have the coefficients + * of one of the components of block at position `mb_index`. When the outermost loop + * finishes first iteration, we'll have all the luminance coefficients for all the + * macroblocks that share the chrominance data. Next two iterations (assuming that + * we are dealing with three components) will fill up the blocks with chroma data. + */ +static bool build_macroblocks(JPGLoadingContext& context, Vector<Macroblock>& macroblocks, u8 hcursor, u8 vcursor) +{ + for (auto it = context.components.begin(); it != context.components.end(); ++it) { + ComponentSpec& component = it->value; + + if (component.dc_destination_id >= context.dc_tables.size()) + return false; + if (component.ac_destination_id >= context.ac_tables.size()) + return false; + + for (u8 vfactor_i = 0; vfactor_i < component.vsample_factor; vfactor_i++) { + for (u8 hfactor_i = 0; hfactor_i < component.hsample_factor; hfactor_i++) { + u32 mb_index = (vcursor + vfactor_i) * context.mblock_meta.hpadded_count + (hfactor_i + hcursor); + Macroblock& block = macroblocks[mb_index]; + + auto& dc_table = context.dc_tables.find(component.dc_destination_id)->value; + auto& ac_table = context.ac_tables.find(component.ac_destination_id)->value; + + auto symbol_or_error = get_next_symbol(context.huffman_stream, dc_table); + if (!symbol_or_error.has_value()) + return false; + + // For DC coefficients, symbol encodes the length of the coefficient. + auto dc_length = symbol_or_error.release_value(); + if (dc_length > 11) { +#ifdef JPG_DEBUG + dbg() << String::format("DC coefficient too long: %i!", dc_length); +#endif + return false; + } + + auto coeff_or_error = read_huffman_bits(context.huffman_stream, dc_length); + if (!coeff_or_error.has_value()) + return false; + + // DC coefficients are encoded as the difference between previous and current DC values. + i32 dc_diff = coeff_or_error.release_value(); + + // If MSB in diff is 0, the difference is -ve. Otherwise +ve. + if (dc_length != 0 && dc_diff < (1 << (dc_length - 1))) + dc_diff -= (1 << dc_length) - 1; + + i32* select_component = component.serial_id == 0 ? block.y : (component.serial_id == 1 ? block.cb : block.cr); + auto& previous_dc = context.previous_dc_values[component.serial_id]; + select_component[0] = previous_dc += dc_diff; + + // Compute the AC coefficients. + for (int j = 1; j < 64;) { + symbol_or_error = get_next_symbol(context.huffman_stream, ac_table); + if (!symbol_or_error.has_value()) + return false; + + // AC symbols encode 2 pieces of information, the high 4 bits represent + // number of zeroes to be stuffed before reading the coefficient. Low 4 + // bits represent the magnitude of the coefficient. + auto ac_symbol = symbol_or_error.release_value(); + if (ac_symbol == 0) + break; + + // ac_symbol = 0xF0 means we need to skip 16 zeroes. + u8 run_length = ac_symbol == 0xF0 ? 16 : ac_symbol >> 4; + j += run_length; + + if (j >= 64) { +#ifdef JPG_DEBUG + dbg() << String::format("Run-length exceeded boundaries. Cursor: %i, Skipping: %i!", j, run_length); +#endif + return false; + } + + u8 coeff_length = ac_symbol & 0x0F; + if (coeff_length > 10) { +#ifdef JPG_DEBUG + dbg() << String::format("AC coefficient too long: %i!", coeff_length); +#endif + return false; + } + + if (coeff_length != 0) { + coeff_or_error = read_huffman_bits(context.huffman_stream, coeff_length); + if (!coeff_or_error.has_value()) + return false; + i32 ac_coefficient = coeff_or_error.release_value(); + if (ac_coefficient < (1 << (coeff_length - 1))) + ac_coefficient -= (1 << coeff_length) - 1; + + select_component[zigzag_map[j++]] = ac_coefficient; + } + } + } + } + } + + return true; +} + +static Optional<Vector<Macroblock>> decode_huffman_stream(JPGLoadingContext& context) +{ + Vector<Macroblock> macroblocks; + macroblocks.resize(context.mblock_meta.padded_total); + +#ifdef JPG_DEBUG + dbg() << "Image width: " << context.frame.width; + dbg() << "Image height: " << context.frame.height; + dbg() << "Macroblocks in a row: " << context.mblock_meta.hpadded_count; + dbg() << "Macroblocks in a column: " << context.mblock_meta.vpadded_count; + dbg() << "Macroblock meta padded total: " << context.mblock_meta.padded_total; +#endif + + // Compute huffman codes for DC and AC tables. + for (auto it = context.dc_tables.begin(); it != context.dc_tables.end(); ++it) + generate_huffman_codes(it->value); + + for (auto it = context.ac_tables.begin(); it != context.ac_tables.end(); ++it) + generate_huffman_codes(it->value); + + for (u32 vcursor = 0; vcursor < context.mblock_meta.vcount; vcursor += context.vsample_factor) { + for (u32 hcursor = 0; hcursor < context.mblock_meta.hcount; hcursor += context.hsample_factor) { + u32 i = vcursor * context.mblock_meta.hpadded_count + hcursor; + if (context.dc_reset_interval > 0) { + if (i % context.dc_reset_interval == 0) { + context.previous_dc_values[0] = 0; + context.previous_dc_values[1] = 0; + context.previous_dc_values[2] = 0; + + // Restart markers are stored in byte boundaries. Advance the huffman stream cursor to + // the 0th bit of the next byte. + if (context.huffman_stream.byte_offset < context.huffman_stream.stream.size()) { + if (context.huffman_stream.bit_offset > 0) { + context.huffman_stream.bit_offset = 0; + context.huffman_stream.byte_offset++; + } + + // Skip the restart marker (RSTn). + context.huffman_stream.byte_offset++; + } + } + } + + if (!build_macroblocks(context, macroblocks, hcursor, vcursor)) { +#ifdef JPG_DEBUG + dbg() << "Failed to build Macroblock " << i; + dbg() << "Huffman stream byte offset " << context.huffman_stream.byte_offset; + dbg() << "Huffman stream bit offset " << context.huffman_stream.bit_offset; +#endif + return {}; + } + } + } + + return macroblocks; +} + +static inline bool bounds_okay(const size_t cursor, const size_t delta, const size_t bound) +{ + return (delta + cursor) < bound; +} + +static inline bool is_valid_marker(const Marker marker) +{ + if (marker >= JPG_APPN0 && marker <= JPG_APPNF) { +#ifdef JPG_DEBUG + if (marker != JPG_APPN0) + dbg() << String::format("%04x not supported yet. The decoder may fail!", marker); +#endif + return true; + } + if (marker >= JPG_RESERVED1 && marker <= JPG_RESERVEDD) + return true; + if (marker >= JPG_RST0 && marker <= JPG_RST7) + return true; + switch (marker) { + case JPG_COM: + case JPG_DHP: + case JPG_EXP: + case JPG_DHT: + case JPG_DQT: + case JPG_RST: + case JPG_SOF0: + case JPG_SOI: + case JPG_SOS: + return true; + } + + if (marker >= 0xFFC0 && marker <= 0xFFCF) { + if (marker != 0xFFC4 && marker != 0xFFC8 && marker != 0xFFCC) { +#ifdef JPG_DEBUG + dbg() << "Decoding this frame-type (SOF" << (marker & 0xf) << ") is not currently supported. Decoder will fail!"; +#endif + return false; + } + } + + return false; +} + +static inline u16 read_be_word(InputMemoryStream& stream) +{ + BigEndian<u16> tmp; + stream >> tmp; + return tmp; +} + +static inline Marker read_marker_at_cursor(InputMemoryStream& stream) +{ + u16 marker = read_be_word(stream); + if (stream.handle_any_error()) + return JPG_INVALID; + if (is_valid_marker(marker)) + return marker; + if (marker != 0xFFFF) + return JPG_INVALID; + u8 next; + do { + stream >> next; + if (stream.handle_any_error() || next == 0x00) + return JPG_INVALID; + } while (next == 0xFF); + marker = 0xFF00 | (u16)next; + return is_valid_marker(marker) ? marker : JPG_INVALID; +} + +static bool read_start_of_scan(InputMemoryStream& stream, JPGLoadingContext& context) +{ + if (context.state < JPGLoadingContext::State::FrameDecoded) { +#ifdef JPG_DEBUG + dbg() << stream.offset() << ": SOS found before reading a SOF!"; +#endif + return false; + } + + u16 bytes_to_read = read_be_word(stream); + if (stream.handle_any_error()) + return false; + bytes_to_read -= 2; + if (!bounds_okay(stream.offset(), bytes_to_read, context.data_size)) + return false; + u8 component_count = 0; + stream >> component_count; + if (stream.handle_any_error()) + return false; + if (component_count != context.component_count) { +#ifdef JPG_DEBUG + dbg() << stream.offset() + << String::format(": Unsupported number of components: %i!", component_count); +#endif + return false; + } + + for (int i = 0; i < component_count; i++) { + ComponentSpec* component = nullptr; + u8 component_id = 0; + stream >> component_id; + if (stream.handle_any_error()) + return false; + + auto it = context.components.find(component_id); + if (it != context.components.end()) { + component = &it->value; + if (i != component->serial_id) { + dbgln("JPEG decode failed (i != component->serial_id)"); + return false; + } + } else { +#ifdef JPG_DEBUG + dbg() << stream.offset() << String::format(": Unsupported component id: %i!", component_id); +#endif + return false; + } + + u8 table_ids = 0; + stream >> table_ids; + if (stream.handle_any_error()) + return false; + + component->dc_destination_id = table_ids >> 4; + component->ac_destination_id = table_ids & 0x0F; + + if (context.dc_tables.size() != context.ac_tables.size()) { +#ifdef JPG_DEBUG + dbg() << stream.offset() << ": DC & AC table count mismatch!"; +#endif + return false; + } + + if (!context.dc_tables.contains(component->dc_destination_id)) { +#ifdef JPG_DEBUG + dbgln("DC table (id: {}) does not exist!", component->dc_destination_id); +#endif + return false; + } + + if (!context.ac_tables.contains(component->ac_destination_id)) { +#ifdef JPG_DEBUG + dbgln("AC table (id: {}) does not exist!", component->ac_destination_id); +#endif + return false; + } + } + + u8 spectral_selection_start = 0; + stream >> spectral_selection_start; + if (stream.handle_any_error()) + return false; + u8 spectral_selection_end = 0; + stream >> spectral_selection_end; + if (stream.handle_any_error()) + return false; + u8 successive_approximation = 0; + stream >> successive_approximation; + if (stream.handle_any_error()) + return false; + // The three values should be fixed for baseline JPEGs utilizing sequential DCT. + if (spectral_selection_start != 0 || spectral_selection_end != 63 || successive_approximation != 0) { +#ifdef JPG_DEBUG + dbg() << stream.offset() << ": ERROR! Start of Selection: " << spectral_selection_start + << ", End of Selection: " << spectral_selection_end + << ", Successive Approximation: " << successive_approximation << "!"; +#endif + return false; + } + return true; +} + +static bool read_reset_marker(InputMemoryStream& stream, JPGLoadingContext& context) +{ + u16 bytes_to_read = read_be_word(stream); + if (stream.handle_any_error()) + return false; + bytes_to_read -= 2; + if (bytes_to_read != 2) { +#ifdef JPG_DEBUG + dbg() << stream.offset() << ": Malformed reset marker found!"; +#endif + return false; + } + context.dc_reset_interval = read_be_word(stream); + if (stream.handle_any_error()) + return false; + return true; +} + +static bool read_huffman_table(InputMemoryStream& stream, JPGLoadingContext& context) +{ + i32 bytes_to_read = read_be_word(stream); + if (stream.handle_any_error()) + return false; + if (!bounds_okay(stream.offset(), bytes_to_read, context.data_size)) + return false; + bytes_to_read -= 2; + while (bytes_to_read > 0) { + HuffmanTableSpec table; + u8 table_info = 0; + stream >> table_info; + if (stream.handle_any_error()) + return false; + u8 table_type = table_info >> 4; + u8 table_destination_id = table_info & 0x0F; + if (table_type > 1) { +#ifdef JPG_DEBUG + dbg() << stream.offset() << String::format(": Unrecognized huffman table: %i!", table_type); +#endif + return false; + } + if (table_destination_id > 1) { +#ifdef JPG_DEBUG + dbg() << stream.offset() + << String::format(": Invalid huffman table destination id: %i!", table_destination_id); +#endif + return false; + } + + table.type = table_type; + table.destination_id = table_destination_id; + u32 total_codes = 0; + + // Read code counts. At each index K, the value represents the number of K+1 bit codes in this header. + for (int i = 0; i < 16; i++) { + u8 count = 0; + stream >> count; + if (stream.handle_any_error()) + return false; + total_codes += count; + table.code_counts[i] = count; + } + + table.codes.ensure_capacity(total_codes); + + // Read symbols. Read X bytes, where X is the sum of the counts of codes read in the previous step. + for (u32 i = 0; i < total_codes; i++) { + u8 symbol = 0; + stream >> symbol; + if (stream.handle_any_error()) + return false; + table.symbols.append(symbol); + } + + if (stream.handle_any_error()) + return false; + + auto& huffman_table = table.type == 0 ? context.dc_tables : context.ac_tables; + huffman_table.set(table.destination_id, table); + ASSERT(huffman_table.size() <= 2); + + bytes_to_read -= 1 + 16 + total_codes; + } + + if (bytes_to_read != 0) { +#ifdef JPG_DEBUG + dbg() << stream.offset() << ": Extra bytes detected in huffman header!"; +#endif + return false; + } + return true; +} + +static inline bool validate_luma_and_modify_context(const ComponentSpec& luma, JPGLoadingContext& context) +{ + if ((luma.hsample_factor == 1 || luma.hsample_factor == 2) && (luma.vsample_factor == 1 || luma.vsample_factor == 2)) { + context.mblock_meta.hpadded_count += luma.hsample_factor == 1 ? 0 : context.mblock_meta.hcount % 2; + context.mblock_meta.vpadded_count += luma.vsample_factor == 1 ? 0 : context.mblock_meta.vcount % 2; + context.mblock_meta.padded_total = context.mblock_meta.hpadded_count * context.mblock_meta.vpadded_count; + // For easy reference to relevant sample factors. + context.hsample_factor = luma.hsample_factor; + context.vsample_factor = luma.vsample_factor; +#ifdef JPG_DEBUG + dbg() << String::format("Horizontal Subsampling Factor: %i", luma.hsample_factor); + dbg() << String::format("Vertical Subsampling Factor: %i", luma.vsample_factor); +#endif + return true; + } + return false; +} + +static inline void set_macroblock_metadata(JPGLoadingContext& context) +{ + context.mblock_meta.hcount = (context.frame.width + 7) / 8; + context.mblock_meta.vcount = (context.frame.height + 7) / 8; + context.mblock_meta.hpadded_count = context.mblock_meta.hcount; + context.mblock_meta.vpadded_count = context.mblock_meta.vcount; + context.mblock_meta.total = context.mblock_meta.hcount * context.mblock_meta.vcount; +} + +static bool read_start_of_frame(InputMemoryStream& stream, JPGLoadingContext& context) +{ + if (context.state == JPGLoadingContext::FrameDecoded) { +#ifdef JPG_DEBUG + dbg() << stream.offset() << ": SOF repeated!"; +#endif + return false; + } + + i32 bytes_to_read = read_be_word(stream); + if (stream.handle_any_error()) + return false; + + bytes_to_read -= 2; + if (!bounds_okay(stream.offset(), bytes_to_read, context.data_size)) + return false; + + stream >> context.frame.precision; + if (stream.handle_any_error()) + return false; + if (context.frame.precision != 8) { +#ifdef JPG_DEBUG + dbg() << stream.offset() << ": SOF precision != 8!"; +#endif + return false; + } + + context.frame.height = read_be_word(stream); + if (stream.handle_any_error()) + return false; + context.frame.width = read_be_word(stream); + if (stream.handle_any_error()) + return false; + if (!context.frame.width || !context.frame.height) { +#ifdef JPG_DEBUG + dbg() << stream.offset() << ": ERROR! Image height: " << context.frame.height << ", Image width: " + << context.frame.width << "!"; +#endif + return false; + } + + if (context.frame.width > maximum_width_for_decoded_images || context.frame.height > maximum_height_for_decoded_images) { + dbgln("This JPEG is too large for comfort: {}x{}", context.frame.width, context.frame.height); + return false; + } + + set_macroblock_metadata(context); + + stream >> context.component_count; + if (stream.handle_any_error()) + return false; + if (context.component_count != 1 && context.component_count != 3) { +#ifdef JPG_DEBUG + dbg() << stream.offset() << ": Unsupported number of components in SOF: " + << context.component_count << "!"; +#endif + return false; + } + + for (u8 i = 0; i < context.component_count; i++) { + ComponentSpec component; + component.serial_id = i; + + stream >> component.id; + if (stream.handle_any_error()) + return false; + + u8 subsample_factors = 0; + stream >> subsample_factors; + if (stream.handle_any_error()) + return false; + component.hsample_factor = subsample_factors >> 4; + component.vsample_factor = subsample_factors & 0x0F; + + if (component.serial_id == 0) { + // By convention, downsampling is applied only on chroma components. So we should + // hope to see the maximum sampling factor in the luma component. + if (!validate_luma_and_modify_context(component, context)) { +#ifdef JPG_DEBUG + dbg() << stream.offset() << ": Unsupported luma subsampling factors: " + << "horizontal: " << component.hsample_factor << ", vertical: " << component.vsample_factor; +#endif + return false; + } + } else { + if (component.hsample_factor != 1 || component.vsample_factor != 1) { +#ifdef JPG_DEBUG + dbg() << stream.offset() << ": Unsupported chroma subsampling factors: " + << "horizontal: " << component.hsample_factor << ", vertical: " << component.vsample_factor; +#endif + return false; + } + } + + stream >> component.qtable_id; + if (stream.handle_any_error()) + return false; + if (component.qtable_id > 1) { +#ifdef JPG_DEBUG + dbg() << stream.offset() << ": Unsupported quantization table id: " + << component.qtable_id << "!"; +#endif + return false; + } + + context.components.set(component.id, component); + } + + return true; +} + +static bool read_quantization_table(InputMemoryStream& stream, JPGLoadingContext& context) +{ + i32 bytes_to_read = read_be_word(stream); + if (stream.handle_any_error()) + return false; + bytes_to_read -= 2; + if (!bounds_okay(stream.offset(), bytes_to_read, context.data_size)) + return false; + while (bytes_to_read > 0) { + u8 info_byte = 0; + stream >> info_byte; + if (stream.handle_any_error()) + return false; + u8 element_unit_hint = info_byte >> 4; + if (element_unit_hint > 1) { +#ifdef JPG_DEBUG + dbg() << stream.offset() + << String::format(": Unsupported unit hint in quantization table: %i!", element_unit_hint); +#endif + return false; + } + u8 table_id = info_byte & 0x0F; + if (table_id > 1) { +#ifdef JPG_DEBUG + dbg() << stream.offset() << String::format(": Unsupported quantization table id: %i!", table_id); +#endif + return false; + } + u32* table = table_id == 0 ? context.luma_table : context.chroma_table; + for (int i = 0; i < 64; i++) { + if (element_unit_hint == 0) { + u8 tmp = 0; + stream >> tmp; + if (stream.handle_any_error()) + return false; + table[zigzag_map[i]] = tmp; + } else { + table[zigzag_map[i]] = read_be_word(stream); + if (stream.handle_any_error()) + return false; + } + } + if (stream.handle_any_error()) + return false; + + bytes_to_read -= 1 + (element_unit_hint == 0 ? 64 : 128); + } + if (bytes_to_read != 0) { +#ifdef JPG_DEBUG + dbg() << stream.offset() << ": Invalid length for one or more quantization tables!"; +#endif + return false; + } + + return true; +} + +static bool skip_marker_with_length(InputMemoryStream& stream) +{ + u16 bytes_to_skip = read_be_word(stream); + bytes_to_skip -= 2; + if (stream.handle_any_error()) + return false; + stream.discard_or_error(bytes_to_skip); + return !stream.handle_any_error(); +} + +static void dequantize(JPGLoadingContext& context, Vector<Macroblock>& macroblocks) +{ + for (u32 vcursor = 0; vcursor < context.mblock_meta.vcount; vcursor += context.vsample_factor) { + for (u32 hcursor = 0; hcursor < context.mblock_meta.hcount; hcursor += context.hsample_factor) { + for (auto it = context.components.begin(); it != context.components.end(); ++it) { + auto& component = it->value; + const u32* table = component.qtable_id == 0 ? context.luma_table : context.chroma_table; + for (u32 vfactor_i = 0; vfactor_i < component.vsample_factor; vfactor_i++) { + for (u32 hfactor_i = 0; hfactor_i < component.hsample_factor; hfactor_i++) { + u32 mb_index = (vcursor + vfactor_i) * context.mblock_meta.hpadded_count + (hfactor_i + hcursor); + Macroblock& block = macroblocks[mb_index]; + int* block_component = component.serial_id == 0 ? block.y : (component.serial_id == 1 ? block.cb : block.cr); + for (u32 k = 0; k < 64; k++) + block_component[k] *= table[k]; + } + } + } + } + } +} + +static void inverse_dct(const JPGLoadingContext& context, Vector<Macroblock>& macroblocks) +{ + static const float m0 = 2.0 * cos(1.0 / 16.0 * 2.0 * M_PI); + static const float m1 = 2.0 * cos(2.0 / 16.0 * 2.0 * M_PI); + static const float m3 = 2.0 * cos(2.0 / 16.0 * 2.0 * M_PI); + static const float m5 = 2.0 * cos(3.0 / 16.0 * 2.0 * M_PI); + static const float m2 = m0 - m5; + static const float m4 = m0 + m5; + static const float s0 = cos(0.0 / 16.0 * M_PI) / sqrt(8); + static const float s1 = cos(1.0 / 16.0 * M_PI) / 2.0; + static const float s2 = cos(2.0 / 16.0 * M_PI) / 2.0; + static const float s3 = cos(3.0 / 16.0 * M_PI) / 2.0; + static const float s4 = cos(4.0 / 16.0 * M_PI) / 2.0; + static const float s5 = cos(5.0 / 16.0 * M_PI) / 2.0; + static const float s6 = cos(6.0 / 16.0 * M_PI) / 2.0; + static const float s7 = cos(7.0 / 16.0 * M_PI) / 2.0; + + for (u32 vcursor = 0; vcursor < context.mblock_meta.vcount; vcursor += context.vsample_factor) { + for (u32 hcursor = 0; hcursor < context.mblock_meta.hcount; hcursor += context.hsample_factor) { + for (auto it = context.components.begin(); it != context.components.end(); ++it) { + auto& component = it->value; + for (u8 vfactor_i = 0; vfactor_i < component.vsample_factor; vfactor_i++) { + for (u8 hfactor_i = 0; hfactor_i < component.hsample_factor; hfactor_i++) { + u32 mb_index = (vcursor + vfactor_i) * context.mblock_meta.hpadded_count + (hfactor_i + hcursor); + Macroblock& block = macroblocks[mb_index]; + i32* block_component = component.serial_id == 0 ? block.y : (component.serial_id == 1 ? block.cb : block.cr); + for (u32 k = 0; k < 8; ++k) { + const float g0 = block_component[0 * 8 + k] * s0; + const float g1 = block_component[4 * 8 + k] * s4; + const float g2 = block_component[2 * 8 + k] * s2; + const float g3 = block_component[6 * 8 + k] * s6; + const float g4 = block_component[5 * 8 + k] * s5; + const float g5 = block_component[1 * 8 + k] * s1; + const float g6 = block_component[7 * 8 + k] * s7; + const float g7 = block_component[3 * 8 + k] * s3; + + const float f0 = g0; + const float f1 = g1; + const float f2 = g2; + const float f3 = g3; + const float f4 = g4 - g7; + const float f5 = g5 + g6; + const float f6 = g5 - g6; + const float f7 = g4 + g7; + + const float e0 = f0; + const float e1 = f1; + const float e2 = f2 - f3; + const float e3 = f2 + f3; + const float e4 = f4; + const float e5 = f5 - f7; + const float e6 = f6; + const float e7 = f5 + f7; + const float e8 = f4 + f6; + + const float d0 = e0; + const float d1 = e1; + const float d2 = e2 * m1; + const float d3 = e3; + const float d4 = e4 * m2; + const float d5 = e5 * m3; + const float d6 = e6 * m4; + const float d7 = e7; + const float d8 = e8 * m5; + + const float c0 = d0 + d1; + const float c1 = d0 - d1; + const float c2 = d2 - d3; + const float c3 = d3; + const float c4 = d4 + d8; + const float c5 = d5 + d7; + const float c6 = d6 - d8; + const float c7 = d7; + const float c8 = c5 - c6; + + const float b0 = c0 + c3; + const float b1 = c1 + c2; + const float b2 = c1 - c2; + const float b3 = c0 - c3; + const float b4 = c4 - c8; + const float b5 = c8; + const float b6 = c6 - c7; + const float b7 = c7; + + block_component[0 * 8 + k] = b0 + b7; + block_component[1 * 8 + k] = b1 + b6; + block_component[2 * 8 + k] = b2 + b5; + block_component[3 * 8 + k] = b3 + b4; + block_component[4 * 8 + k] = b3 - b4; + block_component[5 * 8 + k] = b2 - b5; + block_component[6 * 8 + k] = b1 - b6; + block_component[7 * 8 + k] = b0 - b7; + } + for (u32 l = 0; l < 8; ++l) { + const float g0 = block_component[l * 8 + 0] * s0; + const float g1 = block_component[l * 8 + 4] * s4; + const float g2 = block_component[l * 8 + 2] * s2; + const float g3 = block_component[l * 8 + 6] * s6; + const float g4 = block_component[l * 8 + 5] * s5; + const float g5 = block_component[l * 8 + 1] * s1; + const float g6 = block_component[l * 8 + 7] * s7; + const float g7 = block_component[l * 8 + 3] * s3; + + const float f0 = g0; + const float f1 = g1; + const float f2 = g2; + const float f3 = g3; + const float f4 = g4 - g7; + const float f5 = g5 + g6; + const float f6 = g5 - g6; + const float f7 = g4 + g7; + + const float e0 = f0; + const float e1 = f1; + const float e2 = f2 - f3; + const float e3 = f2 + f3; + const float e4 = f4; + const float e5 = f5 - f7; + const float e6 = f6; + const float e7 = f5 + f7; + const float e8 = f4 + f6; + + const float d0 = e0; + const float d1 = e1; + const float d2 = e2 * m1; + const float d3 = e3; + const float d4 = e4 * m2; + const float d5 = e5 * m3; + const float d6 = e6 * m4; + const float d7 = e7; + const float d8 = e8 * m5; + + const float c0 = d0 + d1; + const float c1 = d0 - d1; + const float c2 = d2 - d3; + const float c3 = d3; + const float c4 = d4 + d8; + const float c5 = d5 + d7; + const float c6 = d6 - d8; + const float c7 = d7; + const float c8 = c5 - c6; + + const float b0 = c0 + c3; + const float b1 = c1 + c2; + const float b2 = c1 - c2; + const float b3 = c0 - c3; + const float b4 = c4 - c8; + const float b5 = c8; + const float b6 = c6 - c7; + const float b7 = c7; + + block_component[l * 8 + 0] = b0 + b7; + block_component[l * 8 + 1] = b1 + b6; + block_component[l * 8 + 2] = b2 + b5; + block_component[l * 8 + 3] = b3 + b4; + block_component[l * 8 + 4] = b3 - b4; + block_component[l * 8 + 5] = b2 - b5; + block_component[l * 8 + 6] = b1 - b6; + block_component[l * 8 + 7] = b0 - b7; + } + } + } + } + } + } +} + +static void ycbcr_to_rgb(const JPGLoadingContext& context, Vector<Macroblock>& macroblocks) +{ + for (u32 vcursor = 0; vcursor < context.mblock_meta.vcount; vcursor += context.vsample_factor) { + for (u32 hcursor = 0; hcursor < context.mblock_meta.hcount; hcursor += context.hsample_factor) { + const u32 chroma_block_index = vcursor * context.mblock_meta.hpadded_count + hcursor; + const Macroblock& chroma = macroblocks[chroma_block_index]; + // Overflows are intentional. + for (u8 vfactor_i = context.vsample_factor - 1; vfactor_i < context.vsample_factor; --vfactor_i) { + for (u8 hfactor_i = context.hsample_factor - 1; hfactor_i < context.hsample_factor; --hfactor_i) { + u32 mb_index = (vcursor + vfactor_i) * context.mblock_meta.hpadded_count + (hcursor + hfactor_i); + i32* y = macroblocks[mb_index].y; + i32* cb = macroblocks[mb_index].cb; + i32* cr = macroblocks[mb_index].cr; + for (u8 i = 7; i < 8; --i) { + for (u8 j = 7; j < 8; --j) { + const u8 pixel = i * 8 + j; + const u32 chroma_pxrow = (i / context.vsample_factor) + 4 * vfactor_i; + const u32 chroma_pxcol = (j / context.hsample_factor) + 4 * hfactor_i; + const u32 chroma_pixel = chroma_pxrow * 8 + chroma_pxcol; + int r = y[pixel] + 1.402f * chroma.cr[chroma_pixel] + 128; + int g = y[pixel] - 0.344f * chroma.cb[chroma_pixel] - 0.714f * chroma.cr[chroma_pixel] + 128; + int b = y[pixel] + 1.772f * chroma.cb[chroma_pixel] + 128; + y[pixel] = r < 0 ? 0 : (r > 255 ? 255 : r); + cb[pixel] = g < 0 ? 0 : (g > 255 ? 255 : g); + cr[pixel] = b < 0 ? 0 : (b > 255 ? 255 : b); + } + } + } + } + } + } +} + +static bool compose_bitmap(JPGLoadingContext& context, const Vector<Macroblock>& macroblocks) +{ + context.bitmap = Bitmap::create_purgeable(BitmapFormat::RGB32, { context.frame.width, context.frame.height }); + if (!context.bitmap) + return false; + + for (u32 y = context.frame.height - 1; y < context.frame.height; y--) { + const u32 block_row = y / 8; + const u32 pixel_row = y % 8; + for (u32 x = 0; x < context.frame.width; x++) { + const u32 block_column = x / 8; + auto& block = macroblocks[block_row * context.mblock_meta.hpadded_count + block_column]; + const u32 pixel_column = x % 8; + const u32 pixel_index = pixel_row * 8 + pixel_column; + const Color color { (u8)block.y[pixel_index], (u8)block.cb[pixel_index], (u8)block.cr[pixel_index] }; + context.bitmap->set_pixel(x, y, color); + } + } + + return true; +} + +static bool parse_header(InputMemoryStream& stream, JPGLoadingContext& context) +{ + auto marker = read_marker_at_cursor(stream); + if (stream.handle_any_error()) + return false; + if (marker != JPG_SOI) { +#ifdef JPG_DEBUG + dbg() << stream.offset() << String::format(": SOI not found: %x!", marker); +#endif + return false; + } + for (;;) { + marker = read_marker_at_cursor(stream); + if (stream.handle_any_error()) + return false; + + // Set frame type if the marker marks a new frame. + if (marker >= 0xFFC0 && marker <= 0xFFCF) { + // Ignore interleaved markers. + if (marker != 0xFFC4 && marker != 0xFFC8 && marker != 0xFFCC) { + context.frame.type = static_cast<StartOfFrame::FrameType>(marker & 0xF); + } + } + + switch (marker) { + case JPG_INVALID: + case JPG_RST0: + case JPG_RST1: + case JPG_RST2: + case JPG_RST3: + case JPG_RST4: + case JPG_RST5: + case JPG_RST6: + case JPG_RST7: + case JPG_SOI: + case JPG_EOI: +#ifdef JPG_DEBUG + dbg() << stream.offset() << String::format(": Unexpected marker %x!", marker); +#endif + return false; + case JPG_SOF0: + if (!read_start_of_frame(stream, context)) + return false; + context.state = JPGLoadingContext::FrameDecoded; + break; + case JPG_DQT: + if (!read_quantization_table(stream, context)) + return false; + break; + case JPG_RST: + if (!read_reset_marker(stream, context)) + return false; + break; + case JPG_DHT: + if (!read_huffman_table(stream, context)) + return false; + break; + case JPG_SOS: + return read_start_of_scan(stream, context); + default: + if (!skip_marker_with_length(stream)) { +#ifdef JPG_DEBUG + dbg() << stream.offset() << String::format(": Error skipping marker: %x!", marker); +#endif + return false; + } + break; + } + } + + ASSERT_NOT_REACHED(); +} + +static bool scan_huffman_stream(InputMemoryStream& stream, JPGLoadingContext& context) +{ + u8 last_byte; + u8 current_byte = 0; + stream >> current_byte; + if (stream.handle_any_error()) + return false; + + for (;;) { + last_byte = current_byte; + stream >> current_byte; + if (stream.handle_any_error()) { +#ifdef JPG_DEBUG + dbg() << stream.offset() << ": EOI not found!"; +#endif + return false; + } + + if (last_byte == 0xFF) { + if (current_byte == 0xFF) + continue; + if (current_byte == 0x00) { + stream >> current_byte; + if (stream.handle_any_error()) + return false; + context.huffman_stream.stream.append(last_byte); + continue; + } + Marker marker = 0xFF00 | current_byte; + if (marker == JPG_EOI) + return true; + if (marker >= JPG_RST0 && marker <= JPG_RST7) { + context.huffman_stream.stream.append(marker); + stream >> current_byte; + if (stream.handle_any_error()) + return false; + continue; + } +#ifdef JPG_DEBUG + dbg() << stream.offset() << String::format(": Invalid marker: %x!", marker); +#endif + return false; + } else { + context.huffman_stream.stream.append(last_byte); + } + } + + ASSERT_NOT_REACHED(); +} + +static bool decode_jpg(JPGLoadingContext& context) +{ + InputMemoryStream stream { { context.data, context.data_size } }; + + if (!parse_header(stream, context)) + return false; + if (!scan_huffman_stream(stream, context)) + return false; + + auto result = decode_huffman_stream(context); + if (!result.has_value()) { +#ifdef JPG_DEBUG + dbg() << stream.offset() << ": Failed to decode Macroblocks!"; +#endif + return false; + } + + auto macroblocks = result.release_value(); + dequantize(context, macroblocks); + inverse_dct(context, macroblocks); + ycbcr_to_rgb(context, macroblocks); + if (!compose_bitmap(context, macroblocks)) + return false; + return true; +} + +static RefPtr<Gfx::Bitmap> load_jpg_impl(const u8* data, size_t data_size) +{ + JPGLoadingContext context; + context.data = data; + context.data_size = data_size; + + if (!decode_jpg(context)) + return nullptr; + + return context.bitmap; +} + +RefPtr<Gfx::Bitmap> load_jpg(const StringView& path) +{ + auto file_or_error = MappedFile::map(path); + if (file_or_error.is_error()) + return nullptr; + auto bitmap = load_jpg_impl((const u8*)file_or_error.value()->data(), file_or_error.value()->size()); + if (bitmap) + bitmap->set_mmap_name(String::formatted("Gfx::Bitmap [{}] - Decoded JPG: {}", bitmap->size(), LexicalPath::canonicalized_path(path))); + return bitmap; +} + +RefPtr<Gfx::Bitmap> load_jpg_from_memory(const u8* data, size_t length) +{ + auto bitmap = load_jpg_impl(data, length); + if (bitmap) + bitmap->set_mmap_name(String::formatted("Gfx::Bitmap [{}] - Decoded jpg: <memory>", bitmap->size())); + return bitmap; +} + +JPGImageDecoderPlugin::JPGImageDecoderPlugin(const u8* data, size_t size) +{ + m_context = make<JPGLoadingContext>(); + m_context->data = data; + m_context->data_size = size; + m_context->huffman_stream.stream.ensure_capacity(50 * KiB); +} + +JPGImageDecoderPlugin::~JPGImageDecoderPlugin() +{ +} + +IntSize JPGImageDecoderPlugin::size() +{ + if (m_context->state == JPGLoadingContext::State::Error) + return {}; + if (m_context->state >= JPGLoadingContext::State::FrameDecoded) + return { m_context->frame.width, m_context->frame.height }; + + return {}; +} + +RefPtr<Gfx::Bitmap> JPGImageDecoderPlugin::bitmap() +{ + if (m_context->state == JPGLoadingContext::State::Error) + return nullptr; + if (m_context->state < JPGLoadingContext::State::BitmapDecoded) { + if (!decode_jpg(*m_context)) { + m_context->state = JPGLoadingContext::State::Error; + return nullptr; + } + m_context->state = JPGLoadingContext::State::BitmapDecoded; + } + + return m_context->bitmap; +} + +void JPGImageDecoderPlugin::set_volatile() +{ + if (m_context->bitmap) + m_context->bitmap->set_volatile(); +} + +bool JPGImageDecoderPlugin::set_nonvolatile() +{ + if (!m_context->bitmap) + return false; + return m_context->bitmap->set_nonvolatile(); +} + +bool JPGImageDecoderPlugin::sniff() +{ + return m_context->data_size > 3 + && m_context->data[0] == 0xFF + && m_context->data[1] == 0xD8 + && m_context->data[2] == 0xFF; +} + +bool JPGImageDecoderPlugin::is_animated() +{ + return false; +} + +size_t JPGImageDecoderPlugin::loop_count() +{ + return 0; +} + +size_t JPGImageDecoderPlugin::frame_count() +{ + return 1; +} + +ImageFrameDescriptor JPGImageDecoderPlugin::frame(size_t i) +{ + if (i > 0) { + return { bitmap(), 0 }; + } + return {}; +} +} diff --git a/Userland/Libraries/LibGfx/JPGLoader.h b/Userland/Libraries/LibGfx/JPGLoader.h new file mode 100644 index 0000000000..67af000882 --- /dev/null +++ b/Userland/Libraries/LibGfx/JPGLoader.h @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2020, The SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Vector.h> +#include <LibGfx/Bitmap.h> +#include <LibGfx/ImageDecoder.h> +#include <LibGfx/Size.h> + +namespace Gfx { + +RefPtr<Gfx::Bitmap> load_jpg(const StringView& path); +RefPtr<Gfx::Bitmap> load_jpg_from_memory(const u8* data, size_t length); + +struct JPGLoadingContext; + +class JPGImageDecoderPlugin : public ImageDecoderPlugin { +public: + virtual ~JPGImageDecoderPlugin() override; + JPGImageDecoderPlugin(const u8*, size_t); + virtual IntSize size() override; + virtual RefPtr<Gfx::Bitmap> bitmap() override; + virtual void set_volatile() override; + [[nodiscard]] virtual bool set_nonvolatile() override; + virtual bool sniff() override; + virtual bool is_animated() override; + virtual size_t loop_count() override; + virtual size_t frame_count() override; + virtual ImageFrameDescriptor frame(size_t i) override; + +private: + OwnPtr<JPGLoadingContext> m_context; +}; +} diff --git a/Userland/Libraries/LibGfx/Matrix.h b/Userland/Libraries/LibGfx/Matrix.h new file mode 100644 index 0000000000..aa9cf5e64f --- /dev/null +++ b/Userland/Libraries/LibGfx/Matrix.h @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2020, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Types.h> +#include <initializer_list> + +namespace Gfx { + +template<size_t N, typename T> +class Matrix { +public: + static constexpr size_t Size = N; + + Matrix() = default; + Matrix(std::initializer_list<T> elements) + { + ASSERT(elements.size() == N * N); + size_t i = 0; + for (auto& element : elements) { + m_elements[i / N][i % N] = element; + ++i; + } + } + + template<typename... Args> + Matrix(Args... args) + : Matrix({ (T)args... }) + { + } + + Matrix(const Matrix& other) + { + __builtin_memcpy(m_elements, other.elements(), sizeof(T) * N * N); + } + + auto elements() const { return m_elements; } + auto elements() { return m_elements; } + + Matrix operator*(const Matrix& other) const + { + Matrix product; + for (int i = 0; i < N; ++i) { + for (int j = 0; j < N; ++j) { + auto& element = product.m_elements[i][j]; + + if constexpr (N == 4) { + element = m_elements[0][j] * other.m_elements[i][0] + + m_elements[1][j] * other.m_elements[i][1] + + m_elements[2][j] * other.m_elements[i][2] + + m_elements[3][j] * other.m_elements[i][3]; + } else if constexpr (N == 3) { + element = m_elements[0][j] * other.m_elements[i][0] + + m_elements[1][j] * other.m_elements[i][1] + + m_elements[2][j] * other.m_elements[i][2]; + } else if constexpr (N == 2) { + element = m_elements[0][j] * other.m_elements[i][0] + + m_elements[1][j] * other.m_elements[i][1]; + } else if constexpr (N == 1) { + element = m_elements[0][j] * other.m_elements[i][0]; + } else { + T value {}; + for (size_t k = 0; k < N; ++k) + value += m_elements[k][j] * other.m_elements[i][k]; + + element = value; + } + } + } + + return product; + } + +private: + T m_elements[N][N]; +}; + +} + +using Gfx::Matrix; diff --git a/Userland/Libraries/LibGfx/Matrix4x4.h b/Userland/Libraries/LibGfx/Matrix4x4.h new file mode 100644 index 0000000000..643ab3ea62 --- /dev/null +++ b/Userland/Libraries/LibGfx/Matrix4x4.h @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2020, Stephan Unverwerth <s.unverwerth@gmx.de> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <LibGfx/Matrix.h> +#include <LibGfx/Vector3.h> +#include <math.h> + +namespace Gfx { + +template<typename T> +class Matrix4x4 final : public Matrix<4, T> { +public: + Matrix4x4() = default; + Matrix4x4(T _11, T _12, T _13, T _14, + T _21, T _22, T _23, T _24, + T _31, T _32, T _33, T _34, + T _41, T _42, T _43, T _44) + : m_elements { + _11, _12, _13, _14, + _21, _22, _23, _24, + _31, _32, _33, _34, + _41, _42, _43, _44 + } + { + } + + auto elements() const { return m_elements; } + auto elements() { return m_elements; } + + Matrix4x4 operator*(const Matrix4x4& other) const + { + Matrix4x4 product; + for (int i = 0; i < 4; ++i) { + for (int j = 0; j < 4; ++j) { + product.m_elements[i][j] = m_elements[0][j] * other.m_elements[i][0] + + m_elements[1][j] * other.m_elements[i][1] + + m_elements[2][j] * other.m_elements[i][2] + + m_elements[3][j] * other.m_elements[i][3]; + } + } + return product; + } + + Vector3<T> transform_point(const Vector3<T>& p) const + { + return Vector3<T>( + p.x() * m_elements[0][0] + p.y() * m_elements[1][0] + p.z() * m_elements[2][0] + m_elements[3][0], + p.x() * m_elements[0][1] + p.y() * m_elements[1][1] + p.z() * m_elements[2][1] + m_elements[3][1], + p.x() * m_elements[0][2] + p.y() * m_elements[1][2] + p.z() * m_elements[2][2] + m_elements[3][2]); + } + + static Matrix4x4 translate(const Vector3<T>& p) + { + return Matrix4x4( + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + p.x(), p.y(), p.z(), 1); + } + + static Matrix4x4 scale(const Vector3<T>& s) + { + return Matrix4x4( + s.x(), 0, 0, 0, + 0, s.y(), 0, 0, + 0, 0, s.z(), 0, + 0, 0, 0, 1); + } + + static Matrix4x4 rotate(const Vector3<T>& axis, T angle) + { + T c = cos(angle); + T s = sin(angle); + T t = 1 - c; + T x = axis.x(); + T y = axis.y(); + T z = axis.z(); + + return Matrix4x4( + t * x * x + c, t * x * y - z * s, t * x * z + y * s, 0, + t * x * y + z * s, t * y * y + c, t * y * z - x * s, 0, + t * x * z - y * s, t * y * z + x * s, t * z * z + c, 0, + 0, 0, 0, 1); + } + +private: + T m_elements[4][4]; +}; + +typedef Matrix4x4<float> FloatMatrix4x4; +typedef Matrix4x4<double> DoubleMatrix4x4; + +} + +using Gfx::DoubleMatrix4x4; +using Gfx::FloatMatrix4x4; +using Gfx::Matrix4x4; diff --git a/Userland/Libraries/LibGfx/Orientation.h b/Userland/Libraries/LibGfx/Orientation.h new file mode 100644 index 0000000000..e83af3f530 --- /dev/null +++ b/Userland/Libraries/LibGfx/Orientation.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +namespace Gfx { + +enum class Orientation { + Horizontal, + Vertical +}; + +} + +using Gfx::Orientation; diff --git a/Userland/Libraries/LibGfx/PBMLoader.cpp b/Userland/Libraries/LibGfx/PBMLoader.cpp new file mode 100644 index 0000000000..4040b09c97 --- /dev/null +++ b/Userland/Libraries/LibGfx/PBMLoader.cpp @@ -0,0 +1,225 @@ +/* + * Copyright (c) 2020, Hüseyin ASLITÜRK <asliturk@hotmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "PBMLoader.h" +#include "PortableImageLoaderCommon.h" +#include "Streamer.h" +#include <AK/Endian.h> +#include <AK/LexicalPath.h> +#include <AK/MappedFile.h> +#include <AK/StringBuilder.h> +#include <string.h> + +namespace Gfx { + +struct PBMLoadingContext { + enum Type { + Unknown, + ASCII, + RAWBITS + }; + + enum State { + NotDecoded = 0, + Error, + MagicNumber, + Width, + Height, + Bitmap, + Decoded + }; + + static constexpr auto ascii_magic_number = '1'; + static constexpr auto binary_magic_number = '4'; + static constexpr auto image_type = "PBM"; + + Type type { Type::Unknown }; + State state { State::NotDecoded }; + const u8* data { nullptr }; + size_t data_size { 0 }; + size_t width { 0 }; + size_t height { 0 }; + RefPtr<Gfx::Bitmap> bitmap; +}; + +static bool read_image_data(PBMLoadingContext& context, Streamer& streamer) +{ + u8 byte; + Vector<Gfx::Color> color_data; + + if (context.type == PBMLoadingContext::ASCII) { + while (streamer.read(byte)) { + if (byte == '0') { + color_data.append(Color::White); + } else if (byte == '1') { + color_data.append(Color::Black); + } + } + } else if (context.type == PBMLoadingContext::RAWBITS) { + size_t color_index = 0; + + while (streamer.read(byte)) { + for (int i = 0; i < 8; i++) { + int val = byte & 0x80; + + if (val == 0) { + color_data.append(Color::White); + } else { + color_data.append(Color::Black); + } + + byte = byte << 1; + color_index++; + + if (color_index % context.width == 0) { + break; + } + } + } + } + + size_t context_size = (u32)context.width * (u32)context.height; + if (context_size != color_data.size()) { + dbgln("Not enough color data in image."); + return false; + } + + if (!create_bitmap(context)) { + return false; + } + + set_pixels(context, color_data); + + context.state = PBMLoadingContext::State::Bitmap; + return true; +} + +RefPtr<Gfx::Bitmap> load_pbm(const StringView& path) +{ + return load<PBMLoadingContext>(path); +} + +RefPtr<Gfx::Bitmap> load_pbm_from_memory(const u8* data, size_t length) +{ + auto bitmap = load_impl<PBMLoadingContext>(data, length); + if (bitmap) + bitmap->set_mmap_name(String::formatted("Gfx::Bitmap [{}] - Decoded PBM: <memory>", bitmap->size())); + return bitmap; +} + +PBMImageDecoderPlugin::PBMImageDecoderPlugin(const u8* data, size_t size) +{ + m_context = make<PBMLoadingContext>(); + m_context->data = data; + m_context->data_size = size; +} + +PBMImageDecoderPlugin::~PBMImageDecoderPlugin() +{ +} + +IntSize PBMImageDecoderPlugin::size() +{ + if (m_context->state == PBMLoadingContext::State::Error) + return {}; + + if (m_context->state < PBMLoadingContext::State::Decoded) { + bool success = decode(*m_context); + if (!success) + return {}; + } + + return { m_context->width, m_context->height }; +} + +RefPtr<Gfx::Bitmap> PBMImageDecoderPlugin::bitmap() +{ + if (m_context->state == PBMLoadingContext::State::Error) + return nullptr; + + if (m_context->state < PBMLoadingContext::State::Decoded) { + bool success = decode(*m_context); + if (!success) + return nullptr; + } + + ASSERT(m_context->bitmap); + return m_context->bitmap; +} + +void PBMImageDecoderPlugin::set_volatile() +{ + if (m_context->bitmap) + m_context->bitmap->set_volatile(); +} + +bool PBMImageDecoderPlugin::set_nonvolatile() +{ + if (!m_context->bitmap) + return false; + + return m_context->bitmap->set_nonvolatile(); +} + +bool PBMImageDecoderPlugin::sniff() +{ + if (m_context->data_size < 2) + return false; + + if (m_context->data[0] == 'P' && m_context->data[1] == '1') + return true; + + if (m_context->data[0] == 'P' && m_context->data[1] == '4') + return true; + + return false; +} + +bool PBMImageDecoderPlugin::is_animated() +{ + return false; +} + +size_t PBMImageDecoderPlugin::loop_count() +{ + return 0; +} + +size_t PBMImageDecoderPlugin::frame_count() +{ + return 1; +} + +ImageFrameDescriptor PBMImageDecoderPlugin::frame(size_t i) +{ + if (i > 0) { + return { bitmap(), 0 }; + } + + return {}; +} + +} diff --git a/Userland/Libraries/LibGfx/PBMLoader.h b/Userland/Libraries/LibGfx/PBMLoader.h new file mode 100644 index 0000000000..93f6e45d77 --- /dev/null +++ b/Userland/Libraries/LibGfx/PBMLoader.h @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2020, Hüseyin ASLITÜRK <asliturk@hotmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <LibGfx/Bitmap.h> +#include <LibGfx/ImageDecoder.h> + +namespace Gfx { + +RefPtr<Gfx::Bitmap> load_pbm(const StringView& path); +RefPtr<Gfx::Bitmap> load_pbm_from_memory(const u8*, size_t); + +struct PBMLoadingContext; + +class PBMImageDecoderPlugin final : public ImageDecoderPlugin { +public: + PBMImageDecoderPlugin(const u8*, size_t); + virtual ~PBMImageDecoderPlugin() override; + + virtual IntSize size() override; + virtual RefPtr<Gfx::Bitmap> bitmap() override; + + virtual void set_volatile() override; + [[nodiscard]] virtual bool set_nonvolatile() override; + + virtual bool sniff() override; + + virtual bool is_animated() override; + virtual size_t loop_count() override; + virtual size_t frame_count() override; + virtual ImageFrameDescriptor frame(size_t i) override; + +private: + OwnPtr<PBMLoadingContext> m_context; +}; + +} diff --git a/Userland/Libraries/LibGfx/PGMLoader.cpp b/Userland/Libraries/LibGfx/PGMLoader.cpp new file mode 100644 index 0000000000..122a698a7b --- /dev/null +++ b/Userland/Libraries/LibGfx/PGMLoader.cpp @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2020, Hüseyin ASLITÜRK <asliturk@hotmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "PGMLoader.h" +#include "PortableImageLoaderCommon.h" +#include "Streamer.h" +#include <AK/Endian.h> +#include <AK/LexicalPath.h> +#include <AK/MappedFile.h> +#include <AK/StringBuilder.h> +#include <string.h> + +namespace Gfx { + +struct PGMLoadingContext { + enum Type { + Unknown, + ASCII, + RAWBITS + }; + + enum State { + NotDecoded = 0, + Error, + MagicNumber, + Width, + Height, + Maxval, + Bitmap, + Decoded + }; + + static constexpr auto ascii_magic_number = '2'; + static constexpr auto binary_magic_number = '5'; + static constexpr auto image_type = "PGM"; + + Type type { Type::Unknown }; + State state { State::NotDecoded }; + const u8* data { nullptr }; + size_t data_size { 0 }; + u16 width { 0 }; + u16 height { 0 }; + u16 max_val { 0 }; + RefPtr<Gfx::Bitmap> bitmap; +}; + +static void set_adjusted_pixels(PGMLoadingContext& context, const AK::Vector<Gfx::Color>& color_data) +{ + size_t index = 0; + for (size_t y = 0; y < context.height; ++y) { + for (size_t x = 0; x < context.width; ++x) { + Color color = color_data.at(index); + if (context.max_val < 255) { + color = adjust_color(context.max_val, color); + } + context.bitmap->set_pixel(x, y, color); + ++index; + } + } +} + +static bool read_image_data(PGMLoadingContext& context, Streamer& streamer) +{ + Vector<Gfx::Color> color_data; + + if (context.type == PGMLoadingContext::ASCII) { + u16 value; + + while (true) { + if (!read_number(streamer, &value)) + break; + + if (!read_white_space(context, streamer)) + break; + + color_data.append({ (u8)value, (u8)value, (u8)value }); + } + } else if (context.type == PGMLoadingContext::RAWBITS) { + u8 pixel; + while (streamer.read(pixel)) { + color_data.append({ pixel, pixel, pixel }); + } + } + + size_t context_size = (u32)context.width * (u32)context.height; + if (context_size != color_data.size()) { + dbgln("Not enough color data in image."); + return false; + } + + if (!create_bitmap(context)) + return false; + + set_adjusted_pixels(context, color_data); + + context.state = PGMLoadingContext::State::Bitmap; + return true; +} + +RefPtr<Gfx::Bitmap> load_pgm(const StringView& path) +{ + return load<PGMLoadingContext>(path); +} + +RefPtr<Gfx::Bitmap> load_pgm_from_memory(const u8* data, size_t length) +{ + auto bitmap = load_impl<PGMLoadingContext>(data, length); + if (bitmap) + bitmap->set_mmap_name(String::formatted("Gfx::Bitmap [{}] - Decoded PGM: <memory>", bitmap->size())); + return bitmap; +} + +PGMImageDecoderPlugin::PGMImageDecoderPlugin(const u8* data, size_t size) +{ + m_context = make<PGMLoadingContext>(); + m_context->data = data; + m_context->data_size = size; +} + +PGMImageDecoderPlugin::~PGMImageDecoderPlugin() +{ +} + +IntSize PGMImageDecoderPlugin::size() +{ + if (m_context->state == PGMLoadingContext::State::Error) + return {}; + + if (m_context->state < PGMLoadingContext::State::Decoded) { + bool success = decode(*m_context); + if (!success) + return {}; + } + + return { m_context->width, m_context->height }; +} + +RefPtr<Gfx::Bitmap> PGMImageDecoderPlugin::bitmap() +{ + if (m_context->state == PGMLoadingContext::State::Error) + return nullptr; + + if (m_context->state < PGMLoadingContext::State::Decoded) { + bool success = decode(*m_context); + if (!success) + return nullptr; + } + + ASSERT(m_context->bitmap); + return m_context->bitmap; +} + +void PGMImageDecoderPlugin::set_volatile() +{ + if (m_context->bitmap) + m_context->bitmap->set_volatile(); +} + +bool PGMImageDecoderPlugin::set_nonvolatile() +{ + if (!m_context->bitmap) + return false; + + return m_context->bitmap->set_nonvolatile(); +} + +bool PGMImageDecoderPlugin::sniff() +{ + if (m_context->data_size < 2) + return false; + + if (m_context->data[0] == 'P' && m_context->data[1] == '2') + return true; + + if (m_context->data[0] == 'P' && m_context->data[1] == '5') + return true; + + return false; +} + +bool PGMImageDecoderPlugin::is_animated() +{ + return false; +} + +size_t PGMImageDecoderPlugin::loop_count() +{ + return 0; +} + +size_t PGMImageDecoderPlugin::frame_count() +{ + return 1; +} + +ImageFrameDescriptor PGMImageDecoderPlugin::frame(size_t i) +{ + if (i > 0) { + return { bitmap(), 0 }; + } + + return {}; +} + +} diff --git a/Userland/Libraries/LibGfx/PGMLoader.h b/Userland/Libraries/LibGfx/PGMLoader.h new file mode 100644 index 0000000000..ae6afcebaf --- /dev/null +++ b/Userland/Libraries/LibGfx/PGMLoader.h @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2020, Hüseyin ASLITÜRK <asliturk@hotmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <LibGfx/Bitmap.h> +#include <LibGfx/ImageDecoder.h> + +namespace Gfx { + +RefPtr<Gfx::Bitmap> load_pgm(const StringView& path); +RefPtr<Gfx::Bitmap> load_pgm_from_memory(const u8*, size_t); + +struct PGMLoadingContext; + +class PGMImageDecoderPlugin final : public ImageDecoderPlugin { +public: + PGMImageDecoderPlugin(const u8*, size_t); + virtual ~PGMImageDecoderPlugin() override; + + virtual IntSize size() override; + virtual RefPtr<Gfx::Bitmap> bitmap() override; + + virtual void set_volatile() override; + [[nodiscard]] virtual bool set_nonvolatile() override; + + virtual bool sniff() override; + + virtual bool is_animated() override; + virtual size_t loop_count() override; + virtual size_t frame_count() override; + virtual ImageFrameDescriptor frame(size_t i) override; + +private: + OwnPtr<PGMLoadingContext> m_context; +}; + +} diff --git a/Userland/Libraries/LibGfx/PNGLoader.cpp b/Userland/Libraries/LibGfx/PNGLoader.cpp new file mode 100644 index 0000000000..48405f8bea --- /dev/null +++ b/Userland/Libraries/LibGfx/PNGLoader.cpp @@ -0,0 +1,1078 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <AK/Endian.h> +#include <AK/LexicalPath.h> +#include <AK/MappedFile.h> +#include <LibCore/puff.h> +#include <LibGfx/PNGLoader.h> +#include <fcntl.h> +#include <math.h> +#include <stdio.h> +#include <string.h> +#include <sys/mman.h> +#include <sys/stat.h> +#include <unistd.h> + +#ifdef __serenity__ +# include <serenity.h> +#endif + +//#define PNG_DEBUG + +namespace Gfx { + +static const u8 png_header[8] = { 0x89, 'P', 'N', 'G', 13, 10, 26, 10 }; + +struct PNG_IHDR { + NetworkOrdered<u32> width; + NetworkOrdered<u32> height; + u8 bit_depth { 0 }; + u8 color_type { 0 }; + u8 compression_method { 0 }; + u8 filter_method { 0 }; + u8 interlace_method { 0 }; +}; + +static_assert(sizeof(PNG_IHDR) == 13); + +struct Scanline { + u8 filter { 0 }; + ReadonlyBytes data {}; +}; + +struct [[gnu::packed]] PaletteEntry { + u8 r; + u8 g; + u8 b; + //u8 a; +}; + +template<typename T> +struct [[gnu::packed]] Tuple { + T gray; + T a; +}; + +template<typename T> +struct [[gnu::packed]] Triplet { + T r; + T g; + T b; +}; + +template<typename T> +struct [[gnu::packed]] Quad { + T r; + T g; + T b; + T a; +}; + +enum PngInterlaceMethod { + Null = 0, + Adam7 = 1 +}; + +struct PNGLoadingContext { + enum State { + NotDecoded = 0, + Error, + HeaderDecoded, + SizeDecoded, + ChunksDecoded, + BitmapDecoded, + }; + State state { State::NotDecoded }; + const u8* data { nullptr }; + size_t data_size { 0 }; + int width { -1 }; + int height { -1 }; + u8 bit_depth { 0 }; + u8 color_type { 0 }; + u8 compression_method { 0 }; + u8 filter_method { 0 }; + u8 interlace_method { 0 }; + u8 channels { 0 }; + bool has_seen_zlib_header { false }; + bool has_alpha() const { return color_type & 4 || palette_transparency_data.size() > 0; } + Vector<Scanline> scanlines; + RefPtr<Gfx::Bitmap> bitmap; + u8* decompression_buffer { nullptr }; + size_t decompression_buffer_size { 0 }; + Vector<u8> compressed_data; + Vector<PaletteEntry> palette_data; + Vector<u8> palette_transparency_data; + + Checked<int> compute_row_size_for_width(int width) + { + Checked<int> row_size = width; + row_size *= channels; + row_size *= bit_depth; + row_size += 7; + row_size /= 8; + if (row_size.has_overflow()) { + dbgln("PNG too large, integer overflow while computing row size"); + state = State::Error; + } + return row_size; + } +}; + +class Streamer { +public: + Streamer(const u8* data, size_t size) + : m_data_ptr(data) + , m_size_remaining(size) + { + } + + template<typename T> + bool read(T& value) + { + if (m_size_remaining < sizeof(T)) + return false; + value = *((const NetworkOrdered<T>*)m_data_ptr); + m_data_ptr += sizeof(T); + m_size_remaining -= sizeof(T); + return true; + } + + bool read_bytes(u8* buffer, size_t count) + { + if (m_size_remaining < count) + return false; + memcpy(buffer, m_data_ptr, count); + m_data_ptr += count; + m_size_remaining -= count; + return true; + } + + bool wrap_bytes(ReadonlyBytes& buffer, size_t count) + { + if (m_size_remaining < count) + return false; + buffer = ReadonlyBytes { m_data_ptr, count }; + m_data_ptr += count; + m_size_remaining -= count; + return true; + } + + bool at_end() const { return !m_size_remaining; } + +private: + const u8* m_data_ptr { nullptr }; + size_t m_size_remaining { 0 }; +}; + +static RefPtr<Gfx::Bitmap> load_png_impl(const u8*, size_t); +static bool process_chunk(Streamer&, PNGLoadingContext& context); + +RefPtr<Gfx::Bitmap> load_png(const StringView& path) +{ + auto file_or_error = MappedFile::map(path); + if (file_or_error.is_error()) + return nullptr; + auto bitmap = load_png_impl((const u8*)file_or_error.value()->data(), file_or_error.value()->size()); + if (bitmap) + bitmap->set_mmap_name(String::formatted("Gfx::Bitmap [{}] - Decoded PNG: {}", bitmap->size(), LexicalPath::canonicalized_path(path))); + return bitmap; +} + +RefPtr<Gfx::Bitmap> load_png_from_memory(const u8* data, size_t length) +{ + auto bitmap = load_png_impl(data, length); + if (bitmap) + bitmap->set_mmap_name(String::formatted("Gfx::Bitmap [{}] - Decoded PNG: <memory>", bitmap->size())); + return bitmap; +} + +ALWAYS_INLINE static u8 paeth_predictor(int a, int b, int c) +{ + int p = a + b - c; + int pa = abs(p - a); + int pb = abs(p - b); + int pc = abs(p - c); + if (pa <= pb && pa <= pc) + return a; + if (pb <= pc) + return b; + return c; +} + +union [[gnu::packed]] Pixel { + RGBA32 rgba { 0 }; + u8 v[4]; + struct { + u8 r; + u8 g; + u8 b; + u8 a; + }; +}; +static_assert(sizeof(Pixel) == 4); + +template<bool has_alpha, u8 filter_type> +ALWAYS_INLINE static void unfilter_impl(Gfx::Bitmap& bitmap, int y, const void* dummy_scanline_data) +{ + auto* dummy_scanline = (const Pixel*)dummy_scanline_data; + if constexpr (filter_type == 0) { + auto* pixels = (Pixel*)bitmap.scanline(y); + for (int i = 0; i < bitmap.width(); ++i) { + auto& x = pixels[i]; + swap(x.r, x.b); + } + } + + if constexpr (filter_type == 1) { + auto* pixels = (Pixel*)bitmap.scanline(y); + swap(pixels[0].r, pixels[0].b); + for (int i = 1; i < bitmap.width(); ++i) { + auto& x = pixels[i]; + swap(x.r, x.b); + auto& a = (const Pixel&)pixels[i - 1]; + x.v[0] += a.v[0]; + x.v[1] += a.v[1]; + x.v[2] += a.v[2]; + if constexpr (has_alpha) + x.v[3] += a.v[3]; + } + return; + } + if constexpr (filter_type == 2) { + auto* pixels = (Pixel*)bitmap.scanline(y); + auto* pixels_y_minus_1 = y == 0 ? dummy_scanline : (const Pixel*)bitmap.scanline(y - 1); + for (int i = 0; i < bitmap.width(); ++i) { + auto& x = pixels[i]; + swap(x.r, x.b); + const Pixel& b = pixels_y_minus_1[i]; + x.v[0] += b.v[0]; + x.v[1] += b.v[1]; + x.v[2] += b.v[2]; + if constexpr (has_alpha) + x.v[3] += b.v[3]; + } + return; + } + if constexpr (filter_type == 3) { + auto* pixels = (Pixel*)bitmap.scanline(y); + auto* pixels_y_minus_1 = y == 0 ? dummy_scanline : (const Pixel*)bitmap.scanline(y - 1); + for (int i = 0; i < bitmap.width(); ++i) { + auto& x = pixels[i]; + swap(x.r, x.b); + Pixel a; + if (i != 0) + a = pixels[i - 1]; + const Pixel& b = pixels_y_minus_1[i]; + x.v[0] = x.v[0] + ((a.v[0] + b.v[0]) / 2); + x.v[1] = x.v[1] + ((a.v[1] + b.v[1]) / 2); + x.v[2] = x.v[2] + ((a.v[2] + b.v[2]) / 2); + if constexpr (has_alpha) + x.v[3] = x.v[3] + ((a.v[3] + b.v[3]) / 2); + } + return; + } + if constexpr (filter_type == 4) { + auto* pixels = (Pixel*)bitmap.scanline(y); + auto* pixels_y_minus_1 = y == 0 ? dummy_scanline : (Pixel*)bitmap.scanline(y - 1); + for (int i = 0; i < bitmap.width(); ++i) { + auto& x = pixels[i]; + swap(x.r, x.b); + Pixel a; + const Pixel& b = pixels_y_minus_1[i]; + Pixel c; + if (i != 0) { + a = pixels[i - 1]; + c = pixels_y_minus_1[i - 1]; + } + x.v[0] += paeth_predictor(a.v[0], b.v[0], c.v[0]); + x.v[1] += paeth_predictor(a.v[1], b.v[1], c.v[1]); + x.v[2] += paeth_predictor(a.v[2], b.v[2], c.v[2]); + if constexpr (has_alpha) + x.v[3] += paeth_predictor(a.v[3], b.v[3], c.v[3]); + } + } +} + +template<typename T> +ALWAYS_INLINE static void unpack_grayscale_without_alpha(PNGLoadingContext& context) +{ + for (int y = 0; y < context.height; ++y) { + auto* gray_values = reinterpret_cast<const T*>(context.scanlines[y].data.data()); + for (int i = 0; i < context.width; ++i) { + auto& pixel = (Pixel&)context.bitmap->scanline(y)[i]; + pixel.r = gray_values[i]; + pixel.g = gray_values[i]; + pixel.b = gray_values[i]; + pixel.a = 0xff; + } + } +} + +template<typename T> +ALWAYS_INLINE static void unpack_grayscale_with_alpha(PNGLoadingContext& context) +{ + for (int y = 0; y < context.height; ++y) { + auto* tuples = reinterpret_cast<const Tuple<T>*>(context.scanlines[y].data.data()); + for (int i = 0; i < context.width; ++i) { + auto& pixel = (Pixel&)context.bitmap->scanline(y)[i]; + pixel.r = tuples[i].gray; + pixel.g = tuples[i].gray; + pixel.b = tuples[i].gray; + pixel.a = tuples[i].a; + } + } +} + +template<typename T> +ALWAYS_INLINE static void unpack_triplets_without_alpha(PNGLoadingContext& context) +{ + for (int y = 0; y < context.height; ++y) { + auto* triplets = reinterpret_cast<const Triplet<T>*>(context.scanlines[y].data.data()); + for (int i = 0; i < context.width; ++i) { + auto& pixel = (Pixel&)context.bitmap->scanline(y)[i]; + pixel.r = triplets[i].r; + pixel.g = triplets[i].g; + pixel.b = triplets[i].b; + pixel.a = 0xff; + } + } +} + +NEVER_INLINE FLATTEN static bool unfilter(PNGLoadingContext& context) +{ + // First unpack the scanlines to RGBA: + switch (context.color_type) { + case 0: + if (context.bit_depth == 8) { + unpack_grayscale_without_alpha<u8>(context); + } else if (context.bit_depth == 16) { + unpack_grayscale_without_alpha<u16>(context); + } else if (context.bit_depth == 1 || context.bit_depth == 2 || context.bit_depth == 4) { + auto bit_depth_squared = context.bit_depth * context.bit_depth; + auto pixels_per_byte = 8 / context.bit_depth; + auto mask = (1 << context.bit_depth) - 1; + for (int y = 0; y < context.height; ++y) { + auto* gray_values = context.scanlines[y].data.data(); + for (int x = 0; x < context.width; ++x) { + auto bit_offset = (8 - context.bit_depth) - (context.bit_depth * (x % pixels_per_byte)); + auto value = (gray_values[x / pixels_per_byte] >> bit_offset) & mask; + auto& pixel = (Pixel&)context.bitmap->scanline(y)[x]; + pixel.r = value * (0xff / bit_depth_squared); + pixel.g = value * (0xff / bit_depth_squared); + pixel.b = value * (0xff / bit_depth_squared); + pixel.a = 0xff; + } + } + } else { + ASSERT_NOT_REACHED(); + } + break; + case 4: + if (context.bit_depth == 8) { + unpack_grayscale_with_alpha<u8>(context); + } else if (context.bit_depth == 16) { + unpack_grayscale_with_alpha<u16>(context); + } else { + ASSERT_NOT_REACHED(); + } + break; + case 2: + if (context.bit_depth == 8) { + unpack_triplets_without_alpha<u8>(context); + } else if (context.bit_depth == 16) { + unpack_triplets_without_alpha<u16>(context); + } else { + ASSERT_NOT_REACHED(); + } + break; + case 6: + if (context.bit_depth == 8) { + for (int y = 0; y < context.height; ++y) { + memcpy(context.bitmap->scanline(y), context.scanlines[y].data.data(), context.scanlines[y].data.size()); + } + } else if (context.bit_depth == 16) { + for (int y = 0; y < context.height; ++y) { + auto* triplets = reinterpret_cast<const Quad<u16>*>(context.scanlines[y].data.data()); + for (int i = 0; i < context.width; ++i) { + auto& pixel = (Pixel&)context.bitmap->scanline(y)[i]; + pixel.r = triplets[i].r & 0xFF; + pixel.g = triplets[i].g & 0xFF; + pixel.b = triplets[i].b & 0xFF; + pixel.a = triplets[i].a & 0xFF; + } + } + } else { + ASSERT_NOT_REACHED(); + } + break; + case 3: + if (context.bit_depth == 8) { + for (int y = 0; y < context.height; ++y) { + auto* palette_index = context.scanlines[y].data.data(); + for (int i = 0; i < context.width; ++i) { + auto& pixel = (Pixel&)context.bitmap->scanline(y)[i]; + if (palette_index[i] >= context.palette_data.size()) + return false; + auto& color = context.palette_data.at((int)palette_index[i]); + auto transparency = context.palette_transparency_data.size() >= palette_index[i] + 1u + ? context.palette_transparency_data.data()[palette_index[i]] + : 0xff; + pixel.r = color.r; + pixel.g = color.g; + pixel.b = color.b; + pixel.a = transparency; + } + } + } else if (context.bit_depth == 1 || context.bit_depth == 2 || context.bit_depth == 4) { + auto pixels_per_byte = 8 / context.bit_depth; + auto mask = (1 << context.bit_depth) - 1; + for (int y = 0; y < context.height; ++y) { + auto* palette_indexes = context.scanlines[y].data.data(); + for (int i = 0; i < context.width; ++i) { + auto bit_offset = (8 - context.bit_depth) - (context.bit_depth * (i % pixels_per_byte)); + auto palette_index = (palette_indexes[i / pixels_per_byte] >> bit_offset) & mask; + auto& pixel = (Pixel&)context.bitmap->scanline(y)[i]; + if ((size_t)palette_index >= context.palette_data.size()) + return false; + auto& color = context.palette_data.at(palette_index); + auto transparency = context.palette_transparency_data.size() >= palette_index + 1u + ? context.palette_transparency_data.data()[palette_index] + : 0xff; + pixel.r = color.r; + pixel.g = color.g; + pixel.b = color.b; + pixel.a = transparency; + } + } + } else { + ASSERT_NOT_REACHED(); + } + break; + default: + ASSERT_NOT_REACHED(); + break; + } + + u8 dummy_scanline[context.width * sizeof(RGBA32)]; + + for (int y = 0; y < context.height; ++y) { + auto filter = context.scanlines[y].filter; + if (filter == 0) { + if (context.has_alpha()) + unfilter_impl<true, 0>(*context.bitmap, y, dummy_scanline); + else + unfilter_impl<false, 0>(*context.bitmap, y, dummy_scanline); + continue; + } + if (filter == 1) { + if (context.has_alpha()) + unfilter_impl<true, 1>(*context.bitmap, y, dummy_scanline); + else + unfilter_impl<false, 1>(*context.bitmap, y, dummy_scanline); + continue; + } + if (filter == 2) { + if (context.has_alpha()) + unfilter_impl<true, 2>(*context.bitmap, y, dummy_scanline); + else + unfilter_impl<false, 2>(*context.bitmap, y, dummy_scanline); + continue; + } + if (filter == 3) { + if (context.has_alpha()) + unfilter_impl<true, 3>(*context.bitmap, y, dummy_scanline); + else + unfilter_impl<false, 3>(*context.bitmap, y, dummy_scanline); + continue; + } + if (filter == 4) { + if (context.has_alpha()) + unfilter_impl<true, 4>(*context.bitmap, y, dummy_scanline); + else + unfilter_impl<false, 4>(*context.bitmap, y, dummy_scanline); + continue; + } + } + + return true; +} + +static bool decode_png_header(PNGLoadingContext& context) +{ + if (context.state >= PNGLoadingContext::HeaderDecoded) + return true; + + if (!context.data || context.data_size < sizeof(png_header)) { +#ifdef PNG_DEBUG + dbgln("Missing PNG header"); +#endif + context.state = PNGLoadingContext::State::Error; + return false; + } + + if (memcmp(context.data, png_header, sizeof(png_header)) != 0) { +#ifdef PNG_DEBUG + dbgln("Invalid PNG header"); +#endif + context.state = PNGLoadingContext::State::Error; + return false; + } + + context.state = PNGLoadingContext::HeaderDecoded; + return true; +} + +static bool decode_png_size(PNGLoadingContext& context) +{ + if (context.state >= PNGLoadingContext::SizeDecoded) + return true; + + if (context.state < PNGLoadingContext::HeaderDecoded) { + if (!decode_png_header(context)) + return false; + } + + const u8* data_ptr = context.data + sizeof(png_header); + size_t data_remaining = context.data_size - sizeof(png_header); + + Streamer streamer(data_ptr, data_remaining); + while (!streamer.at_end()) { + if (!process_chunk(streamer, context)) { + context.state = PNGLoadingContext::State::Error; + return false; + } + if (context.width && context.height) { + context.state = PNGLoadingContext::State::SizeDecoded; + return true; + } + } + + return false; +} + +static bool decode_png_chunks(PNGLoadingContext& context) +{ + if (context.state >= PNGLoadingContext::State::ChunksDecoded) + return true; + + if (context.state < PNGLoadingContext::HeaderDecoded) { + if (!decode_png_header(context)) + return false; + } + + const u8* data_ptr = context.data + sizeof(png_header); + int data_remaining = context.data_size - sizeof(png_header); + + context.compressed_data.ensure_capacity(context.data_size); + + Streamer streamer(data_ptr, data_remaining); + while (!streamer.at_end()) { + if (!process_chunk(streamer, context)) { + context.state = PNGLoadingContext::State::Error; + return false; + } + } + + context.state = PNGLoadingContext::State::ChunksDecoded; + return true; +} + +static bool decode_png_bitmap_simple(PNGLoadingContext& context) +{ + Streamer streamer(context.decompression_buffer, context.decompression_buffer_size); + + for (int y = 0; y < context.height; ++y) { + u8 filter; + if (!streamer.read(filter)) { + context.state = PNGLoadingContext::State::Error; + return false; + } + + if (filter > 4) { +#ifdef PNG_DEBUG + dbg() << "Invalid PNG filter: " << filter; +#endif + context.state = PNGLoadingContext::State::Error; + return false; + } + + context.scanlines.append({ filter }); + auto& scanline_buffer = context.scanlines.last().data; + auto row_size = context.compute_row_size_for_width(context.width); + if (row_size.has_overflow()) + return false; + + if (!streamer.wrap_bytes(scanline_buffer, row_size.value())) { + context.state = PNGLoadingContext::State::Error; + return false; + } + } + + context.bitmap = Bitmap::create_purgeable(context.has_alpha() ? BitmapFormat::RGBA32 : BitmapFormat::RGB32, { context.width, context.height }); + + if (!context.bitmap) { + context.state = PNGLoadingContext::State::Error; + return false; + } + + return unfilter(context); +} + +static int adam7_height(PNGLoadingContext& context, int pass) +{ + switch (pass) { + case 1: + return (context.height + 7) / 8; + case 2: + return (context.height + 7) / 8; + case 3: + return (context.height + 3) / 8; + case 4: + return (context.height + 3) / 4; + case 5: + return (context.height + 1) / 4; + case 6: + return (context.height + 1) / 2; + case 7: + return context.height / 2; + default: + ASSERT_NOT_REACHED(); + } +} + +static int adam7_width(PNGLoadingContext& context, int pass) +{ + switch (pass) { + case 1: + return (context.width + 7) / 8; + case 2: + return (context.width + 3) / 8; + case 3: + return (context.width + 3) / 4; + case 4: + return (context.width + 1) / 4; + case 5: + return (context.width + 1) / 2; + case 6: + return context.width / 2; + case 7: + return context.width; + default: + ASSERT_NOT_REACHED(); + } +} + +// Index 0 unused (non-interlaced case) +static int adam7_starty[8] = { 0, 0, 0, 4, 0, 2, 0, 1 }; +static int adam7_startx[8] = { 0, 0, 4, 0, 2, 0, 1, 0 }; +static int adam7_stepy[8] = { 1, 8, 8, 8, 4, 4, 2, 2 }; +static int adam7_stepx[8] = { 1, 8, 8, 4, 4, 2, 2, 1 }; + +static bool decode_adam7_pass(PNGLoadingContext& context, Streamer& streamer, int pass) +{ + PNGLoadingContext subimage_context; + subimage_context.width = adam7_width(context, pass); + subimage_context.height = adam7_height(context, pass); + subimage_context.channels = context.channels; + subimage_context.color_type = context.color_type; + subimage_context.palette_data = context.palette_data; + subimage_context.palette_transparency_data = context.palette_transparency_data; + subimage_context.bit_depth = context.bit_depth; + subimage_context.filter_method = context.filter_method; + + // For small images, some passes might be empty + if (!subimage_context.width || !subimage_context.height) + return true; + + subimage_context.scanlines.clear_with_capacity(); + for (int y = 0; y < subimage_context.height; ++y) { + u8 filter; + if (!streamer.read(filter)) { + context.state = PNGLoadingContext::State::Error; + return false; + } + + if (filter > 4) { +#ifdef PNG_DEBUG + dbg() << "Invalid PNG filter: " << filter; +#endif + context.state = PNGLoadingContext::State::Error; + return false; + } + + subimage_context.scanlines.append({ filter }); + auto& scanline_buffer = subimage_context.scanlines.last().data; + + auto row_size = context.compute_row_size_for_width(subimage_context.width); + if (row_size.has_overflow()) + return false; + if (!streamer.wrap_bytes(scanline_buffer, row_size.value())) { + context.state = PNGLoadingContext::State::Error; + return false; + } + } + + subimage_context.bitmap = Bitmap::create(context.bitmap->format(), { subimage_context.width, subimage_context.height }); + if (!unfilter(subimage_context)) { + subimage_context.bitmap = nullptr; + return false; + } + + // Copy the subimage data into the main image according to the pass pattern + for (int y = 0, dy = adam7_starty[pass]; y < subimage_context.height && dy < context.height; ++y, dy += adam7_stepy[pass]) { + for (int x = 0, dx = adam7_startx[pass]; x < subimage_context.width && dy < context.width; ++x, dx += adam7_stepx[pass]) { + context.bitmap->set_pixel(dx, dy, subimage_context.bitmap->get_pixel(x, y)); + } + } + return true; +} + +static bool decode_png_adam7(PNGLoadingContext& context) +{ + Streamer streamer(context.decompression_buffer, context.decompression_buffer_size); + context.bitmap = Bitmap::create_purgeable(context.has_alpha() ? BitmapFormat::RGBA32 : BitmapFormat::RGB32, { context.width, context.height }); + if (!context.bitmap) + return false; + + for (int pass = 1; pass <= 7; ++pass) { + if (!decode_adam7_pass(context, streamer, pass)) + return false; + } + return true; +} + +static bool decode_png_bitmap(PNGLoadingContext& context) +{ + if (context.state < PNGLoadingContext::State::ChunksDecoded) { + if (!decode_png_chunks(context)) + return false; + } + + if (context.state >= PNGLoadingContext::State::BitmapDecoded) + return true; + + if (context.width == -1 || context.height == -1) + return false; // Didn't see an IHDR chunk. + + if (context.color_type == 3 && context.palette_data.is_empty()) + return false; // Didn't see a PLTE chunk for a palettized image, or it was empty. + + unsigned long srclen = context.compressed_data.size() - 6; + unsigned long destlen = 0; + int ret = puff(nullptr, &destlen, context.compressed_data.data() + 2, &srclen); + if (ret != 0) { + context.state = PNGLoadingContext::State::Error; + return false; + } + context.decompression_buffer_size = destlen; +#ifdef __serenity__ + context.decompression_buffer = (u8*)mmap_with_name(nullptr, context.decompression_buffer_size, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, 0, 0, "PNG decompression buffer"); +#else + context.decompression_buffer = (u8*)mmap(nullptr, context.decompression_buffer_size, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, 0, 0); +#endif + + ret = puff(context.decompression_buffer, &destlen, context.compressed_data.data() + 2, &srclen); + if (ret != 0) { + context.state = PNGLoadingContext::State::Error; + return false; + } + context.compressed_data.clear(); + + context.scanlines.ensure_capacity(context.height); + switch (context.interlace_method) { + case PngInterlaceMethod::Null: + if (!decode_png_bitmap_simple(context)) + return false; + break; + case PngInterlaceMethod::Adam7: + if (!decode_png_adam7(context)) + return false; + break; + default: + ASSERT_NOT_REACHED(); + } + + munmap(context.decompression_buffer, context.decompression_buffer_size); + context.decompression_buffer = nullptr; + context.decompression_buffer_size = 0; + + context.state = PNGLoadingContext::State::BitmapDecoded; + return true; +} + +static RefPtr<Gfx::Bitmap> load_png_impl(const u8* data, size_t data_size) +{ + PNGLoadingContext context; + context.data = data; + context.data_size = data_size; + + if (!decode_png_chunks(context)) + return nullptr; + + if (!decode_png_bitmap(context)) + return nullptr; + + return context.bitmap; +} + +static bool is_valid_compression_method(u8 compression_method) +{ + return compression_method == 0; +} + +static bool is_valid_filter_method(u8 filter_method) +{ + return filter_method <= 4; +} + +static bool process_IHDR(ReadonlyBytes data, PNGLoadingContext& context) +{ + if (data.size() < (int)sizeof(PNG_IHDR)) + return false; + auto& ihdr = *(const PNG_IHDR*)data.data(); + + if (ihdr.width > maximum_width_for_decoded_images || ihdr.height > maximum_height_for_decoded_images) { + dbgln("This PNG is too large for comfort: {}x{}", (u32)ihdr.width, (u32)ihdr.height); + return false; + } + + if (!is_valid_compression_method(ihdr.compression_method)) { + dbgln("PNG has invalid compression method {}", ihdr.compression_method); + return false; + } + + if (!is_valid_filter_method(ihdr.filter_method)) { + dbgln("PNG has invalid filter method {}", ihdr.filter_method); + return false; + } + + context.width = ihdr.width; + context.height = ihdr.height; + context.bit_depth = ihdr.bit_depth; + context.color_type = ihdr.color_type; + context.compression_method = ihdr.compression_method; + context.filter_method = ihdr.filter_method; + context.interlace_method = ihdr.interlace_method; + +#ifdef PNG_DEBUG + printf("PNG: %dx%d (%d bpp)\n", context.width, context.height, context.bit_depth); + printf(" Color type: %d\n", context.color_type); + printf("Compress Method: %d\n", context.compression_method); + printf(" Filter Method: %d\n", context.filter_method); + printf(" Interlace type: %d\n", context.interlace_method); +#endif + + if (context.interlace_method != PngInterlaceMethod::Null && context.interlace_method != PngInterlaceMethod::Adam7) { +#ifdef PNG_DEBUG + dbgln("PNGLoader::process_IHDR: unknown interlace method: {}", context.interlace_method); +#endif + return false; + } + + switch (context.color_type) { + case 0: // Each pixel is a grayscale sample. + if (context.bit_depth != 1 && context.bit_depth != 2 && context.bit_depth != 4 && context.bit_depth != 8 && context.bit_depth != 16) + return false; + context.channels = 1; + break; + case 4: // Each pixel is a grayscale sample, followed by an alpha sample. + if (context.bit_depth != 8 && context.bit_depth != 16) + return false; + context.channels = 2; + break; + case 2: // Each pixel is an RGB sample + if (context.bit_depth != 8 && context.bit_depth != 16) + return false; + context.channels = 3; + break; + case 3: // Each pixel is a palette index; a PLTE chunk must appear. + if (context.bit_depth != 1 && context.bit_depth != 2 && context.bit_depth != 4 && context.bit_depth != 8) + return false; + context.channels = 1; + break; + case 6: // Each pixel is an RGB sample, followed by an alpha sample. + if (context.bit_depth != 8 && context.bit_depth != 16) + return false; + context.channels = 4; + break; + default: + return false; + } + return true; +} + +static bool process_IDAT(ReadonlyBytes data, PNGLoadingContext& context) +{ + context.compressed_data.append(data.data(), data.size()); + return true; +} + +static bool process_PLTE(ReadonlyBytes data, PNGLoadingContext& context) +{ + context.palette_data.append((const PaletteEntry*)data.data(), data.size() / 3); + return true; +} + +static bool process_tRNS(ReadonlyBytes data, PNGLoadingContext& context) +{ + switch (context.color_type) { + case 3: + context.palette_transparency_data.append(data.data(), data.size()); + break; + } + return true; +} + +static bool process_chunk(Streamer& streamer, PNGLoadingContext& context) +{ + u32 chunk_size; + if (!streamer.read(chunk_size)) { +#ifdef PNG_DEBUG + printf("Bail at chunk_size\n"); +#endif + return false; + } + u8 chunk_type[5]; + chunk_type[4] = '\0'; + if (!streamer.read_bytes(chunk_type, 4)) { +#ifdef PNG_DEBUG + printf("Bail at chunk_type\n"); +#endif + return false; + } + ReadonlyBytes chunk_data; + if (!streamer.wrap_bytes(chunk_data, chunk_size)) { +#ifdef PNG_DEBUG + printf("Bail at chunk_data\n"); +#endif + return false; + } + u32 chunk_crc; + if (!streamer.read(chunk_crc)) { +#ifdef PNG_DEBUG + printf("Bail at chunk_crc\n"); +#endif + return false; + } +#ifdef PNG_DEBUG + printf("Chunk type: '%s', size: %u, crc: %x\n", chunk_type, chunk_size, chunk_crc); +#endif + + if (!strcmp((const char*)chunk_type, "IHDR")) + return process_IHDR(chunk_data, context); + if (!strcmp((const char*)chunk_type, "IDAT")) + return process_IDAT(chunk_data, context); + if (!strcmp((const char*)chunk_type, "PLTE")) + return process_PLTE(chunk_data, context); + if (!strcmp((const char*)chunk_type, "tRNS")) + return process_tRNS(chunk_data, context); + return true; +} + +PNGImageDecoderPlugin::PNGImageDecoderPlugin(const u8* data, size_t size) +{ + m_context = make<PNGLoadingContext>(); + m_context->data = data; + m_context->data_size = size; +} + +PNGImageDecoderPlugin::~PNGImageDecoderPlugin() +{ +} + +IntSize PNGImageDecoderPlugin::size() +{ + if (m_context->state == PNGLoadingContext::State::Error) + return {}; + + if (m_context->state < PNGLoadingContext::State::SizeDecoded) { + bool success = decode_png_size(*m_context); + if (!success) + return {}; + } + + return { m_context->width, m_context->height }; +} + +RefPtr<Gfx::Bitmap> PNGImageDecoderPlugin::bitmap() +{ + if (m_context->state == PNGLoadingContext::State::Error) + return nullptr; + + if (m_context->state < PNGLoadingContext::State::BitmapDecoded) { + // NOTE: This forces the chunk decoding to happen. + bool success = decode_png_bitmap(*m_context); + if (!success) + return nullptr; + } + + ASSERT(m_context->bitmap); + return m_context->bitmap; +} + +void PNGImageDecoderPlugin::set_volatile() +{ + if (m_context->bitmap) + m_context->bitmap->set_volatile(); +} + +bool PNGImageDecoderPlugin::set_nonvolatile() +{ + if (!m_context->bitmap) + return false; + return m_context->bitmap->set_nonvolatile(); +} + +bool PNGImageDecoderPlugin::sniff() +{ + return decode_png_header(*m_context); +} + +bool PNGImageDecoderPlugin::is_animated() +{ + return false; +} + +size_t PNGImageDecoderPlugin::loop_count() +{ + return 0; +} + +size_t PNGImageDecoderPlugin::frame_count() +{ + return 1; +} + +ImageFrameDescriptor PNGImageDecoderPlugin::frame(size_t i) +{ + if (i > 0) { + return { bitmap(), 0 }; + } + return {}; +} + +} diff --git a/Userland/Libraries/LibGfx/PNGLoader.h b/Userland/Libraries/LibGfx/PNGLoader.h new file mode 100644 index 0000000000..2432f231e0 --- /dev/null +++ b/Userland/Libraries/LibGfx/PNGLoader.h @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <LibGfx/Bitmap.h> +#include <LibGfx/ImageDecoder.h> + +namespace Gfx { + +RefPtr<Gfx::Bitmap> load_png(const StringView& path); +RefPtr<Gfx::Bitmap> load_png_from_memory(const u8*, size_t); + +struct PNGLoadingContext; + +class PNGImageDecoderPlugin final : public ImageDecoderPlugin { +public: + virtual ~PNGImageDecoderPlugin() override; + PNGImageDecoderPlugin(const u8*, size_t); + + virtual IntSize size() override; + virtual RefPtr<Gfx::Bitmap> bitmap() override; + virtual void set_volatile() override; + [[nodiscard]] virtual bool set_nonvolatile() override; + virtual bool sniff() override; + virtual bool is_animated() override; + virtual size_t loop_count() override; + virtual size_t frame_count() override; + virtual ImageFrameDescriptor frame(size_t i) override; + +private: + OwnPtr<PNGLoadingContext> m_context; +}; + +} diff --git a/Userland/Libraries/LibGfx/PPMLoader.cpp b/Userland/Libraries/LibGfx/PPMLoader.cpp new file mode 100644 index 0000000000..80d99d8966 --- /dev/null +++ b/Userland/Libraries/LibGfx/PPMLoader.cpp @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2020, Hüseyin ASLITÜRK <asliturk@hotmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "PPMLoader.h" +#include "PortableImageLoaderCommon.h" +#include "Streamer.h" +#include <AK/Endian.h> +#include <AK/LexicalPath.h> +#include <AK/MappedFile.h> +#include <AK/ScopeGuard.h> +#include <AK/StringBuilder.h> +#include <string.h> + +namespace Gfx { + +struct PPMLoadingContext { + enum Type { + Unknown, + ASCII, + RAWBITS + }; + + enum State { + NotDecoded = 0, + Error, + MagicNumber, + Width, + Height, + Maxval, + Bitmap, + Decoded + }; + + static constexpr auto ascii_magic_number = '3'; + static constexpr auto binary_magic_number = '6'; + static constexpr auto image_type = "PPM"; + + Type type { Type::Unknown }; + State state { State::NotDecoded }; + const u8* data { nullptr }; + size_t data_size { 0 }; + u16 width { 0 }; + u16 height { 0 }; + u16 max_val { 0 }; + RefPtr<Gfx::Bitmap> bitmap; +}; + +static bool read_image_data(PPMLoadingContext& context, Streamer& streamer) +{ + Vector<Gfx::Color> color_data; + color_data.ensure_capacity(context.width * context.height); + + if (context.type == PPMLoadingContext::ASCII) { + u16 red; + u16 green; + u16 blue; + + while (true) { + if (!read_number(streamer, &red)) + break; + + if (!read_white_space(context, streamer)) + break; + + if (!read_number(streamer, &green)) + break; + + if (!read_white_space(context, streamer)) + break; + + if (!read_number(streamer, &blue)) + break; + + if (!read_white_space(context, streamer)) + break; + + Color color { (u8)red, (u8)green, (u8)blue }; + if (context.max_val < 255) + color = adjust_color(context.max_val, color); + color_data.append(color); + } + } else if (context.type == PPMLoadingContext::RAWBITS) { + u8 pixel[3]; + while (streamer.read_bytes(pixel, 3)) { + color_data.append({ pixel[0], pixel[1], pixel[2] }); + } + } + + if (context.width * context.height != color_data.size()) + return false; + + if (!create_bitmap(context)) { + return false; + } + + set_pixels(context, color_data); + + context.state = PPMLoadingContext::State::Bitmap; + return true; +} + +RefPtr<Gfx::Bitmap> load_ppm(const StringView& path) +{ + return load<PPMLoadingContext>(path); +} + +RefPtr<Gfx::Bitmap> load_ppm_from_memory(const u8* data, size_t length) +{ + auto bitmap = load_impl<PPMLoadingContext>(data, length); + if (bitmap) + bitmap->set_mmap_name(String::formatted("Gfx::Bitmap [{}] - Decoded PPM: <memory>", bitmap->size())); + return bitmap; +} + +PPMImageDecoderPlugin::PPMImageDecoderPlugin(const u8* data, size_t size) +{ + m_context = make<PPMLoadingContext>(); + m_context->data = data; + m_context->data_size = size; +} + +PPMImageDecoderPlugin::~PPMImageDecoderPlugin() +{ +} + +IntSize PPMImageDecoderPlugin::size() +{ + if (m_context->state == PPMLoadingContext::State::Error) + return {}; + + if (m_context->state < PPMLoadingContext::State::Decoded) { + bool success = decode(*m_context); + if (!success) + return {}; + } + + return { m_context->width, m_context->height }; +} + +RefPtr<Gfx::Bitmap> PPMImageDecoderPlugin::bitmap() +{ + if (m_context->state == PPMLoadingContext::State::Error) + return nullptr; + + if (m_context->state < PPMLoadingContext::State::Decoded) { + bool success = decode(*m_context); + if (!success) + return nullptr; + } + + ASSERT(m_context->bitmap); + return m_context->bitmap; +} + +void PPMImageDecoderPlugin::set_volatile() +{ + if (m_context->bitmap) + m_context->bitmap->set_volatile(); +} + +bool PPMImageDecoderPlugin::set_nonvolatile() +{ + if (!m_context->bitmap) + return false; + + return m_context->bitmap->set_nonvolatile(); +} + +bool PPMImageDecoderPlugin::sniff() +{ + if (m_context->data_size < 2) + return false; + + if (m_context->data[0] == 'P' && m_context->data[1] == '3') + return true; + + if (m_context->data[0] == 'P' && m_context->data[1] == '6') + return true; + + return false; +} + +bool PPMImageDecoderPlugin::is_animated() +{ + return false; +} + +size_t PPMImageDecoderPlugin::loop_count() +{ + return 0; +} + +size_t PPMImageDecoderPlugin::frame_count() +{ + return 1; +} + +ImageFrameDescriptor PPMImageDecoderPlugin::frame(size_t i) +{ + if (i > 0) { + return { bitmap(), 0 }; + } + + return {}; +} + +} diff --git a/Userland/Libraries/LibGfx/PPMLoader.h b/Userland/Libraries/LibGfx/PPMLoader.h new file mode 100644 index 0000000000..bddd019d76 --- /dev/null +++ b/Userland/Libraries/LibGfx/PPMLoader.h @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2020, Hüseyin ASLITÜRK <asliturk@hotmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <LibGfx/Bitmap.h> +#include <LibGfx/ImageDecoder.h> + +namespace Gfx { + +RefPtr<Gfx::Bitmap> load_ppm(const StringView& path); +RefPtr<Gfx::Bitmap> load_ppm_from_memory(const u8*, size_t); + +struct PPMLoadingContext; + +class PPMImageDecoderPlugin final : public ImageDecoderPlugin { +public: + PPMImageDecoderPlugin(const u8*, size_t); + virtual ~PPMImageDecoderPlugin() override; + + virtual IntSize size() override; + virtual RefPtr<Gfx::Bitmap> bitmap() override; + + virtual void set_volatile() override; + [[nodiscard]] virtual bool set_nonvolatile() override; + + virtual bool sniff() override; + + virtual bool is_animated() override; + virtual size_t loop_count() override; + virtual size_t frame_count() override; + virtual ImageFrameDescriptor frame(size_t i) override; + +private: + OwnPtr<PPMLoadingContext> m_context; +}; + +} diff --git a/Userland/Libraries/LibGfx/Painter.cpp b/Userland/Libraries/LibGfx/Painter.cpp new file mode 100644 index 0000000000..7e7f5f16c7 --- /dev/null +++ b/Userland/Libraries/LibGfx/Painter.cpp @@ -0,0 +1,1629 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "Painter.h" +#include "Bitmap.h" +#include "Emoji.h" +#include "Font.h" +#include "FontDatabase.h" +#include "Gamma.h" +#include <AK/Assertions.h> +#include <AK/Function.h> +#include <AK/Memory.h> +#include <AK/QuickSort.h> +#include <AK/StdLibExtras.h> +#include <AK/StringBuilder.h> +#include <AK/Utf32View.h> +#include <AK/Utf8View.h> +#include <LibGfx/CharacterBitmap.h> +#include <LibGfx/Palette.h> +#include <LibGfx/Path.h> +#include <math.h> +#include <stdio.h> + +#if defined(__GNUC__) && !defined(__clang__) +# pragma GCC optimize("O3") +#endif + +namespace Gfx { + +template<BitmapFormat format = BitmapFormat::Invalid> +ALWAYS_INLINE Color get_pixel(const Gfx::Bitmap& bitmap, int x, int y) +{ + if constexpr (format == BitmapFormat::Indexed8) + return bitmap.palette_color(bitmap.scanline_u8(y)[x]); + if constexpr (format == BitmapFormat::Indexed4) + return bitmap.palette_color(bitmap.scanline_u8(y)[x]); + if constexpr (format == BitmapFormat::Indexed2) + return bitmap.palette_color(bitmap.scanline_u8(y)[x]); + if constexpr (format == BitmapFormat::Indexed1) + return bitmap.palette_color(bitmap.scanline_u8(y)[x]); + if constexpr (format == BitmapFormat::RGB32) + return Color::from_rgb(bitmap.scanline(y)[x]); + if constexpr (format == BitmapFormat::RGBA32) + return Color::from_rgba(bitmap.scanline(y)[x]); + return bitmap.get_pixel(x, y); +} + +Painter::Painter(Gfx::Bitmap& bitmap) + : m_target(bitmap) +{ + ASSERT(bitmap.format() == Gfx::BitmapFormat::RGB32 || bitmap.format() == Gfx::BitmapFormat::RGBA32); + m_state_stack.append(State()); + state().font = &FontDatabase::default_font(); + state().clip_rect = { { 0, 0 }, bitmap.size() }; + m_clip_origin = state().clip_rect; +} + +Painter::~Painter() +{ +} + +void Painter::fill_rect_with_draw_op(const IntRect& a_rect, Color color) +{ + auto rect = a_rect.translated(translation()).intersected(clip_rect()); + if (rect.is_empty()) + return; + + RGBA32* dst = m_target->scanline(rect.top()) + rect.left(); + const size_t dst_skip = m_target->pitch() / sizeof(RGBA32); + + for (int i = rect.height() - 1; i >= 0; --i) { + for (int j = 0; j < rect.width(); ++j) + set_pixel_with_draw_op(dst[j], color); + dst += dst_skip; + } +} + +void Painter::clear_rect(const IntRect& a_rect, Color color) +{ + auto rect = a_rect.translated(translation()).intersected(clip_rect()); + if (rect.is_empty()) + return; + + ASSERT(m_target->rect().contains(rect)); + + RGBA32* dst = m_target->scanline(rect.top()) + rect.left(); + const size_t dst_skip = m_target->pitch() / sizeof(RGBA32); + + for (int i = rect.height() - 1; i >= 0; --i) { + fast_u32_fill(dst, color.value(), rect.width()); + dst += dst_skip; + } +} + +void Painter::fill_rect(const IntRect& a_rect, Color color) +{ + if (color.alpha() == 0) + return; + + if (draw_op() != DrawOp::Copy) { + fill_rect_with_draw_op(a_rect, color); + return; + } + + if (color.alpha() == 0xff) { + clear_rect(a_rect, color); + return; + } + + auto rect = a_rect.translated(translation()).intersected(clip_rect()); + if (rect.is_empty()) + return; + + ASSERT(m_target->rect().contains(rect)); + + RGBA32* dst = m_target->scanline(rect.top()) + rect.left(); + const size_t dst_skip = m_target->pitch() / sizeof(RGBA32); + + for (int i = rect.height() - 1; i >= 0; --i) { + for (int j = 0; j < rect.width(); ++j) + dst[j] = Color::from_rgba(dst[j]).blend(color).value(); + dst += dst_skip; + } +} + +void Painter::fill_rect_with_dither_pattern(const IntRect& a_rect, Color color_a, Color color_b) +{ + auto rect = a_rect.translated(translation()).intersected(clip_rect()); + if (rect.is_empty()) + return; + + RGBA32* dst = m_target->scanline(rect.top()) + rect.left(); + const size_t dst_skip = m_target->pitch() / sizeof(RGBA32); + + for (int i = 0; i < rect.height(); ++i) { + for (int j = 0; j < rect.width(); ++j) { + bool checkboard_use_a = (i & 1) ^ (j & 1); + if (checkboard_use_a && !color_a.alpha()) + continue; + if (!checkboard_use_a && !color_b.alpha()) + continue; + dst[j] = checkboard_use_a ? color_a.value() : color_b.value(); + } + dst += dst_skip; + } +} + +void Painter::fill_rect_with_checkerboard(const IntRect& a_rect, const IntSize& cell_size, Color color_dark, Color color_light) +{ + auto rect = a_rect.translated(translation()).intersected(clip_rect()); + if (rect.is_empty()) + return; + + RGBA32* dst = m_target->scanline(rect.top()) + rect.left(); + const size_t dst_skip = m_target->pitch() / sizeof(RGBA32); + + for (int i = 0; i < rect.height(); ++i) { + for (int j = 0; j < rect.width(); ++j) { + int cell_row = i / cell_size.height(); + int cell_col = j / cell_size.width(); + dst[j] = ((cell_row % 2) ^ (cell_col % 2)) ? color_light.value() : color_dark.value(); + } + dst += dst_skip; + } +} + +void Painter::fill_rect_with_gradient(Orientation orientation, const IntRect& a_rect, Color gradient_start, Color gradient_end) +{ +#ifdef NO_FPU + return fill_rect(a_rect, gradient_start); +#endif + auto rect = a_rect.translated(translation()); + auto clipped_rect = IntRect::intersection(rect, clip_rect()); + if (clipped_rect.is_empty()) + return; + + int offset = clipped_rect.primary_offset_for_orientation(orientation) - rect.primary_offset_for_orientation(orientation); + + RGBA32* dst = m_target->scanline(clipped_rect.top()) + clipped_rect.left(); + const size_t dst_skip = m_target->pitch() / sizeof(RGBA32); + + float increment = (1.0 / ((rect.primary_size_for_orientation(orientation)))); + + if (orientation == Orientation::Horizontal) { + for (int i = clipped_rect.height() - 1; i >= 0; --i) { + float c = offset * increment; + for (int j = 0; j < clipped_rect.width(); ++j) { + dst[j] = gamma_accurate_blend(gradient_start, gradient_end, c).value(); + c += increment; + } + dst += dst_skip; + } + } else { + float c = offset * increment; + for (int i = clipped_rect.height() - 1; i >= 0; --i) { + auto color = gamma_accurate_blend(gradient_start, gradient_end, c); + for (int j = 0; j < clipped_rect.width(); ++j) { + dst[j] = color.value(); + } + c += increment; + dst += dst_skip; + } + } +} + +void Painter::fill_rect_with_gradient(const IntRect& a_rect, Color gradient_start, Color gradient_end) +{ + return fill_rect_with_gradient(Orientation::Horizontal, a_rect, gradient_start, gradient_end); +} + +void Painter::fill_ellipse(const IntRect& a_rect, Color color) +{ + auto rect = a_rect.translated(translation()).intersected(clip_rect()); + if (rect.is_empty()) + return; + + ASSERT(m_target->rect().contains(rect)); + + RGBA32* dst = m_target->scanline(rect.top()) + rect.left() + rect.width() / 2; + const size_t dst_skip = m_target->pitch() / sizeof(RGBA32); + + for (int i = 0; i < rect.height(); i++) { + double y = rect.height() * 0.5 - i; + double x = rect.width() * sqrt(0.25 - y * y / rect.height() / rect.height()); + fast_u32_fill(dst - (int)x, color.value(), 2 * (int)x); + dst += dst_skip; + } +} + +void Painter::draw_ellipse_intersecting(const IntRect& rect, Color color, int thickness) +{ + constexpr int number_samples = 100; // FIXME: dynamically work out the number of samples based upon the rect size + double increment = M_PI / number_samples; + + auto ellipse_x = [&](double theta) -> int { + return (cos(theta) * rect.width() / sqrt(2)) + rect.center().x(); + }; + + auto ellipse_y = [&](double theta) -> int { + return (sin(theta) * rect.height() / sqrt(2)) + rect.center().y(); + }; + + for (float theta = 0; theta < 2 * M_PI; theta += increment) { + draw_line({ ellipse_x(theta), ellipse_y(theta) }, { ellipse_x(theta + increment), ellipse_y(theta + increment) }, color, thickness); + } +} + +template<typename RectType, typename Callback> +static void for_each_pixel_around_rect_clockwise(const RectType& rect, Callback callback) +{ + if (rect.is_empty()) + return; + for (auto x = rect.left(); x <= rect.right(); ++x) { + callback(x, rect.top()); + } + for (auto y = rect.top() + 1; y <= rect.bottom(); ++y) { + callback(rect.right(), y); + } + for (auto x = rect.right() - 1; x >= rect.left(); --x) { + callback(x, rect.bottom()); + } + for (auto y = rect.bottom() - 1; y > rect.top(); --y) { + callback(rect.left(), y); + } +} + +void Painter::draw_focus_rect(const IntRect& rect, Color color) +{ + if (rect.is_empty()) + return; + bool state = false; + for_each_pixel_around_rect_clockwise(rect, [&](auto x, auto y) { + if (state) + set_pixel(x, y, color); + state = !state; + }); +} + +void Painter::draw_rect(const IntRect& a_rect, Color color, bool rough) +{ + IntRect rect = a_rect.translated(translation()); + auto clipped_rect = rect.intersected(clip_rect()); + if (clipped_rect.is_empty()) + return; + + int min_y = clipped_rect.top(); + int max_y = clipped_rect.bottom(); + + if (rect.top() >= clipped_rect.top() && rect.top() <= clipped_rect.bottom()) { + int start_x = rough ? max(rect.x() + 1, clipped_rect.x()) : clipped_rect.x(); + int width = rough ? min(rect.width() - 2, clipped_rect.width()) : clipped_rect.width(); + fill_scanline_with_draw_op(rect.top(), start_x, width, color); + ++min_y; + } + if (rect.bottom() >= clipped_rect.top() && rect.bottom() <= clipped_rect.bottom()) { + int start_x = rough ? max(rect.x() + 1, clipped_rect.x()) : clipped_rect.x(); + int width = rough ? min(rect.width() - 2, clipped_rect.width()) : clipped_rect.width(); + fill_scanline_with_draw_op(rect.bottom(), start_x, width, color); + --max_y; + } + + bool draw_left_side = rect.left() >= clipped_rect.left(); + bool draw_right_side = rect.right() == clipped_rect.right(); + + if (draw_left_side && draw_right_side) { + // Specialized loop when drawing both sides. + for (int y = min_y; y <= max_y; ++y) { + auto* bits = m_target->scanline(y); + set_pixel_with_draw_op(bits[rect.left()], color); + set_pixel_with_draw_op(bits[rect.right()], color); + } + } else { + for (int y = min_y; y <= max_y; ++y) { + auto* bits = m_target->scanline(y); + if (draw_left_side) + set_pixel_with_draw_op(bits[rect.left()], color); + if (draw_right_side) + set_pixel_with_draw_op(bits[rect.right()], color); + } + } +} + +void Painter::draw_bitmap(const IntPoint& p, const CharacterBitmap& bitmap, Color color) +{ + auto rect = IntRect(p, bitmap.size()).translated(translation()); + auto clipped_rect = rect.intersected(clip_rect()); + if (clipped_rect.is_empty()) + return; + const int first_row = clipped_rect.top() - rect.top(); + const int last_row = clipped_rect.bottom() - rect.top(); + const int first_column = clipped_rect.left() - rect.left(); + const int last_column = clipped_rect.right() - rect.left(); + RGBA32* dst = m_target->scanline(clipped_rect.y()) + clipped_rect.x(); + const size_t dst_skip = m_target->pitch() / sizeof(RGBA32); + const char* bitmap_row = &bitmap.bits()[first_row * bitmap.width() + first_column]; + const size_t bitmap_skip = bitmap.width(); + + for (int row = first_row; row <= last_row; ++row) { + for (int j = 0; j <= (last_column - first_column); ++j) { + char fc = bitmap_row[j]; + if (fc == '#') + dst[j] = color.value(); + } + bitmap_row += bitmap_skip; + dst += dst_skip; + } +} + +void Painter::draw_bitmap(const IntPoint& p, const GlyphBitmap& bitmap, Color color) +{ + auto dst_rect = IntRect(p, bitmap.size()).translated(translation()); + auto clipped_rect = dst_rect.intersected(clip_rect()); + if (clipped_rect.is_empty()) + return; + const int first_row = clipped_rect.top() - dst_rect.top(); + const int last_row = clipped_rect.bottom() - dst_rect.top(); + const int first_column = clipped_rect.left() - dst_rect.left(); + const int last_column = clipped_rect.right() - dst_rect.left(); + RGBA32* dst = m_target->scanline(clipped_rect.y()) + clipped_rect.x(); + const size_t dst_skip = m_target->pitch() / sizeof(RGBA32); + + for (int row = first_row; row <= last_row; ++row) { + for (int j = 0; j <= (last_column - first_column); ++j) { + if (bitmap.bit_at(j + first_column, row)) + dst[j] = color.value(); + } + dst += dst_skip; + } +} + +void Painter::draw_triangle(const IntPoint& a, const IntPoint& b, const IntPoint& c, Color color) +{ + RGBA32 rgba = color.value(); + + IntPoint p0(a); + IntPoint p1(b); + IntPoint p2(c); + + if (p0.y() > p1.y()) + swap(p0, p1); + if (p0.y() > p2.y()) + swap(p0, p2); + if (p1.y() > p2.y()) + swap(p1, p2); + + auto clip = clip_rect(); + if (p0.y() >= clip.bottom()) + return; + if (p2.y() < clip.top()) + return; + + float dx01 = (float)(p1.x() - p0.x()) / (p1.y() - p0.y()); + float dx02 = (float)(p2.x() - p0.x()) / (p2.y() - p0.y()); + float dx12 = (float)(p2.x() - p1.x()) / (p2.y() - p1.y()); + + float x01 = p0.x(); + float x02 = p0.x(); + + int top = p0.y(); + if (top < clip.top()) { + x01 += dx01 * (clip.top() - top); + x02 += dx02 * (clip.top() - top); + top = clip.top(); + } + + for (int y = top; y < p1.y() && y < clip.bottom(); ++y) { + int start = x01 > x02 ? max((int)x02, clip.left()) : max((int)x01, clip.left()); + int end = x01 > x02 ? min((int)x01, clip.right()) : min((int)x02, clip.right()); + auto* scanline = m_target->scanline(y); + for (int x = start; x < end; x++) { + scanline[x] = rgba; + } + x01 += dx01; + x02 += dx02; + } + + x02 = p0.x() + dx02 * (p1.y() - p0.y()); + float x12 = p1.x(); + + top = p1.y(); + if (top < clip.top()) { + x02 += dx02 * (clip.top() - top); + x12 += dx12 * (clip.top() - top); + top = clip.top(); + } + + for (int y = top; y < p2.y() && y < clip.bottom(); ++y) { + int start = x12 > x02 ? max((int)x02, clip.left()) : max((int)x12, clip.left()); + int end = x12 > x02 ? min((int)x12, clip.right()) : min((int)x02, clip.right()); + auto* scanline = m_target->scanline(y); + for (int x = start; x < end; x++) { + scanline[x] = rgba; + } + x02 += dx02; + x12 += dx12; + } +} + +void Painter::blit_scaled(const IntRect& dst_rect_raw, const Gfx::Bitmap& source, const IntRect& src_rect, float hscale, float vscale) +{ + auto dst_rect = IntRect(dst_rect_raw.location(), dst_rect_raw.size()).translated(translation()); + auto clipped_rect = dst_rect.intersected(clip_rect()); + if (clipped_rect.is_empty()) + return; + const int first_row = (clipped_rect.top() - dst_rect.top()); + const int last_row = (clipped_rect.bottom() - dst_rect.top()); + const int first_column = (clipped_rect.left() - dst_rect.left()); + RGBA32* dst = m_target->scanline(clipped_rect.y()) + clipped_rect.x(); + const size_t dst_skip = m_target->pitch() / sizeof(RGBA32); + + int x_start = first_column + src_rect.left(); + for (int row = first_row; row <= last_row; ++row) { + int sr = (row + src_rect.top()) * vscale; + if (sr >= source.size().height() || sr < 0) { + dst += dst_skip; + continue; + } + const RGBA32* sl = source.scanline(sr); + for (int x = x_start; x < clipped_rect.width() + x_start; ++x) { + int sx = x * hscale; + if (sx < source.size().width() && sx >= 0) + dst[x - x_start] = sl[sx]; + } + dst += dst_skip; + } + return; +} + +void Painter::blit_with_opacity(const IntPoint& position, const Gfx::Bitmap& source, const IntRect& src_rect, float opacity) +{ + ASSERT(!m_target->has_alpha_channel()); + + if (!opacity) + return; + if (opacity >= 1.0f) + return blit(position, source, src_rect); + + u8 alpha = 255 * opacity; + + IntRect safe_src_rect = IntRect::intersection(src_rect, source.rect()); + IntRect dst_rect(position, safe_src_rect.size()); + dst_rect.move_by(state().translation); + auto clipped_rect = IntRect::intersection(dst_rect, clip_rect()); + if (clipped_rect.is_empty()) + return; + const int first_row = clipped_rect.top() - dst_rect.top(); + const int last_row = clipped_rect.bottom() - dst_rect.top(); + const int first_column = clipped_rect.left() - dst_rect.left(); + const int last_column = clipped_rect.right() - dst_rect.left(); + RGBA32* dst = m_target->scanline(clipped_rect.y()) + clipped_rect.x(); + const RGBA32* src = source.scanline(src_rect.top() + first_row) + src_rect.left() + first_column; + const size_t dst_skip = m_target->pitch() / sizeof(RGBA32); + const unsigned src_skip = source.pitch() / sizeof(RGBA32); + + for (int row = first_row; row <= last_row; ++row) { + for (int x = 0; x <= (last_column - first_column); ++x) { + Color src_color_with_alpha = Color::from_rgb(src[x]); + src_color_with_alpha.set_alpha(alpha); + Color dst_color = Color::from_rgb(dst[x]); + dst[x] = dst_color.blend(src_color_with_alpha).value(); + } + dst += dst_skip; + src += src_skip; + } +} + +void Painter::blit_filtered(const IntPoint& position, const Gfx::Bitmap& source, const IntRect& src_rect, Function<Color(Color)> filter) +{ + IntRect safe_src_rect = src_rect.intersected(source.rect()); + auto dst_rect = IntRect(position, safe_src_rect.size()).translated(translation()); + auto clipped_rect = dst_rect.intersected(clip_rect()); + if (clipped_rect.is_empty()) + return; + const int first_row = clipped_rect.top() - dst_rect.top(); + const int last_row = clipped_rect.bottom() - dst_rect.top(); + const int first_column = clipped_rect.left() - dst_rect.left(); + const int last_column = clipped_rect.right() - dst_rect.left(); + RGBA32* dst = m_target->scanline(clipped_rect.y()) + clipped_rect.x(); + const RGBA32* src = source.scanline(src_rect.top() + first_row) + src_rect.left() + first_column; + const size_t dst_skip = m_target->pitch() / sizeof(RGBA32); + const size_t src_skip = source.pitch() / sizeof(RGBA32); + + for (int row = first_row; row <= last_row; ++row) { + for (int x = 0; x <= (last_column - first_column); ++x) { + u8 alpha = Color::from_rgba(src[x]).alpha(); + if (alpha == 0xff) + dst[x] = filter(Color::from_rgba(src[x])).value(); + else if (!alpha) + continue; + else + dst[x] = Color::from_rgba(dst[x]).blend(filter(Color::from_rgba(src[x]))).value(); + } + dst += dst_skip; + src += src_skip; + } +} + +void Painter::blit_brightened(const IntPoint& position, const Gfx::Bitmap& source, const IntRect& src_rect) +{ + return blit_filtered(position, source, src_rect, [](Color src) { + return src.lightened(); + }); +} + +void Painter::blit_dimmed(const IntPoint& position, const Gfx::Bitmap& source, const IntRect& src_rect) +{ + return blit_filtered(position, source, src_rect, [](Color src) { + return src.to_grayscale().lightened(); + }); +} + +void Painter::draw_tiled_bitmap(const IntRect& a_dst_rect, const Gfx::Bitmap& source) +{ + auto dst_rect = a_dst_rect.translated(translation()); + auto clipped_rect = dst_rect.intersected(clip_rect()); + if (clipped_rect.is_empty()) + return; + const int first_row = (clipped_rect.top() - dst_rect.top()); + const int last_row = (clipped_rect.bottom() - dst_rect.top()); + const int first_column = (clipped_rect.left() - dst_rect.left()); + RGBA32* dst = m_target->scanline(clipped_rect.y()) + clipped_rect.x(); + const size_t dst_skip = m_target->pitch() / sizeof(RGBA32); + + if (source.format() == BitmapFormat::RGB32 || source.format() == BitmapFormat::RGBA32) { + int x_start = first_column + a_dst_rect.left(); + for (int row = first_row; row <= last_row; ++row) { + const RGBA32* sl = source.scanline((row + a_dst_rect.top()) + % source.size().height()); + for (int x = x_start; x < clipped_rect.width() + x_start; ++x) { + dst[x - x_start] = sl[x % source.size().width()]; + } + dst += dst_skip; + } + return; + } + + ASSERT_NOT_REACHED(); +} + +void Painter::blit_offset(const IntPoint& position, + const Gfx::Bitmap& source, + const IntRect& src_rect, + const IntPoint& offset) +{ + auto dst_rect = IntRect(position, src_rect.size()).translated(translation()); + auto clipped_rect = dst_rect.intersected(clip_rect()); + if (clipped_rect.is_empty()) + return; + const int first_row = (clipped_rect.top() - dst_rect.top()); + const int last_row = (clipped_rect.bottom() - dst_rect.top()); + const int first_column = (clipped_rect.left() - dst_rect.left()); + RGBA32* dst = m_target->scanline(clipped_rect.y()) + clipped_rect.x(); + const size_t dst_skip = m_target->pitch() / sizeof(RGBA32); + + if (source.format() == BitmapFormat::RGB32 || source.format() == BitmapFormat::RGBA32) { + int x_start = first_column + src_rect.left(); + for (int row = first_row; row <= last_row; ++row) { + int sr = row - offset.y() + src_rect.top(); + if (sr >= source.size().height() || sr < 0) { + dst += dst_skip; + continue; + } + const RGBA32* sl = source.scanline(sr); + for (int x = x_start; x < clipped_rect.width() + x_start; ++x) { + int sx = x - offset.x(); + if (sx < source.size().width() && sx >= 0) + dst[x - x_start] = sl[sx]; + } + dst += dst_skip; + } + return; + } + + ASSERT_NOT_REACHED(); +} + +void Painter::blit_with_alpha(const IntPoint& position, const Gfx::Bitmap& source, const IntRect& src_rect) +{ + ASSERT(source.has_alpha_channel()); + IntRect safe_src_rect = src_rect.intersected(source.rect()); + auto dst_rect = IntRect(position, safe_src_rect.size()).translated(translation()); + auto clipped_rect = dst_rect.intersected(clip_rect()); + if (clipped_rect.is_empty()) + return; + const int first_row = clipped_rect.top() - dst_rect.top(); + const int last_row = clipped_rect.bottom() - dst_rect.top(); + const int first_column = clipped_rect.left() - dst_rect.left(); + const int last_column = clipped_rect.right() - dst_rect.left(); + RGBA32* dst = m_target->scanline(clipped_rect.y()) + clipped_rect.x(); + const RGBA32* src = source.scanline(src_rect.top() + first_row) + src_rect.left() + first_column; + const size_t dst_skip = m_target->pitch() / sizeof(RGBA32); + const size_t src_skip = source.pitch() / sizeof(RGBA32); + + for (int row = first_row; row <= last_row; ++row) { + for (int x = 0; x <= (last_column - first_column); ++x) { + u8 alpha = Color::from_rgba(src[x]).alpha(); + if (alpha == 0xff) + dst[x] = src[x]; + else if (!alpha) + continue; + else + dst[x] = Color::from_rgba(dst[x]).blend(Color::from_rgba(src[x])).value(); + } + dst += dst_skip; + src += src_skip; + } +} + +void Painter::blit(const IntPoint& position, const Gfx::Bitmap& source, const IntRect& src_rect, float opacity) +{ + if (opacity < 1.0f) + return blit_with_opacity(position, source, src_rect, opacity); + if (source.has_alpha_channel()) + return blit_with_alpha(position, source, src_rect); + auto safe_src_rect = src_rect.intersected(source.rect()); + ASSERT(source.rect().contains(safe_src_rect)); + auto dst_rect = IntRect(position, safe_src_rect.size()).translated(translation()); + auto clipped_rect = dst_rect.intersected(clip_rect()); + if (clipped_rect.is_empty()) + return; + const int first_row = clipped_rect.top() - dst_rect.top(); + const int last_row = clipped_rect.bottom() - dst_rect.top(); + const int first_column = clipped_rect.left() - dst_rect.left(); + RGBA32* dst = m_target->scanline(clipped_rect.y()) + clipped_rect.x(); + const size_t dst_skip = m_target->pitch() / sizeof(RGBA32); + + if (source.format() == BitmapFormat::RGB32 || source.format() == BitmapFormat::RGBA32) { + const RGBA32* src = source.scanline(src_rect.top() + first_row) + src_rect.left() + first_column; + const size_t src_skip = source.pitch() / sizeof(RGBA32); + for (int row = first_row; row <= last_row; ++row) { + fast_u32_copy(dst, src, clipped_rect.width()); + dst += dst_skip; + src += src_skip; + } + return; + } + + if (Bitmap::is_indexed(source.format())) { + const u8* src = source.scanline_u8(src_rect.top() + first_row) + src_rect.left() + first_column; + const size_t src_skip = source.pitch(); + for (int row = first_row; row <= last_row; ++row) { + for (int i = 0; i < clipped_rect.width(); ++i) + dst[i] = source.palette_color(src[i]).value(); + dst += dst_skip; + src += src_skip; + } + return; + } + + ASSERT_NOT_REACHED(); +} + +template<bool has_alpha_channel, typename GetPixel> +ALWAYS_INLINE static void do_draw_integer_scaled_bitmap(Gfx::Bitmap& target, const IntRect& dst_rect, const Gfx::Bitmap& source, int hfactor, int vfactor, GetPixel get_pixel, float opacity) +{ + bool has_opacity = opacity != 1.0f; + for (int y = source.rect().top(); y <= source.rect().bottom(); ++y) { + int dst_y = dst_rect.y() + y * vfactor; + for (int x = source.rect().left(); x <= source.rect().right(); ++x) { + auto src_pixel = get_pixel(source, x, y); + if (has_opacity) + src_pixel.set_alpha(src_pixel.alpha() * opacity); + for (int yo = 0; yo < vfactor; ++yo) { + auto* scanline = (Color*)target.scanline(dst_y + yo); + int dst_x = dst_rect.x() + x * hfactor; + for (int xo = 0; xo < hfactor; ++xo) { + if constexpr (has_alpha_channel) + scanline[dst_x + xo] = scanline[dst_x + xo].blend(src_pixel); + else + scanline[dst_x + xo] = src_pixel; + } + } + } + } +} + +template<bool has_alpha_channel, typename GetPixel> +ALWAYS_INLINE static void do_draw_scaled_bitmap(Gfx::Bitmap& target, const IntRect& dst_rect, const IntRect& clipped_rect, const Gfx::Bitmap& source, const IntRect& src_rect, int hscale, int vscale, GetPixel get_pixel, float opacity) +{ + if (dst_rect == clipped_rect && !(dst_rect.width() % src_rect.width()) && !(dst_rect.height() % src_rect.height())) { + int hfactor = dst_rect.width() / src_rect.width(); + int vfactor = dst_rect.height() / src_rect.height(); + if (hfactor == 2 && vfactor == 2) + return do_draw_integer_scaled_bitmap<has_alpha_channel>(target, dst_rect, source, 2, 2, get_pixel, opacity); + if (hfactor == 3 && vfactor == 3) + return do_draw_integer_scaled_bitmap<has_alpha_channel>(target, dst_rect, source, 3, 3, get_pixel, opacity); + if (hfactor == 4 && vfactor == 4) + return do_draw_integer_scaled_bitmap<has_alpha_channel>(target, dst_rect, source, 4, 4, get_pixel, opacity); + return do_draw_integer_scaled_bitmap<has_alpha_channel>(target, dst_rect, source, hfactor, vfactor, get_pixel, opacity); + } + + bool has_opacity = opacity != 1.0f; + + for (int y = clipped_rect.top(); y <= clipped_rect.bottom(); ++y) { + auto* scanline = (Color*)target.scanline(y); + for (int x = clipped_rect.left(); x <= clipped_rect.right(); ++x) { + auto scaled_x = ((x - dst_rect.x()) * hscale) >> 16; + auto scaled_y = ((y - dst_rect.y()) * vscale) >> 16; + auto src_pixel = get_pixel(source, scaled_x, scaled_y); + if (has_opacity) + src_pixel.set_alpha(src_pixel.alpha() * opacity); + if constexpr (has_alpha_channel) { + scanline[x] = scanline[x].blend(src_pixel); + } else + scanline[x] = src_pixel; + } + } +} + +void Painter::draw_scaled_bitmap(const IntRect& a_dst_rect, const Gfx::Bitmap& source, const IntRect& src_rect, float opacity) +{ + auto dst_rect = a_dst_rect; + if (dst_rect.size() == src_rect.size()) + return blit(dst_rect.location(), source, src_rect, opacity); + + auto safe_src_rect = src_rect.intersected(source.rect()); + ASSERT(source.rect().contains(safe_src_rect)); + dst_rect.move_by(state().translation); + auto clipped_rect = dst_rect.intersected(clip_rect()); + if (clipped_rect.is_empty()) + return; + + int hscale = (src_rect.width() << 16) / dst_rect.width(); + int vscale = (src_rect.height() << 16) / dst_rect.height(); + + if (source.has_alpha_channel()) { + switch (source.format()) { + case BitmapFormat::RGB32: + do_draw_scaled_bitmap<true>(*m_target, dst_rect, clipped_rect, source, src_rect, hscale, vscale, get_pixel<BitmapFormat::RGB32>, opacity); + break; + case BitmapFormat::RGBA32: + do_draw_scaled_bitmap<true>(*m_target, dst_rect, clipped_rect, source, src_rect, hscale, vscale, get_pixel<BitmapFormat::RGBA32>, opacity); + break; + case BitmapFormat::Indexed8: + do_draw_scaled_bitmap<true>(*m_target, dst_rect, clipped_rect, source, src_rect, hscale, vscale, get_pixel<BitmapFormat::Indexed8>, opacity); + break; + case BitmapFormat::Indexed4: + do_draw_scaled_bitmap<true>(*m_target, dst_rect, clipped_rect, source, src_rect, hscale, vscale, get_pixel<BitmapFormat::Indexed4>, opacity); + break; + case BitmapFormat::Indexed2: + do_draw_scaled_bitmap<true>(*m_target, dst_rect, clipped_rect, source, src_rect, hscale, vscale, get_pixel<BitmapFormat::Indexed2>, opacity); + break; + case BitmapFormat::Indexed1: + do_draw_scaled_bitmap<true>(*m_target, dst_rect, clipped_rect, source, src_rect, hscale, vscale, get_pixel<BitmapFormat::Indexed1>, opacity); + break; + default: + do_draw_scaled_bitmap<true>(*m_target, dst_rect, clipped_rect, source, src_rect, hscale, vscale, get_pixel<BitmapFormat::Invalid>, opacity); + break; + } + } else { + switch (source.format()) { + case BitmapFormat::RGB32: + do_draw_scaled_bitmap<false>(*m_target, dst_rect, clipped_rect, source, src_rect, hscale, vscale, get_pixel<BitmapFormat::RGB32>, opacity); + break; + case BitmapFormat::RGBA32: + do_draw_scaled_bitmap<false>(*m_target, dst_rect, clipped_rect, source, src_rect, hscale, vscale, get_pixel<BitmapFormat::RGBA32>, opacity); + break; + case BitmapFormat::Indexed8: + do_draw_scaled_bitmap<false>(*m_target, dst_rect, clipped_rect, source, src_rect, hscale, vscale, get_pixel<BitmapFormat::Indexed8>, opacity); + break; + default: + do_draw_scaled_bitmap<false>(*m_target, dst_rect, clipped_rect, source, src_rect, hscale, vscale, get_pixel<BitmapFormat::Invalid>, opacity); + break; + } + } +} + +FLATTEN void Painter::draw_glyph(const IntPoint& point, u32 code_point, Color color) +{ + draw_glyph(point, code_point, font(), color); +} + +FLATTEN void Painter::draw_glyph(const IntPoint& point, u32 code_point, const Font& font, Color color) +{ + draw_bitmap(point, font.glyph_bitmap(code_point), color); +} + +void Painter::draw_emoji(const IntPoint& point, const Gfx::Bitmap& emoji, const Font& font) +{ + if (!font.is_fixed_width()) + blit(point, emoji, emoji.rect()); + else { + IntRect dst_rect { + point.x(), + point.y(), + font.glyph_width('x'), + font.glyph_height() + }; + draw_scaled_bitmap(dst_rect, emoji, emoji.rect()); + } +} + +void Painter::draw_glyph_or_emoji(const IntPoint& point, u32 code_point, const Font& font, Color color) +{ + if (code_point < (u32)font.glyph_count()) { + // This looks like a regular character. + draw_glyph(point, (size_t)code_point, font, color); + return; + } + + // Perhaps it's an emoji? + auto* emoji = Emoji::emoji_for_code_point(code_point); + if (emoji == nullptr) { +#ifdef EMOJI_DEBUG + dbg() << "Failed to find an emoji for code_point " << code_point; +#endif + draw_glyph(point, '?', font, color); + return; + } + + draw_emoji(point, *emoji, font); +} + +static void apply_elision(Utf8View& final_text, String& elided_text, size_t offset) +{ + StringBuilder builder; + builder.append(final_text.substring_view(0, offset).as_string()); + builder.append("..."); + elided_text = builder.to_string(); + final_text = Utf8View { elided_text }; +} + +static void apply_elision(Utf32View& final_text, Vector<u32>& elided_text, size_t offset) +{ + elided_text.append(final_text.code_points(), offset); + elided_text.append('.'); + elided_text.append('.'); + elided_text.append('.'); + final_text = Utf32View { elided_text.data(), elided_text.size() }; +} + +template<typename TextType> +struct ElidedText { +}; + +template<> +struct ElidedText<Utf8View> { + typedef String Type; +}; + +template<> +struct ElidedText<Utf32View> { + typedef Vector<u32> Type; +}; + +template<typename TextType, typename DrawGlyphFunction> +void draw_text_line(const IntRect& a_rect, const TextType& text, const Font& font, TextAlignment alignment, TextElision elision, DrawGlyphFunction draw_glyph) +{ + auto rect = a_rect; + TextType final_text(text); + typename ElidedText<TextType>::Type elided_text; + if (elision == TextElision::Right) { + int text_width = font.width(final_text); + if (font.width(final_text) > rect.width()) { + int glyph_spacing = font.glyph_spacing(); + int new_width = font.width("..."); + if (new_width < text_width) { + size_t offset = 0; + for (auto it = text.begin(); it != text.end(); ++it) { + auto code_point = *it; + int glyph_width = font.glyph_or_emoji_width(code_point); + // NOTE: Glyph spacing should not be added after the last glyph on the line, + // but since we are here because the last glyph does not actually fit on the line, + // we don't have to worry about spacing. + int width_with_this_glyph_included = new_width + glyph_width + glyph_spacing; + if (width_with_this_glyph_included > rect.width()) + break; + new_width += glyph_width + glyph_spacing; + offset = text.iterator_offset(it); + } + apply_elision(final_text, elided_text, offset); + } + } + } + + switch (alignment) { + case TextAlignment::TopLeft: + case TextAlignment::CenterLeft: + break; + case TextAlignment::TopRight: + case TextAlignment::CenterRight: + case TextAlignment::BottomRight: + rect.set_x(rect.right() - font.width(final_text)); + break; + case TextAlignment::Center: { + auto shrunken_rect = rect; + shrunken_rect.set_width(font.width(final_text)); + shrunken_rect.center_within(rect); + rect = shrunken_rect; + break; + } + default: + ASSERT_NOT_REACHED(); + } + + if (is_vertically_centered_text_alignment(alignment)) { + int distance_from_baseline_to_bottom = (font.glyph_height() - 1) - font.baseline(); + rect.move_by(0, distance_from_baseline_to_bottom / 2); + } + + auto point = rect.location(); + int space_width = font.glyph_width(' ') + font.glyph_spacing(); + + for (u32 code_point : final_text) { + if (code_point == ' ') { + point.move_by(space_width, 0); + continue; + } + IntSize glyph_size(font.glyph_or_emoji_width(code_point) + font.glyph_spacing(), font.glyph_height()); + draw_glyph({ point, glyph_size }, code_point); + point.move_by(glyph_size.width(), 0); + } +} + +static inline size_t draw_text_iterator_offset(const Utf8View& text, const Utf8View::Iterator& it) +{ + return text.byte_offset_of(it); +} + +static inline size_t draw_text_iterator_offset(const Utf32View& text, const Utf32View::Iterator& it) +{ + return it - text.begin(); +} + +static inline size_t draw_text_get_length(const Utf8View& text) +{ + return text.byte_length(); +} + +static inline size_t draw_text_get_length(const Utf32View& text) +{ + return text.length(); +} + +template<typename TextType, typename DrawGlyphFunction> +void do_draw_text(const IntRect& rect, const TextType& text, const Font& font, TextAlignment alignment, TextElision elision, DrawGlyphFunction draw_glyph) +{ + Vector<TextType, 32> lines; + + size_t start_of_current_line = 0; + for (auto it = text.begin(); it != text.end(); ++it) { + u32 code_point = *it; + if (code_point == '\n') { + auto offset = draw_text_iterator_offset(text, it); + TextType line = text.substring_view(start_of_current_line, offset - start_of_current_line); + lines.append(line); + start_of_current_line = offset + 1; + } + } + + if (start_of_current_line != draw_text_get_length(text)) { + TextType line = text.substring_view(start_of_current_line, draw_text_get_length(text) - start_of_current_line); + lines.append(line); + } + + static const int line_spacing = 4; + int line_height = font.glyph_height() + line_spacing; + IntRect bounding_rect { 0, 0, 0, (static_cast<int>(lines.size()) * line_height) - line_spacing }; + + for (auto& line : lines) { + auto line_width = font.width(line); + if (line_width > bounding_rect.width()) + bounding_rect.set_width(line_width); + } + + switch (alignment) { + case TextAlignment::TopLeft: + bounding_rect.set_location(rect.location()); + break; + case TextAlignment::TopRight: + bounding_rect.set_location({ (rect.right() + 1) - bounding_rect.width(), rect.y() }); + break; + case TextAlignment::CenterLeft: + bounding_rect.set_location({ rect.x(), rect.center().y() - (bounding_rect.height() / 2) }); + break; + case TextAlignment::CenterRight: + bounding_rect.set_location({ (rect.right() + 1) - bounding_rect.width(), rect.center().y() - (bounding_rect.height() / 2) }); + break; + case TextAlignment::Center: + bounding_rect.center_within(rect); + break; + case TextAlignment::BottomRight: + bounding_rect.set_location({ (rect.right() + 1) - bounding_rect.width(), (rect.bottom() + 1) - bounding_rect.height() }); + break; + default: + ASSERT_NOT_REACHED(); + } + + for (size_t i = 0; i < lines.size(); ++i) { + auto& line = lines[i]; + IntRect line_rect { bounding_rect.x(), bounding_rect.y() + static_cast<int>(i) * line_height, bounding_rect.width(), line_height }; + line_rect.intersect(rect); + draw_text_line(line_rect, line, font, alignment, elision, draw_glyph); + } +} + +void Painter::draw_text(const IntRect& rect, const StringView& text, TextAlignment alignment, Color color, TextElision elision) +{ + draw_text(rect, text, font(), alignment, color, elision); +} + +void Painter::draw_text(const IntRect& rect, const Utf32View& text, TextAlignment alignment, Color color, TextElision elision) +{ + draw_text(rect, text, font(), alignment, color, elision); +} + +void Painter::draw_text(const IntRect& rect, const StringView& raw_text, const Font& font, TextAlignment alignment, Color color, TextElision elision) +{ + Utf8View text { raw_text }; + do_draw_text(rect, Utf8View(text), font, alignment, elision, [&](const IntRect& r, u32 code_point) { + draw_glyph_or_emoji(r.location(), code_point, font, color); + }); +} + +void Painter::draw_text(const IntRect& rect, const Utf32View& text, const Font& font, TextAlignment alignment, Color color, TextElision elision) +{ + do_draw_text(rect, text, font, alignment, elision, [&](const IntRect& r, u32 code_point) { + draw_glyph_or_emoji(r.location(), code_point, font, color); + }); +} + +void Painter::draw_text(Function<void(const IntRect&, u32)> draw_one_glyph, const IntRect& rect, const StringView& raw_text, const Font& font, TextAlignment alignment, TextElision elision) +{ + Utf8View text { raw_text }; + do_draw_text(rect, text, font, alignment, elision, [&](const IntRect& r, u32 code_point) { + draw_one_glyph(r, code_point); + }); +} + +void Painter::draw_text(Function<void(const IntRect&, u32)> draw_one_glyph, const IntRect& rect, const Utf8View& text, const Font& font, TextAlignment alignment, TextElision elision) +{ + do_draw_text(rect, text, font, alignment, elision, [&](const IntRect& r, u32 code_point) { + draw_one_glyph(r, code_point); + }); +} + +void Painter::draw_text(Function<void(const IntRect&, u32)> draw_one_glyph, const IntRect& rect, const Utf32View& text, const Font& font, TextAlignment alignment, TextElision elision) +{ + do_draw_text(rect, text, font, alignment, elision, [&](const IntRect& r, u32 code_point) { + draw_one_glyph(r, code_point); + }); +} + +void Painter::set_pixel(const IntPoint& p, Color color) +{ + auto point = p; + point.move_by(state().translation); + if (!clip_rect().contains(point)) + return; + m_target->scanline(point.y())[point.x()] = color.value(); +} + +ALWAYS_INLINE void Painter::set_pixel_with_draw_op(u32& pixel, const Color& color) +{ + switch (draw_op()) { + case DrawOp::Copy: + pixel = color.value(); + break; + case DrawOp::Xor: + pixel = color.xored(Color::from_rgba(pixel)).value(); + break; + case DrawOp::Invert: + pixel = Color::from_rgba(pixel).inverted().value(); + break; + } +} + +ALWAYS_INLINE void Painter::fill_scanline_with_draw_op(int y, int x, int width, const Color& color) +{ + switch (draw_op()) { + case DrawOp::Copy: + fast_u32_fill(m_target->scanline(y) + x, color.value(), width); + break; + case DrawOp::Xor: { + auto* pixel = m_target->scanline(y) + x; + auto* end = pixel + width; + while (pixel < end) { + *pixel = Color::from_rgba(*pixel).xored(color).value(); + pixel++; + } + break; + } + case DrawOp::Invert: { + auto* pixel = m_target->scanline(y) + x; + auto* end = pixel + width; + while (pixel < end) { + *pixel = Color::from_rgba(*pixel).inverted().value(); + pixel++; + } + break; + } + } +} + +void Painter::draw_pixel(const IntPoint& position, Color color, int thickness) +{ + if (thickness == 1) + return set_pixel_with_draw_op(m_target->scanline(position.y())[position.x()], color); + IntRect rect { position.translated(-(thickness / 2), -(thickness / 2)), { thickness, thickness } }; + fill_rect(rect.translated(-state().translation), color); +} + +void Painter::draw_line(const IntPoint& p1, const IntPoint& p2, Color color, int thickness, LineStyle style) +{ + if (color.alpha() == 0) + return; + + auto clip_rect = this->clip_rect(); + + auto point1 = p1; + point1.move_by(state().translation); + + auto point2 = p2; + point2.move_by(state().translation); + + // Special case: vertical line. + if (point1.x() == point2.x()) { + const int x = point1.x(); + if (x < clip_rect.left() || x > clip_rect.right()) + return; + if (point1.y() > point2.y()) + swap(point1, point2); + if (point1.y() > clip_rect.bottom()) + return; + if (point2.y() < clip_rect.top()) + return; + int min_y = max(point1.y(), clip_rect.top()); + int max_y = min(point2.y(), clip_rect.bottom()); + if (style == LineStyle::Dotted) { + for (int y = min_y; y <= max_y; y += thickness * 2) + draw_pixel({ x, y }, color, thickness); + } else if (style == LineStyle::Dashed) { + for (int y = min_y; y <= max_y; y += thickness * 6) { + draw_pixel({ x, y }, color, thickness); + draw_pixel({ x, min(y + thickness, max_y) }, color, thickness); + draw_pixel({ x, min(y + thickness * 2, max_y) }, color, thickness); + } + } else { + for (int y = min_y; y <= max_y; ++y) + draw_pixel({ x, y }, color, thickness); + } + return; + } + + // Special case: horizontal line. + if (point1.y() == point2.y()) { + const int y = point1.y(); + if (y < clip_rect.top() || y > clip_rect.bottom()) + return; + if (point1.x() > point2.x()) + swap(point1, point2); + if (point1.x() > clip_rect.right()) + return; + if (point2.x() < clip_rect.left()) + return; + int min_x = max(point1.x(), clip_rect.left()); + int max_x = min(point2.x(), clip_rect.right()); + if (style == LineStyle::Dotted) { + for (int x = min_x; x <= max_x; x += thickness * 2) + draw_pixel({ x, y }, color, thickness); + } else if (style == LineStyle::Dashed) { + for (int x = min_x; x <= max_x; x += thickness * 6) { + draw_pixel({ x, y }, color, thickness); + draw_pixel({ min(x + thickness, max_x), y }, color, thickness); + draw_pixel({ min(x + thickness * 2, max_x), y }, color, thickness); + } + } else { + for (int x = min_x; x <= max_x; ++x) + draw_pixel({ x, y }, color, thickness); + } + return; + } + + // FIXME: Implement dotted/dashed diagonal lines. + ASSERT(style == LineStyle::Solid); + + const double adx = abs(point2.x() - point1.x()); + const double ady = abs(point2.y() - point1.y()); + + if (adx > ady) { + if (point1.x() > point2.x()) + swap(point1, point2); + } else { + if (point1.y() > point2.y()) + swap(point1, point2); + } + + // FIXME: Implement clipping below. + const double dx = point2.x() - point1.x(); + const double dy = point2.y() - point1.y(); + double error = 0; + + if (dx > dy) { + const double y_step = dy == 0 ? 0 : (dy > 0 ? 1 : -1); + const double delta_error = fabs(dy / dx); + int y = point1.y(); + for (int x = point1.x(); x <= point2.x(); ++x) { + if (clip_rect.contains(x, y)) + draw_pixel({ x, y }, color, thickness); + error += delta_error; + if (error >= 0.5) { + y = (double)y + y_step; + error -= 1.0; + } + } + } else { + const double x_step = dx == 0 ? 0 : (dx > 0 ? 1 : -1); + const double delta_error = fabs(dx / dy); + int x = point1.x(); + for (int y = point1.y(); y <= point2.y(); ++y) { + if (clip_rect.contains(x, y)) + draw_pixel({ x, y }, color, thickness); + error += delta_error; + if (error >= 0.5) { + x = (double)x + x_step; + error -= 1.0; + } + } + } +} + +static void split_quadratic_bezier_curve(const FloatPoint& original_control, const FloatPoint& p1, const FloatPoint& p2, Function<void(const FloatPoint&, const FloatPoint&)>& callback) +{ + auto po1_midpoint = original_control + p1; + po1_midpoint /= 2; + + auto po2_midpoint = original_control + p2; + po2_midpoint /= 2; + + auto new_segment = po1_midpoint + po2_midpoint; + new_segment /= 2; + + Painter::for_each_line_segment_on_bezier_curve(po1_midpoint, p1, new_segment, callback); + Painter::for_each_line_segment_on_bezier_curve(po2_midpoint, new_segment, p2, callback); +} + +static bool can_approximate_bezier_curve(const FloatPoint& p1, const FloatPoint& p2, const FloatPoint& control) +{ + constexpr static int tolerance = 15; + + auto p1x = 3 * control.x() - 2 * p1.x() - p2.x(); + auto p1y = 3 * control.y() - 2 * p1.y() - p2.y(); + auto p2x = 3 * control.x() - 2 * p2.x() - p1.x(); + auto p2y = 3 * control.y() - 2 * p2.y() - p1.y(); + + p1x = p1x * p1x; + p1y = p1y * p1y; + p2x = p2x * p2x; + p2y = p2y * p2y; + + return max(p1x, p2x) + max(p1y, p2y) <= tolerance; +} + +void Painter::for_each_line_segment_on_bezier_curve(const FloatPoint& control_point, const FloatPoint& p1, const FloatPoint& p2, Function<void(const FloatPoint&, const FloatPoint&)>& callback) +{ + if (can_approximate_bezier_curve(p1, p2, control_point)) { + callback(p1, p2); + } else { + split_quadratic_bezier_curve(control_point, p1, p2, callback); + } +} + +void Painter::for_each_line_segment_on_bezier_curve(const FloatPoint& control_point, const FloatPoint& p1, const FloatPoint& p2, Function<void(const FloatPoint&, const FloatPoint&)>&& callback) +{ + for_each_line_segment_on_bezier_curve(control_point, p1, p2, callback); +} + +static void split_elliptical_arc(const FloatPoint& p1, const FloatPoint& p2, const FloatPoint& center, const FloatPoint radii, float x_axis_rotation, float theta_1, float theta_delta, Function<void(const FloatPoint&, const FloatPoint&)>& callback) +{ + auto half_theta_delta = theta_delta / 2; + auto theta_mid = theta_1 + half_theta_delta; + + auto xc = cosf(x_axis_rotation); + auto xs = sinf(x_axis_rotation); + auto tc = cosf(theta_1 + half_theta_delta); + auto ts = sinf(theta_1 + half_theta_delta); + + auto x2 = xc * radii.x() * tc - xs * radii.y() * ts + center.x(); + auto y2 = xs * radii.x() * tc + xc * radii.y() * ts + center.y(); + + FloatPoint mid_point = { x2, y2 }; + + Painter::for_each_line_segment_on_elliptical_arc(p1, mid_point, center, radii, x_axis_rotation, theta_1, half_theta_delta, callback); + Painter::for_each_line_segment_on_elliptical_arc(mid_point, p2, center, radii, x_axis_rotation, theta_mid, half_theta_delta, callback); +} + +static bool can_approximate_elliptical_arc(const FloatPoint& p1, const FloatPoint& p2, const FloatPoint& center, const FloatPoint radii, float x_axis_rotation, float theta_1, float theta_delta) +{ + constexpr static float tolerance = 1; + + auto half_theta_delta = theta_delta / 2.0f; + + auto xc = cosf(x_axis_rotation); + auto xs = sinf(x_axis_rotation); + auto tc = cosf(theta_1 + half_theta_delta); + auto ts = sinf(theta_1 + half_theta_delta); + + auto x2 = xc * radii.x() * tc - xs * radii.y() * ts + center.x(); + auto y2 = xs * radii.x() * tc + xc * radii.y() * ts + center.y(); + + auto ellipse_mid_point = FloatPoint { x2, y2 }; + auto line_mid_point = p1 + (p2 - p1) / 2.0f; + + return ellipse_mid_point.distance_from(line_mid_point) < tolerance; +} + +void Painter::draw_quadratic_bezier_curve(const IntPoint& control_point, const IntPoint& p1, const IntPoint& p2, Color color, int thickness, LineStyle style) +{ + for_each_line_segment_on_bezier_curve(FloatPoint(control_point), FloatPoint(p1), FloatPoint(p2), [&](const FloatPoint& fp1, const FloatPoint& fp2) { + draw_line(IntPoint(fp1.x(), fp1.y()), IntPoint(fp2.x(), fp2.y()), color, thickness, style); + }); +} + +void Painter::for_each_line_segment_on_elliptical_arc(const FloatPoint& p1, const FloatPoint& p2, const FloatPoint& center, const FloatPoint radii, float x_axis_rotation, float theta_1, float theta_delta, Function<void(const FloatPoint&, const FloatPoint&)>& callback) +{ + if (can_approximate_elliptical_arc(p1, p2, center, radii, x_axis_rotation, theta_1, theta_delta)) { + callback(p1, p2); + } else { + split_elliptical_arc(p1, p2, center, radii, x_axis_rotation, theta_1, theta_delta, callback); + } +} + +void Painter::for_each_line_segment_on_elliptical_arc(const FloatPoint& p1, const FloatPoint& p2, const FloatPoint& center, const FloatPoint radii, float x_axis_rotation, float theta_1, float theta_delta, Function<void(const FloatPoint&, const FloatPoint&)>&& callback) +{ + for_each_line_segment_on_elliptical_arc(p1, p2, center, radii, x_axis_rotation, theta_1, theta_delta, callback); +} + +void Painter::draw_elliptical_arc(const IntPoint& p1, const IntPoint& p2, const IntPoint& center, const FloatPoint& radii, float x_axis_rotation, float theta_1, float theta_delta, Color color, int thickness, LineStyle style) +{ + for_each_line_segment_on_elliptical_arc(FloatPoint(p1), FloatPoint(p2), FloatPoint(center), radii, x_axis_rotation, theta_1, theta_delta, [&](const FloatPoint& fp1, const FloatPoint& fp2) { + draw_line(IntPoint(fp1.x(), fp1.y()), IntPoint(fp2.x(), fp2.y()), color, thickness, style); + }); +} + +void Painter::add_clip_rect(const IntRect& rect) +{ + state().clip_rect.intersect(rect.translated(translation())); + state().clip_rect.intersect(m_target->rect()); +} + +void Painter::clear_clip_rect() +{ + state().clip_rect = m_clip_origin; +} + +PainterStateSaver::PainterStateSaver(Painter& painter) + : m_painter(painter) +{ + m_painter.save(); +} + +PainterStateSaver::~PainterStateSaver() +{ + m_painter.restore(); +} + +void Painter::stroke_path(const Path& path, Color color, int thickness) +{ + FloatPoint cursor; + + for (auto& segment : path.segments()) { + switch (segment.type()) { + case Segment::Type::Invalid: + ASSERT_NOT_REACHED(); + break; + case Segment::Type::MoveTo: + cursor = segment.point(); + break; + case Segment::Type::LineTo: + draw_line(cursor.to_type<int>(), segment.point().to_type<int>(), color, thickness); + cursor = segment.point(); + break; + case Segment::Type::QuadraticBezierCurveTo: { + auto& through = static_cast<const QuadraticBezierCurveSegment&>(segment).through(); + draw_quadratic_bezier_curve(through.to_type<int>(), cursor.to_type<int>(), segment.point().to_type<int>(), color, thickness); + cursor = segment.point(); + break; + } + case Segment::Type::EllipticalArcTo: + auto& arc = static_cast<const EllipticalArcSegment&>(segment); + draw_elliptical_arc(cursor.to_type<int>(), segment.point().to_type<int>(), arc.center().to_type<int>(), arc.radii(), arc.x_axis_rotation(), arc.theta_1(), arc.theta_delta(), color, thickness); + cursor = segment.point(); + break; + } + } +} + +//#define FILL_PATH_DEBUG + +[[maybe_unused]] static void approximately_place_on_int_grid(FloatPoint ffrom, FloatPoint fto, IntPoint& from, IntPoint& to, Optional<IntPoint> previous_to) +{ + auto diffs = fto - ffrom; + // Truncate all first (round down). + from = ffrom.to_type<int>(); + to = fto.to_type<int>(); + // There are 16 possible configurations, by deciding to round each + // coord up or down (and there are four coords, from.x from.y to.x to.y) + // we will simply choose one which most closely matches the correct slope + // with the following heuristic: + // - if the x diff is positive or zero (that is, a right-to-left slant), round 'from.x' up and 'to.x' down. + // - if the x diff is negative (that is, a left-to-right slant), round 'from.x' down and 'to.x' up. + // Note that we do not need to touch the 'y' attribute, as that is our scanline. + if (diffs.x() >= 0) { + from.set_x(from.x() + 1); + } else { + to.set_x(to.x() + 1); + } + if (previous_to.has_value() && from.x() != previous_to.value().x()) // The points have to line up, since we're using these lines to fill a shape. + from.set_x(previous_to.value().x()); +} + +void Painter::fill_path(Path& path, Color color, WindingRule winding_rule) +{ + const auto& segments = path.split_lines(); + + if (segments.size() == 0) + return; + + Vector<Path::SplitLineSegment> active_list; + active_list.ensure_capacity(segments.size()); + + // first, grab the segments for the very first scanline + int first_y = path.bounding_box().bottom_right().y() + 1; + int last_y = path.bounding_box().top_left().y() - 1; + float scanline = first_y; + + size_t last_active_segment { 0 }; + + for (auto& segment : segments) { + if (segment.maximum_y != scanline) + break; + active_list.append(segment); + ++last_active_segment; + } + + auto is_inside_shape = [winding_rule](int winding_number) { + if (winding_rule == WindingRule::Nonzero) + return winding_number != 0; + + if (winding_rule == WindingRule::EvenOdd) + return winding_number % 2 == 0; + + ASSERT_NOT_REACHED(); + }; + + auto increment_winding = [winding_rule](int& winding_number, const IntPoint& from, const IntPoint& to) { + if (winding_rule == WindingRule::EvenOdd) { + ++winding_number; + return; + } + + if (winding_rule == WindingRule::Nonzero) { + if (from.dy_relative_to(to) < 0) + ++winding_number; + else + --winding_number; + return; + } + + ASSERT_NOT_REACHED(); + }; + + while (scanline >= last_y) { + Optional<IntPoint> previous_to; + if (active_list.size()) { + // sort the active list by 'x' from right to left + quick_sort(active_list, [](const auto& line0, const auto& line1) { + return line1.x < line0.x; + }); +#ifdef FILL_PATH_DEBUG + if ((int)scanline % 10 == 0) { + draw_text(IntRect(active_list.last().x - 20, scanline, 20, 10), String::number((int)scanline)); + } +#endif + + if (active_list.size() > 1) { + auto winding_number { 0 }; + for (size_t i = 1; i < active_list.size(); ++i) { + auto& previous = active_list[i - 1]; + auto& current = active_list[i]; + + IntPoint from, to; + IntPoint truncated_from { previous.x, scanline }; + IntPoint truncated_to { current.x, scanline }; + approximately_place_on_int_grid({ previous.x, scanline }, { current.x, scanline }, from, to, previous_to); + + if (is_inside_shape(winding_number)) { + // The points between this segment and the previous are + // inside the shape +#ifdef FILL_PATH_DEBUG + dbg() << "y=" << scanline << ": " << winding_number << " at " << i << ": " << from << " -- " << to; +#endif + draw_line(from, to, color, 1); + } + + auto is_passing_through_maxima = scanline == previous.maximum_y + || scanline == previous.minimum_y + || scanline == current.maximum_y + || scanline == current.minimum_y; + + auto is_passing_through_vertex = false; + + if (is_passing_through_maxima) { + is_passing_through_vertex = previous.x == current.x; + } + + if (!is_passing_through_vertex || previous.inverse_slope * current.inverse_slope < 0) + increment_winding(winding_number, truncated_from, truncated_to); + + // update the x coord + active_list[i - 1].x -= active_list[i - 1].inverse_slope; + } + active_list.last().x -= active_list.last().inverse_slope; + } else { + auto point = IntPoint(active_list[0].x, scanline); + draw_line(point, point, color); + + // update the x coord + active_list.first().x -= active_list.first().inverse_slope; + } + } + + --scanline; + // remove any edge that goes out of bound from the active list + for (size_t i = 0, count = active_list.size(); i < count; ++i) { + if (scanline <= active_list[i].minimum_y) { + active_list.remove(i); + --count; + --i; + } + } + for (size_t j = last_active_segment; j < segments.size(); ++j, ++last_active_segment) { + auto& segment = segments[j]; + if (segment.maximum_y < scanline) + break; + if (segment.minimum_y >= scanline) + continue; + + active_list.append(segment); + } + } + +#ifdef FILL_PATH_DEBUG + size_t i { 0 }; + for (auto& segment : segments) { + draw_line(Point<int>(segment.from), Point<int>(segment.to), Color::from_hsv(i++ * 360.0 / segments.size(), 1.0, 1.0), 1); + } +#endif +} + +void Painter::blit_disabled(const IntPoint& location, const Gfx::Bitmap& bitmap, const IntRect& rect, const Palette& palette) +{ + auto bright_color = palette.threed_highlight(); + auto dark_color = palette.threed_shadow1(); + blit_filtered(location.translated(1, 1), bitmap, rect, [&](auto) { + return bright_color; + }); + blit_filtered(location, bitmap, rect, [&](Color src) { + int gray = src.to_grayscale().red(); + if (gray > 160) + return bright_color; + return dark_color; + }); +} + +} diff --git a/Userland/Libraries/LibGfx/Painter.h b/Userland/Libraries/LibGfx/Painter.h new file mode 100644 index 0000000000..8ae33fc207 --- /dev/null +++ b/Userland/Libraries/LibGfx/Painter.h @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Forward.h> +#include <AK/NonnullRefPtr.h> +#include <AK/Vector.h> +#include <LibGfx/Color.h> +#include <LibGfx/Forward.h> +#include <LibGfx/Point.h> +#include <LibGfx/Rect.h> +#include <LibGfx/Size.h> +#include <LibGfx/TextAlignment.h> +#include <LibGfx/TextElision.h> + +namespace Gfx { + +class Painter { +public: + explicit Painter(Gfx::Bitmap&); + ~Painter(); + + enum class LineStyle { + Solid, + Dotted, + Dashed, + }; + + void clear_rect(const IntRect&, Color); + void fill_rect(const IntRect&, Color); + void fill_rect_with_dither_pattern(const IntRect&, Color, Color); + void fill_rect_with_checkerboard(const IntRect&, const IntSize&, Color color_dark, Color color_light); + void fill_rect_with_gradient(Orientation, const IntRect&, Color gradient_start, Color gradient_end); + void fill_rect_with_gradient(const IntRect&, Color gradient_start, Color gradient_end); + void fill_ellipse(const IntRect&, Color); + void draw_rect(const IntRect&, Color, bool rough = false); + void draw_focus_rect(const IntRect&, Color); + void draw_bitmap(const IntPoint&, const CharacterBitmap&, Color = Color()); + void draw_bitmap(const IntPoint&, const GlyphBitmap&, Color = Color()); + void draw_scaled_bitmap(const IntRect& dst_rect, const Gfx::Bitmap&, const IntRect& src_rect, float opacity = 1.0f); + void draw_triangle(const IntPoint&, const IntPoint&, const IntPoint&, Color); + void draw_ellipse_intersecting(const IntRect&, Color, int thickness = 1); + void set_pixel(const IntPoint&, Color); + void set_pixel(int x, int y, Color color) { set_pixel({ x, y }, color); } + void draw_line(const IntPoint&, const IntPoint&, Color, int thickness = 1, LineStyle style = LineStyle::Solid); + void draw_quadratic_bezier_curve(const IntPoint& control_point, const IntPoint&, const IntPoint&, Color, int thickness = 1, LineStyle style = LineStyle::Solid); + void draw_elliptical_arc(const IntPoint& p1, const IntPoint& p2, const IntPoint& center, const FloatPoint& radii, float x_axis_rotation, float theta_1, float theta_delta, Color, int thickness = 1, LineStyle style = LineStyle::Solid); + void blit(const IntPoint&, const Gfx::Bitmap&, const IntRect& src_rect, float opacity = 1.0f); + void blit_dimmed(const IntPoint&, const Gfx::Bitmap&, const IntRect& src_rect); + void blit_brightened(const IntPoint&, const Gfx::Bitmap&, const IntRect& src_rect); + void blit_filtered(const IntPoint&, const Gfx::Bitmap&, const IntRect& src_rect, Function<Color(Color)>); + void draw_tiled_bitmap(const IntRect& dst_rect, const Gfx::Bitmap&); + void blit_offset(const IntPoint&, const Gfx::Bitmap&, const IntRect& src_rect, const IntPoint&); + void blit_scaled(const IntRect&, const Gfx::Bitmap&, const IntRect&, float, float); + void blit_disabled(const IntPoint&, const Gfx::Bitmap&, const IntRect&, const Palette&); + void draw_text(const IntRect&, const StringView&, const Font&, TextAlignment = TextAlignment::TopLeft, Color = Color::Black, TextElision = TextElision::None); + void draw_text(const IntRect&, const StringView&, TextAlignment = TextAlignment::TopLeft, Color = Color::Black, TextElision = TextElision::None); + void draw_text(const IntRect&, const Utf32View&, const Font&, TextAlignment = TextAlignment::TopLeft, Color = Color::Black, TextElision = TextElision::None); + void draw_text(const IntRect&, const Utf32View&, TextAlignment = TextAlignment::TopLeft, Color = Color::Black, TextElision = TextElision::None); + void draw_text(Function<void(const IntRect&, u32)>, const IntRect&, const StringView&, const Font&, TextAlignment = TextAlignment::TopLeft, TextElision = TextElision::None); + void draw_text(Function<void(const IntRect&, u32)>, const IntRect&, const Utf8View&, const Font&, TextAlignment = TextAlignment::TopLeft, TextElision = TextElision::None); + void draw_text(Function<void(const IntRect&, u32)>, const IntRect&, const Utf32View&, const Font&, TextAlignment = TextAlignment::TopLeft, TextElision = TextElision::None); + void draw_glyph(const IntPoint&, u32, Color); + void draw_glyph(const IntPoint&, u32, const Font&, Color); + void draw_emoji(const IntPoint&, const Gfx::Bitmap&, const Font&); + void draw_glyph_or_emoji(const IntPoint&, u32 code_point, const Font&, Color); + + static void for_each_line_segment_on_bezier_curve(const FloatPoint& control_point, const FloatPoint& p1, const FloatPoint& p2, Function<void(const FloatPoint&, const FloatPoint&)>&); + static void for_each_line_segment_on_bezier_curve(const FloatPoint& control_point, const FloatPoint& p1, const FloatPoint& p2, Function<void(const FloatPoint&, const FloatPoint&)>&&); + + static void for_each_line_segment_on_elliptical_arc(const FloatPoint& p1, const FloatPoint& p2, const FloatPoint& center, const FloatPoint radii, float x_axis_rotation, float theta_1, float theta_delta, Function<void(const FloatPoint&, const FloatPoint&)>&); + static void for_each_line_segment_on_elliptical_arc(const FloatPoint& p1, const FloatPoint& p2, const FloatPoint& center, const FloatPoint radii, float x_axis_rotation, float theta_1, float theta_delta, Function<void(const FloatPoint&, const FloatPoint&)>&&); + + void stroke_path(const Path&, Color, int thickness); + + enum class WindingRule { + Nonzero, + EvenOdd, + }; + void fill_path(Path&, Color, WindingRule rule = WindingRule::Nonzero); + + const Font& font() const { return *state().font; } + void set_font(const Font& font) { state().font = &font; } + + enum class DrawOp { + Copy, + Xor, + Invert + }; + void set_draw_op(DrawOp op) { state().draw_op = op; } + DrawOp draw_op() const { return state().draw_op; } + + void add_clip_rect(const IntRect& rect); + void clear_clip_rect(); + IntRect clip_rect() const { return state().clip_rect; } + + void translate(int dx, int dy) { state().translation.move_by(dx, dy); } + void translate(const IntPoint& delta) { state().translation.move_by(delta); } + + IntPoint translation() const { return state().translation; } + + Gfx::Bitmap* target() { return m_target.ptr(); } + + void save() { m_state_stack.append(m_state_stack.last()); } + void restore() + { + ASSERT(m_state_stack.size() > 1); + m_state_stack.take_last(); + } + +protected: + void set_pixel_with_draw_op(u32& pixel, const Color&); + void fill_scanline_with_draw_op(int y, int x, int width, const Color& color); + void fill_rect_with_draw_op(const IntRect&, Color); + void blit_with_alpha(const IntPoint&, const Gfx::Bitmap&, const IntRect& src_rect); + void blit_with_opacity(const IntPoint&, const Gfx::Bitmap&, const IntRect& src_rect, float opacity); + void draw_pixel(const IntPoint&, Color, int thickness = 1); + + struct State { + const Font* font; + IntPoint translation; + IntRect clip_rect; + DrawOp draw_op; + }; + + State& state() { return m_state_stack.last(); } + const State& state() const { return m_state_stack.last(); } + + IntRect m_clip_origin; + NonnullRefPtr<Gfx::Bitmap> m_target; + Vector<State, 4> m_state_stack; +}; + +class PainterStateSaver { +public: + explicit PainterStateSaver(Painter&); + ~PainterStateSaver(); + +private: + Painter& m_painter; +}; + +} diff --git a/Userland/Libraries/LibGfx/Palette.cpp b/Userland/Libraries/LibGfx/Palette.cpp new file mode 100644 index 0000000000..de6915d7ba --- /dev/null +++ b/Userland/Libraries/LibGfx/Palette.cpp @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <AK/Badge.h> +#include <AK/SharedBuffer.h> +#include <LibGfx/Palette.h> +#include <string.h> + +namespace Gfx { + +NonnullRefPtr<PaletteImpl> PaletteImpl::create_with_shared_buffer(SharedBuffer& buffer) +{ + return adopt(*new PaletteImpl(buffer)); +} + +PaletteImpl::PaletteImpl(SharedBuffer& buffer) + : m_theme_buffer(buffer) +{ +} + +Palette::Palette(const PaletteImpl& impl) + : m_impl(impl) +{ +} + +Palette::~Palette() +{ +} + +const SystemTheme& PaletteImpl::theme() const +{ + return *m_theme_buffer->data<SystemTheme>(); +} + +Color PaletteImpl::color(ColorRole role) const +{ + ASSERT((int)role < (int)ColorRole::__Count); + return Color::from_rgba(theme().color[(int)role]); +} + +int PaletteImpl::metric(MetricRole role) const +{ + ASSERT((int)role < (int)MetricRole::__Count); + return theme().metric[(int)role]; +} + +String PaletteImpl::path(PathRole role) const +{ + ASSERT((int)role < (int)PathRole::__Count); + return theme().path[(int)role]; +} + +NonnullRefPtr<PaletteImpl> PaletteImpl::clone() const +{ + auto new_theme_buffer = SharedBuffer::create_with_size(m_theme_buffer->size()); + memcpy(new_theme_buffer->data<SystemTheme>(), &theme(), m_theme_buffer->size()); + return adopt(*new PaletteImpl(*new_theme_buffer)); +} + +void Palette::set_color(ColorRole role, Color color) +{ + if (m_impl->ref_count() != 1) + m_impl = m_impl->clone(); + auto& theme = const_cast<SystemTheme&>(impl().theme()); + theme.color[(int)role] = color.value(); +} + +void Palette::set_metric(MetricRole role, int value) +{ + if (m_impl->ref_count() != 1) + m_impl = m_impl->clone(); + auto& theme = const_cast<SystemTheme&>(impl().theme()); + theme.metric[(int)role] = value; +} + +void Palette::set_path(PathRole role, String path) +{ + if (m_impl->ref_count() != 1) + m_impl = m_impl->clone(); + auto& theme = const_cast<SystemTheme&>(impl().theme()); + memcpy(theme.path[(int)role], path.characters(), min(path.length() + 1, sizeof(theme.path[(int)role]))); + theme.path[(int)role][sizeof(theme.path[(int)role]) - 1] = '\0'; +} + +PaletteImpl::~PaletteImpl() +{ +} + +void PaletteImpl::replace_internal_buffer(Badge<GUI::Application>, SharedBuffer& buffer) +{ + m_theme_buffer = buffer; +} + +} diff --git a/Userland/Libraries/LibGfx/Palette.h b/Userland/Libraries/LibGfx/Palette.h new file mode 100644 index 0000000000..dfd47e90ff --- /dev/null +++ b/Userland/Libraries/LibGfx/Palette.h @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Forward.h> +#include <AK/Noncopyable.h> +#include <AK/RefCounted.h> +#include <AK/RefPtr.h> +#include <LibGUI/Forward.h> +#include <LibGfx/SystemTheme.h> + +namespace Gfx { + +class PaletteImpl : public RefCounted<PaletteImpl> { + AK_MAKE_NONCOPYABLE(PaletteImpl); + AK_MAKE_NONMOVABLE(PaletteImpl); + +public: + ~PaletteImpl(); + static NonnullRefPtr<PaletteImpl> create_with_shared_buffer(SharedBuffer&); + NonnullRefPtr<PaletteImpl> clone() const; + + Color color(ColorRole) const; + int metric(MetricRole) const; + String path(PathRole) const; + const SystemTheme& theme() const; + + void replace_internal_buffer(Badge<GUI::Application>, SharedBuffer& buffer); + +private: + explicit PaletteImpl(SharedBuffer&); + + RefPtr<SharedBuffer> m_theme_buffer; +}; + +class Palette { + +public: + explicit Palette(const PaletteImpl&); + ~Palette(); + + Color window() const { return color(ColorRole::Window); } + Color window_text() const { return color(ColorRole::WindowText); } + Color selection() const { return color(ColorRole::Selection); } + Color selection_text() const { return color(ColorRole::SelectionText); } + Color inactive_selection() const { return color(ColorRole::InactiveSelection); } + Color inactive_selection_text() const { return color(ColorRole::InactiveSelectionText); } + Color desktop_background() const { return color(ColorRole::DesktopBackground); } + Color active_window_border1() const { return color(ColorRole::ActiveWindowBorder1); } + Color active_window_border2() const { return color(ColorRole::ActiveWindowBorder2); } + Color active_window_title() const { return color(ColorRole::ActiveWindowTitle); } + Color active_window_title_stripes() const { return color(ColorRole::ActiveWindowTitleStripes); } + Color active_window_title_shadow() const { return color(ColorRole::ActiveWindowTitleShadow); } + Color inactive_window_border1() const { return color(ColorRole::InactiveWindowBorder1); } + Color inactive_window_border2() const { return color(ColorRole::InactiveWindowBorder2); } + Color inactive_window_title() const { return color(ColorRole::InactiveWindowTitle); } + Color inactive_window_title_stripes() const { return color(ColorRole::InactiveWindowTitleStripes); } + Color inactive_window_title_shadow() const { return color(ColorRole::InactiveWindowTitleShadow); } + Color moving_window_border1() const { return color(ColorRole::MovingWindowBorder1); } + Color moving_window_border2() const { return color(ColorRole::MovingWindowBorder2); } + Color moving_window_title() const { return color(ColorRole::MovingWindowTitle); } + Color moving_window_title_stripes() const { return color(ColorRole::MovingWindowTitleStripes); } + Color moving_window_title_shadow() const { return color(ColorRole::MovingWindowTitleShadow); } + Color highlight_window_border1() const { return color(ColorRole::HighlightWindowBorder1); } + Color highlight_window_border2() const { return color(ColorRole::HighlightWindowBorder2); } + Color highlight_window_title() const { return color(ColorRole::HighlightWindowTitle); } + Color highlight_window_title_stripes() const { return color(ColorRole::HighlightWindowTitleStripes); } + Color highlight_window_title_shadow() const { return color(ColorRole::HighlightWindowTitleShadow); } + Color highlight_searching() const { return color(ColorRole::HighlightSearching); } + Color highlight_searching_text() const { return color(ColorRole::HighlightSearchingText); } + Color menu_stripe() const { return color(ColorRole::MenuStripe); } + Color menu_base() const { return color(ColorRole::MenuBase); } + Color menu_base_text() const { return color(ColorRole::MenuBaseText); } + Color menu_selection() const { return color(ColorRole::MenuSelection); } + Color menu_selection_text() const { return color(ColorRole::MenuSelectionText); } + Color base() const { return color(ColorRole::Base); } + Color base_text() const { return color(ColorRole::BaseText); } + Color button() const { return color(ColorRole::Button); } + Color button_text() const { return color(ColorRole::ButtonText); } + Color threed_highlight() const { return color(ColorRole::ThreedHighlight); } + Color threed_shadow1() const { return color(ColorRole::ThreedShadow1); } + Color threed_shadow2() const { return color(ColorRole::ThreedShadow2); } + Color hover_highlight() const { return color(ColorRole::HoverHighlight); } + Color rubber_band_fill() const { return color(ColorRole::RubberBandFill); } + Color rubber_band_border() const { return color(ColorRole::RubberBandBorder); } + Color ruler() const { return color(ColorRole::Ruler); } + Color ruler_border() const { return color(ColorRole::RulerBorder); } + Color ruler_active_text() const { return color(ColorRole::RulerActiveText); } + Color ruler_inactive_text() const { return color(ColorRole::RulerInactiveText); } + Color text_cursor() const { return color(ColorRole::TextCursor); } + Color focus_outline() const { return color(ColorRole::FocusOutline); } + + Color link() const { return color(ColorRole::Link); } + Color active_link() const { return color(ColorRole::ActiveLink); } + Color visited_link() const { return color(ColorRole::VisitedLink); } + + Color syntax_comment() const { return color(ColorRole::SyntaxComment); } + Color syntax_number() const { return color(ColorRole::SyntaxNumber); } + Color syntax_string() const { return color(ColorRole::SyntaxString); } + Color syntax_identifier() const { return color(ColorRole::SyntaxIdentifier); } + Color syntax_type() const { return color(ColorRole::SyntaxType); } + Color syntax_punctuation() const { return color(ColorRole::SyntaxPunctuation); } + Color syntax_operator() const { return color(ColorRole::SyntaxOperator); } + Color syntax_keyword() const { return color(ColorRole::SyntaxKeyword); } + Color syntax_control_keyword() const { return color(ColorRole::SyntaxControlKeyword); } + Color syntax_preprocessor_statement() const { return color(ColorRole::SyntaxPreprocessorStatement); } + Color syntax_preprocessor_value() const { return color(ColorRole::SyntaxPreprocessorValue); } + + int window_title_height() const { return metric(MetricRole::TitleHeight); } + int window_title_button_width() const { return metric(MetricRole::TitleButtonWidth); } + int window_title_button_height() const { return metric(MetricRole::TitleButtonHeight); } + + String title_button_icons_path() const { return path(PathRole::TitleButtonIcons); } + + Color color(ColorRole role) const { return m_impl->color(role); } + int metric(MetricRole role) const { return m_impl->metric(role); } + String path(PathRole role) const { return m_impl->path(role); } + + void set_color(ColorRole, Color); + void set_metric(MetricRole, int); + void set_path(PathRole, String); + + const SystemTheme& theme() const { return m_impl->theme(); } + + PaletteImpl& impl() { return *m_impl; } + const PaletteImpl& impl() const { return *m_impl; } + +private: + NonnullRefPtr<PaletteImpl> m_impl; +}; + +} + +using Gfx::Palette; diff --git a/Userland/Libraries/LibGfx/Path.cpp b/Userland/Libraries/LibGfx/Path.cpp new file mode 100644 index 0000000000..5fe2bb2f63 --- /dev/null +++ b/Userland/Libraries/LibGfx/Path.cpp @@ -0,0 +1,234 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <AK/Function.h> +#include <AK/HashTable.h> +#include <AK/QuickSort.h> +#include <AK/StringBuilder.h> +#include <LibGfx/Painter.h> +#include <LibGfx/Path.h> + +namespace Gfx { + +void Path::close() +{ + if (m_segments.size() <= 1) + return; + + invalidate_split_lines(); + + auto& last_point = m_segments.last().point(); + + for (ssize_t i = m_segments.size() - 1; i >= 0; --i) { + auto& segment = m_segments[i]; + if (segment.type() == Segment::Type::MoveTo) { + if (last_point == segment.point()) + return; + append_segment<LineSegment>(segment.point()); + return; + } + } +} + +void Path::close_all_subpaths() +{ + if (m_segments.size() <= 1) + return; + + invalidate_split_lines(); + + Optional<FloatPoint> cursor, start_of_subpath; + bool is_first_point_in_subpath { false }; + + for (auto& segment : m_segments) { + switch (segment.type()) { + case Segment::Type::MoveTo: { + if (cursor.has_value() && !is_first_point_in_subpath) { + // This is a move from a subpath to another + // connect the two ends of this subpath before + // moving on to the next one + ASSERT(start_of_subpath.has_value()); + + append_segment<MoveSegment>(cursor.value()); + append_segment<LineSegment>(start_of_subpath.value()); + } + is_first_point_in_subpath = true; + cursor = segment.point(); + break; + } + case Segment::Type::LineTo: + case Segment::Type::QuadraticBezierCurveTo: + case Segment::Type::EllipticalArcTo: + if (is_first_point_in_subpath) { + start_of_subpath = cursor; + is_first_point_in_subpath = false; + } + cursor = segment.point(); + break; + case Segment::Type::Invalid: + ASSERT_NOT_REACHED(); + break; + } + } +} + +String Path::to_string() const +{ + StringBuilder builder; + builder.append("Path { "); + for (auto& segment : m_segments) { + switch (segment.type()) { + case Segment::Type::MoveTo: + builder.append("MoveTo"); + break; + case Segment::Type::LineTo: + builder.append("LineTo"); + break; + case Segment::Type::QuadraticBezierCurveTo: + builder.append("QuadraticBezierCurveTo"); + break; + case Segment::Type::EllipticalArcTo: + builder.append("EllipticalArcTo"); + break; + case Segment::Type::Invalid: + builder.append("Invalid"); + break; + } + builder.appendf("(%s", segment.point().to_string().characters()); + + switch (segment.type()) { + case Segment::Type::QuadraticBezierCurveTo: + builder.append(", "); + builder.append(static_cast<const QuadraticBezierCurveSegment&>(segment).through().to_string()); + break; + case Segment::Type::EllipticalArcTo: { + auto& arc = static_cast<const EllipticalArcSegment&>(segment); + builder.appendf(", %s, %s, %f, %f, %f", + arc.radii().to_string().characters(), + arc.center().to_string().characters(), + arc.x_axis_rotation(), + arc.theta_1(), + arc.theta_delta()); + break; + } + default: + break; + } + + builder.append(") "); + } + builder.append("}"); + return builder.to_string(); +} + +void Path::segmentize_path() +{ + Vector<SplitLineSegment> segments; + float min_x = 0; + float min_y = 0; + float max_x = 0; + float max_y = 0; + + auto add_point_to_bbox = [&](const Gfx::FloatPoint& point) { + float x = point.x(); + float y = point.y(); + min_x = min(min_x, x); + min_y = min(min_y, y); + max_x = max(max_x, x); + max_y = max(max_y, y); + }; + + auto add_line = [&](const auto& p0, const auto& p1) { + float ymax = p0.y(), ymin = p1.y(), x_of_ymin = p1.x(), x_of_ymax = p0.x(); + auto slope = p0.x() == p1.x() ? 0 : ((float)(p0.y() - p1.y())) / ((float)(p0.x() - p1.x())); + if (p0.y() < p1.y()) { + swap(ymin, ymax); + swap(x_of_ymin, x_of_ymax); + } + + segments.append({ FloatPoint(p0.x(), p0.y()), + FloatPoint(p1.x(), p1.y()), + slope == 0 ? 0 : 1 / slope, + x_of_ymin, + ymax, ymin, x_of_ymax }); + + add_point_to_bbox(p1); + }; + + FloatPoint cursor { 0, 0 }; + bool first = true; + + for (auto& segment : m_segments) { + switch (segment.type()) { + case Segment::Type::MoveTo: + if (first) { + min_x = segment.point().x(); + min_y = segment.point().y(); + max_x = segment.point().x(); + max_y = segment.point().y(); + } else { + add_point_to_bbox(segment.point()); + } + cursor = segment.point(); + break; + case Segment::Type::LineTo: { + add_line(cursor, segment.point()); + cursor = segment.point(); + break; + } + case Segment::Type::QuadraticBezierCurveTo: { + auto& control = static_cast<QuadraticBezierCurveSegment&>(segment).through(); + Painter::for_each_line_segment_on_bezier_curve(control, cursor, segment.point(), [&](const FloatPoint& p0, const FloatPoint& p1) { + add_line(p0, p1); + }); + cursor = segment.point(); + break; + } + case Segment::Type::EllipticalArcTo: { + auto& arc = static_cast<EllipticalArcSegment&>(segment); + Painter::for_each_line_segment_on_elliptical_arc(cursor, arc.point(), arc.center(), arc.radii(), arc.x_axis_rotation(), arc.theta_1(), arc.theta_delta(), [&](const FloatPoint& p0, const FloatPoint& p1) { + add_line(p0, p1); + }); + cursor = segment.point(); + break; + } + case Segment::Type::Invalid: + ASSERT_NOT_REACHED(); + } + + first = false; + } + + // sort segments by ymax + quick_sort(segments, [](const auto& line0, const auto& line1) { + return line1.maximum_y < line0.maximum_y; + }); + + m_split_lines = move(segments); + m_bounding_box = Gfx::FloatRect { min_x, min_y, max_x - min_x, max_y - min_y }; +} + +} diff --git a/Userland/Libraries/LibGfx/Path.h b/Userland/Libraries/LibGfx/Path.h new file mode 100644 index 0000000000..7a8dfe9567 --- /dev/null +++ b/Userland/Libraries/LibGfx/Path.h @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/HashMap.h> +#include <AK/NonnullRefPtrVector.h> +#include <AK/Optional.h> +#include <AK/String.h> +#include <AK/Vector.h> +#include <LibGfx/Forward.h> +#include <LibGfx/Point.h> +#include <LibGfx/Rect.h> + +namespace Gfx { + +class Segment : public RefCounted<Segment> { +public: + enum class Type { + Invalid, + MoveTo, + LineTo, + QuadraticBezierCurveTo, + EllipticalArcTo, + }; + + Segment(const FloatPoint& point) + : m_point(point) + { + } + + virtual ~Segment() = default; + + const FloatPoint& point() const { return m_point; } + virtual Type type() const = 0; + +protected: + FloatPoint m_point; +}; + +class MoveSegment final : public Segment { +public: + MoveSegment(const FloatPoint& point) + : Segment(point) + { + } + +private: + virtual Type type() const override { return Segment::Type::MoveTo; } +}; + +class LineSegment final : public Segment { +public: + LineSegment(const FloatPoint& point) + : Segment(point) + { + } + + virtual ~LineSegment() override = default; + +private: + virtual Type type() const override { return Segment::Type::LineTo; } +}; + +class QuadraticBezierCurveSegment final : public Segment { +public: + QuadraticBezierCurveSegment(const FloatPoint& point, const FloatPoint& through) + : Segment(point) + , m_through(through) + { + } + + virtual ~QuadraticBezierCurveSegment() override = default; + + const FloatPoint& through() const { return m_through; } + +private: + virtual Type type() const override { return Segment::Type::QuadraticBezierCurveTo; } + + FloatPoint m_through; +}; + +class EllipticalArcSegment final : public Segment { +public: + EllipticalArcSegment(const FloatPoint& point, const FloatPoint& center, const FloatPoint radii, float x_axis_rotation, float theta_1, float theta_delta) + : Segment(point) + , m_center(center) + , m_radii(radii) + , m_x_axis_rotation(x_axis_rotation) + , m_theta_1(theta_1) + , m_theta_delta(theta_delta) + { + } + + virtual ~EllipticalArcSegment() override = default; + + const FloatPoint& center() const { return m_center; } + const FloatPoint& radii() const { return m_radii; } + float x_axis_rotation() const { return m_x_axis_rotation; } + float theta_1() const { return m_theta_1; } + float theta_delta() const { return m_theta_delta; } + +private: + virtual Type type() const override { return Segment::Type::EllipticalArcTo; } + + FloatPoint m_center; + FloatPoint m_radii; + float m_x_axis_rotation; + float m_theta_1; + float m_theta_delta; +}; + +class Path { +public: + Path() { } + + void move_to(const FloatPoint& point) + { + append_segment<MoveSegment>(point); + } + + void line_to(const FloatPoint& point) + { + append_segment<LineSegment>(point); + invalidate_split_lines(); + } + + void quadratic_bezier_curve_to(const FloatPoint& through, const FloatPoint& point) + { + append_segment<QuadraticBezierCurveSegment>(point, through); + invalidate_split_lines(); + } + + void elliptical_arc_to(const FloatPoint& point, const FloatPoint& center, const FloatPoint& radii, float x_axis_rotation, float theta_1, float theta_delta) + { + append_segment<EllipticalArcSegment>(point, center, radii, x_axis_rotation, theta_1, theta_delta); + invalidate_split_lines(); + } + + void close(); + void close_all_subpaths(); + + struct SplitLineSegment { + FloatPoint from, to; + float inverse_slope; + float x_of_minimum_y; + float maximum_y; + float minimum_y; + float x; + }; + + const NonnullRefPtrVector<Segment>& segments() const { return m_segments; } + const auto& split_lines() + { + if (!m_split_lines.has_value()) { + segmentize_path(); + ASSERT(m_split_lines.has_value()); + } + return m_split_lines.value(); + } + + const Gfx::FloatRect& bounding_box() + { + if (!m_bounding_box.has_value()) { + segmentize_path(); + ASSERT(m_bounding_box.has_value()); + } + return m_bounding_box.value(); + } + + String to_string() const; + +private: + void invalidate_split_lines() + { + m_split_lines.clear(); + } + void segmentize_path(); + + template<typename T, typename... Args> + void append_segment(Args&&... args) + { + m_segments.append(adopt(*new T(forward<Args>(args)...))); + } + + NonnullRefPtrVector<Segment> m_segments {}; + + Optional<Vector<SplitLineSegment>> m_split_lines {}; + Optional<Gfx::FloatRect> m_bounding_box; +}; + +inline const LogStream& operator<<(const LogStream& stream, const Path& path) +{ + return stream << path.to_string(); +} + +} diff --git a/Userland/Libraries/LibGfx/Point.cpp b/Userland/Libraries/LibGfx/Point.cpp new file mode 100644 index 0000000000..27ae1248ba --- /dev/null +++ b/Userland/Libraries/LibGfx/Point.cpp @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <AK/String.h> +#include <LibGfx/Point.h> +#include <LibGfx/Rect.h> +#include <LibIPC/Decoder.h> +#include <LibIPC/Encoder.h> + +namespace Gfx { + +template<typename T> +void Point<T>::constrain(const Rect<T>& rect) +{ + if (x() < rect.left()) { + set_x(rect.left()); + } else if (x() > rect.right()) { + set_x(rect.right()); + } + + if (y() < rect.top()) { + set_y(rect.top()); + } else if (y() > rect.bottom()) { + set_y(rect.bottom()); + } +} + +template<> +String IntPoint::to_string() const +{ + return String::formatted("[{},{}]", x(), y()); +} + +template<> +String FloatPoint::to_string() const +{ + return String::formatted("[{},{}]", x(), y()); +} + +} + +namespace IPC { + +bool encode(Encoder& encoder, const Gfx::IntPoint& point) +{ + encoder << point.x() << point.y(); + return true; +} + +bool decode(Decoder& decoder, Gfx::IntPoint& point) +{ + int x = 0; + int y = 0; + if (!decoder.decode(x)) + return false; + if (!decoder.decode(y)) + return false; + point = { x, y }; + return true; +} + +} + +template class Gfx::Point<int>; +template class Gfx::Point<float>; diff --git a/Userland/Libraries/LibGfx/Point.h b/Userland/Libraries/LibGfx/Point.h new file mode 100644 index 0000000000..753d04055a --- /dev/null +++ b/Userland/Libraries/LibGfx/Point.h @@ -0,0 +1,257 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Format.h> +#include <AK/StdLibExtras.h> +#include <LibGfx/Forward.h> +#include <LibGfx/Orientation.h> +#include <LibIPC/Forward.h> +#include <math.h> +#include <stdlib.h> + +namespace Gfx { + +template<typename T> +class Point { +public: + Point() { } + + Point(T x, T y) + : m_x(x) + , m_y(y) + { + } + + template<typename U> + Point(U x, U y) + : m_x(x) + , m_y(y) + { + } + + template<typename U> + explicit Point(const Point<U>& other) + : m_x(other.x()) + , m_y(other.y()) + { + } + + T x() const { return m_x; } + T y() const { return m_y; } + + void set_x(T x) { m_x = x; } + void set_y(T y) { m_y = y; } + + void move_by(T dx, T dy) + { + m_x += dx; + m_y += dy; + } + + void move_by(const Point<T>& delta) + { + move_by(delta.x(), delta.y()); + } + + Point<T> translated(const Point<T>& delta) const + { + Point<T> point = *this; + point.move_by(delta); + return point; + } + + Point<T> translated(T dx, T dy) const + { + Point<T> point = *this; + point.move_by(dx, dy); + return point; + } + + Point<T> translated(T dboth) const + { + Point<T> point = *this; + point.move_by(dboth, dboth); + return point; + } + + void constrain(const Rect<T>&); + Point<T> constrained(const Rect<T>& rect) const + { + Point<T> point = *this; + point.constrain(rect); + return point; + } + + bool operator==(const Point<T>& other) const + { + return m_x == other.m_x && m_y == other.m_y; + } + + bool operator!=(const Point<T>& other) const + { + return !(*this == other); + } + + Point<T> operator+(const Point<T>& other) const { return { m_x + other.m_x, m_y + other.m_y }; } + + Point<T>& operator+=(const Point<T>& other) + { + m_x += other.m_x; + m_y += other.m_y; + return *this; + } + + Point<T> operator-() const { return { -m_x, -m_y }; } + + Point<T> operator-(const Point<T>& other) const { return { m_x - other.m_x, m_y - other.m_y }; } + + Point<T>& operator-=(const Point<T>& other) + { + m_x -= other.m_x; + m_y -= other.m_y; + return *this; + } + + Point<T> operator*(T factor) const { return { m_x * factor, m_y * factor }; } + + Point<T>& operator*=(T factor) + { + m_x *= factor; + m_y *= factor; + return *this; + } + + Point<T> operator/(T factor) const { return { m_x / factor, m_y / factor }; } + + Point<T>& operator/=(T factor) + { + m_x /= factor; + m_y /= factor; + return *this; + } + + bool is_null() const { return !m_x && !m_y; } + + T primary_offset_for_orientation(Orientation orientation) const + { + return orientation == Orientation::Vertical ? y() : x(); + } + + void set_primary_offset_for_orientation(Orientation orientation, T value) + { + if (orientation == Orientation::Vertical) { + set_y(value); + } else { + set_x(value); + } + } + + T secondary_offset_for_orientation(Orientation orientation) const + { + return orientation == Orientation::Vertical ? x() : y(); + } + + void set_secondary_offset_for_orientation(Orientation orientation, T value) + { + if (orientation == Orientation::Vertical) { + set_x(value); + } else { + set_y(value); + } + } + + T dx_relative_to(const Point<T>& other) const + { + return x() - other.x(); + } + + T dy_relative_to(const Point<T>& other) const + { + return y() - other.y(); + } + + // Returns pixels moved from other in either direction + T pixels_moved(const Point<T>& other) const + { + return max(abs(dx_relative_to(other)), abs(dy_relative_to(other))); + } + + float distance_from(const Point<T>& other) const + { + if (*this == other) + return 0; + return sqrtf(powf(m_x - other.m_x, 2.0f) + powf(m_y - other.m_y, 2.0f)); + } + + Point absolute_relative_distance_to(const Point& other) const + { + return { abs(dx_relative_to(other)), abs(dy_relative_to(other)) }; + } + + template<typename U> + Point<U> to_type() const + { + return Point<U>(*this); + } + + String to_string() const; + +private: + T m_x { 0 }; + T m_y { 0 }; +}; + +template<typename T> +const LogStream& operator<<(const LogStream& stream, const Point<T>& point) +{ + return stream << point.to_string(); +} + +using IntPoint = Point<int>; +using FloatPoint = Point<float>; + +} + +namespace AK { + +template<typename T> +struct Formatter<Gfx::Point<T>> : Formatter<StringView> { + void format(FormatBuilder& builder, const Gfx::Point<T>& value) + { + Formatter<StringView>::format(builder, value.to_string()); + } +}; + +} + +namespace IPC { + +bool encode(Encoder&, const Gfx::IntPoint&); +bool decode(Decoder&, Gfx::IntPoint&); + +} diff --git a/Userland/Libraries/LibGfx/PortableImageLoaderCommon.h b/Userland/Libraries/LibGfx/PortableImageLoaderCommon.h new file mode 100644 index 0000000000..5f3c51679c --- /dev/null +++ b/Userland/Libraries/LibGfx/PortableImageLoaderCommon.h @@ -0,0 +1,309 @@ +/* + * Copyright (c) 2020, Hüseyin Aslıtürk <asliturk@hotmail.com> + * Copyright (c) 2020, the SerenityOS developers + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Array.h> +#include <AK/Endian.h> +#include <AK/LexicalPath.h> +#include <AK/MappedFile.h> +#include <AK/ScopeGuard.h> +#include <AK/String.h> +#include <AK/StringBuilder.h> +#include <AK/Types.h> +#include <AK/Vector.h> +#include <LibGfx/Bitmap.h> +#include <LibGfx/Color.h> +#include <LibGfx/ImageDecoder.h> +#include <LibGfx/Streamer.h> + +//#define PORTABLE_IMAGE_LOADER_DEBUG + +namespace Gfx { + +static constexpr Color adjust_color(u16 max_val, Color color) +{ + color.set_red((color.red() * 255) / max_val); + color.set_green((color.green() * 255) / max_val); + color.set_blue((color.blue() * 255) / max_val); + + return color; +} + +template<typename TValue> +static bool read_number(Streamer& streamer, TValue* value) +{ + u8 byte {}; + StringBuilder sb {}; + + while (streamer.read(byte)) { + if (byte == ' ' || byte == '\t' || byte == '\n' || byte == '\r') { + streamer.step_back(); + break; + } + + sb.append(byte); + } + + const auto opt_value = sb.to_string().to_uint(); + if (!opt_value.has_value()) { + *value = 0; + return false; + } + + *value = static_cast<u16>(opt_value.value()); + return true; +} + +template<typename TContext> +static bool read_comment([[maybe_unused]] TContext& context, Streamer& streamer) +{ + bool exist = false; + u8 byte {}; + + while (streamer.read(byte)) { + if (byte == '#') { + exist = true; + } else if (byte == '\t' || byte == '\n') { + return exist; + } + } + + return exist; +} + +template<typename TContext> +static bool read_magic_number(TContext& context, Streamer& streamer) +{ + if (context.state >= TContext::State::MagicNumber) { + return true; + } + + if (!context.data || context.data_size < 2) { + context.state = TContext::State::Error; +#ifdef PORTABLE_IMAGE_LOADER_DEBUG + dbg() << "There is no enough data for " << TContext::image_type; +#endif + return false; + } + + u8 magic_number[2] {}; + if (!streamer.read_bytes(magic_number, 2)) { + context.state = TContext::State::Error; +#ifdef PORTABLE_IMAGE_LOADER_DEBUG + dbg() << "We can't read magic number for " << TContext::image_type; +#endif + return false; + } + + if (magic_number[0] == 'P' && magic_number[1] == TContext::ascii_magic_number) { + context.type = TContext::Type::ASCII; + context.state = TContext::State::MagicNumber; + return true; + } + + if (magic_number[0] == 'P' && magic_number[1] == TContext::binary_magic_number) { + context.type = TContext::Type::RAWBITS; + context.state = TContext::State::MagicNumber; + return true; + } + + context.state = TContext::State::Error; +#ifdef PORTABLE_IMAGE_LOADER_DEBUG + dbg() << "Magic number is not valid for " + << magic_number[0] << magic_number[1] << TContext::image_type; +#endif + return false; +} + +template<typename TContext> +static bool read_white_space(TContext& context, Streamer& streamer) +{ + bool exist = false; + u8 byte {}; + + while (streamer.read(byte)) { + if (byte == ' ' || byte == '\t' || byte == '\n' || byte == '\r') { + exist = true; + } else if (byte == '#') { + streamer.step_back(); + read_comment(context, streamer); + } else { + streamer.step_back(); + return exist; + } + } + + return exist; +} + +template<typename TContext> +static bool read_width(TContext& context, Streamer& streamer) +{ + if (const bool result = read_number(streamer, &context.width); + !result || context.width == 0) { + return false; + } + + context.state = TContext::Width; + return true; +} + +template<typename TContext> +static bool read_height(TContext& context, Streamer& streamer) +{ + if (const bool result = read_number(streamer, &context.height); + !result || context.height == 0) { + return false; + } + + context.state = TContext::Height; + return true; +} + +template<typename TContext> +static bool read_max_val(TContext& context, Streamer& streamer) +{ + if (const bool result = read_number(streamer, &context.max_val); + !result || context.max_val == 0) { + return false; + } + + if (context.max_val > 255) { +#ifdef PORTABLE_IMAGE_LOADER_DEBUG + dbg() << "We can't parse 2 byte color for " << TContext::image_type; +#endif + context.state = TContext::Error; + return false; + } + + context.state = TContext::Maxval; + return true; +} + +template<typename TContext> +static bool create_bitmap(TContext& context) +{ + context.bitmap = Bitmap::create_purgeable(BitmapFormat::RGB32, { context.width, context.height }); + if (!context.bitmap) { + context.state = TContext::State::Error; + return false; + } + return true; +} + +template<typename TContext> +static void set_pixels(TContext& context, const AK::Vector<Gfx::Color>& color_data) +{ + size_t index = 0; + for (size_t y = 0; y < context.height; ++y) { + for (size_t x = 0; x < context.width; ++x) { + context.bitmap->set_pixel(x, y, color_data.at(index)); + index++; + } + } +} + +template<typename TContext> +static bool decode(TContext& context) +{ + if (context.state >= TContext::State::Decoded) + return true; + + auto error_guard = ArmedScopeGuard([&] { + context.state = TContext::State::Error; + }); + + Streamer streamer(context.data, context.data_size); + + if (!read_magic_number(context, streamer)) + return false; + + if (!read_white_space(context, streamer)) + return false; + + if (!read_width(context, streamer)) + return false; + + if (!read_white_space(context, streamer)) + return false; + + if (!read_height(context, streamer)) + return false; + + if (context.width > maximum_width_for_decoded_images || context.height > maximum_height_for_decoded_images) { + dbgln("This portable network image is too large for comfort: {}x{}", context.width, context.height); + return false; + } + + if (!read_white_space(context, streamer)) + return false; + + if constexpr (requires { context.max_val; }) { + if (!read_max_val(context, streamer)) + return false; + + if (!read_white_space(context, streamer)) + return false; + } + + if (!read_image_data(context, streamer)) + return false; + + error_guard.disarm(); + context.state = TContext::State::Decoded; + return true; +} + +template<typename TContext> +static RefPtr<Gfx::Bitmap> load_impl(const u8* data, size_t data_size) +{ + TContext context {}; + context.data = data; + context.data_size = data_size; + + if (!decode(context)) { + return nullptr; + } + return context.bitmap; +} +template<typename TContext> +static RefPtr<Gfx::Bitmap> load(const StringView& path) +{ + auto file_or_error = MappedFile::map(path); + if (file_or_error.is_error()) + return nullptr; + auto bitmap = load_impl<TContext>((const u8*)file_or_error.value()->data(), file_or_error.value()->size()); + if (bitmap) + bitmap->set_mmap_name(String::formatted("Gfx::Bitmap [{}] - Decoded {}: {}", + bitmap->size(), + TContext::image_type, + LexicalPath::canonicalized_path(path))); + return bitmap; +} + +} diff --git a/Userland/Libraries/LibGfx/Rect.cpp b/Userland/Libraries/LibGfx/Rect.cpp new file mode 100644 index 0000000000..1f0d39d440 --- /dev/null +++ b/Userland/Libraries/LibGfx/Rect.cpp @@ -0,0 +1,181 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <AK/StdLibExtras.h> +#include <AK/String.h> +#include <AK/Vector.h> +#include <LibGfx/Rect.h> +#include <LibIPC/Decoder.h> +#include <LibIPC/Encoder.h> + +namespace Gfx { + +template<typename T> +void Rect<T>::intersect(const Rect<T>& other) +{ + T l = max(left(), other.left()); + T r = min(right(), other.right()); + T t = max(top(), other.top()); + T b = min(bottom(), other.bottom()); + + if (l > r || t > b) { + m_location = {}; + m_size = {}; + return; + } + + m_location.set_x(l); + m_location.set_y(t); + m_size.set_width((r - l) + 1); + m_size.set_height((b - t) + 1); +} + +template<typename T> +Rect<T> Rect<T>::united(const Rect<T>& other) const +{ + if (is_null()) + return other; + if (other.is_null()) + return *this; + Rect<T> rect; + rect.set_left(min(left(), other.left())); + rect.set_top(min(top(), other.top())); + rect.set_right(max(right(), other.right())); + rect.set_bottom(max(bottom(), other.bottom())); + return rect; +} + +template<typename T> +Vector<Rect<T>, 4> Rect<T>::shatter(const Rect<T>& hammer) const +{ + Vector<Rect<T>, 4> pieces; + if (!intersects(hammer)) { + pieces.unchecked_append(*this); + return pieces; + } + Rect<T> top_shard { + x(), + y(), + width(), + hammer.y() - y() + }; + Rect<T> bottom_shard { + x(), + hammer.y() + hammer.height(), + width(), + (y() + height()) - (hammer.y() + hammer.height()) + }; + Rect<T> left_shard { + x(), + max(hammer.y(), y()), + hammer.x() - x(), + min((hammer.y() + hammer.height()), (y() + height())) - max(hammer.y(), y()) + }; + Rect<T> right_shard { + hammer.x() + hammer.width(), + max(hammer.y(), y()), + right() - hammer.right(), + min((hammer.y() + hammer.height()), (y() + height())) - max(hammer.y(), y()) + }; + if (!top_shard.is_empty()) + pieces.unchecked_append(top_shard); + if (!bottom_shard.is_empty()) + pieces.unchecked_append(bottom_shard); + if (!left_shard.is_empty()) + pieces.unchecked_append(left_shard); + if (!right_shard.is_empty()) + pieces.unchecked_append(right_shard); + + return pieces; +} + +template<typename T> +void Rect<T>::align_within(const Rect<T>& other, TextAlignment alignment) +{ + switch (alignment) { + case TextAlignment::Center: + center_within(other); + return; + case TextAlignment::TopLeft: + set_location(other.location()); + return; + case TextAlignment::TopRight: + set_x(other.x() + other.width() - width()); + set_y(other.y()); + return; + case TextAlignment::CenterLeft: + set_x(other.x()); + center_vertically_within(other); + return; + case TextAlignment::CenterRight: + set_x(other.x() + other.width() - width()); + center_vertically_within(other); + return; + case TextAlignment::BottomRight: + set_x(other.x() + other.width() - width()); + set_y(other.y() + other.height() - height()); + return; + } +} + +template<> +String IntRect::to_string() const +{ + return String::format("[%d,%d %dx%d]", x(), y(), width(), height()); +} + +template<> +String FloatRect::to_string() const +{ + return String::format("[%f,%f %fx%f]", x(), y(), width(), height()); +} + +} + +namespace IPC { + +bool encode(Encoder& encoder, const Gfx::IntRect& rect) +{ + encoder << rect.location() << rect.size(); + return true; +} + +bool decode(Decoder& decoder, Gfx::IntRect& rect) +{ + Gfx::IntPoint point; + Gfx::IntSize size; + if (!decoder.decode(point)) + return false; + if (!decoder.decode(size)) + return false; + rect = { point, size }; + return true; +} + +} + +template class Gfx::Rect<int>; +template class Gfx::Rect<float>; diff --git a/Userland/Libraries/LibGfx/Rect.h b/Userland/Libraries/LibGfx/Rect.h new file mode 100644 index 0000000000..f54ff43fbf --- /dev/null +++ b/Userland/Libraries/LibGfx/Rect.h @@ -0,0 +1,471 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Format.h> +#include <LibGfx/Orientation.h> +#include <LibGfx/Point.h> +#include <LibGfx/Size.h> +#include <LibGfx/TextAlignment.h> +#include <math.h> + +namespace Gfx { + +template<typename T> +T abst(T value) +{ + return value < 0 ? -value : value; +} + +template<typename T> +class Rect { +public: + Rect() { } + + Rect(T x, T y, T width, T height) + : m_location(x, y) + , m_size(width, height) + { + } + + template<typename U> + Rect(U x, U y, U width, U height) + : m_location(x, y) + , m_size(width, height) + { + } + + Rect(const Point<T>& location, const Size<T>& size) + : m_location(location) + , m_size(size) + { + } + + template<typename U> + Rect(const Point<U>& location, const Size<U>& size) + : m_location(location) + , m_size(size) + { + } + + template<typename U> + explicit Rect(const Rect<U>& other) + : m_location(other.location()) + , m_size(other.size()) + { + } + + bool is_null() const + { + return width() == 0 && height() == 0; + } + + bool is_empty() const + { + return width() <= 0 || height() <= 0; + } + + void move_by(T dx, T dy) + { + m_location.move_by(dx, dy); + } + + void move_by(const Point<T>& delta) + { + m_location.move_by(delta); + } + + Point<T> center() const + { + return { x() + width() / 2, y() + height() / 2 }; + } + + void set_location(const Point<T>& location) + { + m_location = location; + } + + void set_size(const Size<T>& size) + { + m_size = size; + } + + void set_size(T width, T height) + { + m_size.set_width(width); + m_size.set_height(height); + } + + void inflate(T w, T h) + { + set_x(x() - w / 2); + set_width(width() + w); + set_y(y() - h / 2); + set_height(height() + h); + } + + void inflate(const Size<T>& size) + { + set_x(x() - size.width() / 2); + set_width(width() + size.width()); + set_y(y() - size.height() / 2); + set_height(height() + size.height()); + } + + void shrink(T w, T h) + { + set_x(x() + w / 2); + set_width(width() - w); + set_y(y() + h / 2); + set_height(height() - h); + } + + void shrink(const Size<T>& size) + { + set_x(x() + size.width() / 2); + set_width(width() - size.width()); + set_y(y() + size.height() / 2); + set_height(height() - size.height()); + } + + Rect<T> shrunken(T w, T h) const + { + Rect<T> rect = *this; + rect.shrink(w, h); + return rect; + } + + Rect<T> shrunken(const Size<T>& size) const + { + Rect<T> rect = *this; + rect.shrink(size); + return rect; + } + + Rect<T> inflated(T w, T h) const + { + Rect<T> rect = *this; + rect.inflate(w, h); + return rect; + } + + Rect<T> inflated(const Size<T>& size) const + { + Rect<T> rect = *this; + rect.inflate(size); + return rect; + } + + Rect<T> translated(T dx, T dy) const + { + Rect<T> rect = *this; + rect.move_by(dx, dy); + return rect; + } + + Rect<T> translated(const Point<T>& delta) const + { + Rect<T> rect = *this; + rect.move_by(delta); + return rect; + } + + bool contains_vertically(T y) const + { + return y >= top() && y <= bottom(); + } + + bool contains_horizontally(T x) const + { + return x >= left() && x <= right(); + } + + bool contains(T x, T y) const + { + return x >= m_location.x() && x <= right() && y >= m_location.y() && y <= bottom(); + } + + bool contains(const Point<T>& point) const + { + return contains(point.x(), point.y()); + } + + bool contains(const Rect<T>& other) const + { + return left() <= other.left() + && right() >= other.right() + && top() <= other.top() + && bottom() >= other.bottom(); + } + + template<typename Container> + bool contains(const Container& others) const + { + bool have_any = false; + for (const auto& other : others) { + if (!contains(other)) + return false; + have_any = true; + } + return have_any; + } + + int primary_offset_for_orientation(Orientation orientation) const { return m_location.primary_offset_for_orientation(orientation); } + void set_primary_offset_for_orientation(Orientation orientation, int value) { m_location.set_primary_offset_for_orientation(orientation, value); } + int secondary_offset_for_orientation(Orientation orientation) const { return m_location.secondary_offset_for_orientation(orientation); } + void set_secondary_offset_for_orientation(Orientation orientation, int value) { m_location.set_secondary_offset_for_orientation(orientation, value); } + + int primary_size_for_orientation(Orientation orientation) const { return m_size.primary_size_for_orientation(orientation); } + int secondary_size_for_orientation(Orientation orientation) const { return m_size.secondary_size_for_orientation(orientation); } + void set_primary_size_for_orientation(Orientation orientation, int value) { m_size.set_primary_size_for_orientation(orientation, value); } + void set_secondary_size_for_orientation(Orientation orientation, int value) { m_size.set_secondary_size_for_orientation(orientation, value); } + + T first_edge_for_orientation(Orientation orientation) const + { + if (orientation == Orientation::Vertical) + return top(); + return left(); + } + + T last_edge_for_orientation(Orientation orientation) const + { + if (orientation == Orientation::Vertical) + return bottom(); + return right(); + } + + T left() const { return x(); } + T right() const { return x() + width() - 1; } + T top() const { return y(); } + T bottom() const { return y() + height() - 1; } + + void set_left(T left) + { + set_x(left); + } + + void set_top(T top) + { + set_y(top); + } + + void set_right(T right) + { + set_width(right - x() + 1); + } + + void set_bottom(T bottom) + { + set_height(bottom - y() + 1); + } + + void set_right_without_resize(T new_right) + { + int delta = new_right - right(); + move_by(delta, 0); + } + + void set_bottom_without_resize(T new_bottom) + { + int delta = new_bottom - bottom(); + move_by(0, delta); + } + + bool intersects_vertically(const Rect<T>& other) const + { + return top() <= other.bottom() && other.top() <= bottom(); + } + + bool intersects_horizontally(const Rect<T>& other) const + { + return left() <= other.right() && other.left() <= right(); + } + + bool intersects(const Rect<T>& other) const + { + return left() <= other.right() + && other.left() <= right() + && top() <= other.bottom() + && other.top() <= bottom(); + } + + template<typename Container> + bool intersects(const Container& others) const + { + for (const auto& other : others) { + if (intersects(other)) + return true; + } + return false; + } + + template<typename Container, typename Function> + IterationDecision for_each_intersected(const Container& others, Function f) const + { + if (is_empty()) + return IterationDecision::Continue; + for (const auto& other : others) { + auto intersected_rect = intersected(other); + if (!intersected_rect.is_empty()) { + IterationDecision decision = f(intersected_rect); + if (decision != IterationDecision::Continue) + return decision; + } + } + return IterationDecision::Continue; + } + + T x() const { return location().x(); } + T y() const { return location().y(); } + T width() const { return m_size.width(); } + T height() const { return m_size.height(); } + + void set_x(T x) { m_location.set_x(x); } + void set_y(T y) { m_location.set_y(y); } + void set_width(T width) { m_size.set_width(width); } + void set_height(T height) { m_size.set_height(height); } + + const Point<T>& location() const { return m_location; } + const Size<T>& size() const { return m_size; } + + Vector<Rect<T>, 4> shatter(const Rect<T>& hammer) const; + + bool operator==(const Rect<T>& other) const + { + return m_location == other.m_location && m_size == other.m_size; + } + + bool operator!=(const Rect<T>& other) const + { + return !(*this == other); + } + + Rect<T> operator*(T factor) const { return { m_location * factor, m_size * factor }; } + + Rect<T>& operator*=(T factor) + { + m_location *= factor; + m_size *= factor; + return *this; + } + + void intersect(const Rect<T>&); + + static Rect<T> from_two_points(const Point<T>& a, const Point<T>& b) + { + return { min(a.x(), b.x()), min(a.y(), b.y()), abst(a.x() - b.x()), abst(a.y() - b.y()) }; + } + + static Rect<T> intersection(const Rect<T>& a, const Rect<T>& b) + { + Rect<T> r = a; + r.intersect(b); + return r; + } + + Rect<T> intersected(const Rect<T>& other) const + { + return intersection(*this, other); + } + + Rect<T> united(const Rect<T>&) const; + + Point<T> top_left() const { return { left(), top() }; } + Point<T> top_right() const { return { right(), top() }; } + Point<T> bottom_left() const { return { left(), bottom() }; } + Point<T> bottom_right() const { return { right(), bottom() }; } + + void align_within(const Rect<T>&, TextAlignment); + + void center_within(const Rect<T>& other) + { + center_horizontally_within(other); + center_vertically_within(other); + } + + void center_horizontally_within(const Rect<T>& other) + { + set_x(other.center().x() - width() / 2); + } + + void center_vertically_within(const Rect<T>& other) + { + set_y(other.center().y() - height() / 2); + } + + template<typename U> + Rect<U> to() const + { + return Rect<U>(*this); + } + + String to_string() const; + +private: + Point<T> m_location; + Size<T> m_size; +}; + +template<typename T> +const LogStream& operator<<(const LogStream& stream, const Rect<T>& rect) +{ + return stream << rect.to_string(); +} + +using IntRect = Rect<int>; +using FloatRect = Rect<float>; + +ALWAYS_INLINE IntRect enclosing_int_rect(const FloatRect& float_rect) +{ + return { + (int)float_rect.x(), + (int)float_rect.y(), + (int)ceilf(float_rect.width()), + (int)ceilf(float_rect.height()), + }; +} + +} + +namespace AK { + +template<typename T> +struct Formatter<Gfx::Rect<T>> : Formatter<StringView> { + void format(FormatBuilder& builder, const Gfx::Rect<T>& value) + { + Formatter<StringView>::format(builder, value.to_string()); + } +}; + +} + +namespace IPC { + +bool decode(Decoder&, Gfx::IntRect&); +bool encode(Encoder&, const Gfx::IntRect&); + +} diff --git a/Userland/Libraries/LibGfx/ShareableBitmap.cpp b/Userland/Libraries/LibGfx/ShareableBitmap.cpp new file mode 100644 index 0000000000..6413140827 --- /dev/null +++ b/Userland/Libraries/LibGfx/ShareableBitmap.cpp @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <AK/SharedBuffer.h> +#include <LibGfx/Bitmap.h> +#include <LibGfx/ShareableBitmap.h> +#include <LibGfx/Size.h> +#include <LibIPC/Decoder.h> +#include <LibIPC/Encoder.h> + +namespace Gfx { + +ShareableBitmap::ShareableBitmap(const Bitmap& bitmap) + : m_bitmap(bitmap.to_bitmap_backed_by_shared_buffer()) +{ +} + +} + +namespace IPC { + +bool encode(Encoder& encoder, const Gfx::ShareableBitmap& shareable_bitmap) +{ + encoder << shareable_bitmap.shbuf_id(); + encoder << shareable_bitmap.width(); + encoder << shareable_bitmap.height(); + return true; +} + +bool decode(Decoder& decoder, Gfx::ShareableBitmap& shareable_bitmap) +{ + i32 shbuf_id = 0; + Gfx::IntSize size; + if (!decoder.decode(shbuf_id)) + return false; + if (!decoder.decode(size)) + return false; + + if (shbuf_id == -1) + return true; + + dbgln("Decoding a ShareableBitmap with shbuf_id={}, size={}", shbuf_id, size); + + auto shared_buffer = SharedBuffer::create_from_shbuf_id(shbuf_id); + if (!shared_buffer) + return false; + + auto bitmap = Gfx::Bitmap::create_with_shared_buffer(Gfx::BitmapFormat::RGBA32, shared_buffer.release_nonnull(), size); + shareable_bitmap = bitmap->to_shareable_bitmap(); + return true; +} + +} diff --git a/Userland/Libraries/LibGfx/ShareableBitmap.h b/Userland/Libraries/LibGfx/ShareableBitmap.h new file mode 100644 index 0000000000..69ed9e7c78 --- /dev/null +++ b/Userland/Libraries/LibGfx/ShareableBitmap.h @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/RefPtr.h> +#include <LibGfx/Bitmap.h> +#include <LibGfx/Size.h> + +namespace Gfx { + +class ShareableBitmap { +public: + ShareableBitmap() { } + explicit ShareableBitmap(const Gfx::Bitmap&); + + bool is_valid() const { return m_bitmap; } + + i32 shbuf_id() const { return m_bitmap ? m_bitmap->shbuf_id() : -1; } + + const Bitmap* bitmap() const { return m_bitmap; } + Bitmap* bitmap() { return m_bitmap; } + + IntSize size() const { return m_bitmap ? m_bitmap->size() : IntSize(); } + IntRect rect() const { return m_bitmap ? m_bitmap->rect() : IntRect(); } + + int width() const { return size().width(); } + int height() const { return size().height(); } + +private: + RefPtr<Bitmap> m_bitmap; +}; + +} + +namespace IPC { + +bool encode(Encoder&, const Gfx::ShareableBitmap&); +bool decode(Decoder&, Gfx::ShareableBitmap&); + +} diff --git a/Userland/Libraries/LibGfx/Size.cpp b/Userland/Libraries/LibGfx/Size.cpp new file mode 100644 index 0000000000..e2cc6151ff --- /dev/null +++ b/Userland/Libraries/LibGfx/Size.cpp @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <AK/String.h> +#include <LibGfx/Size.h> +#include <LibIPC/Decoder.h> +#include <LibIPC/Encoder.h> + +namespace Gfx { + +template<> +String IntSize::to_string() const +{ + return String::formatted("[{}x{}]", m_width, m_height); +} + +template<> +String FloatSize::to_string() const +{ + return String::formatted("[{}x{}]", m_width, m_height); +} + +} + +namespace IPC { + +bool encode(Encoder& encoder, const Gfx::IntSize& size) +{ + encoder << size.width() << size.height(); + return true; +} + +bool decode(Decoder& decoder, Gfx::IntSize& size) +{ + int width = 0; + int height = 0; + if (!decoder.decode(width)) + return false; + if (!decoder.decode(height)) + return false; + size = { width, height }; + return true; +} + +} + +template class Gfx::Size<int>; +template class Gfx::Size<float>; diff --git a/Userland/Libraries/LibGfx/Size.h b/Userland/Libraries/LibGfx/Size.h new file mode 100644 index 0000000000..7b0cbe96fc --- /dev/null +++ b/Userland/Libraries/LibGfx/Size.h @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Format.h> +#include <LibGfx/Orientation.h> +#include <LibIPC/Forward.h> + +namespace Gfx { + +template<typename T> +class Size { +public: + Size() { } + + Size(T w, T h) + : m_width(w) + , m_height(h) + { + } + + template<typename U> + Size(U width, U height) + : m_width(width) + , m_height(height) + { + } + + template<typename U> + explicit Size(const Size<U>& other) + : m_width(other.width()) + , m_height(other.height()) + { + } + + bool is_null() const { return !m_width && !m_height; } + bool is_empty() const { return m_width <= 0 || m_height <= 0; } + + T width() const { return m_width; } + T height() const { return m_height; } + + T area() const { return width() * height(); } + + void set_width(T w) { m_width = w; } + void set_height(T h) { m_height = h; } + + template<typename U> + bool contains(const Size<U>& other) const + { + return other.m_width <= m_width && other.m_height <= m_height; + } + + bool operator==(const Size<T>& other) const + { + return m_width == other.m_width && m_height == other.m_height; + } + + bool operator!=(const Size<T>& other) const + { + return !(*this == other); + } + + Size<T>& operator-=(const Size<T>& other) + { + m_width -= other.m_width; + m_height -= other.m_height; + return *this; + } + + Size<T>& operator+=(const Size<T>& other) + { + m_width += other.m_width; + m_height += other.m_height; + return *this; + } + + Size<T> operator*(T factor) const { return { m_width * factor, m_height * factor }; } + + Size<T>& operator*=(T factor) + { + m_width *= factor; + m_height *= factor; + return *this; + } + + T primary_size_for_orientation(Orientation orientation) const + { + return orientation == Orientation::Vertical ? height() : width(); + } + + void set_primary_size_for_orientation(Orientation orientation, T value) + { + if (orientation == Orientation::Vertical) { + set_height(value); + } else { + set_width(value); + } + } + + T secondary_size_for_orientation(Orientation orientation) const + { + return orientation == Orientation::Vertical ? width() : height(); + } + + void set_secondary_size_for_orientation(Orientation orientation, T value) + { + if (orientation == Orientation::Vertical) { + set_width(value); + } else { + set_height(value); + } + } + + template<typename U> + Size<U> to_type() const + { + return Size<U>(*this); + } + + String to_string() const; + +private: + T m_width { 0 }; + T m_height { 0 }; +}; + +template<typename T> +const LogStream& operator<<(const LogStream& stream, const Gfx::Size<T>& size) +{ + return stream << size.to_string(); +} + +using IntSize = Size<int>; +using FloatSize = Size<float>; + +} + +namespace AK { + +template<typename T> +struct Formatter<Gfx::Size<T>> : Formatter<StringView> { + void format(FormatBuilder& builder, const Gfx::Size<T>& value) + { + Formatter<StringView>::format(builder, value.to_string()); + } +}; + +} + +namespace IPC { + +bool encode(Encoder&, const Gfx::IntSize&); +bool decode(Decoder&, Gfx::IntSize&); + +} diff --git a/Userland/Libraries/LibGfx/StandardCursor.h b/Userland/Libraries/LibGfx/StandardCursor.h new file mode 100644 index 0000000000..38c35b7076 --- /dev/null +++ b/Userland/Libraries/LibGfx/StandardCursor.h @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +namespace Gfx { + +enum class StandardCursor { + None = 0, + Hidden, + Arrow, + Crosshair, + IBeam, + ResizeHorizontal, + ResizeVertical, + ResizeDiagonalTLBR, + ResizeDiagonalBLTR, + ResizeColumn, + ResizeRow, + Hand, + Help, + Drag, + Move, + Wait, + __Count, +}; + +} diff --git a/Userland/Libraries/LibGfx/Streamer.h b/Userland/Libraries/LibGfx/Streamer.h new file mode 100644 index 0000000000..475726585b --- /dev/null +++ b/Userland/Libraries/LibGfx/Streamer.h @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2020, Hüseyin ASLITÜRK <asliturk@hotmail.com>, the SerenityOS developers + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Array.h> +#include <AK/Endian.h> +#include <AK/Types.h> +#include <string.h> + +namespace Gfx { + +class Streamer { +public: + constexpr Streamer(const u8* data, size_t size) + : m_data_ptr(data) + , m_size_remaining(size) + { + } + + template<typename T> + constexpr bool read(T& value) + { + AK::Array<u8, sizeof(T)> network_buffer {}; + auto network_value = new (network_buffer.data()) AK::NetworkOrdered<T> {}; + auto res = read_bytes(network_buffer.data(), sizeof(T)); + value = T(*network_value); + return res; + } + + constexpr bool read_bytes(u8* buffer, size_t count) + { + if (m_size_remaining < count) { + return false; + } + memcpy(buffer, m_data_ptr, count); + m_data_ptr += count; + m_size_remaining -= count; + return true; + } + + constexpr bool at_end() const { return !m_size_remaining; } + + constexpr void step_back() + { + m_data_ptr -= 1; + m_size_remaining += 1; + } + +private: + const u8* m_data_ptr { nullptr }; + size_t m_size_remaining { 0 }; +}; + +} diff --git a/Userland/Libraries/LibGfx/StylePainter.cpp b/Userland/Libraries/LibGfx/StylePainter.cpp new file mode 100644 index 0000000000..ad830958d8 --- /dev/null +++ b/Userland/Libraries/LibGfx/StylePainter.cpp @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <LibGfx/Bitmap.h> +#include <LibGfx/ClassicStylePainter.h> +#include <LibGfx/Painter.h> +#include <LibGfx/Palette.h> +#include <LibGfx/StylePainter.h> + +namespace Gfx { + +BaseStylePainter& StylePainter::current() +{ + static ClassicStylePainter style; + return style; +} + +void StylePainter::paint_tab_button(Painter& painter, const IntRect& rect, const Palette& palette, bool active, bool hovered, bool enabled, bool top) +{ + current().paint_tab_button(painter, rect, palette, active, hovered, enabled, top); +} + +void StylePainter::paint_button(Painter& painter, const IntRect& rect, const Palette& palette, ButtonStyle button_style, bool pressed, bool hovered, bool checked, bool enabled, bool focused) +{ + current().paint_button(painter, rect, palette, button_style, pressed, hovered, checked, enabled, focused); +} + +void StylePainter::paint_surface(Painter& painter, const IntRect& rect, const Palette& palette, bool paint_vertical_lines, bool paint_top_line) +{ + current().paint_surface(painter, rect, palette, paint_vertical_lines, paint_top_line); +} + +void StylePainter::paint_frame(Painter& painter, const IntRect& rect, const Palette& palette, FrameShape shape, FrameShadow shadow, int thickness, bool skip_vertical_lines) +{ + current().paint_frame(painter, rect, palette, shape, shadow, thickness, skip_vertical_lines); +} + +void StylePainter::paint_window_frame(Painter& painter, const IntRect& rect, const Palette& palette) +{ + current().paint_window_frame(painter, rect, palette); +} + +void StylePainter::paint_progress_bar(Painter& painter, const IntRect& rect, const Palette& palette, int min, int max, int value, const StringView& text) +{ + current().paint_progress_bar(painter, rect, palette, min, max, value, text); +} + +void StylePainter::paint_radio_button(Painter& painter, const IntRect& rect, const Palette& palette, bool is_checked, bool is_being_pressed) +{ + current().paint_radio_button(painter, rect, palette, is_checked, is_being_pressed); +} + +void StylePainter::paint_check_box(Painter& painter, const IntRect& rect, const Palette& palette, bool is_enabled, bool is_checked, bool is_being_pressed) +{ + current().paint_check_box(painter, rect, palette, is_enabled, is_checked, is_being_pressed); +} + +void StylePainter::paint_transparency_grid(Painter& painter, const IntRect& rect, const Palette& palette) +{ + current().paint_transparency_grid(painter, rect, palette); +} + +} diff --git a/Userland/Libraries/LibGfx/StylePainter.h b/Userland/Libraries/LibGfx/StylePainter.h new file mode 100644 index 0000000000..779241c252 --- /dev/null +++ b/Userland/Libraries/LibGfx/StylePainter.h @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Forward.h> +#include <LibGfx/Forward.h> + +namespace Gfx { + +enum class ButtonStyle { + Normal, + CoolBar +}; +enum class FrameShadow { + Plain, + Raised, + Sunken +}; +enum class FrameShape { + NoFrame, + Box, + Container, + Panel, + VerticalLine, + HorizontalLine +}; + +// FIXME: should this be in its own header? +class BaseStylePainter { +public: + virtual ~BaseStylePainter() { } + + virtual void paint_button(Painter&, const IntRect&, const Palette&, ButtonStyle, bool pressed, bool hovered = false, bool checked = false, bool enabled = true, bool focused = false) = 0; + virtual void paint_tab_button(Painter&, const IntRect&, const Palette&, bool active, bool hovered, bool enabled, bool top) = 0; + virtual void paint_surface(Painter&, const IntRect&, const Palette&, bool paint_vertical_lines = true, bool paint_top_line = true) = 0; + virtual void paint_frame(Painter&, const IntRect&, const Palette&, FrameShape, FrameShadow, int thickness, bool skip_vertical_lines = false) = 0; + virtual void paint_window_frame(Painter&, const IntRect&, const Palette&) = 0; + virtual void paint_progress_bar(Painter&, const IntRect&, const Palette&, int min, int max, int value, const StringView& text) = 0; + virtual void paint_radio_button(Painter&, const IntRect&, const Palette&, bool is_checked, bool is_being_pressed) = 0; + virtual void paint_check_box(Painter&, const IntRect&, const Palette&, bool is_enabled, bool is_checked, bool is_being_pressed) = 0; + virtual void paint_transparency_grid(Painter&, const IntRect&, const Palette&) = 0; + +protected: + BaseStylePainter() { } +}; + +class StylePainter { +public: + static BaseStylePainter& current(); + + // FIXME: These are here for API compatibility, we should probably remove them and move BaseStylePainter into here + static void paint_button(Painter&, const IntRect&, const Palette&, ButtonStyle, bool pressed, bool hovered = false, bool checked = false, bool enabled = true, bool focused = false); + static void paint_tab_button(Painter&, const IntRect&, const Palette&, bool active, bool hovered, bool enabled, bool top); + static void paint_surface(Painter&, const IntRect&, const Palette&, bool paint_vertical_lines = true, bool paint_top_line = true); + static void paint_frame(Painter&, const IntRect&, const Palette&, FrameShape, FrameShadow, int thickness, bool skip_vertical_lines = false); + static void paint_window_frame(Painter&, const IntRect&, const Palette&); + static void paint_progress_bar(Painter&, const IntRect&, const Palette&, int min, int max, int value, const StringView& text); + static void paint_radio_button(Painter&, const IntRect&, const Palette&, bool is_checked, bool is_being_pressed); + static void paint_check_box(Painter&, const IntRect&, const Palette&, bool is_enabled, bool is_checked, bool is_being_pressed); + static void paint_transparency_grid(Painter&, const IntRect&, const Palette&); +}; + +} diff --git a/Userland/Libraries/LibGfx/SystemTheme.cpp b/Userland/Libraries/LibGfx/SystemTheme.cpp new file mode 100644 index 0000000000..5ccb04256d --- /dev/null +++ b/Userland/Libraries/LibGfx/SystemTheme.cpp @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <AK/SharedBuffer.h> +#include <LibCore/ConfigFile.h> +#include <LibGfx/SystemTheme.h> +#include <string.h> + +namespace Gfx { + +static SystemTheme dummy_theme; +static const SystemTheme* theme_page = &dummy_theme; +static RefPtr<SharedBuffer> theme_buffer; + +const SystemTheme& current_system_theme() +{ + ASSERT(theme_page); + return *theme_page; +} + +int current_system_theme_buffer_id() +{ + ASSERT(theme_buffer); + return theme_buffer->shbuf_id(); +} + +void set_system_theme(SharedBuffer& buffer) +{ + theme_buffer = buffer; + theme_page = theme_buffer->data<SystemTheme>(); +} + +RefPtr<SharedBuffer> load_system_theme(const String& path) +{ + auto file = Core::ConfigFile::open(path); + auto buffer = SharedBuffer::create_with_size(sizeof(SystemTheme)); + + auto* data = buffer->data<SystemTheme>(); + + auto get_color = [&](auto& name) { + auto color_string = file->read_entry("Colors", name); + auto color = Color::from_string(color_string); + if (!color.has_value()) + return Color(Color::Black); + return color.value(); + }; + + auto get_metric = [&](auto& name, auto role) { + int metric = file->read_num_entry("Metrics", name, -1); + if (metric == -1) { + switch (role) { + case (int)MetricRole::TitleHeight: + return 19; + case (int)MetricRole::TitleButtonHeight: + return 15; + case (int)MetricRole::TitleButtonWidth: + return 15; + default: + dbgln("Metric {} has no fallback value!", name); + return 16; + } + } + return metric; + }; + + auto get_path = [&](auto& name, auto role) { + auto path = file->read_entry("Paths", name); + if (path.is_empty()) { + switch (role) { + case (int)PathRole::TitleButtonIcons: + return "/res/icons/16x16/"; + default: + return "/res/"; + } + } + return &path[0]; + }; + +#undef __ENUMERATE_COLOR_ROLE +#define __ENUMERATE_COLOR_ROLE(role) \ + data->color[(int)ColorRole::role] = get_color(#role).value(); + ENUMERATE_COLOR_ROLES(__ENUMERATE_COLOR_ROLE) +#undef __ENUMERATE_COLOR_ROLE + +#define DO_METRIC(x) \ + data->metric[(int)MetricRole::x] = get_metric(#x, (int)MetricRole::x) + + DO_METRIC(TitleHeight); + DO_METRIC(TitleButtonWidth); + DO_METRIC(TitleButtonHeight); + +#define DO_PATH(x) \ + do { \ + auto path = get_path(#x, (int)PathRole::x); \ + memcpy(data->path[(int)PathRole::x], path, min(strlen(path) + 1, sizeof(data->path[(int)PathRole::x]))); \ + data->path[(int)PathRole::x][sizeof(data->path[(int)PathRole::x]) - 1] = '\0'; \ + } while (0) + + DO_PATH(TitleButtonIcons); + + buffer->seal(); + buffer->share_globally(); + + return buffer; +} + +} diff --git a/Userland/Libraries/LibGfx/SystemTheme.h b/Userland/Libraries/LibGfx/SystemTheme.h new file mode 100644 index 0000000000..60e382c65f --- /dev/null +++ b/Userland/Libraries/LibGfx/SystemTheme.h @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Forward.h> +#include <AK/String.h> +#include <AK/Types.h> +#include <LibGfx/Color.h> + +namespace Gfx { + +#define ENUMERATE_COLOR_ROLES(C) \ + C(ActiveLink) \ + C(ActiveWindowBorder1) \ + C(ActiveWindowBorder2) \ + C(ActiveWindowTitle) \ + C(ActiveWindowTitleShadow) \ + C(ActiveWindowTitleStripes) \ + C(Base) \ + C(BaseText) \ + C(Button) \ + C(ButtonText) \ + C(DesktopBackground) \ + C(FocusOutline) \ + C(HighlightWindowBorder1) \ + C(HighlightWindowBorder2) \ + C(HighlightWindowTitle) \ + C(HighlightWindowTitleShadow) \ + C(HighlightWindowTitleStripes) \ + C(HighlightSearching) \ + C(HighlightSearchingText) \ + C(HoverHighlight) \ + C(InactiveSelection) \ + C(InactiveSelectionText) \ + C(InactiveWindowBorder1) \ + C(InactiveWindowBorder2) \ + C(InactiveWindowTitle) \ + C(InactiveWindowTitleShadow) \ + C(InactiveWindowTitleStripes) \ + C(Link) \ + C(MenuBase) \ + C(MenuBaseText) \ + C(MenuSelection) \ + C(MenuSelectionText) \ + C(MenuStripe) \ + C(MovingWindowBorder1) \ + C(MovingWindowBorder2) \ + C(MovingWindowTitle) \ + C(MovingWindowTitleShadow) \ + C(MovingWindowTitleStripes) \ + C(PlaceholderText) \ + C(RubberBandBorder) \ + C(RubberBandFill) \ + C(Ruler) \ + C(RulerActiveText) \ + C(RulerBorder) \ + C(RulerInactiveText) \ + C(Selection) \ + C(SelectionText) \ + C(SyntaxComment) \ + C(SyntaxControlKeyword) \ + C(SyntaxIdentifier) \ + C(SyntaxKeyword) \ + C(SyntaxNumber) \ + C(SyntaxOperator) \ + C(SyntaxPreprocessorStatement) \ + C(SyntaxPreprocessorValue) \ + C(SyntaxPunctuation) \ + C(SyntaxString) \ + C(SyntaxType) \ + C(TextCursor) \ + C(ThreedHighlight) \ + C(ThreedShadow1) \ + C(ThreedShadow2) \ + C(Tooltip) \ + C(TooltipText) \ + C(VisitedLink) \ + C(Window) \ + C(WindowText) + +enum class ColorRole { + NoRole, + +#undef __ENUMERATE_COLOR_ROLE +#define __ENUMERATE_COLOR_ROLE(role) role, + ENUMERATE_COLOR_ROLES(__ENUMERATE_COLOR_ROLE) +#undef __ENUMERATE_COLOR_ROLE + + __Count, + + Background = Window, + DisabledText = ThreedShadow1, +}; + +inline const char* to_string(ColorRole role) +{ + switch (role) { + case ColorRole::NoRole: + return "NoRole"; +#undef __ENUMERATE_COLOR_ROLE +#define __ENUMERATE_COLOR_ROLE(role) \ + case ColorRole::role: \ + return #role; + ENUMERATE_COLOR_ROLES(__ENUMERATE_COLOR_ROLE) +#undef __ENUMERATE_COLOR_ROLE + default: + ASSERT_NOT_REACHED(); + } +} + +enum class MetricRole { + NoRole, + TitleHeight, + TitleButtonWidth, + TitleButtonHeight, + __Count, +}; + +enum class PathRole { + NoRole, + TitleButtonIcons, + __Count, +}; + +struct SystemTheme { + RGBA32 color[(int)ColorRole::__Count]; + int metric[(int)MetricRole::__Count]; + char path[(int)PathRole::__Count][256]; // TODO: PATH_MAX? +}; + +const SystemTheme& current_system_theme(); +int current_system_theme_buffer_id(); +void set_system_theme(SharedBuffer&); +RefPtr<SharedBuffer> load_system_theme(const String& path); + +} + +using Gfx::ColorRole; diff --git a/Userland/Libraries/LibGfx/TextAlignment.h b/Userland/Libraries/LibGfx/TextAlignment.h new file mode 100644 index 0000000000..5faea7f480 --- /dev/null +++ b/Userland/Libraries/LibGfx/TextAlignment.h @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Optional.h> +#include <AK/StringView.h> + +namespace Gfx { + +#define GFX_ENUMERATE_TEXT_ALIGNMENTS(M) \ + M(TopLeft) \ + M(CenterLeft) \ + M(Center) \ + M(CenterRight) \ + M(TopRight) \ + M(BottomRight) + +enum class TextAlignment { +#define __ENUMERATE(x) x, + GFX_ENUMERATE_TEXT_ALIGNMENTS(__ENUMERATE) +#undef __ENUMERATE +}; + +inline bool is_right_text_alignment(TextAlignment alignment) +{ + switch (alignment) { + case TextAlignment::CenterRight: + case TextAlignment::TopRight: + case TextAlignment::BottomRight: + return true; + default: + return false; + } +} + +inline bool is_vertically_centered_text_alignment(TextAlignment alignment) +{ + switch (alignment) { + case TextAlignment::CenterLeft: + case TextAlignment::CenterRight: + case TextAlignment::Center: + return true; + default: + return false; + } +} + +inline Optional<TextAlignment> text_alignment_from_string(const StringView& string) +{ +#define __ENUMERATE(x) \ + if (string == #x) \ + return TextAlignment::x; + GFX_ENUMERATE_TEXT_ALIGNMENTS(__ENUMERATE) +#undef __ENUMERATE + return {}; +} + +inline const char* to_string(TextAlignment text_alignment) +{ +#define __ENUMERATE(x) \ + if (text_alignment == TextAlignment::x) \ + return #x; + GFX_ENUMERATE_TEXT_ALIGNMENTS(__ENUMERATE) +#undef __ENUMERATE + return {}; +} + +} diff --git a/Userland/Libraries/LibGfx/TextAttributes.h b/Userland/Libraries/LibGfx/TextAttributes.h new file mode 100644 index 0000000000..1de9e5c01d --- /dev/null +++ b/Userland/Libraries/LibGfx/TextAttributes.h @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2021, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Optional.h> +#include <LibGfx/Color.h> + +namespace Gfx { + +struct TextAttributes { + Color color; + Optional<Color> background_color; + bool underline { false }; + bool bold { false }; +}; + +} diff --git a/Userland/Libraries/LibGfx/TextElision.h b/Userland/Libraries/LibGfx/TextElision.h new file mode 100644 index 0000000000..cf8cecc001 --- /dev/null +++ b/Userland/Libraries/LibGfx/TextElision.h @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +namespace Gfx { + +enum class TextElision { + None, + Right, +}; + +} diff --git a/Userland/Libraries/LibGfx/Triangle.cpp b/Userland/Libraries/LibGfx/Triangle.cpp new file mode 100644 index 0000000000..d326b59f1c --- /dev/null +++ b/Userland/Libraries/LibGfx/Triangle.cpp @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020, Shannon Booth <shannon.ml.booth@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <AK/String.h> +#include <LibGfx/Triangle.h> + +namespace Gfx { + +String Triangle::to_string() const +{ + return String::formatted("({},{},{})", m_a, m_b, m_c); +} + +const LogStream& operator<<(const LogStream& stream, const Triangle& value) +{ + return stream << value.to_string(); +} + +} diff --git a/Userland/Libraries/LibGfx/Triangle.h b/Userland/Libraries/LibGfx/Triangle.h new file mode 100644 index 0000000000..7d8e69e80d --- /dev/null +++ b/Userland/Libraries/LibGfx/Triangle.h @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2020, Shannon Booth <shannon.ml.booth@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Forward.h> +#include <LibGfx/Point.h> + +namespace Gfx { + +class Triangle { +public: + Triangle(IntPoint a, IntPoint b, IntPoint c) + : m_a(a) + , m_b(b) + , m_c(c) + { + m_det = (m_b.x() - m_a.x()) * (m_c.y() - m_a.y()) - (m_b.y() - m_a.y()) * (m_c.x() - m_a.x()); + } + + IntPoint a() const { return m_a; } + IntPoint b() const { return m_b; } + IntPoint c() const { return m_c; } + + bool contains(IntPoint p) const + { + int x = p.x(); + int y = p.y(); + + int ax = m_a.x(); + int bx = m_b.x(); + int cx = m_c.x(); + + int ay = m_a.y(); + int by = m_b.y(); + int cy = m_c.y(); + + if (m_det * ((bx - ax) * (y - ay) - (by - ay) * (x - ax)) <= 0) + return false; + if (m_det * ((cx - bx) * (y - by) - (cy - by) * (x - bx)) <= 0) + return false; + if (m_det * ((ax - cx) * (y - cy) - (ay - cy) * (x - cx)) <= 0) + return false; + return true; + } + + String to_string() const; + +private: + int m_det; + IntPoint m_a; + IntPoint m_b; + IntPoint m_c; +}; + +const LogStream& operator<<(const LogStream&, const Triangle&); + +} diff --git a/Userland/Libraries/LibGfx/Vector3.h b/Userland/Libraries/LibGfx/Vector3.h new file mode 100644 index 0000000000..0a93e5075d --- /dev/null +++ b/Userland/Libraries/LibGfx/Vector3.h @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2020, Stephan Unverwerth <s.unverwerth@gmx.de> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <math.h> + +namespace Gfx { +template<typename T> +class Vector3 { +public: + Vector3() = default; + Vector3(T x, T y, T z) + : m_x(x) + , m_y(y) + , m_z(z) + { + } + + T x() const { return m_x; } + T y() const { return m_y; } + T z() const { return m_z; } + + void set_x(T value) { m_x = value; } + void set_y(T value) { m_y = value; } + void set_z(T value) { m_z = value; } + + Vector3 operator+(const Vector3& other) const + { + return Vector3(m_x + other.m_x, m_y + other.m_y, m_z + other.m_z); + } + + Vector3 operator-(const Vector3& other) const + { + return Vector3(m_x - other.m_x, m_y - other.m_y, m_z - other.m_z); + } + + Vector3 operator*(T f) const + { + return Vector3(m_x * f, m_y * f, m_z * f); + } + + Vector3 operator/(T f) const + { + return Vector3(m_x / f, m_y / f, m_z / f); + } + + T dot(const Vector3& other) const + { + return m_x * other.m_x + m_y * other.m_y + m_z * other.m_z; + } + + Vector3 cross(const Vector3& other) const + { + return Vector3( + m_y * other.m_z - m_z * other.m_y, + m_z * other.m_x - m_x * other.m_z, + m_x * other.m_y - m_y * other.m_x); + } + + Vector3 normalized() const + { + T inv_length = 1 / length(); + return *this * inv_length; + } + + Vector3 clamped(T m, T x) const + { + Vector3 copy { *this }; + copy.clamp(m, x); + return copy; + } + + void clamp(T min_value, T max_value) + { + m_x = max(min_value, m_x); + m_y = max(min_value, m_y); + m_z = max(min_value, m_z); + m_x = min(max_value, m_x); + m_y = min(max_value, m_y); + m_z = min(max_value, m_z); + } + + void normalize() + { + T inv_length = 1 / length(); + m_x *= inv_length; + m_y *= inv_length; + m_z *= inv_length; + } + + T length() const + { + return sqrt(m_x * m_x + m_y * m_y + m_z * m_z); + } + +private: + T m_x; + T m_y; + T m_z; +}; + +typedef Vector3<float> FloatVector3; +typedef Vector3<double> DoubleVector3; + +} + +using Gfx::DoubleVector3; +using Gfx::FloatVector3; +using Gfx::Vector3; diff --git a/Userland/Libraries/LibGfx/WindowTheme.cpp b/Userland/Libraries/LibGfx/WindowTheme.cpp new file mode 100644 index 0000000000..dddf8d0630 --- /dev/null +++ b/Userland/Libraries/LibGfx/WindowTheme.cpp @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <LibGfx/ClassicWindowTheme.h> +#include <LibGfx/WindowTheme.h> + +namespace Gfx { + +WindowTheme& WindowTheme::current() +{ + static ClassicWindowTheme theme; + return theme; +} + +WindowTheme::~WindowTheme() +{ +} + +} diff --git a/Userland/Libraries/LibGfx/WindowTheme.h b/Userland/Libraries/LibGfx/WindowTheme.h new file mode 100644 index 0000000000..8ae0174723 --- /dev/null +++ b/Userland/Libraries/LibGfx/WindowTheme.h @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Forward.h> +#include <LibGfx/Forward.h> + +namespace Gfx { + +class WindowTheme { +public: + enum class WindowType { + Normal, + Notification, + Other, + }; + + enum class WindowState { + Active, + Inactive, + Highlighted, + Moving, + }; + + virtual ~WindowTheme(); + + static WindowTheme& current(); + + virtual void paint_normal_frame(Painter&, WindowState, const IntRect& window_rect, const StringView& title, const Bitmap& icon, const Palette&, const IntRect& leftmost_button_rect) const = 0; + virtual void paint_notification_frame(Painter&, const IntRect& window_rect, const Palette&, const IntRect& close_button_rect) const = 0; + + virtual int title_bar_height(const Palette&) const = 0; + virtual IntRect title_bar_rect(WindowType, const IntRect& window_rect, const Palette&) const = 0; + virtual IntRect title_bar_icon_rect(WindowType, const IntRect& window_rect, const Palette&) const = 0; + virtual IntRect title_bar_text_rect(WindowType, const IntRect& window_rect, const Palette&) const = 0; + + virtual IntRect frame_rect_for_window(WindowType, const IntRect& window_rect, const Palette&) const = 0; + + virtual Vector<IntRect> layout_buttons(WindowType, const IntRect& window_rect, const Palette&, size_t buttons) const = 0; + +protected: + WindowTheme() { } +}; + +} |