/* * Copyright (c) 2023, MacDue * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #include #include #include namespace Gfx { using VarUInt = LEB128; static constexpr Array TVG_MAGIC { 0x72, 0x56 }; enum class ColorEncoding : u8 { RGBA8888 = 0, RGB565 = 1, RGBAF32 = 2, Custom = 3 }; enum class CoordinateRange : u8 { Default = 0, Reduced = 1, Enhanced = 2 }; enum class StyleType : u8 { FlatColored = 0, LinearGradient = 1, RadialGradinet = 2 }; enum class Command : u8 { EndOfDocument = 0, FillPolygon = 1, FillRectangles = 2, FillPath = 3, DrawLines = 4, DrawLineLoop = 5, DrawLineStrip = 6, DrawLinePath = 7, OutlineFillPolygon = 8, OutlineFillRectangles = 9, OutLineFillPath = 10 }; struct FillCommandHeader { u32 count; TinyVGDecodedImageData::Style style; }; struct DrawCommandHeader { u32 count; TinyVGDecodedImageData::Style line_style; float line_width; }; struct OutlineFillCommandHeader { u32 count; TinyVGDecodedImageData::Style fill_style; TinyVGDecodedImageData::Style line_style; float line_width; }; enum class PathCommand : u8 { Line = 0, HorizontalLine = 1, VerticalLine = 2, CubicBezier = 3, ArcCircle = 4, ArcEllipse = 5, ClosePath = 6, QuadraticBezier = 7 }; struct TinyVGHeader { u8 version; u8 scale; ColorEncoding color_encoding; CoordinateRange coordinate_range; u32 width; u32 height; u32 color_count; }; static ErrorOr decode_tinyvg_header(Stream& stream) { TinyVGHeader header {}; Array magic_bytes; TRY(stream.read_until_filled(magic_bytes)); if (magic_bytes != TVG_MAGIC) return Error::from_string_literal("Invalid TinyVG: Incorrect header magic"); header.version = TRY(stream.read_value()); u8 properties = TRY(stream.read_value()); header.scale = properties & 0xF; header.color_encoding = static_cast((properties >> 4) & 0x3); header.coordinate_range = static_cast((properties >> 6) & 0x3); switch (header.coordinate_range) { case CoordinateRange::Default: header.width = TRY(stream.read_value>()); header.height = TRY(stream.read_value>()); break; case CoordinateRange::Reduced: header.width = TRY(stream.read_value()); header.height = TRY(stream.read_value()); break; case CoordinateRange::Enhanced: header.width = TRY(stream.read_value>()); header.height = TRY(stream.read_value>()); break; default: return Error::from_string_literal("Invalid TinyVG: Bad coordinate range"); } header.color_count = TRY(stream.read_value()); return header; } static ErrorOr> decode_color_table(Stream& stream, ColorEncoding encoding, u32 color_count) { if (encoding == ColorEncoding::Custom) return Error::from_string_literal("Invalid TinyVG: Unsupported color encoding"); static constexpr size_t MAX_INITIAL_COLOR_TABLE_SIZE = 65536; Vector color_table; TRY(color_table.try_ensure_capacity(min(MAX_INITIAL_COLOR_TABLE_SIZE, color_count))); auto parse_color = [&]() -> ErrorOr { switch (encoding) { case ColorEncoding::RGBA8888: { Array rgba; TRY(stream.read_until_filled(rgba)); return Color(rgba[0], rgba[1], rgba[2], rgba[3]); } case ColorEncoding::RGB565: { u16 color = TRY(stream.read_value>()); auto red = (color >> (6 + 5)) & 0x1f; auto green = (color >> 5) & 0x3f; auto blue = (color >> 0) & 0x1f; return Color((red * 255 + 15) / 31, (green * 255 + 31) / 63, (blue * 255 + 15) / 31); } case ColorEncoding::RGBAF32: { auto red = TRY(stream.read_value>()); auto green = TRY(stream.read_value>()); auto blue = TRY(stream.read_value>()); auto alpha = TRY(stream.read_value>()); return Color( clamp(red * 255.0f, 0.0f, 255.0f), clamp(green * 255.0f, 0.0f, 255.0f), clamp(blue * 255.0f, 0.0f, 255.0f), clamp(alpha * 255.0f, 0.0f, 255.0f)); } default: return Error::from_string_literal("Invalid TinyVG: Bad color encoding"); } }; while (color_count-- > 0) { TRY(color_table.try_append(TRY(parse_color()))); } return color_table; } class TinyVGReader { public: TinyVGReader(Stream& stream, TinyVGHeader const& header, ReadonlySpan color_table) : m_stream(stream) , m_scale(powf(0.5, header.scale)) , m_coordinate_range(header.coordinate_range) , m_color_table(color_table) { } ErrorOr read_unit() { auto read_value = [&]() -> ErrorOr { switch (m_coordinate_range) { case CoordinateRange::Default: return TRY(m_stream.read_value>()); case CoordinateRange::Reduced: return TRY(m_stream.read_value()); case CoordinateRange::Enhanced: return TRY(m_stream.read_value>()); default: // Note: Already checked while reading the header. VERIFY_NOT_REACHED(); } }; return TRY(read_value()) * m_scale; } ErrorOr read_var_uint() { return TRY(m_stream.read_value()); } ErrorOr read_point() { return FloatPoint { TRY(read_unit()), TRY(read_unit()) }; } ErrorOr read_style(StyleType type) { auto read_color = [&]() -> ErrorOr { auto color_index = TRY(m_stream.read_value()); if (color_index >= m_color_table.size()) return Error::from_string_literal("Invalid color table index"); return m_color_table[color_index]; }; auto read_gradient = [&]() -> ErrorOr> { auto point_0 = TRY(read_point()); auto point_1 = TRY(read_point()); auto color_0 = TRY(read_color()); auto color_1 = TRY(read_color()); // Map TinyVG gradients to SVG gradients (since we already have those). // This is not entirely consistent with the spec, which uses gamma sRGB for gradients // (but this matches the TVG -> SVG renderings). auto svg_gradient = TRY([&]() -> ErrorOr> { if (type == StyleType::LinearGradient) return TRY(SVGLinearGradientPaintStyle::create(point_0, point_1)); auto radius = point_1.distance_from(point_0); return TRY(SVGRadialGradientPaintStyle::create(point_0, 0, point_0, radius)); }()); TRY(svg_gradient->add_color_stop(0, color_0)); TRY(svg_gradient->add_color_stop(1, color_1)); return svg_gradient; }; switch (type) { case StyleType::FlatColored: return TRY(read_color()); case StyleType::LinearGradient: case StyleType::RadialGradinet: return TRY(read_gradient()); } return Error::from_string_literal("Invalid TinyVG: Bad style data"); } ErrorOr read_rectangle() { return FloatRect { TRY(read_unit()), TRY(read_unit()), TRY(read_unit()), TRY(read_unit()) }; } ErrorOr read_line() { return FloatLine { TRY(read_point()), TRY(read_point()) }; } ErrorOr read_path(u32 segment_count) { Path path; auto segment_lengths = TRY(FixedArray::create(segment_count)); for (auto& command_count : segment_lengths) { command_count = TRY(read_var_uint()) + 1; } for (auto command_count : segment_lengths) { auto start_point = TRY(read_point()); path.move_to(start_point); for (u32 i = 0; i < command_count; i++) { u8 command_tag = TRY(m_stream.read_value()); auto path_command = static_cast(command_tag & 0x7); bool has_line_width = (command_tag >> 4) & 0b1; if (has_line_width) { // FIXME: TinyVG allows changing the line width within a path. // This is not supported in LibGfx, so we currently ignore this. (void)TRY(read_unit()); } switch (path_command) { case PathCommand::Line: path.line_to(TRY(read_point())); break; case PathCommand::HorizontalLine: path.line_to({ TRY(read_unit()), path.last_point().y() }); break; case PathCommand::VerticalLine: path.line_to({ path.last_point().x(), TRY(read_unit()) }); break; case PathCommand::CubicBezier: { auto control_0 = TRY(read_point()); auto control_1 = TRY(read_point()); auto point_1 = TRY(read_point()); path.cubic_bezier_curve_to(control_0, control_1, point_1); break; } case PathCommand::ArcCircle: { u8 flags = TRY(m_stream.read_value()); bool large_arc = (flags >> 0) & 0b1; bool sweep = (flags >> 1) & 0b1; auto radius = TRY(read_unit()); auto target = TRY(read_point()); path.arc_to(target, radius, large_arc, !sweep); break; } case PathCommand::ArcEllipse: { u8 flags = TRY(m_stream.read_value()); bool large_arc = (flags >> 0) & 0b1; bool sweep = (flags >> 1) & 0b1; auto radius_x = TRY(read_unit()); auto radius_y = TRY(read_unit()); auto rotation = TRY(read_unit()); auto target = TRY(read_point()); path.elliptical_arc_to(target, { radius_x, radius_y }, rotation, large_arc, !sweep); break; } case PathCommand::ClosePath: { path.close(); break; } case PathCommand::QuadraticBezier: { auto control = TRY(read_point()); auto point_1 = TRY(read_point()); path.quadratic_bezier_curve_to(control, point_1); break; } default: return Error::from_string_literal("Invalid TinyVG: Bad path command"); } } } return path; } ErrorOr read_fill_command_header(StyleType style_type) { return FillCommandHeader { TRY(read_var_uint()) + 1, TRY(read_style(style_type)) }; } ErrorOr read_draw_command_header(StyleType style_type) { return DrawCommandHeader { TRY(read_var_uint()) + 1, TRY(read_style(style_type)), TRY(read_unit()) }; } ErrorOr read_outline_fill_command_header(StyleType style_type) { u8 header = TRY(m_stream.read_value()); u8 count = (header & 0x3f) + 1; auto stroke_type = static_cast((header >> 6) & 0x3); return OutlineFillCommandHeader { count, TRY(read_style(style_type)), TRY(read_style(stroke_type)), TRY(read_unit()) }; } private: Stream& m_stream; float m_scale {}; CoordinateRange m_coordinate_range; ReadonlySpan m_color_table; }; ErrorOr> TinyVGDecodedImageData::decode(Stream& stream) { return decode(stream, TRY(decode_tinyvg_header(stream))); } ErrorOr> TinyVGDecodedImageData::decode(Stream& stream, TinyVGHeader const& header) { if (header.version != 1) return Error::from_string_literal("Invalid TinyVG: Unsupported version"); auto color_table = TRY(decode_color_table(stream, header.color_encoding, header.color_count)); TinyVGReader reader { stream, header, color_table.span() }; auto rectangle_to_path = [](FloatRect const& rect) -> Path { Path path; path.move_to({ rect.x(), rect.y() }); path.line_to({ rect.x() + rect.width(), rect.y() }); path.line_to({ rect.x() + rect.width(), rect.y() + rect.height() }); path.line_to({ rect.x(), rect.y() + rect.height() }); path.close(); return path; }; Vector draw_commands; bool at_end = false; while (!at_end) { u8 command_info = TRY(stream.read_value()); auto command = static_cast(command_info & 0x3f); auto style_type = static_cast((command_info >> 6) & 0x3); switch (command) { case Command::EndOfDocument: at_end = true; break; case Command::FillPolygon: { auto header = TRY(reader.read_fill_command_header(style_type)); Path polygon; polygon.move_to(TRY(reader.read_point())); for (u32 i = 0; i < header.count - 1; i++) polygon.line_to(TRY(reader.read_point())); TRY(draw_commands.try_append(DrawCommand { move(polygon), move(header.style) })); break; } case Command::FillRectangles: { auto header = TRY(reader.read_fill_command_header(style_type)); for (u32 i = 0; i < header.count; i++) { TRY(draw_commands.try_append(DrawCommand { rectangle_to_path(TRY(reader.read_rectangle())), header.style })); } break; } case Command::FillPath: { auto header = TRY(reader.read_fill_command_header(style_type)); auto path = TRY(reader.read_path(header.count)); TRY(draw_commands.try_append(DrawCommand { move(path), move(header.style) })); break; } case Command::DrawLines: { auto header = TRY(reader.read_draw_command_header(style_type)); Path path; for (u32 i = 0; i < header.count; i++) { auto line = TRY(reader.read_line()); path.move_to(line.a()); path.line_to(line.b()); } TRY(draw_commands.try_append(DrawCommand { move(path), {}, move(header.line_style), header.line_width })); break; } case Command::DrawLineStrip: case Command::DrawLineLoop: { auto header = TRY(reader.read_draw_command_header(style_type)); Path path; path.move_to(TRY(reader.read_point())); for (u32 i = 0; i < header.count - 1; i++) path.line_to(TRY(reader.read_point())); if (command == Command::DrawLineLoop) path.close(); TRY(draw_commands.try_append(DrawCommand { move(path), {}, move(header.line_style), header.line_width })); break; } case Command::DrawLinePath: { auto header = TRY(reader.read_draw_command_header(style_type)); auto path = TRY(reader.read_path(header.count)); TRY(draw_commands.try_append(DrawCommand { move(path), {}, move(header.line_style), header.line_width })); break; } case Command::OutlineFillPolygon: { auto header = TRY(reader.read_outline_fill_command_header(style_type)); Path polygon; polygon.move_to(TRY(reader.read_point())); for (u32 i = 0; i < header.count - 1; i++) polygon.line_to(TRY(reader.read_point())); polygon.close(); TRY(draw_commands.try_append(DrawCommand { move(polygon), move(header.fill_style), move(header.line_style), header.line_width })); break; } case Command::OutlineFillRectangles: { auto header = TRY(reader.read_outline_fill_command_header(style_type)); for (u32 i = 0; i < header.count; i++) { TRY(draw_commands.try_append(DrawCommand { rectangle_to_path(TRY(reader.read_rectangle())), header.fill_style, header.line_style, header.line_width })); } break; } case Command::OutLineFillPath: { auto header = TRY(reader.read_outline_fill_command_header(style_type)); auto path = TRY(reader.read_path(header.count)); TRY(draw_commands.try_append(DrawCommand { move(path), move(header.fill_style), move(header.line_style), header.line_width })); break; } default: return Error::from_string_literal("Invalid TinyVG: Bad command"); } } return TRY(adopt_nonnull_ref_or_enomem(new (nothrow) TinyVGDecodedImageData({ header.width, header.height }, move(draw_commands)))); } void TinyVGDecodedImageData::draw_transformed(Painter& painter, AffineTransform transform) const { // FIXME: Correctly handle non-uniform scales. auto scale = max(transform.x_scale(), transform.y_scale()); AntiAliasingPainter aa_painter { painter }; for (auto const& command : draw_commands()) { auto draw_path = command.path.copy_transformed(transform); if (command.fill.has_value()) { auto fill_path = draw_path; fill_path.close_all_subpaths(); command.fill->visit( [&](Color color) { aa_painter.fill_path(fill_path, color, Painter::WindingRule::EvenOdd); }, [&](NonnullRefPtr style) { const_cast(*style).set_gradient_transform(transform); aa_painter.fill_path(fill_path, style, 1.0f, Painter::WindingRule::EvenOdd); }); } if (command.stroke.has_value()) { command.stroke->visit( [&](Color color) { aa_painter.stroke_path(draw_path, color, command.stroke_width * scale); }, [&](NonnullRefPtr style) { const_cast(*style).set_gradient_transform(transform); aa_painter.stroke_path(draw_path, style, command.stroke_width * scale); }); } } } struct TinyVGLoadingContext { FixedMemoryStream stream; TinyVGHeader header {}; RefPtr decoded_image {}; RefPtr bitmap {}; enum class State { NotDecoded = 0, HeaderDecoded, ImageDecoded, Error, }; State state { State::NotDecoded }; }; static ErrorOr decode_header_and_update_context(TinyVGLoadingContext& context) { VERIFY(context.state == TinyVGLoadingContext::State::NotDecoded); context.header = TRY(decode_tinyvg_header(context.stream)); context.state = TinyVGLoadingContext::State::HeaderDecoded; return {}; } static ErrorOr decode_image_data_and_update_context(TinyVGLoadingContext& context) { VERIFY(context.state == TinyVGLoadingContext::State::HeaderDecoded); auto image_data_or_error = TinyVGDecodedImageData::decode(context.stream, context.header); if (image_data_or_error.is_error()) { context.state = TinyVGLoadingContext::State::Error; return image_data_or_error.release_error(); } context.state = TinyVGLoadingContext::State::ImageDecoded; context.decoded_image = image_data_or_error.release_value(); return {}; } static ErrorOr ensure_fully_decoded(TinyVGLoadingContext& context) { if (context.state == TinyVGLoadingContext::State::Error) return Error::from_string_literal("TinyVGImageDecoderPlugin: Decoding failed!"); if (context.state == TinyVGLoadingContext::State::HeaderDecoded) TRY(decode_image_data_and_update_context(context)); VERIFY(context.state == TinyVGLoadingContext::State::ImageDecoded); return {}; } TinyVGImageDecoderPlugin::TinyVGImageDecoderPlugin(ReadonlyBytes bytes) : m_context { make(FixedMemoryStream { bytes }) } { } ErrorOr> TinyVGImageDecoderPlugin::create(ReadonlyBytes bytes) { auto plugin = TRY(adopt_nonnull_own_or_enomem(new (nothrow) TinyVGImageDecoderPlugin(bytes))); TRY(decode_header_and_update_context(*plugin->m_context)); return plugin; } bool TinyVGImageDecoderPlugin::sniff(ReadonlyBytes bytes) { FixedMemoryStream stream { { bytes.data(), bytes.size() } }; return !decode_tinyvg_header(stream).is_error(); } IntSize TinyVGImageDecoderPlugin::size() { return { m_context->header.width, m_context->header.height }; } ErrorOr TinyVGImageDecoderPlugin::frame(size_t, Optional ideal_size) { TRY(ensure_fully_decoded(*m_context)); auto target_size = ideal_size.value_or(m_context->decoded_image->size()); if (!m_context->bitmap || m_context->bitmap->size() != target_size) m_context->bitmap = TRY(m_context->decoded_image->bitmap(target_size)); return ImageFrameDescriptor { m_context->bitmap }; } ErrorOr TinyVGImageDecoderPlugin::vector_frame(size_t) { TRY(ensure_fully_decoded(*m_context)); return VectorImageFrameDescriptor { m_context->decoded_image, 0 }; } }