diff --git a/CMakeLists.txt b/CMakeLists.txt index 4f3eb45a..16c85be2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -56,6 +56,10 @@ if (MACOS AND CLION_IDE AND NOT HasParent) option(TGFX_BUILD_TESTS "Build tgfx tests" ON) endif () +if (TGFX_BUILD_SVG) + set(TGFX_USE_PNG_ENCODE ON) +endif () + if (TGFX_BUILD_TESTS) set(TGFX_USE_FREETYPE ON) set(TGFX_BUILD_DRAWERS ON) @@ -204,6 +208,11 @@ if (TGFX_USE_ZLIB) list(APPEND TGFX_STATIC_VENDORS zlib) endif () +if (TGFX_BUILD_SVG) + # SVG and PDF exports require glyphs to be convertible back to Unicode. + list(APPEND TGFX_DEFINES TGFX_USE_GLYPH_TO_UNICODE) +endif () + if (TGFX_USE_FREETYPE) list(APPEND TGFX_DEFINES TGFX_USE_FREETYPE) list(APPEND TGFX_STATIC_VENDORS freetype) diff --git a/include/tgfx/core/Canvas.h b/include/tgfx/core/Canvas.h index 7c8959ab..5fa69a81 100644 --- a/include/tgfx/core/Canvas.h +++ b/include/tgfx/core/Canvas.h @@ -27,6 +27,7 @@ #include "tgfx/core/SamplingOptions.h" #include "tgfx/core/Shape.h" #include "tgfx/core/TextBlob.h" +#include "tgfx/svg/SVGExporter.h" namespace tgfx { class Surface; @@ -430,5 +431,6 @@ class Canvas { friend class Surface; friend class Picture; friend class Recorder; + friend class SVGExporter; }; } // namespace tgfx diff --git a/include/tgfx/core/ColorFilter.h b/include/tgfx/core/ColorFilter.h index 87a9183e..f7bc2a48 100644 --- a/include/tgfx/core/ColorFilter.h +++ b/include/tgfx/core/ColorFilter.h @@ -91,6 +91,14 @@ class ColorFilter { return false; } + protected: + enum class Type { Blend, Matrix, AlphaThreshold, Compose }; + + /** + * Returns the type of this color filter. + */ + virtual Type type() const = 0; + private: virtual std::unique_ptr asFragmentProcessor() const = 0; @@ -98,5 +106,6 @@ class ColorFilter { friend class ColorFilterShader; friend class ComposeColorFilter; friend class ColorImageFilter; + friend class ColorFilterCaster; }; } // namespace tgfx diff --git a/include/tgfx/core/ImageFilter.h b/include/tgfx/core/ImageFilter.h index b9adb4f4..b30c256f 100644 --- a/include/tgfx/core/ImageFilter.h +++ b/include/tgfx/core/ImageFilter.h @@ -169,5 +169,6 @@ class ImageFilter { friend class InnerShadowImageFilter; friend class ComposeImageFilter; friend class FilterImage; + friend class ImageFilterCaster; }; } // namespace tgfx diff --git a/include/tgfx/core/Picture.h b/include/tgfx/core/Picture.h index 70cc953f..f01b52ce 100644 --- a/include/tgfx/core/Picture.h +++ b/include/tgfx/core/Picture.h @@ -25,6 +25,7 @@ namespace tgfx { class Record; class Canvas; class DrawContext; +class SVGExportingContext; class MCState; class Image; @@ -64,6 +65,7 @@ class Picture { friend class MeasureContext; friend class RenderContext; friend class RecordingContext; + friend class SVGExportingContext; friend class Image; friend class PictureImage; friend class Canvas; diff --git a/include/tgfx/core/Shader.h b/include/tgfx/core/Shader.h index b470b25d..d4eecbc4 100644 --- a/include/tgfx/core/Shader.h +++ b/include/tgfx/core/Shader.h @@ -151,5 +151,6 @@ class Shader { friend class FragmentProcessor; friend class Canvas; + friend class ShaderCaster; }; } // namespace tgfx diff --git a/include/tgfx/core/Typeface.h b/include/tgfx/core/Typeface.h index 8e2123ee..6c4bd6a5 100644 --- a/include/tgfx/core/Typeface.h +++ b/include/tgfx/core/Typeface.h @@ -20,6 +20,7 @@ #include #include +#include #include "tgfx/core/Data.h" namespace tgfx { @@ -131,11 +132,19 @@ class Typeface { virtual std::shared_ptr copyTableData(FontTableTag tag) const = 0; protected: + /** + * Gets the mapping from GlyphID to unicode. The array index is GlyphID, and the array value is + * unicode. The array length is glyphsCount(). + * This method is only implemented when compiling the SVG or PDF export module. + */ + virtual std::vector getGlyphToUnicodeMap() const; + mutable std::mutex locker = {}; private: std::unordered_map> scalerContexts = {}; friend class ScalerContext; + friend class GlyphConverter; }; } // namespace tgfx diff --git a/include/tgfx/svg/SVGExporter.h b/include/tgfx/svg/SVGExporter.h new file mode 100644 index 00000000..db598ed1 --- /dev/null +++ b/include/tgfx/svg/SVGExporter.h @@ -0,0 +1,121 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making tgfx available. +// +// Copyright (C) 2024 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// https://opensource.org/licenses/BSD-3-Clause +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include "tgfx/core/Canvas.h" +#include "tgfx/core/Rect.h" +#include "tgfx/core/WriteStream.h" +#include "tgfx/gpu/Context.h" + +namespace tgfx { +class SVGExportingContext; + +/** + * Defines flags for SVG exporting that influence the readability and functionality of the exported + * SVG. + */ +class SVGExportFlags { + public: + /** + * Forces text to be converted to paths in the exported SVG. By default, text is exported as is. + * Note that this only applies to fonts with outlines. Fonts without outlines, such as emoji and + * web fonts, will still be exported as text. + */ + static constexpr uint32_t ConvertTextToPaths = 1 << 0; + + /** + * Disable pretty XML formatting in the exported SVG. By default, spaces ('\t') and + * newlines ('\n') are added to the exported SVG text for better readability. + */ + static constexpr uint32_t DisablePrettyXML = 1 << 1; + + /** + * Disable warnings for unsupported attributes. By default, warnings are logged to the console + * when exporting attributes that the SVG standard does not support. + */ + static constexpr uint32_t DisableWarnings = 1 << 2; +}; + +/** + * SVGExporter is used to convert drawing commands from the Canvas into SVG text. + * + * Some features are not supported when exporting to SVG: + * - Blend modes: + * Clear, Src, Dst, DstOver, SrcIn, DstIn, SrcOut, DstOut, SrcATop, DstATop, Xor, and Modulate are + * not supported. + * + * - Image filters: + * Compose and Runtime are not supported. + * + * - Color filters: + * Compose and AlphaThreshold filters are not supported. The Blend filter is partially supported, + * similar to blend modes. + * + * - Shaders: + * ColorFilter, Blend, and Matrix are not supported. Gradient shaders are partially supported. + * + * - Gradient shaders: + * Conic gradients are not supported. + */ +class SVGExporter { + public: + /** + * Creates a shared pointer to an SVG exporter object, which can be used to export SVG text. + * + * @param svgStream The stream to store the SVG text. + * @param context The context used to convert some rendering commands into image data. + * @param viewBox The viewBox of the SVG. Content that exceeds this area will be clipped. + * @param exportFlags Flags for exporting SVG text. + * @return A shared pointer to the SVG exporter object. If svgStream is nullptr, context is + * nullptr, or viewBox is empty, nullptr is returned. + */ + static std::shared_ptr Make(const std::shared_ptr& svgStream, + Context* context, const Rect& viewBox, + uint32_t exportFlags = 0); + + /** + * Destroys the SVG exporter object. If close() hasn't been called, it will be invoked + * automatically. + */ + ~SVGExporter(); + + /** + * Returns the canvas for exporting if the SVGExporter is not closed; otherwise, returns nullptr. + */ + Canvas* getCanvas() const; + + /** + * Closes the SVG exporter, finalizing any unfinished drawing commands and writing the SVG end + * tag. + */ + void close(); + + private: + /** + * Construct a SVG exporter object + */ + SVGExporter(const std::shared_ptr& svgStream, Context* context, const Rect& viewBox, + uint32_t exportFlags); + + SVGExportingContext* drawContext = nullptr; + Canvas* canvas = nullptr; +}; + +} // namespace tgfx \ No newline at end of file diff --git a/src/core/Typeface.cpp b/src/core/Typeface.cpp index b926b089..63cd4d74 100644 --- a/src/core/Typeface.cpp +++ b/src/core/Typeface.cpp @@ -17,6 +17,7 @@ ///////////////////////////////////////////////////////////////////////////////////////////////// #include "tgfx/core/Typeface.h" +#include #include "core/utils/UniqueID.h" #include "tgfx/core/UTF.h" @@ -63,6 +64,11 @@ class EmptyTypeface : public Typeface { return nullptr; } + protected: + std::vector getGlyphToUnicodeMap() const override { + return {}; + } + private: uint32_t _uniqueID = UniqueID::Next(); }; @@ -84,4 +90,9 @@ GlyphID Typeface::getGlyphID(const std::string& name) const { auto unichar = UTF::NextUTF8(&start, start + name.size()); return getGlyphID(unichar); } + +std::vector Typeface::getGlyphToUnicodeMap() const { + return {}; +}; + } // namespace tgfx \ No newline at end of file diff --git a/src/core/filters/AlphaThresholdColorFilter.h b/src/core/filters/AlphaThresholdColorFilter.h index e2293cf7..7811174a 100644 --- a/src/core/filters/AlphaThresholdColorFilter.h +++ b/src/core/filters/AlphaThresholdColorFilter.h @@ -25,6 +25,11 @@ class AlphaThresholdColorFilter : public ColorFilter { public: explicit AlphaThresholdColorFilter(float threshold) : threshold(threshold){}; + protected: + Type type() const override { + return Type::AlphaThreshold; + }; + private: std::unique_ptr asFragmentProcessor() const override; diff --git a/src/core/filters/ComposeColorFilter.h b/src/core/filters/ComposeColorFilter.h index a55c3acf..138ce30b 100644 --- a/src/core/filters/ComposeColorFilter.h +++ b/src/core/filters/ComposeColorFilter.h @@ -28,6 +28,11 @@ class ComposeColorFilter : public ColorFilter { bool isAlphaUnchanged() const override; + protected: + Type type() const override { + return Type::Compose; + }; + private: std::shared_ptr inner = nullptr; std::shared_ptr outer = nullptr; diff --git a/src/core/filters/MatrixColorFilter.h b/src/core/filters/MatrixColorFilter.h index 4f388caf..b97d3613 100644 --- a/src/core/filters/MatrixColorFilter.h +++ b/src/core/filters/MatrixColorFilter.h @@ -29,8 +29,14 @@ class MatrixColorFilter : public ColorFilter { return alphaIsUnchanged; } - private: std::array matrix; + + protected: + Type type() const override { + return Type::Matrix; + }; + + private: bool alphaIsUnchanged; std::unique_ptr asFragmentProcessor() const override; diff --git a/src/core/filters/ModeColorFilter.h b/src/core/filters/ModeColorFilter.h index 91bdd179..beb79397 100644 --- a/src/core/filters/ModeColorFilter.h +++ b/src/core/filters/ModeColorFilter.h @@ -31,10 +31,15 @@ class ModeColorFilter : public ColorFilter { bool asColorMode(Color* color, BlendMode* mode) const override; - private: Color color; BlendMode mode; + protected: + Type type() const override { + return Type::Blend; + }; + + private: std::unique_ptr asFragmentProcessor() const override; }; } // namespace tgfx diff --git a/src/core/utils/Caster.cpp b/src/core/utils/Caster.cpp new file mode 100644 index 00000000..aaca833a --- /dev/null +++ b/src/core/utils/Caster.cpp @@ -0,0 +1,84 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making tgfx available. +// +// Copyright (C) 2024 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// https://opensource.org/licenses/BSD-3-Clause +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "Caster.h" + +namespace tgfx { +std::shared_ptr ShaderCaster::AsColorShader( + const std::shared_ptr& shader) { + if (shader->type() == Shader::Type::Color) { + return std::static_pointer_cast(shader); + } + return nullptr; +} + +std::shared_ptr ShaderCaster::AsImageShader( + const std::shared_ptr& shader) { + if (shader->type() == Shader::Type::Image) { + return std::static_pointer_cast(shader); + } + return nullptr; +} + +std::shared_ptr ShaderCaster::AsGradientShader( + const std::shared_ptr& shader) { + if (shader->type() == Shader::Type::Gradient) { + return std::static_pointer_cast(shader); + } + return nullptr; +} + +std::shared_ptr ImageFilterCaster::AsBlurImageFilter( + const std::shared_ptr& imageFilter) { + if (imageFilter->type() == ImageFilter::Type::Blur) { + return std::static_pointer_cast(imageFilter); + } + return nullptr; +} +std::shared_ptr ImageFilterCaster::AsDropShadowImageFilter( + const std::shared_ptr& imageFilter) { + if (imageFilter->type() == ImageFilter::Type::DropShadow) { + return std::static_pointer_cast(imageFilter); + } + return nullptr; +} + +std::shared_ptr ImageFilterCaster::AsInnerShadowImageFilter( + const std::shared_ptr& imageFilter) { + if (imageFilter->type() == ImageFilter::Type::InnerShadow) { + return std::static_pointer_cast(imageFilter); + } + return nullptr; +} + +std::shared_ptr ColorFilterCaster::AsModeColorFilter( + const std::shared_ptr& colorFilter) { + if (colorFilter->type() == ColorFilter::Type::Blend) { + return std::static_pointer_cast(colorFilter); + } + return nullptr; +} + +std::shared_ptr ColorFilterCaster::AsMatrixColorFilter( + const std::shared_ptr& colorFilter) { + if (colorFilter->type() == ColorFilter::Type::Matrix) { + return std::static_pointer_cast(colorFilter); + } + return nullptr; +} +} // namespace tgfx \ No newline at end of file diff --git a/src/core/utils/Caster.h b/src/core/utils/Caster.h new file mode 100644 index 00000000..bd07780c --- /dev/null +++ b/src/core/utils/Caster.h @@ -0,0 +1,64 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making tgfx available. +// +// Copyright (C) 2024 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// https://opensource.org/licenses/BSD-3-Clause +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include +#include "core/filters/BlurImageFilter.h" +#include "core/filters/DropShadowImageFilter.h" +#include "core/filters/InnerShadowImageFilter.h" +#include "core/filters/MatrixColorFilter.h" +#include "core/filters/ModeColorFilter.h" +#include "core/shaders/ColorShader.h" +#include "core/shaders/GradientShader.h" +#include "core/shaders/ImageShader.h" + +#pragma once + +namespace tgfx { + +class ShaderCaster { + public: + static std::shared_ptr AsColorShader(const std::shared_ptr& shader); + + static std::shared_ptr AsImageShader(const std::shared_ptr& shader); + + static std::shared_ptr AsGradientShader( + const std::shared_ptr& shader); +}; + +class ImageFilterCaster { + public: + static std::shared_ptr AsBlurImageFilter( + const std::shared_ptr& imageFilter); + + static std::shared_ptr AsDropShadowImageFilter( + const std::shared_ptr& imageFilter); + + static std::shared_ptr AsInnerShadowImageFilter( + const std::shared_ptr& imageFilter); +}; + +class ColorFilterCaster { + public: + static std::shared_ptr AsModeColorFilter( + const std::shared_ptr& colorFilter); + + static std::shared_ptr AsMatrixColorFilter( + const std::shared_ptr& colorFilter); +}; + +} // namespace tgfx \ No newline at end of file diff --git a/src/core/utils/GlyphConverter.cpp b/src/core/utils/GlyphConverter.cpp new file mode 100644 index 00000000..c098f0c0 --- /dev/null +++ b/src/core/utils/GlyphConverter.cpp @@ -0,0 +1,51 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making tgfx available. +// +// Copyright (C) 2024 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// https://opensource.org/licenses/BSD-3-Clause +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "GlyphConverter.h" +#include "tgfx/core/Typeface.h" + +namespace tgfx { + +#ifdef TGFX_USE_GLYPH_TO_UNICODE +std::vector GlyphConverter::glyphsToUnichars(const Font& font, + const std::vector& glyphs) { + auto typeface = font.getTypeface(); + if (!typeface) { + return {}; + } + std::vector result(glyphs.size(), 0); + auto glyphMap = getGlyphToUnicodeMap(typeface); + for (size_t i = 0; i < glyphs.size(); i++) { + result[i] = glyphMap[glyphs[i]]; + } + return result; +} + +const std::vector& GlyphConverter::getGlyphToUnicodeMap( + const std::shared_ptr& typeface) { + auto iter = fontToGlyphMap.find(typeface->uniqueID()); + if (iter != fontToGlyphMap.end()) { + return iter->second; + } + fontToGlyphMap[typeface->uniqueID()] = typeface->getGlyphToUnicodeMap(); + return fontToGlyphMap[typeface->uniqueID()]; +} + +#endif + +} // namespace tgfx \ No newline at end of file diff --git a/src/core/utils/GlyphConverter.h b/src/core/utils/GlyphConverter.h new file mode 100644 index 00000000..6d6f16d3 --- /dev/null +++ b/src/core/utils/GlyphConverter.h @@ -0,0 +1,46 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making tgfx available. +// +// Copyright (C) 2024 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// https://opensource.org/licenses/BSD-3-Clause +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include "tgfx/core/Font.h" +#include "tgfx/core/Typeface.h" +namespace tgfx { + +#ifdef TGFX_USE_GLYPH_TO_UNICODE + +/** + * The glyph converter can convert glyphs to Unicode characters and cache the mapping of glyphs to + * Unicode. The cache is released when the converter is destructed. + */ +class GlyphConverter { + public: + GlyphConverter() = default; + ~GlyphConverter() = default; + + std::vector glyphsToUnichars(const Font& font, const std::vector& glyphs); + + private: + const std::vector& getGlyphToUnicodeMap(const std::shared_ptr& typeface); + + std::unordered_map> fontToGlyphMap; +}; + +#endif +} // namespace tgfx \ No newline at end of file diff --git a/src/core/vectors/coregraphics/CGTypeface.cpp b/src/core/vectors/coregraphics/CGTypeface.cpp index 2781971f..e3dad9c1 100644 --- a/src/core/vectors/coregraphics/CGTypeface.cpp +++ b/src/core/vectors/coregraphics/CGTypeface.cpp @@ -19,6 +19,7 @@ #include "CGTypeface.h" #include "CGScalerContext.h" #include "core/utils/UniqueID.h" +#include "tgfx/core/Typeface.h" #include "tgfx/core/UTF.h" namespace tgfx { @@ -221,4 +222,101 @@ std::shared_ptr CGTypeface::copyTableData(FontTableTag tag) const { bytePtr, length, [](const void*, void* context) { CFRelease((CFDataRef)context); }, (void*)cfData); } + +#ifdef TGFX_USE_GLYPH_TO_UNICODE + +static std::vector GetGlyphMapByChar(CTFontRef ctFont, CFIndex glyphCount) { + std::vector returnMap(static_cast(glyphCount), 0); + UniChar unichar = 0; + while (glyphCount > 0) { + CGGlyph glyph; + if (CTFontGetGlyphsForCharacters(ctFont, &unichar, &glyph, 1)) { + if (returnMap[glyph] == 0) { + returnMap[glyph] = unichar; + --glyphCount; + } + } + if (++unichar == 0) { + break; + } + } + return returnMap; +} + +static constexpr uint16_t PLANE_SIZE = 1 << 13; + +static void GetGlyphMapByPlane(const uint8_t* bits, CTFontRef ctFont, std::vector& map, + uint8_t planeIndex) { + Unichar planeOrigin = planeIndex << 16; // top half of codepoint. + for (uint16_t i = 0; i < PLANE_SIZE; i++) { + uint8_t mask = bits[i]; + if (!mask) { + continue; + } + for (uint8_t j = 0; j < 8; j++) { + if (0 == (mask & (static_cast(1) << j))) { + continue; + } + auto planeOffset = static_cast((i << 3) | j); + Unichar codepoint = planeOrigin | static_cast(planeOffset); + uint16_t utf16[2] = {planeOffset, 0}; + size_t count = 1; + if (planeOrigin != 0) { + count = ToUTF16(codepoint, utf16); + } + CGGlyph glyphs[2] = {0, 0}; + if (CTFontGetGlyphsForCharacters(ctFont, utf16, glyphs, static_cast(count))) { + if (map[glyphs[0]] < 0x20) { + map[glyphs[0]] = codepoint; + } + } + } + } +} + +std::vector CGTypeface::getGlyphToUnicodeMap() const { + auto glyphCount = CTFontGetGlyphCount(ctFont); + + const auto* charSet = CTFontCopyCharacterSet(ctFont); + if (!charSet) { + return GetGlyphMapByChar(ctFont, glyphCount); + } + + const auto* bitmap = CFCharacterSetCreateBitmapRepresentation(nullptr, charSet); + if (!bitmap) { + return {}; + } + + CFIndex dataLength = CFDataGetLength(bitmap); + if (dataLength == 0) { + return {}; + } + + std::vector returnMap(static_cast(glyphCount), 0); + const auto* bits = CFDataGetBytePtr(bitmap); + GetGlyphMapByPlane(bits, ctFont, returnMap, 0); + /* + A CFData object that specifies the bitmap representation of the Unicode + character points the for the new character set. The bitmap representation could + contain all the Unicode character range starting from BMP to Plane 16. The + first 8KiB (8192 bytes) of the data represent the BMP range. The BMP range 8KiB + can be followed by zero to sixteen 8KiB bitmaps, each prepended with the plane + index byte. For example, the bitmap representing the BMP and Plane 2 has the + size of 16385 bytes (8KiB for BMP, 1 byte index, and a 8KiB bitmap for Plane + 2). The plane index byte, in this case, contains the integer value two. + */ + + if (dataLength <= PLANE_SIZE) { + return returnMap; + } + auto extraPlaneCount = (dataLength - PLANE_SIZE) / (1 + PLANE_SIZE); + while (extraPlaneCount-- > 0) { + bits += PLANE_SIZE; + uint8_t planeIndex = *bits++; + GetGlyphMapByPlane(bits, ctFont, returnMap, planeIndex); + } + return returnMap; +} +#endif + } // namespace tgfx diff --git a/src/core/vectors/coregraphics/CGTypeface.h b/src/core/vectors/coregraphics/CGTypeface.h index 428bfd33..c0486d8c 100644 --- a/src/core/vectors/coregraphics/CGTypeface.h +++ b/src/core/vectors/coregraphics/CGTypeface.h @@ -60,6 +60,11 @@ class CGTypeface : public Typeface { std::shared_ptr copyTableData(FontTableTag tag) const override; + protected: +#ifdef TGFX_USE_GLYPH_TO_UNICODE + std::vector getGlyphToUnicodeMap() const override; +#endif + private: CGTypeface(CTFontRef ctFont, std::shared_ptr data); diff --git a/src/core/vectors/freetype/FTTypeface.cpp b/src/core/vectors/freetype/FTTypeface.cpp index dd2d35d5..4d48df2c 100644 --- a/src/core/vectors/freetype/FTTypeface.cpp +++ b/src/core/vectors/freetype/FTTypeface.cpp @@ -17,6 +17,7 @@ ///////////////////////////////////////////////////////////////////////////////////////////////// #include "FTTypeface.h" +#include #include "FTLibrary.h" #include FT_TRUETYPE_TABLES_H #include "FTScalerContext.h" @@ -165,4 +166,22 @@ std::shared_ptr FTTypeface::copyTableData(FontTableTag tag) const { } return Data::MakeAdopted(tableData, tableLength); } + +#ifdef TGFX_USE_GLYPH_TO_UNICODE +std::vector FTTypeface::getGlyphToUnicodeMap() const { + auto numGlyphs = static_cast(face->num_glyphs); + std::vector returnMap(numGlyphs, 0); + + FT_UInt glyphIndex = 0; + auto charCode = FT_Get_First_Char(face, &glyphIndex); + while (glyphIndex) { + if (0 == returnMap[glyphIndex]) { + returnMap[glyphIndex] = static_cast(charCode); + } + charCode = FT_Get_Next_Char(face, charCode, &glyphIndex); + } + return returnMap; +} +#endif + } // namespace tgfx \ No newline at end of file diff --git a/src/core/vectors/freetype/FTTypeface.h b/src/core/vectors/freetype/FTTypeface.h index eab8803b..964fc0f0 100644 --- a/src/core/vectors/freetype/FTTypeface.h +++ b/src/core/vectors/freetype/FTTypeface.h @@ -54,6 +54,11 @@ class FTTypeface : public Typeface { std::shared_ptr copyTableData(FontTableTag tag) const override; + protected: +#ifdef TGFX_USE_GLYPH_TO_UNICODE + std::vector getGlyphToUnicodeMap() const override; +#endif + private: uint32_t _uniqueID = 0; FTFontData data; diff --git a/src/core/vectors/web/WebTypeface.cpp b/src/core/vectors/web/WebTypeface.cpp index 975df344..94f213b9 100644 --- a/src/core/vectors/web/WebTypeface.cpp +++ b/src/core/vectors/web/WebTypeface.cpp @@ -101,4 +101,11 @@ std::string WebTypeface::getText(GlyphID glyphID) const { auto unichar = glyphs.at(glyphID - 1); return UTF::ToUTF8(unichar); } + +#ifdef TGFX_USE_GLYPH_TO_UNICODE +std::vector WebTypeface::getGlyphToUnicodeMap() const { + return GlyphsMap()[webFontFamily]; +} +#endif + } // namespace tgfx diff --git a/src/core/vectors/web/WebTypeface.h b/src/core/vectors/web/WebTypeface.h index 874df2af..9a4555cf 100644 --- a/src/core/vectors/web/WebTypeface.h +++ b/src/core/vectors/web/WebTypeface.h @@ -67,6 +67,11 @@ class WebTypeface : public Typeface { return nullptr; } + protected: +#ifdef TGFX_USE_GLYPH_TO_UNICODE + std::vector getGlyphToUnicodeMap() const override; +#endif + private: explicit WebTypeface(std::string name, std::string style); diff --git a/src/svg/ElementWriter.cpp b/src/svg/ElementWriter.cpp new file mode 100644 index 00000000..13ee23bf --- /dev/null +++ b/src/svg/ElementWriter.cpp @@ -0,0 +1,610 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making tgfx available. +// +// Copyright (C) 2024 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// https://opensource.org/licenses/BSD-3-Clause +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "ElementWriter.h" +#include <_types/_uint32_t.h> +#include +#include +#include "SVGExportingContext.h" +#include "SVGUtils.h" +#include "core/CanvasState.h" +#include "core/utils/Caster.h" +#include "core/utils/Log.h" +#include "core/utils/MathExtra.h" +#include "tgfx/core/BlendMode.h" +#include "tgfx/core/GradientType.h" +#include "tgfx/core/Pixmap.h" +#include "tgfx/core/Rect.h" +#include "tgfx/core/Size.h" +#include "tgfx/core/Surface.h" +#include "tgfx/gpu/Context.h" + +namespace tgfx { + +Resources::Resources(const FillStyle& fill) { + paintColor = ToSVGColor(fill.color); +} + +ElementWriter::ElementWriter(const std::string& name, XMLWriter* writer) : writer(writer) { + writer->startElement(name); +} + +ElementWriter::ElementWriter(const std::string& name, const std::unique_ptr& writer) + : ElementWriter(name, writer.get()) { +} + +ElementWriter::ElementWriter(const std::string& name, const std::unique_ptr& writer, + ResourceStore* bucket) + : writer(writer.get()), resourceStore(bucket) { + writer->startElement(name); +} + +ElementWriter::ElementWriter(const std::string& name, Context* context, + const std::unique_ptr& writer, ResourceStore* bucket, + bool disableWarning, const MCState& state, const FillStyle& fill, + const Stroke* stroke) + : writer(writer.get()), resourceStore(bucket), disableWarning(disableWarning) { + Resources res = addResources(fill, context); + + writer->startElement(name); + + addFillAndStroke(fill, stroke, res); + + if (!state.matrix.isIdentity()) { + addAttribute("transform", ToSVGTransform(state.matrix)); + } +} + +ElementWriter::~ElementWriter() { + writer->endElement(); + resourceStore = nullptr; +} + +void ElementWriter::reportUnsupportedElement(const char* message) const { + if (!disableWarning) { + LOGE("[SVG exporting]:%s", message); + } +} + +void ElementWriter::addFillAndStroke(const FillStyle& fill, const Stroke* stroke, + const Resources& resources) { + if (!stroke) { //fill draw + static const std::string defaultFill = "black"; + if (resources.paintColor != defaultFill) { + addAttribute("fill", resources.paintColor); + } + if (!fill.isOpaque()) { + addAttribute("fill-opacity", fill.color.alpha); + } + } else { //stroke draw + addAttribute("fill", "none"); + + addAttribute("stroke", resources.paintColor); + + float strokeWidth = stroke->width; + if (strokeWidth == 0) { + // Hairline stroke + strokeWidth = 1; + addAttribute("vector-effect", "non-scaling-stroke"); + } + addAttribute("stroke-width", strokeWidth); + + auto cap = ToSVGCap(stroke->cap); + if (!cap.empty()) { + addAttribute("stroke-linecap", cap); + } + + auto join = ToSVGJoin(stroke->join); + if (!join.empty()) { + addAttribute("stroke-linejoin", join); + } + + if (stroke->join == LineJoin::Miter && !FloatNearlyEqual(stroke->miterLimit, 4.f)) { + addAttribute("stroke-miterlimit", stroke->miterLimit); + } + + if (!fill.isOpaque()) { + addAttribute("stroke-opacity", fill.color.alpha); + } + } + + if (fill.blendMode != BlendMode::SrcOver) { + auto blendModeString = ToSVGBlendMode(fill.blendMode); + if (!blendModeString.empty()) { + addAttribute("style", blendModeString); + } else { + reportUnsupportedElement("Unsupported blend mode"); + } + } + + if (!resources.filter.empty()) { + addAttribute("filter", resources.filter); + } +} + +void ElementWriter::addAttribute(const std::string& name, const std::string& val) { + writer->addAttribute(name, val); +} + +void ElementWriter::addAttribute(const std::string& name, int32_t val) { + writer->addS32Attribute(name, val); +} + +void ElementWriter::addAttribute(const std::string& name, float val) { + writer->addScalarAttribute(name, val); +} + +void ElementWriter::addText(const std::string& text) { + writer->addText(text); +} + +void ElementWriter::addFontAttributes(const Font& font) { + addAttribute("font-size", font.getSize()); + + auto typeface = font.getTypeface(); + if (typeface == nullptr) { + return; + } + auto familyName = typeface->fontFamily(); + if (!familyName.empty()) { + addAttribute("font-family", familyName); + } + + if (font.isFauxItalic()) { + addAttribute("font-style", "italic"); + } + if (font.isFauxBold()) { + addAttribute("font-weight", "bold"); + } +} + +void ElementWriter::addRectAttributes(const Rect& rect) { + // x, y default to 0 + if (rect.x() != 0) { + addAttribute("x", rect.x()); + } + if (rect.y() != 0) { + addAttribute("y", rect.y()); + } + + addAttribute("width", rect.width()); + addAttribute("height", rect.height()); +} + +void ElementWriter::addRoundRectAttributes(const RRect& roundRect) { + addRectAttributes(roundRect.rect); + if (FloatNearlyZero(roundRect.radii.x) && FloatNearlyZero(roundRect.radii.y)) { + return; + } + addAttribute("rx", roundRect.radii.x); + addAttribute("ry", roundRect.radii.y); +} + +void ElementWriter::addCircleAttributes(const Rect& bound) { + addAttribute("cx", bound.centerX()); + addAttribute("cy", bound.centerY()); + addAttribute("r", bound.width() * 0.5f); +} + +void ElementWriter::addEllipseAttributes(const Rect& bound) { + addAttribute("cx", bound.centerX()); + addAttribute("cy", bound.centerY()); + addAttribute("rx", bound.width() * 0.5f); + addAttribute("ry", bound.height() * 0.5f); +} + +void ElementWriter::addPathAttributes(const Path& path, PathEncoding encoding) { + addAttribute("d", ToSVGPath(path, encoding)); +} + +Resources ElementWriter::addImageFilterResource(const std::shared_ptr& imageFilter, + Rect bound) { + std::string filterID = resourceStore->addFilter(); + { + ElementWriter filterElement("filter", writer); + filterElement.addAttribute("id", filterID); + if (auto blurFilter = ImageFilterCaster::AsBlurImageFilter(imageFilter)) { + bound = blurFilter->filterBounds(bound); + filterElement.addAttribute("x", bound.x()); + filterElement.addAttribute("y", bound.y()); + filterElement.addAttribute("width", bound.width()); + filterElement.addAttribute("height", bound.height()); + filterElement.addAttribute("filterUnits", "userSpaceOnUse"); + addBlurImageFilter(blurFilter); + } else if (auto dropShadowFilter = ImageFilterCaster::AsDropShadowImageFilter(imageFilter)) { + bound = blurFilter->filterBounds(bound); + filterElement.addAttribute("x", bound.x()); + filterElement.addAttribute("y", bound.y()); + filterElement.addAttribute("width", bound.width()); + filterElement.addAttribute("height", bound.height()); + filterElement.addAttribute("filterUnits", "userSpaceOnUse"); + addDropShadowImageFilter(dropShadowFilter); + } else if (auto innerShadowFilter = ImageFilterCaster::AsInnerShadowImageFilter(imageFilter)) { + bound = blurFilter->filterBounds(bound); + filterElement.addAttribute("x", bound.x()); + filterElement.addAttribute("y", bound.y()); + filterElement.addAttribute("width", bound.width() + innerShadowFilter->dx); + filterElement.addAttribute("height", bound.height() + innerShadowFilter->dy); + filterElement.addAttribute("filterUnits", "userSpaceOnUse"); + addInnerShadowImageFilter(innerShadowFilter); + } else { + // TODO (YGaurora): The compose filter can be expanded into multiple filters for export. This + // can be implemented. + reportUnsupportedElement("Unsupported image filter"); + } + } + Resources resources; + resources.filter = "url(#" + filterID + ")"; + return resources; +} + +void ElementWriter::addBlurImageFilter(const std::shared_ptr& filter) { + ElementWriter blurElement("feGaussianBlur", writer); + auto blurSize = filter->filterBounds(Rect::MakeEmpty()).size(); + blurElement.addAttribute("stdDeviation", std::max(blurSize.width / 4.f, blurSize.height / 4.f)); + blurElement.addAttribute("result", "blur"); +} + +void ElementWriter::addDropShadowImageFilter( + const std::shared_ptr& filter) { + if (!filter->blurFilter) { + return; + } + { + ElementWriter offsetElement("feOffset", writer); + offsetElement.addAttribute("dx", filter->dx); + offsetElement.addAttribute("dy", filter->dy); + } + { + ElementWriter blurElement("feGaussianBlur", writer); + auto blurSize = filter->blurFilter->filterBounds(Rect::MakeEmpty()).size(); + blurElement.addAttribute("stdDeviation", std::max(blurSize.width / 4.f, blurSize.height / 4.f)); + blurElement.addAttribute("result", "blur"); + } + { + ElementWriter colorMatrixElement("feColorMatrix", writer); + colorMatrixElement.addAttribute("type", "matrix"); + auto color = filter->color; + colorMatrixElement.addAttribute("values", "0 0 0 0 " + FloatToString(color.red) + " 0 0 0 0 " + + FloatToString(color.green) + " 0 0 0 0 " + + FloatToString(color.blue) + " 0 0 0 " + + FloatToString(color.alpha) + " 0"); + } + if (!filter->shadowOnly) { + ElementWriter blendElement("feBlend", writer); + blendElement.addAttribute("mode", "normal"); + blendElement.addAttribute("in", "SourceGraphic"); + } +} +void ElementWriter::addInnerShadowImageFilter( + const std::shared_ptr& filter) { + if (!filter->blurFilter) { + return; + } + { + ElementWriter colorMatrixElement("feColorMatrix", writer); + colorMatrixElement.addAttribute("in", "SourceAlpha"); + colorMatrixElement.addAttribute("type", "matrix"); + colorMatrixElement.addAttribute("values", "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"); + colorMatrixElement.addAttribute("result", "hardAlpha"); + } + if (!filter->shadowOnly) { + { + ElementWriter floodElement("feFlood", writer); + floodElement.addAttribute("flood-opacity", "0"); + floodElement.addAttribute("result", "BackgroundImageFix"); + } + { + ElementWriter blendElement("feBlend", writer); + blendElement.addAttribute("mode", "normal"); + blendElement.addAttribute("in", "SourceGraphic"); + blendElement.addAttribute("in2", "BackgroundImageFix"); + blendElement.addAttribute("result", "shape"); + } + } + { + ElementWriter offsetElement("feOffset", writer); + offsetElement.addAttribute("dx", filter->dx); + offsetElement.addAttribute("dy", filter->dy); + } + { + ElementWriter blurElement("feGaussianBlur", writer); + auto blurSize = filter->blurFilter->filterBounds(Rect::MakeEmpty()).size(); + blurElement.addAttribute("stdDeviation", std::max(blurSize.width / 4.f, blurSize.height / 4.f)); + } + { + ElementWriter compositeElement("feComposite", writer); + compositeElement.addAttribute("in2", "hardAlpha"); + compositeElement.addAttribute("operator", "arithmetic"); + compositeElement.addAttribute("k2", "-1"); + compositeElement.addAttribute("k3", "1"); + } + { + ElementWriter colorMatrixElement("feColorMatrix", writer); + colorMatrixElement.addAttribute("type", "matrix"); + auto color = filter->color; + colorMatrixElement.addAttribute("values", "0 0 0 0 " + FloatToString(color.red) + " 0 0 0 0 " + + FloatToString(color.green) + " 0 0 0 0 " + + FloatToString(color.blue) + " 0 0 0 " + + FloatToString(color.alpha) + " 0"); + } + if (!filter->shadowOnly) { + ElementWriter blendElement("feBlend", writer); + blendElement.addAttribute("mode", "normal"); + blendElement.addAttribute("in2", "shape"); + } +} + +Resources ElementWriter::addResources(const FillStyle& fill, Context* context) { + Resources resources(fill); + + if (auto shader = fill.shader) { + ElementWriter defs("defs", writer); + addShaderResources(shader, context, &resources); + } + + if (auto colorFilter = fill.colorFilter) { + if (auto blendFilter = ColorFilterCaster::AsModeColorFilter(colorFilter)) { + addBlendColorFilterResources(*blendFilter, &resources); + } else if (auto matrixFilter = ColorFilterCaster::AsMatrixColorFilter(colorFilter)) { + addMatrixColorFilterResources(*matrixFilter, &resources); + } else { + reportUnsupportedElement("Unsupported color filter"); + } + } + + return resources; +} + +void ElementWriter::addShaderResources(const std::shared_ptr& shader, Context* context, + Resources* resources) { + if (auto colorShader = ShaderCaster::AsColorShader(shader)) { + addColorShaderResources(colorShader, resources); + } else if (auto gradientShader = ShaderCaster::AsGradientShader(shader)) { + addGradientShaderResources(gradientShader, resources); + } else if (auto imageShader = ShaderCaster::AsImageShader(shader)) { + addImageShaderResources(imageShader, context, resources); + } else { + // TODO(YGaurora): + // Export color filter shaders as color filters. + // Export blend shaders as a combination of a shader and blend mode. + // Export matrix shaders as a combination of a shader and matrix. The SVG standard allows + // writing the matrix into using patternTransform. + reportUnsupportedElement("shader"); + } +} + +void ElementWriter::addColorShaderResources(const std::shared_ptr& shader, + Resources* resources) { + Color color; + if (shader->asColor(&color)) { + resources->paintColor = ToSVGColor(color); + } +} + +void ElementWriter::addGradientShaderResources(const std::shared_ptr& shader, + Resources* resources) { + GradientInfo info; + GradientType type = shader->asGradient(&info); + + DEBUG_ASSERT(info.colors.size() == info.positions.size()); + if (type == GradientType::Linear) { + resources->paintColor = "url(#" + addLinearGradientDef(info) + ")"; + } else if (type == GradientType::Radial) { + resources->paintColor = "url(#" + addRadialGradientDef(info) + ")"; + } else { + resources->paintColor = "url(#" + addUnsupportedGradientDef(info) + ")"; + reportUnsupportedElement("Unsupported gradient type"); + } +} + +void ElementWriter::addGradientColors(const GradientInfo& info) { + DEBUG_ASSERT(info.colors.size() >= 2); + for (uint32_t i = 0; i < info.colors.size(); ++i) { + auto color = info.colors[i]; + auto colorStr = ToSVGColor(color); + + ElementWriter stop("stop", writer); + stop.addAttribute("offset", info.positions[i]); + stop.addAttribute("stop-color", colorStr); + + if (!color.isOpaque()) { + stop.addAttribute("stop-opacity", color.alpha); + } + } +} + +std::string ElementWriter::addLinearGradientDef(const GradientInfo& info) { + DEBUG_ASSERT(resourceStore); + auto id = resourceStore->addGradient(); + + { + ElementWriter gradient("linearGradient", writer); + + gradient.addAttribute("id", id); + gradient.addAttribute("gradientUnits", "userSpaceOnUse"); + gradient.addAttribute("x1", info.points[0].x); + gradient.addAttribute("y1", info.points[0].y); + gradient.addAttribute("x2", info.points[1].x); + gradient.addAttribute("y2", info.points[1].y); + addGradientColors(info); + } + return id; +} + +std::string ElementWriter::addRadialGradientDef(const GradientInfo& info) { + DEBUG_ASSERT(resourceStore); + auto id = resourceStore->addGradient(); + + { + ElementWriter gradient("radialGradient", writer); + + gradient.addAttribute("id", id); + gradient.addAttribute("gradientUnits", "userSpaceOnUse"); + gradient.addAttribute("r", info.radiuses[0]); + gradient.addAttribute("cx", info.points[0].x); + gradient.addAttribute("cy", info.points[0].y); + addGradientColors(info); + } + return id; +} + +std::string ElementWriter::addUnsupportedGradientDef(const GradientInfo& info) { + DEBUG_ASSERT(resourceStore); + auto id = resourceStore->addGradient(); + + { + ElementWriter gradient("linearGradient", writer); + + gradient.addAttribute("id", id); + gradient.addAttribute("gradientUnits", "objectBoundingBox"); + gradient.addAttribute("x1", 0); + gradient.addAttribute("y1", 0); + gradient.addAttribute("x2", 1); + gradient.addAttribute("y2", 0); + addGradientColors(info); + } + return id; +}; + +void ElementWriter::addImageShaderResources(const std::shared_ptr& shader, + Context* context, Resources* resources) { + auto image = shader->image; + DEBUG_ASSERT(shader->image); + + DEBUG_ASSERT(context); + Bitmap bitmap = SVGExportingContext::ImageExportToBitmap(context, image); + if (bitmap.isEmpty()) { + return; + } + auto dataUri = AsDataUri(Pixmap(bitmap)); + if (!dataUri) { + return; + } + + auto imageWidth = image->width(); + auto imageHeight = image->height(); + auto transDimension = [](TileMode mode, int length) -> std::string { + if (mode == TileMode::Repeat) { + return std::to_string(length); + } else { + return "100%"; + } + }; + std::string widthValue = transDimension(shader->tileModeX, imageWidth); + std::string heightValue = transDimension(shader->tileModeY, imageHeight); + + std::string patternID = resourceStore->addPattern(); + { + ElementWriter pattern("pattern", writer); + pattern.addAttribute("id", patternID); + pattern.addAttribute("patternUnits", "userSpaceOnUse"); + pattern.addAttribute("patternContentUnits", "userSpaceOnUse"); + pattern.addAttribute("width", widthValue); + pattern.addAttribute("height", heightValue); + pattern.addAttribute("x", 0); + pattern.addAttribute("y", 0); + + { + std::string imageID = resourceStore->addImage(); + ElementWriter imageTag("image", writer); + imageTag.addAttribute("id", imageID); + imageTag.addAttribute("x", 0); + imageTag.addAttribute("y", 0); + imageTag.addAttribute("width", image->width()); + imageTag.addAttribute("height", image->height()); + imageTag.addAttribute("xlink:href", static_cast(dataUri->data())); + } + } + resources->paintColor = "url(#" + patternID + ")"; +} + +void ElementWriter::addBlendColorFilterResources(const ModeColorFilter& modeColorFilter, + Resources* resources) { + + auto BlendModeString = ToSVGBlendMode(modeColorFilter.mode); + if (BlendModeString.empty()) { + reportUnsupportedElement("Unsupported blend mode in color filter"); + return; + } + + std::string filterID = resourceStore->addFilter(); + { + ElementWriter filterElement("filter", writer); + filterElement.addAttribute("id", filterID); + filterElement.addAttribute("x", "0%"); + filterElement.addAttribute("y", "0%"); + filterElement.addAttribute("width", "100%"); + filterElement.addAttribute("height", "100%"); + + { + // first flood with filter color + ElementWriter floodElement("feFlood", writer); + floodElement.addAttribute("flood-color", ToSVGColor(modeColorFilter.color)); + floodElement.addAttribute("flood-opacity", modeColorFilter.color.alpha); + floodElement.addAttribute("result", "flood"); + } + + { + ElementWriter blendElement("feBlend", writer); + blendElement.addAttribute("in", "SourceGraphic"); + blendElement.addAttribute("in2", "flood"); + blendElement.addAttribute("mode", BlendModeString); + blendElement.addAttribute("result", "blend"); + } + + { + // apply the transform to filter color + ElementWriter compositeElement("feComposite", writer); + compositeElement.addAttribute("in", "blend"); + compositeElement.addAttribute("operator", "in"); + } + } + resources->filter = "url(#" + filterID + ")"; +} + +void ElementWriter::addMatrixColorFilterResources(const MatrixColorFilter& matrixColorFilter, + Resources* resources) { + std::string filterID = resourceStore->addFilter(); + { + ElementWriter filterElement("filter", writer); + filterElement.addAttribute("id", filterID); + filterElement.addAttribute("x", "0%"); + filterElement.addAttribute("y", "0%"); + filterElement.addAttribute("width", "100%"); + filterElement.addAttribute("height", "100%"); + + { + ElementWriter colorMatrixElement("feColorMatrix", writer); + colorMatrixElement.addAttribute("in", "SourceGraphic"); + colorMatrixElement.addAttribute("type", "matrix"); + auto colorMatrix = matrixColorFilter.matrix; + std::string matrixString; + for (uint32_t i = 0; i < colorMatrix.size(); i++) { + matrixString += FloatToString(colorMatrix[i]); + if (i != 19) { + matrixString += " "; + } + } + colorMatrixElement.addAttribute("values", matrixString); + } + } + resources->filter = "url(#" + filterID + ")"; +} + +} // namespace tgfx \ No newline at end of file diff --git a/src/svg/ElementWriter.h b/src/svg/ElementWriter.h new file mode 100644 index 00000000..d389e246 --- /dev/null +++ b/src/svg/ElementWriter.h @@ -0,0 +1,102 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making tgfx available. +// +// Copyright (C) 2024 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// https://opensource.org/licenses/BSD-3-Clause +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include "ResourceStore.h" +#include "SVGUtils.h" +#include "core/FillStyle.h" +#include "core/filters/BlurImageFilter.h" +#include "core/filters/DropShadowImageFilter.h" +#include "core/filters/InnerShadowImageFilter.h" +#include "core/filters/MatrixColorFilter.h" +#include "core/filters/ModeColorFilter.h" +#include "core/shaders/ColorShader.h" +#include "core/shaders/GradientShader.h" +#include "core/shaders/ImageShader.h" +#include "tgfx/core/ImageFilter.h" +#include "tgfx/core/Rect.h" +#include "tgfx/core/Stroke.h" +#include "tgfx/gpu/Context.h" +#include "xml/XMLWriter.h" + +namespace tgfx { + +class ElementWriter { + public: + ElementWriter(const std::string& name, XMLWriter* writer); + ElementWriter(const std::string& name, const std::unique_ptr& writer); + ElementWriter(const std::string& name, const std::unique_ptr& writer, + ResourceStore* bucket); + ElementWriter(const std::string& name, Context* context, const std::unique_ptr& writer, + ResourceStore* bucket, bool disableWarning, const MCState& state, + const FillStyle& fill, const Stroke* stroke = nullptr); + ~ElementWriter(); + + void addAttribute(const std::string& name, const std::string& val); + void addAttribute(const std::string& name, int32_t val); + void addAttribute(const std::string& name, float val); + + void addText(const std::string& text); + void addFontAttributes(const Font& font); + void addRectAttributes(const Rect& rect); + void addRoundRectAttributes(const RRect& roundRect); + void addCircleAttributes(const Rect& bound); + void addEllipseAttributes(const Rect& bound); + void addPathAttributes(const Path& path, PathEncoding encoding); + + Resources addImageFilterResource(const std::shared_ptr& imageFilter, Rect bound); + + private: + Resources addResources(const FillStyle& fill, Context* context); + + void addShaderResources(const std::shared_ptr& shader, Context* context, + Resources* resources); + void addColorShaderResources(const std::shared_ptr& shader, + Resources* resources); + void addGradientShaderResources(const std::shared_ptr& shader, + Resources* resources); + void addImageShaderResources(const std::shared_ptr& shader, Context* context, + Resources* resources); + + void addBlendColorFilterResources(const ModeColorFilter& modeColorFilter, Resources* resources); + + void addMatrixColorFilterResources(const MatrixColorFilter& matrixColorFilter, + Resources* resources); + + void addFillAndStroke(const FillStyle& fill, const Stroke* stroke, const Resources& resources); + + void addGradientColors(const GradientInfo& info); + std::string addLinearGradientDef(const GradientInfo& info); + std::string addRadialGradientDef(const GradientInfo& info); + std::string addUnsupportedGradientDef(const GradientInfo& info); + + void addBlurImageFilter(const std::shared_ptr& filter); + void addDropShadowImageFilter(const std::shared_ptr& filter); + void addInnerShadowImageFilter(const std::shared_ptr& filter); + + void reportUnsupportedElement(const char* message) const; + + XMLWriter* writer = nullptr; + ResourceStore* resourceStore = nullptr; + bool disableWarning = false; +}; + +} // namespace tgfx \ No newline at end of file diff --git a/src/svg/ResourceStore.h b/src/svg/ResourceStore.h new file mode 100644 index 00000000..0ad425b2 --- /dev/null +++ b/src/svg/ResourceStore.h @@ -0,0 +1,70 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making tgfx available. +// +// Copyright (C) 2024 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// https://opensource.org/licenses/BSD-3-Clause +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "core/FillStyle.h" + +namespace tgfx { + +struct Resources { + Resources() = default; + explicit Resources(const FillStyle& fill); + std::string paintColor; + std::string filter; +}; + +// TODO(YGAurora) implements the feature to reuse resources +class ResourceStore { + public: + ResourceStore() = default; + + std::string addGradient() { + return "gradient_" + std::to_string(gradientCount++); + } + + std::string addPath() { + return "path_" + std::to_string(pathCount++); + } + + std::string addImage() { + return "img_" + std::to_string(imageCount++); + } + + std::string addFilter() { + return "filter_" + std::to_string(filterCount++); + } + + std::string addPattern() { + return "pattern_" + std::to_string(patternCount++); + } + + std::string addClip() { + return "clip_" + std::to_string(clipCount++); + } + + private: + uint32_t gradientCount = 0; + uint32_t pathCount = 0; + uint32_t imageCount = 0; + uint32_t patternCount = 0; + uint32_t filterCount = 0; + uint32_t clipCount = 0; +}; + +} // namespace tgfx \ No newline at end of file diff --git a/src/svg/SVGExporter.cpp b/src/svg/SVGExporter.cpp new file mode 100644 index 00000000..82716dfe --- /dev/null +++ b/src/svg/SVGExporter.cpp @@ -0,0 +1,66 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making tgfx available. +// +// Copyright (C) 2024 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// https://opensource.org/licenses/BSD-3-Clause +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "tgfx/svg/SVGExporter.h" +#include +#include +#include +#include "ElementWriter.h" +#include "core/utils/Log.h" +#include "svg/SVGExportingContext.h" +#include "svg/xml/XMLWriter.h" +#include "tgfx/core/Canvas.h" +#include "tgfx/core/Size.h" + +namespace tgfx { + +std::shared_ptr SVGExporter::Make(const std::shared_ptr& svgStream, + Context* context, const Rect& viewBox, + uint32_t exportFlags) { + if (!context || !svgStream || viewBox.isEmpty()) { + return nullptr; + } + return std::shared_ptr(new SVGExporter(svgStream, context, viewBox, exportFlags)); +} + +SVGExporter::SVGExporter(const std::shared_ptr& svgStream, Context* context, + const Rect& viewBox, uint32_t exportFlags) { + auto writer = + std::make_unique(svgStream, exportFlags & SVGExportFlags::DisablePrettyXML); + drawContext = new SVGExportingContext(context, viewBox, std::move(writer), exportFlags); + canvas = new Canvas(drawContext); + drawContext->setCanvas(canvas); +}; + +SVGExporter::~SVGExporter() { + delete canvas; + delete drawContext; +}; + +Canvas* SVGExporter::getCanvas() const { + return canvas; +}; + +void SVGExporter::close() { + delete canvas; + canvas = nullptr; + delete drawContext; + drawContext = nullptr; +}; + +} // namespace tgfx \ No newline at end of file diff --git a/src/svg/SVGExportingContext.cpp b/src/svg/SVGExportingContext.cpp new file mode 100644 index 00000000..724d9804 --- /dev/null +++ b/src/svg/SVGExportingContext.cpp @@ -0,0 +1,392 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making tgfx available. +// +// Copyright (C) 2024 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// https://opensource.org/licenses/BSD-3-Clause +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "SVGExportingContext.h" +#include <_types/_uint32_t.h> +#include +#include +#include +#include "ElementWriter.h" +#include "SVGUtils.h" +#include "core/CanvasState.h" +#include "core/FillStyle.h" +#include "core/utils/Caster.h" +#include "core/utils/Log.h" +#include "core/utils/MathExtra.h" +#include "svg/SVGTextBuilder.h" +#include "tgfx/core/Bitmap.h" +#include "tgfx/core/Font.h" +#include "tgfx/core/Image.h" +#include "tgfx/core/Matrix.h" +#include "tgfx/core/Path.h" +#include "tgfx/core/PathTypes.h" +#include "tgfx/core/Pixmap.h" +#include "tgfx/core/RRect.h" +#include "tgfx/core/Rect.h" +#include "tgfx/core/Stroke.h" +#include "tgfx/core/Surface.h" +#include "tgfx/core/TileMode.h" +#include "tgfx/gpu/Context.h" +#include "tgfx/svg/SVGExporter.h" + +namespace tgfx { + +SVGExportingContext::SVGExportingContext(Context* context, const Rect& viewBox, + std::unique_ptr xmlWriter, uint32_t exportFlags) + : exportFlags(exportFlags), context(context), writer(std::move(xmlWriter)), + resourceBucket(new ResourceStore) { + if (viewBox.isEmpty()) { + return; + } + + writer->writeHeader(); + // The root tag gets closed by the destructor. + rootElement = std::make_unique("svg", writer); + + rootElement->addAttribute("xmlns", "http://www.w3.org/2000/svg"); + rootElement->addAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink"); + if (viewBox.x() == 0.0f && viewBox.y() == 0.0f) { + rootElement->addAttribute("width", viewBox.width()); + rootElement->addAttribute("height", viewBox.height()); + } else { + std::string viewBoxString = FloatToString(viewBox.x()) + " " + FloatToString(viewBox.y()) + + " " + FloatToString(viewBox.width()) + " " + + FloatToString(viewBox.height()); + rootElement->addAttribute("viewBox", viewBoxString); + } +} + +void SVGExportingContext::drawRect(const Rect& rect, const MCState& state, const FillStyle& fill) { + + std::unique_ptr svg; + if (RequiresViewportReset(fill)) { + svg = std::make_unique("svg", context, writer, resourceBucket.get(), + exportFlags & SVGExportFlags::ConvertTextToPaths, state, + fill); + svg->addRectAttributes(rect); + } + + applyClipPath(state.clip); + ElementWriter rectElement("rect", context, writer, resourceBucket.get(), + exportFlags & SVGExportFlags::ConvertTextToPaths, state, fill); + + if (svg) { + rectElement.addAttribute("x", 0); + rectElement.addAttribute("y", 0); + rectElement.addAttribute("width", "100%"); + rectElement.addAttribute("height", "100%"); + } else { + rectElement.addRectAttributes(rect); + } +} + +void SVGExportingContext::drawRRect(const RRect& roundRect, const MCState& state, + const FillStyle& fill) { + applyClipPath(state.clip); + if (roundRect.isOval()) { + if (roundRect.rect.width() == roundRect.rect.height()) { + ElementWriter circleElement("circle", context, writer, resourceBucket.get(), + exportFlags & SVGExportFlags::ConvertTextToPaths, state, fill); + circleElement.addCircleAttributes(roundRect.rect); + return; + } else { + ElementWriter ovalElement("ellipse", context, writer, resourceBucket.get(), + exportFlags & SVGExportFlags::ConvertTextToPaths, state, fill); + ovalElement.addEllipseAttributes(roundRect.rect); + } + } else { + ElementWriter rrectElement("rect", context, writer, resourceBucket.get(), + exportFlags & SVGExportFlags::ConvertTextToPaths, state, fill); + rrectElement.addRoundRectAttributes(roundRect); + } +} + +void SVGExportingContext::drawShape(std::shared_ptr shape, const MCState& state, + const FillStyle& style) { + applyClipPath(state.clip); + auto path = shape->getPath(); + ElementWriter pathElement("path", context, writer, resourceBucket.get(), + exportFlags & SVGExportFlags::ConvertTextToPaths, state, style); + pathElement.addPathAttributes(path, tgfx::SVGExportingContext::PathEncoding()); + if (path.getFillType() == PathFillType::EvenOdd) { + pathElement.addAttribute("fill-rule", "evenodd"); + } +} + +void SVGExportingContext::drawImage(std::shared_ptr image, const SamplingOptions& sampling, + const MCState& state, const FillStyle& style) { + if (image == nullptr) { + return; + } + auto rect = Rect::MakeWH(image->width(), image->height()); + return drawImageRect(std::move(image), rect, sampling, state, style); +} + +void SVGExportingContext::drawImageRect(std::shared_ptr image, const Rect& rect, + const SamplingOptions&, const MCState& state, + const FillStyle& style) { + if (image == nullptr) { + return; + } + + Bitmap bitmap = ImageExportToBitmap(context, image); + if (!bitmap.isEmpty()) { + Rect srcRect = Rect::MakeWH(image->width(), image->height()); + float scaleX = rect.width() / srcRect.width(); + float scaleY = rect.height() / srcRect.height(); + float transX = rect.left - srcRect.left * scaleX; + float transY = rect.top - srcRect.top * scaleY; + + MCState newState; + newState.matrix = state.matrix; + newState.matrix.postScale(scaleX, scaleY); + newState.matrix.postTranslate(transX, transY); + + exportPixmap(Pixmap(bitmap), newState, style); + } +} + +void SVGExportingContext::exportPixmap(const Pixmap& pixmap, const MCState& state, + const FillStyle& style) { + auto dataUri = AsDataUri(pixmap); + if (!dataUri) { + return; + } + + std::string imageID = resourceBucket->addImage(); + { + ElementWriter defElement("defs", writer); + { + ElementWriter imageElement("image", writer); + imageElement.addAttribute("id", imageID); + imageElement.addAttribute("width", pixmap.width()); + imageElement.addAttribute("height", pixmap.height()); + imageElement.addAttribute("xlink:href", static_cast(dataUri->data())); + } + } + { + applyClipPath(state.clip); + ElementWriter imageUse("use", context, writer, resourceBucket.get(), + exportFlags & SVGExportFlags::ConvertTextToPaths, state, style); + imageUse.addAttribute("xlink:href", "#" + imageID); + } +} + +void SVGExportingContext::drawGlyphRunList(std::shared_ptr glyphRunList, + const MCState& state, const FillStyle& style, + const Stroke* stroke) { + if (!glyphRunList) { + return; + } + + bool hasFont = glyphRunList->glyphRuns()[0].glyphFace->asFont(nullptr); + + // If the font needs to be converted to a path but lacks outlines (e.g., emoji font, web font), + // it cannot be converted. + applyClipPath(state.clip); + if (hasFont) { + if (glyphRunList->hasOutlines() && !glyphRunList->hasColor() && + exportFlags & SVGExportFlags::ConvertTextToPaths) { + exportGlyphsAsPath(glyphRunList, state, style, stroke); + } else { + exportGlyphsAsText(glyphRunList, state, style, stroke); + } + } else { + if (glyphRunList->hasColor()) { + exportGlyphsAsImage(glyphRunList, state, style); + } else { + exportGlyphsAsPath(glyphRunList, state, style, stroke); + } + } +} + +void SVGExportingContext::exportGlyphsAsPath(const std::shared_ptr& glyphRunList, + const MCState& state, const FillStyle& style, + const Stroke* stroke) { + Path path; + if (glyphRunList->getPath(&path)) { + ElementWriter pathElement("path", context, writer, resourceBucket.get(), + exportFlags & SVGExportFlags::ConvertTextToPaths, state, style, + stroke); + pathElement.addPathAttributes(path, tgfx::SVGExportingContext::PathEncoding()); + if (path.getFillType() == PathFillType::EvenOdd) { + pathElement.addAttribute("fill-rule", "evenodd"); + } + } +} + +void SVGExportingContext::exportGlyphsAsText(const std::shared_ptr& glyphRunList, + const MCState& state, const FillStyle& style, + const Stroke* stroke) { + for (const auto& glyphRun : glyphRunList->glyphRuns()) { + ElementWriter textElement("text", context, writer, resourceBucket.get(), + exportFlags & SVGExportFlags::ConvertTextToPaths, state, style, + stroke); + + Font font; + if (glyphRun.glyphFace->asFont(&font)) { + textElement.addFontAttributes(font); + + auto unicharInfo = textBuilder.glyphToUnicharsInfo(glyphRun); + textElement.addAttribute("x", unicharInfo.posX); + textElement.addAttribute("y", unicharInfo.posY); + textElement.addText(unicharInfo.text); + } + } +} + +void SVGExportingContext::exportGlyphsAsImage(const std::shared_ptr& glyphRunList, + const MCState& state, const FillStyle& style) { + auto viewMatrix = state.matrix; + auto scale = viewMatrix.getMaxScale(); + if (scale <= 0) { + return; + } + viewMatrix.preScale(1.0f / scale, 1.0f / scale); + for (const auto& glyphRun : glyphRunList->glyphRuns()) { + auto glyphFace = glyphRun.glyphFace; + glyphFace = glyphFace->makeScaled(scale); + if (glyphFace == nullptr) { + continue; + } + const auto& glyphIDs = glyphRun.glyphs; + auto glyphCount = glyphIDs.size(); + const auto& positions = glyphRun.positions; + auto glyphState = state; + for (size_t i = 0; i < glyphCount; ++i) { + const auto& glyphID = glyphIDs[i]; + const auto& position = positions[i]; + auto glyphImage = glyphFace->getImage(glyphID, &glyphState.matrix); + if (glyphImage == nullptr) { + continue; + } + glyphState.matrix.postTranslate(position.x * scale, position.y * scale); + glyphState.matrix.postConcat(viewMatrix); + auto rect = Rect::MakeWH(glyphImage->width(), glyphImage->height()); + drawImageRect(std::move(glyphImage), rect, {}, glyphState, style); + } + } +} + +void SVGExportingContext::drawPicture(std::shared_ptr picture, const MCState& state) { + if (picture != nullptr) { + picture->playback(this, state); + } +} + +void SVGExportingContext::drawLayer(std::shared_ptr picture, const MCState& state, + const FillStyle&, std::shared_ptr imageFilter) { + if (picture == nullptr) { + return; + } + + Resources resources; + if (imageFilter) { + ElementWriter defs("defs", writer, resourceBucket.get()); + auto bound = picture->getBounds(); + resources = defs.addImageFilterResource(imageFilter, bound); + } + { + applyClipPath(state.clip); + auto groupElement = std::make_unique("g", writer, resourceBucket.get()); + if (imageFilter) { + groupElement->addAttribute("filter", resources.filter); + } + picture->playback(this, state); + } +} + +bool SVGExportingContext::RequiresViewportReset(const FillStyle& fill) { + auto shader = fill.shader; + if (!shader) { + return false; + } + + if (auto imageShader = ShaderCaster::AsImageShader(shader)) { + return imageShader->tileModeX == TileMode::Repeat || imageShader->tileModeY == TileMode::Repeat; + } + return false; +} + +PathEncoding SVGExportingContext::PathEncoding() { + return PathEncoding::Absolute; +} + +void SVGExportingContext::applyClipPath(const Path& clipPath) { + auto defineClip = [this](const Path& clipPath) -> std::string { + std::string clipID = resourceBucket->addClip(); + ElementWriter clipPathElement("clipPath", writer); + clipPathElement.addAttribute("id", clipID); + { + std::unique_ptr element; + Rect rect; + RRect rrect; + Rect ovalBound; + if (clipPath.isRect(&rect)) { + element = std::make_unique("rect", writer); + element->addRectAttributes(rect); + } else if (clipPath.isRRect(&rrect)) { + element = std::make_unique("rect", writer); + element->addRoundRectAttributes(rrect); + } else if (clipPath.isOval(&ovalBound)) { + if (FloatNearlyEqual(ovalBound.width(), ovalBound.height())) { + element = std::make_unique("circle", writer); + element->addCircleAttributes(ovalBound); + } else { + element = std::make_unique("ellipse", writer); + element->addEllipseAttributes(ovalBound); + } + } else { + element = std::make_unique("path", writer); + element->addPathAttributes(clipPath, tgfx::SVGExportingContext::PathEncoding()); + if (clipPath.getFillType() == PathFillType::EvenOdd) { + element->addAttribute("clip-rule", "evenodd"); + } + } + } + return clipID; + }; + + if (clipPath == currentClipPath) { + return; + } + clipGroupElement = nullptr; + if (clipPath.isEmpty()) { + return; + } + currentClipPath = clipPath; + auto clipID = defineClip(currentClipPath); + clipGroupElement = std::make_unique("g", writer); + clipGroupElement->addAttribute("clip-path", "url(#" + clipID + ")"); +} + +Bitmap SVGExportingContext::ImageExportToBitmap(Context* context, + const std::shared_ptr& image) { + auto surface = Surface::Make(context, image->width(), image->height()); + auto* canvas = surface->getCanvas(); + canvas->drawImage(image); + + Bitmap bitmap(image->width(), image->height(), false, false); + auto* pixels = bitmap.lockPixels(); + if (surface->readPixels(bitmap.info(), pixels)) { + return bitmap; + } + return Bitmap(); +} + +} // namespace tgfx \ No newline at end of file diff --git a/src/svg/SVGExportingContext.h b/src/svg/SVGExportingContext.h new file mode 100644 index 00000000..66f23a17 --- /dev/null +++ b/src/svg/SVGExportingContext.h @@ -0,0 +1,120 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making tgfx available. +// +// Copyright (C) 2024 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// https://opensource.org/licenses/BSD-3-Clause +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include <_types/_uint32_t.h> +#include +#include +#include +#include +#include "core/CanvasState.h" +#include "core/DrawContext.h" +#include "core/FillStyle.h" +#include "svg/SVGTextBuilder.h" +#include "svg/SVGUtils.h" +#include "svg/xml/XMLWriter.h" +#include "tgfx/core/Bitmap.h" +#include "tgfx/core/Canvas.h" +#include "tgfx/core/Path.h" +#include "tgfx/core/Pixmap.h" +#include "tgfx/core/Rect.h" +#include "tgfx/core/Size.h" +#include "tgfx/gpu/Context.h" +#include "tgfx/svg/SVGExporter.h" + +namespace tgfx { + +class ResourceStore; +class ElementWriter; + +class SVGExportingContext : public DrawContext { + public: + SVGExportingContext(Context* context, const Rect& viewBox, std::unique_ptr writer, + uint32_t exportFlags); + ~SVGExportingContext() override = default; + + void setCanvas(Canvas* inputCanvas) { + canvas = inputCanvas; + } + + void clear() override{}; + + void drawRect(const Rect&, const MCState&, const FillStyle&) override; + + void drawRRect(const RRect&, const MCState&, const FillStyle&) override; + + void drawShape(std::shared_ptr, const MCState&, const FillStyle&) override; + + void drawImage(std::shared_ptr image, const SamplingOptions& sampling, + const MCState& state, const FillStyle& style) override; + + void drawImageRect(std::shared_ptr image, const Rect& rect, + const SamplingOptions& sampling, const MCState& state, + const FillStyle& style) override; + + void drawGlyphRunList(std::shared_ptr, const MCState&, const FillStyle&, + const Stroke*) override; + + void drawPicture(std::shared_ptr, const MCState&) override; + + void drawLayer(std::shared_ptr, const MCState&, const FillStyle&, + std::shared_ptr) override; + + XMLWriter* getWriter() const { + return writer.get(); + } + + /** + * Draws a image onto a surface and reads the pixels from the surface. + */ + static Bitmap ImageExportToBitmap(Context* context, const std::shared_ptr& image); + + private: + /** + * Determine if the paint requires us to reset the viewport.Currently, we do this whenever the + * paint shader calls for a repeating image. + */ + static bool RequiresViewportReset(const FillStyle& fill); + + void exportPixmap(const Pixmap& pixmap, const MCState& state, const FillStyle& style); + + void exportGlyphsAsPath(const std::shared_ptr& glyphRunList, const MCState& state, + const FillStyle& style, const Stroke* stroke); + + void exportGlyphsAsText(const std::shared_ptr& glyphRunList, const MCState& state, + const FillStyle& style, const Stroke* stroke); + + void exportGlyphsAsImage(const std::shared_ptr& glyphRunList, const MCState& state, + const FillStyle& style); + + void applyClipPath(const Path& clipPath); + + static PathEncoding PathEncoding(); + + uint32_t exportFlags = {}; + Context* context = nullptr; + Canvas* canvas = nullptr; + const std::unique_ptr writer = nullptr; + const std::unique_ptr resourceBucket = nullptr; + std::unique_ptr rootElement = nullptr; + SVGTextBuilder textBuilder = {}; + Path currentClipPath = {}; + std::unique_ptr clipGroupElement = nullptr; +}; +} // namespace tgfx \ No newline at end of file diff --git a/src/svg/SVGTextBuilder.cpp b/src/svg/SVGTextBuilder.cpp new file mode 100644 index 00000000..cf0c2704 --- /dev/null +++ b/src/svg/SVGTextBuilder.cpp @@ -0,0 +1,111 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making tgfx available. +// +// Copyright (C) 2024 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// https://opensource.org/licenses/BSD-3-Clause +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "SVGTextBuilder.h" +#include +#include +#include "core/utils/MathExtra.h" +#include "svg/SVGUtils.h" +#include "tgfx/core/Font.h" +#include "tgfx/core/Point.h" +#include "tgfx/core/Typeface.h" +#include "tgfx/core/UTF.h" + +namespace tgfx { + +SVGTextBuilder::UnicharsInfo SVGTextBuilder::glyphToUnicharsInfo(const GlyphRun& glyphRun) { + + Font font; + if (!glyphRun.glyphFace->asFont(&font)) { + return {}; + } + + auto unicodeChars = converter.glyphsToUnichars(font, glyphRun.glyphs); + + std::string _text; + std::string posXString; + std::string posYString; + std::string constYString; + float constY = 0.f; + bool lastCharWasWhitespace = true; + bool hasConstY = true; + + for (uint32_t i = 0; i < unicodeChars.size(); i++) { + auto c = unicodeChars[i]; + auto position = glyphRun.positions[i]; + bool discardPos = false; + bool isWhitespace = false; + + switch (c) { + case ' ': + case '\t': + // consolidate whitespace to match SVG's xml:space=default munging + // (http://www.w3.org/TR/SVG/text.html#WhiteSpace) + if (lastCharWasWhitespace) { + discardPos = true; + } else { + _text.append(UTF::ToUTF8(c)); + } + isWhitespace = true; + break; + case '\0': + // '\0' for inconvertible glyphs, but these are not legal XML characters + // (http://www.w3.org/TR/REC-xml/#charsets) + discardPos = true; + isWhitespace = lastCharWasWhitespace; // preserve whitespace consolidation + break; + case '&': + _text.append("&"); + break; + case '"': + _text.append("""); + break; + case '\'': + _text.append("'"); + break; + case '<': + _text.append("<"); + break; + case '>': + _text.append(">"); + break; + default: + _text.append(UTF::ToUTF8(c)); + break; + } + + lastCharWasWhitespace = isWhitespace; + + if (discardPos) { + break; + } + + posXString.append(FloatToString(position.x) + ", "); + posYString.append(FloatToString(position.y) + ", "); + + if (constYString.empty()) { + constYString = posYString; + constY = position.y; + } else { + hasConstY &= FloatNearlyEqual(constY, position.y); + } + } + return {std::move(_text), std::move(posXString), + std::move(hasConstY ? constYString : posYString)}; +} +} // namespace tgfx \ No newline at end of file diff --git a/src/svg/SVGTextBuilder.h b/src/svg/SVGTextBuilder.h new file mode 100644 index 00000000..98b41bb6 --- /dev/null +++ b/src/svg/SVGTextBuilder.h @@ -0,0 +1,44 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making tgfx available. +// +// Copyright (C) 2024 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// https://opensource.org/licenses/BSD-3-Clause +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include "core/utils/GlyphConverter.h" +#include "tgfx/core/GlyphRun.h" +#include "tgfx/core/Typeface.h" + +namespace tgfx { +class SVGTextBuilder { + public: + struct UnicharsInfo { + std::string text; + std::string posX; + std::string posY; + }; + + SVGTextBuilder() = default; + ~SVGTextBuilder() = default; + + UnicharsInfo glyphToUnicharsInfo(const GlyphRun& glyphRun); + + private: + GlyphConverter converter; +}; +} // namespace tgfx \ No newline at end of file diff --git a/src/svg/SVGUtils.cpp b/src/svg/SVGUtils.cpp new file mode 100644 index 00000000..2bf1802e --- /dev/null +++ b/src/svg/SVGUtils.cpp @@ -0,0 +1,274 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making tgfx available. +// +// Copyright (C) 2024 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// https://opensource.org/licenses/BSD-3-Clause +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "SVGUtils.h" +#include +#include +#include +#include "core/utils/Log.h" +#include "tgfx/core/BlendMode.h" +#include "tgfx/core/Data.h" +#include "tgfx/core/ImageCodec.h" +#include "tgfx/core/Matrix.h" +#include "tgfx/core/Pixmap.h" + +namespace tgfx { + +std::string ToSVGTransform(const Matrix& matrix) { + DEBUG_ASSERT(!matrix.isIdentity()); + + std::stringstream strStream; + // http://www.w3.org/TR/SVG/coords.html#TransformMatrixDefined + // | a c e | + // | b d f | + // | 0 0 1 | + //matrix(scaleX skewY skewX scaleY transX transY) + strStream << "matrix(" << matrix.getScaleX() << " " << matrix.getSkewY() << " " + << matrix.getSkewX() << " " << matrix.getScaleY() << " " << matrix.getTranslateX() + << " " << matrix.getTranslateY() << ")"; + return strStream.str(); +} + +// For maximum compatibility, do not convert colors to named colors, convert them to hex strings. +std::string ToSVGColor(Color color) { + auto r = static_cast(color.red * 255); + auto g = static_cast(color.green * 255); + auto b = static_cast(color.blue * 255); + + // Some users care about every byte here, so we'll use hex colors with single-digit channels + // when possible. + uint8_t rh = r >> 4; + uint8_t rl = r & 0xf; + uint8_t gh = g >> 4; + uint8_t gl = g & 0xf; + uint8_t bh = b >> 4; + uint8_t bl = b & 0xf; + if ((rh == rl) && (gh == gl) && (bh == bl)) { + char buffer[8]; + snprintf(buffer, sizeof(buffer), "#%1X%1X%1X", rh, gh, bh); + return std::string(buffer); + } + char buffer[8]; + snprintf(buffer, sizeof(buffer), "#%02X%02X%02X", r, g, b); + return std::string(buffer); +} + +std::string ToSVGCap(LineCap cap) { + static const std::array capMap = { + "", // Butt_Cap (default) + "round", // Round_Cap + "square" // Square_Cap + }; + + auto index = static_cast(cap); + DEBUG_ASSERT(index < capMap.size()); + return capMap[index]; +} + +std::string ToSVGJoin(LineJoin join) { + static const std::array joinMap = { + "", // Miter_Join (default) + "round", // Round_Join + "bevel" // Bevel_Join + }; + + auto index = static_cast(join); + DEBUG_ASSERT(index < joinMap.size()); + return joinMap[index]; +} + +std::string ToSVGBlendMode(BlendMode mode) { + // Not all blend modes have corresponding SVG properties. Use an empty string for those, + // which will later be converted to "normal". + constexpr size_t blendModeCount = static_cast(BlendMode::PlusDarker) + 1; + static const std::array blendModeMap = { + "", // Clear + "", // Src + "", // Dst + "normal", // SrcOver + "", // DstOver + "", // SrcIn + "", // DstIn + "", // SrcOut + "", // DstOut + "", // SrcATop + "", // DstATop + "", // Xor + "plus-lighter", // PlusLighter + "", // Modulate + "screen", //Screen + "overlay", // Overlay + "darken", // Darken + "lighten", // Lighten + "color-dodge", // ColorDodge + "color-burn", // ColorBurn + "hard-light", // HardLight + "soft-light", // SoftLight + "difference", // Difference + "exclusion", // Exclusion + "multiply", // Multiply + "hue", // Hue + "saturation", // Saturation + "color", // Color + "luminosity", // Luminosity + "plus-darker" // PlusDarker + }; + auto index = static_cast(mode); + DEBUG_ASSERT(index < blendModeCount); + const auto& blendStr = blendModeMap[index]; + if (blendStr.empty()) { + return ""; + } + return "mix-blend-mode:" + blendStr; +} + +std::string ToSVGPath(const Path& path, PathEncoding encoding) { + Point currentPoint = Point::Zero(); + const int relSelector = encoding == PathEncoding::Relative ? 1 : 0; + + const auto appendCommand = [&](std::string& inputString, char commandChar, const Point points[], + size_t offset, size_t count) { + // Use lower case cmds for relative encoding. + commandChar = static_cast(commandChar + 32 * relSelector); + inputString.push_back(commandChar); + for (size_t i = 0; i < count; ++i) { + const auto pt = points[offset + i] - currentPoint; + if (i > 0) { + inputString.push_back(' '); + } + inputString.append(FloatToString(pt.x) + " " + FloatToString(pt.y)); + } + + DEBUG_ASSERT(count > 0); + // For relative encoding, track the current point (otherwise == origin). + currentPoint = {points[offset + count - 1].x * static_cast(relSelector), + points[offset + count - 1].y * static_cast(relSelector)}; + }; + + auto pathIter = [&](PathVerb verb, const Point points[4], void* info) -> void { + auto* castedString = reinterpret_cast(info); + switch (verb) { + case PathVerb::Move: + appendCommand(*castedString, 'M', points, 0, 1); + break; + case PathVerb::Line: + appendCommand(*castedString, 'L', points, 1, 1); + break; + case PathVerb::Quad: + appendCommand(*castedString, 'Q', points, 1, 2); + break; + case PathVerb::Cubic: + appendCommand(*castedString, 'C', points, 1, 3); + break; + case PathVerb::Close: + castedString->push_back('Z'); + break; + } + }; + + std::string svgString; + path.decompose(pathIter, &svgString); + return svgString; +} + +std::string FloatToString(float value) { + std::ostringstream out; + out << std::fixed << std::setprecision(4) << value; + std::string result = out.str(); + result.erase(result.find_last_not_of('0') + 1, std::string::npos); // Remove trailing zeros + if (result.back() == '.') { + result.pop_back(); + } + return result; +} + +void Base64Encode(unsigned char const* bytesToEncode, size_t length, char* ret) { + static const std::string base64_chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + int i = 0; + int j = 0; + unsigned char char_array_3[3]; + unsigned char char_array_4[4]; + + while (length--) { + char_array_3[i++] = *(bytesToEncode++); + if (i == 3) { + char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; + char_array_4[1] = static_cast(((char_array_3[0] & 0x03) << 4) + + ((char_array_3[1] & 0xf0) >> 4)); + char_array_4[2] = static_cast(((char_array_3[1] & 0x0f) << 2) + + ((char_array_3[2] & 0xc0) >> 6)); + char_array_4[3] = char_array_3[2] & 0x3f; + + for (i = 0; (i < 4); i++) { + *ret++ = base64_chars[char_array_4[i]]; + } + i = 0; + } + } + + if (i) { + for (j = i; j < 3; j++) { + char_array_3[j] = '\0'; + } + + char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; + char_array_4[1] = static_cast(((char_array_3[0] & 0x03) << 4) + + ((char_array_3[1] & 0xf0) >> 4)); + char_array_4[2] = static_cast(((char_array_3[1] & 0x0f) << 2) + + ((char_array_3[2] & 0xc0) >> 6)); + char_array_4[3] = char_array_3[2] & 0x3f; + + for (j = 0; (j < i + 1); j++) { + ret += base64_chars[char_array_4[j]]; + } + + while ((i++ < 3)) { + *ret++ = '='; + } + } +} + +// Returns data uri from bytes. +// it will use any cached data if available, otherwise will +// encode as png. +std::shared_ptr AsDataUri(const Pixmap& pixmap) { + if (pixmap.isEmpty()) { + return nullptr; + } + static constexpr auto pngPrefix = "data:image/png;base64,"; + size_t prefixLength = strlen(pngPrefix); + + auto imageData = ImageCodec::Encode(pixmap, EncodedFormat::PNG, 100); + if (!imageData) { + return nullptr; + } + + size_t base64Size = ((imageData->size() + 2) / 3) * 4; + auto bufferSize = prefixLength + base64Size; + auto* dest = static_cast(malloc(bufferSize)); + memcpy(dest, pngPrefix, prefixLength); + Base64Encode(imageData->bytes(), base64Size, dest + prefixLength); + dest[bufferSize - 1] = 0; + auto dataUri = Data::MakeAdopted(dest, bufferSize, Data::FreeProc); + return dataUri; +} + +} // namespace tgfx \ No newline at end of file diff --git a/src/svg/SVGUtils.h b/src/svg/SVGUtils.h new file mode 100644 index 00000000..b3af4692 --- /dev/null +++ b/src/svg/SVGUtils.h @@ -0,0 +1,78 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making tgfx available. +// +// Copyright (C) 2024 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// https://opensource.org/licenses/BSD-3-Clause +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include "tgfx/core/Bitmap.h" +#include "tgfx/core/Color.h" +#include "tgfx/core/Data.h" +#include "tgfx/core/Image.h" +#include "tgfx/core/Matrix.h" +#include "tgfx/core/Pixmap.h" +#include "tgfx/core/Stroke.h" +#include "tgfx/core/Surface.h" +#include "tgfx/core/Typeface.h" +#include "tgfx/gpu/Context.h" + +namespace tgfx { + +/** + * Two ways to describe paths in SVG + */ +enum class PathEncoding { + /** + * Each step's point is an absolute coordinate, and the step letter is uppercase + */ + Absolute, + /** + * Each step's point is a relative coordinate to the previous point, and the step letter is + *lowercase + */ + Relative, +}; + +std::string ToSVGPath(const Path& path, PathEncoding = PathEncoding::Absolute); + +std::string ToSVGTransform(const Matrix& matrix); + +/** + * For maximum compatibility, do not convert colors to named colors, convert them to hex strings. + */ +std::string ToSVGColor(Color color); + +std::string ToSVGCap(LineCap cap); + +std::string ToSVGJoin(LineJoin join); + +std::string ToSVGBlendMode(BlendMode mode); + +/** + * Retain 4 decimal places and remove trailing zeros. + */ +std::string FloatToString(float value); + +void Base64Encode(unsigned char const* bytesToEncode, size_t length, char* ret); + +/** + * Returns data uri from bytes. + * it will use any cached data if available, otherwise will encode as png. + */ +std::shared_ptr AsDataUri(const Pixmap& pixmap); + +} // namespace tgfx \ No newline at end of file diff --git a/src/svg/xml/XMLWriter.cpp b/src/svg/xml/XMLWriter.cpp index 9331949a..6ff3ddf8 100644 --- a/src/svg/xml/XMLWriter.cpp +++ b/src/svg/xml/XMLWriter.cpp @@ -18,8 +18,11 @@ #include "XMLWriter.h" #include +#include +#include #include "XMLParser.h" #include "core/utils/Log.h" +#include "svg/SVGUtils.h" #include "tgfx/svg/xml/XMLDOM.h" namespace tgfx { @@ -54,7 +57,7 @@ void XMLWriter::addHexAttribute(const std::string& name, uint32_t value, uint32_ } void XMLWriter::addScalarAttribute(const std::string& name, float value) { - this->addAttribute(name, std::to_string(value)); + this->addAttribute(name, FloatToString(value)); } void XMLWriter::addText(const std::string& text) { @@ -87,7 +90,7 @@ std::string_view XMLWriter::getHeader() { return R"()"; } -static const char* escape_char(char c, char storage[2]) { +const char* escape_char(char c, char storage[2]) { static const char* gEscapeChars[] = {"<<", ">>", //"\""", //"''", @@ -104,7 +107,7 @@ static const char* escape_char(char c, char storage[2]) { return storage; } -static size_t escape_markup(char dst[], const char src[], size_t length) { +size_t escape_markup(char dst[], const char src[], size_t length) { size_t extra = 0; const char* stop = src + length; @@ -147,7 +150,7 @@ void XMLWriter::startElement(const std::string& element) { //////////////////////////////////////////////////////////////////////////////////////// -static void write_dom(std::shared_ptr node, XMLWriter* writer, bool skipRoot) { +void write_dom(std::shared_ptr node, XMLWriter* writer, bool skipRoot) { if (!skipRoot) { auto element = node->name; if (node->type == DOMNodeType::Text) { @@ -183,29 +186,30 @@ void XMLWriter::writeDOM(const std::shared_ptr& DOM, bool skipRoot) { void XMLWriter::writeHeader() { } -XMLStreamWriter::XMLStreamWriter(std::stringstream& stream, uint32_t flags) - : _stream(stream), _flags(flags) { +XMLStreamWriter::XMLStreamWriter(std::shared_ptr writeStream, bool disablePretty) + : stream(std::move(writeStream)), disablePrettyXML(disablePretty) { } XMLStreamWriter::~XMLStreamWriter() { this->flush(); + stream->flush(); } void XMLStreamWriter::onAddAttribute(const std::string& name, const std::string& value) { - ASSERT(!_elementsStack.top().hasChildren && !_elementsStack.top().hasText); - _stream << " " << name << "=\"" << value << "\"" << value << "\""; + DEBUG_ASSERT(!_elementsStack.top().hasChildren && !_elementsStack.top().hasText); + stream->writeText(" " + name + "=\"" + value + "\""); } void XMLStreamWriter::onAddText(const std::string& text) { auto elem = _elementsStack.top(); if (!elem.hasChildren && !elem.hasText) { - _stream << ">"; + stream->writeText(">"); this->newline(); } this->tab(static_cast(_elementsStack.size()) + 1); - _stream << text; + stream->writeText(text); this->newline(); } @@ -213,9 +217,9 @@ void XMLStreamWriter::onEndElement() { auto element = getEnd(); if (element.hasChildren || element.hasText) { this->tab(static_cast(_elementsStack.size())); - _stream << ""; + stream->writeText(""); } else { - _stream << "/>"; + stream->writeText("/>"); } this->newline(); doEnd(); @@ -225,31 +229,30 @@ void XMLStreamWriter::onStartElement(const std::string& element) { auto level = _elementsStack.size(); if (this->doStart(element)) { // the first child, need to close with '>' - _stream << ">"; + stream->writeText(">"); this->newline(); } this->tab(static_cast(level)); - _stream << "<"; - _stream << element; + stream->writeText("<" + element); } void XMLStreamWriter::writeHeader() { auto header = getHeader(); - _stream << header; + stream->writeText(std::string(header)); this->newline(); } void XMLStreamWriter::newline() { - if (!(_flags & NoPretty_Flag)) { - _stream << std::endl; + if (!disablePrettyXML) { + stream->writeText("\n"); } } void XMLStreamWriter::tab(int level) { - if (!(_flags & NoPretty_Flag)) { + if (!disablePrettyXML) { for (int i = 0; i < level; i++) { - _stream << "\t"; + stream->writeText("\t"); } } } @@ -263,9 +266,9 @@ XMLParserWriter::~XMLParserWriter() { } void XMLParserWriter::onAddAttribute(const std::string& name, const std::string& value) { - ASSERT(_elementsStack.empty() || - (!_elementsStack.top().hasChildren && !_elementsStack.top().hasText)); - _parser.addAttribute(name.c_str(), value.c_str()); + DEBUG_ASSERT(_elementsStack.empty() || + (!_elementsStack.top().hasChildren && !_elementsStack.top().hasText)); + _parser.addAttribute(name, value); } void XMLParserWriter::onAddText(const std::string& text) { @@ -274,13 +277,13 @@ void XMLParserWriter::onAddText(const std::string& text) { void XMLParserWriter::onEndElement() { Elem elem = this->getEnd(); - _parser.endElement(elem.name.c_str()); + _parser.endElement(elem.name); this->doEnd(); } void XMLParserWriter::onStartElement(const std::string& element) { this->doStart(element); - _parser.startElement(element.c_str()); + _parser.startElement(element); } } // namespace tgfx \ No newline at end of file diff --git a/src/svg/xml/XMLWriter.h b/src/svg/xml/XMLWriter.h index 5f0124f5..3d5997ba 100644 --- a/src/svg/xml/XMLWriter.h +++ b/src/svg/xml/XMLWriter.h @@ -25,6 +25,7 @@ #include #include #include +#include "tgfx/core/WriteStream.h" #include "tgfx/svg/xml/XMLDOM.h" class XMLParser; @@ -53,8 +54,12 @@ class XMLWriter { this->onEndElement(); } void writeDOM(const std::shared_ptr& DOM, bool skipRoot); + virtual void writeHeader(); + // illegal operator + XMLWriter& operator=(const XMLWriter&) = delete; + protected: virtual void onStartElement(const std::string& element) = 0; virtual void onAddAttribute(const std::string& name, const std::string& value) = 0; @@ -80,8 +85,6 @@ class XMLWriter { private: bool _doEscapeFlag = true; - // illegal operator - XMLWriter& operator=(const XMLWriter&) = delete; }; /** @@ -89,11 +92,7 @@ class XMLWriter { */ class XMLStreamWriter : public XMLWriter { public: - enum : uint32_t { - NoPretty_Flag = 0x01, - }; - - explicit XMLStreamWriter(std::stringstream& stream, uint32_t flags = 0); + explicit XMLStreamWriter(std::shared_ptr writeStream, bool disablePretty = false); ~XMLStreamWriter() override; void writeHeader() override; @@ -107,8 +106,8 @@ class XMLStreamWriter : public XMLWriter { void newline(); void tab(int level); - std::stringstream& _stream; - const uint32_t _flags; + std::shared_ptr stream; + const bool disablePrettyXML; }; /** diff --git a/test/src/CanvasTest.cpp b/test/src/CanvasTest.cpp index b955c1d5..59e2b788 100644 --- a/test/src/CanvasTest.cpp +++ b/test/src/CanvasTest.cpp @@ -626,12 +626,14 @@ TGFX_TEST(CanvasTest, simpleShape) { auto point = Point::Make(width / 2, height / 2); auto radius = image->width() / 2; auto rect = Rect::MakeWH(radius * 2, radius * 2); - canvas->drawCircle(point, radius + 30, paint); - canvas->setMatrix(Matrix::MakeTrans(point.x - radius, point.y - radius)); + canvas->drawCircle(point, static_cast(radius) + 30.0f, paint); + canvas->setMatrix(Matrix::MakeTrans(point.x - static_cast(radius), + point.y - static_cast(radius))); canvas->drawRoundRect(rect, 10, 10, paint); - canvas->setMatrix(Matrix::MakeTrans(point.x - radius, point.y - radius)); - canvas->rotate(45, radius, radius); + canvas->setMatrix(Matrix::MakeTrans(point.x - static_cast(radius), + point.y - static_cast(radius))); + canvas->rotate(45.0f, static_cast(radius), static_cast(radius)); canvas->drawImage(image, SamplingOptions(FilterMode::Linear)); EXPECT_TRUE(Baseline::Compare(surface, "CanvasTest/shape")); } diff --git a/test/src/FilterTest.cpp b/test/src/FilterTest.cpp index c22342d0..cd914c1c 100644 --- a/test/src/FilterTest.cpp +++ b/test/src/FilterTest.cpp @@ -378,8 +378,8 @@ TGFX_TEST(FilterTest, GetFilterProperties) { EXPECT_EQ(imageFilter->type(), ImageFilter::Type::Blur); auto blurFilter = std::static_pointer_cast(imageFilter); Size blurSize = blurFilter->filterBounds(Rect::MakeEmpty()).size(); - EXPECT_EQ(blurSize.width, 18); - EXPECT_EQ(blurSize.height, 36); + EXPECT_EQ(blurSize.width, 18.f); + EXPECT_EQ(blurSize.height, 36.f); } { @@ -387,8 +387,8 @@ TGFX_TEST(FilterTest, GetFilterProperties) { EXPECT_EQ(imageFilter->type(), ImageFilter::Type::DropShadow); auto dropShadowFilter = std::static_pointer_cast(imageFilter); Size blurSize = dropShadowFilter->blurFilter->filterBounds(Rect::MakeEmpty()).size(); - EXPECT_EQ(blurSize.width, 18); - EXPECT_EQ(blurSize.height, 36); + EXPECT_EQ(blurSize.width, 18.f); + EXPECT_EQ(blurSize.height, 36.f); EXPECT_EQ(dropShadowFilter->dx, 15.f); EXPECT_EQ(dropShadowFilter->dy, 15.f); EXPECT_EQ(dropShadowFilter->color, Color::White()); @@ -400,8 +400,8 @@ TGFX_TEST(FilterTest, GetFilterProperties) { EXPECT_EQ(imageFilter->type(), ImageFilter::Type::DropShadow); auto dropShadowFilter = std::static_pointer_cast(imageFilter); Size blurSize = dropShadowFilter->blurFilter->filterBounds(Rect::MakeEmpty()).size(); - EXPECT_EQ(blurSize.width, 18); - EXPECT_EQ(blurSize.height, 36); + EXPECT_EQ(blurSize.width, 18.f); + EXPECT_EQ(blurSize.height, 36.f); EXPECT_EQ(dropShadowFilter->dx, 15.f); EXPECT_EQ(dropShadowFilter->dy, 15.f); EXPECT_EQ(dropShadowFilter->color, Color::White()); @@ -413,8 +413,8 @@ TGFX_TEST(FilterTest, GetFilterProperties) { EXPECT_EQ(imageFilter->type(), ImageFilter::Type::InnerShadow); auto innerShadowFilter = std::static_pointer_cast(imageFilter); Size blurSize = innerShadowFilter->blurFilter->filterBounds(Rect::MakeEmpty()).size(); - EXPECT_EQ(blurSize.width, 18); - EXPECT_EQ(blurSize.height, 36); + EXPECT_EQ(blurSize.width, 18.f); + EXPECT_EQ(blurSize.height, 36.f); EXPECT_EQ(innerShadowFilter->dx, 15.f); EXPECT_EQ(innerShadowFilter->dy, 15.f); EXPECT_EQ(innerShadowFilter->color, Color::White()); @@ -426,8 +426,8 @@ TGFX_TEST(FilterTest, GetFilterProperties) { EXPECT_EQ(imageFilter->type(), ImageFilter::Type::InnerShadow); auto innerShadowFilter = std::static_pointer_cast(imageFilter); Size blurSize = innerShadowFilter->blurFilter->filterBounds(Rect::MakeEmpty()).size(); - EXPECT_EQ(blurSize.width, 18); - EXPECT_EQ(blurSize.height, 36); + EXPECT_EQ(blurSize.width, 18.f); + EXPECT_EQ(blurSize.height, 36.f); EXPECT_EQ(innerShadowFilter->dx, 15.f); EXPECT_EQ(innerShadowFilter->dy, 15.f); EXPECT_EQ(innerShadowFilter->color, Color::White()); diff --git a/test/src/SVGExportTest.cpp b/test/src/SVGExportTest.cpp new file mode 100644 index 00000000..90d7316e --- /dev/null +++ b/test/src/SVGExportTest.cpp @@ -0,0 +1,502 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making tgfx available. +// +// Copyright (C) 2024 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// https://opensource.org/licenses/BSD-3-Clause +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include "base/TGFXTest.h" +#include "core/images/TransformImage.h" +#include "gpu/opengl/GLCaps.h" +#include "gpu/opengl/GLUtil.h" +#include "gtest/gtest.h" +#include "tgfx/core/Buffer.h" +#include "tgfx/core/Color.h" +#include "tgfx/core/Paint.h" +#include "tgfx/core/Path.h" +#include "tgfx/core/Rect.h" +#include "tgfx/core/Size.h" +#include "tgfx/core/Stream.h" +#include "tgfx/core/WriteStream.h" +#include "tgfx/gpu/opengl/GLDevice.h" +#include "tgfx/svg/SVGExporter.h" +#include "utils/TestUtils.h" + +namespace tgfx { + +TGFX_TEST(SVGExportTest, PureColor) { + std::string compareString = + ""; + + ContextScope scope; + auto* context = scope.getContext(); + ASSERT_TRUE(context != nullptr); + + tgfx::Paint paint; + paint.setColor(Color::Blue()); + + auto SVGStream = MemoryWriteStream::Make(); + auto exporter = SVGExporter::Make(SVGStream, context, Rect::MakeWH(200, 200), + SVGExportFlags::DisablePrettyXML); + auto* canvas = exporter->getCanvas(); + + canvas->drawRect(Rect::MakeXYWH(50, 50, 100, 100), paint); + + exporter->close(); + auto SVGString = SVGStream->readString(); + ASSERT_EQ(SVGString, compareString); +} + +TGFX_TEST(SVGExportTest, PureColorFile) { + std::string compareString = + ""; + + auto path = ProjectPath::Absolute("test/out/FileWrite.txt"); + std::filesystem::path filePath = path; + std::filesystem::create_directories(filePath.parent_path()); + + ContextScope scope; + auto* context = scope.getContext(); + ASSERT_TRUE(context != nullptr); + + tgfx::Paint paint; + paint.setColor(Color::Blue()); + + auto SVGStream = WriteStream::MakeFromFile(path); + auto exporter = SVGExporter::Make(SVGStream, context, Rect::MakeWH(200, 200), + SVGExportFlags::DisablePrettyXML); + auto* canvas = exporter->getCanvas(); + + canvas->drawRect(Rect::MakeXYWH(50, 50, 100, 100), paint); + + exporter->close(); + + auto readStream = Stream::MakeFromFile(path); + EXPECT_TRUE(readStream != nullptr); + EXPECT_EQ(readStream->size(), 211U); + Buffer buffer(readStream->size()); + readStream->read(buffer.data(), buffer.size()); + EXPECT_EQ(std::string((char*)buffer.data(), buffer.size()), compareString); + + std::filesystem::remove(path); +} + +TGFX_TEST(SVGExportTest, OpacityColor) { + std::string compareString = + ""; + + ContextScope scope; + auto* context = scope.getContext(); + ASSERT_TRUE(context != nullptr); + + tgfx::Paint paint; + paint.setColor(Color::Blue()); + paint.setAlpha(0.5f); + + auto SVGStream = MemoryWriteStream::Make(); + auto exporter = SVGExporter::Make(SVGStream, context, Rect::MakeWH(200, 200), + SVGExportFlags::DisablePrettyXML); + auto* canvas = exporter->getCanvas(); + + canvas->drawCircle(100, 100, 100, paint); + + exporter->close(); + auto SVGString = SVGStream->readString(); + ASSERT_EQ(SVGString, compareString); +} + +TGFX_TEST(SVGExportTest, OpacityColorFile) { + std::string compareString = + ""; + + ContextScope scope; + auto* context = scope.getContext(); + ASSERT_TRUE(context != nullptr); + + auto path = ProjectPath::Absolute("test/out/FileWrite.txt"); + std::filesystem::path filePath = path; + std::filesystem::create_directories(filePath.parent_path()); + + tgfx::Paint paint; + paint.setColor(Color::Blue()); + paint.setAlpha(0.5f); + + auto SVGStream = WriteStream::MakeFromFile(path); + auto exporter = SVGExporter::Make(SVGStream, context, Rect::MakeWH(200, 200), + SVGExportFlags::DisablePrettyXML); + auto* canvas = exporter->getCanvas(); + + canvas->drawCircle(100, 100, 100, paint); + + exporter->close(); + + auto readStream = Stream::MakeFromFile(path); + EXPECT_TRUE(readStream != nullptr); + EXPECT_EQ(readStream->size(), 222U); + Buffer buffer(readStream->size()); + readStream->read(buffer.data(), buffer.size()); + EXPECT_EQ(std::string((char*)buffer.data(), buffer.size()), compareString); + + std::filesystem::remove(path); +} + +TGFX_TEST(SVGExportTest, LinearGradient) { + std::string compareString = + ""; + + ContextScope scope; + auto* context = scope.getContext(); + ASSERT_TRUE(context != nullptr); + + tgfx::Paint paint; + auto shader = tgfx::Shader::MakeLinearGradient( + tgfx::Point{50.f, 50.f}, tgfx::Point{150.f, 150.f}, + {tgfx::Color{0.f, 1.f, 0.f, 1.f}, tgfx::Color{0.f, 0.f, 0.f, 1.f}}, {}); + paint.setShader(shader); + + auto SVGStream = MemoryWriteStream::Make(); + auto exporter = SVGExporter::Make(SVGStream, context, Rect::MakeWH(200, 200), + SVGExportFlags::DisablePrettyXML); + auto* canvas = exporter->getCanvas(); + + canvas->drawCircle(100, 100, 100, paint); + + exporter->close(); + auto SVGString = SVGStream->readString(); + ASSERT_EQ(SVGString, compareString); +} + +TGFX_TEST(SVGExportTest, RadialGradient) { + std::string compareString = + ""; + + ContextScope scope; + auto* context = scope.getContext(); + ASSERT_TRUE(context != nullptr); + + tgfx::Paint paint; + tgfx::Point center{100.f, 100.f}; + auto shader = tgfx::Shader::MakeRadialGradient( + center, 50, {tgfx::Color::Red(), tgfx::Color::Blue(), tgfx::Color::Black()}, {0, 0.5, 1.0}); + paint.setShader(shader); + + auto SVGStream = MemoryWriteStream::Make(); + auto exporter = SVGExporter::Make(SVGStream, context, Rect::MakeWH(200, 200), + SVGExportFlags::DisablePrettyXML); + auto* canvas = exporter->getCanvas(); + + canvas->drawRect(Rect::MakeXYWH(50, 50, 100, 100), paint); + + exporter->close(); + auto SVGString = SVGStream->readString(); + ASSERT_EQ(SVGString, compareString); +} + +TGFX_TEST(SVGExportTest, UnsupportedGradient) { + + std::string compareString = + ""; + + ContextScope scope; + auto* context = scope.getContext(); + ASSERT_TRUE(context != nullptr); + + tgfx::Paint paint; + tgfx::Point center{100.f, 100.f}; + auto shader = tgfx::Shader::MakeConicGradient( + center, 0, 360, {tgfx::Color::Red(), tgfx::Color::Blue(), tgfx::Color::Black()}, + {0, 0.5, 1.0}); + paint.setShader(shader); + + auto SVGStream = MemoryWriteStream::Make(); + auto exporter = SVGExporter::Make(SVGStream, context, Rect::MakeWH(200, 200), + SVGExportFlags::DisablePrettyXML); + auto* canvas = exporter->getCanvas(); + + canvas->drawRect(Rect::MakeXYWH(50, 50, 100, 100), paint); + + exporter->close(); + auto SVGString = SVGStream->readString(); + ASSERT_EQ(SVGString, compareString); +} + +TGFX_TEST(SVGExportTest, BlendMode) { + std::string compareString = + ""; + + ContextScope scope; + auto* context = scope.getContext(); + ASSERT_TRUE(context != nullptr); + + Paint paintBackground; + paintBackground.setColor(Color::White()); + + Paint paint; + paint.setColor(Color::Red()); + paint.setBlendMode(BlendMode::Difference); + + auto SVGStream = MemoryWriteStream::Make(); + auto exporter = SVGExporter::Make(SVGStream, context, Rect::MakeWH(200, 200), + SVGExportFlags::DisablePrettyXML); + auto* canvas = exporter->getCanvas(); + + canvas->drawRect(tgfx::Rect::MakeXYWH(0, 0, 100, 100), paintBackground); + canvas->drawRect(tgfx::Rect::MakeXYWH(50, 50, 100, 100), paint); + + exporter->close(); + auto SVGString = SVGStream->readString(); + ASSERT_EQ(SVGString, compareString); +} + +TGFX_TEST(SVGExportTest, StrokeWidth) { + std::string compareString = + ""; + + ContextScope scope; + auto* context = scope.getContext(); + ASSERT_TRUE(context != nullptr); + + Paint paint; + paint.setColor(Color::Red()); + paint.setStyle(PaintStyle::Stroke); + paint.setStrokeWidth(5); + + auto SVGStream = MemoryWriteStream::Make(); + auto exporter = SVGExporter::Make(SVGStream, context, Rect::MakeWH(200, 200), + SVGExportFlags::DisablePrettyXML); + auto* canvas = exporter->getCanvas(); + + canvas->drawRect(tgfx::Rect::MakeXYWH(50, 50, 100, 100), paint); + + exporter->close(); + auto SVGString = SVGStream->readString(); + ASSERT_EQ(SVGString, compareString); +} + +TGFX_TEST(SVGExportTest, SimpleTextAsText) { + std::string compareString = + "Hello TGFX"; + + ContextScope scope; + auto* context = scope.getContext(); + ASSERT_TRUE(context != nullptr); + + auto typeface = + Typeface::MakeFromPath(ProjectPath::Absolute("resources/font/NotoSerifSC-Regular.otf")); + Font font(typeface, 50.f); + Paint paint; + paint.setColor(Color::Red()); + + auto SVGStream = MemoryWriteStream::Make(); + auto exporter = SVGExporter::Make(SVGStream, context, Rect::MakeWH(400, 200), + SVGExportFlags::DisablePrettyXML); + auto* canvas = exporter->getCanvas(); + + canvas->drawSimpleText("Hello TGFX", 0, 80, font, paint); + + exporter->close(); + auto SVGString = SVGStream->readString(); + ASSERT_EQ(SVGString, compareString); +} + +TGFX_TEST(SVGExportTest, SimpleTextAsPath) { + std::string compareString = + ""; + + ContextScope scope; + auto* context = scope.getContext(); + ASSERT_TRUE(context != nullptr); + + auto typeface = + Typeface::MakeFromPath(ProjectPath::Absolute("resources/font/NotoSansSC-Regular.otf")); + Font font(typeface, 50.f); + Paint paint; + paint.setColor(Color::Red()); + + auto SVGStream = MemoryWriteStream::Make(); + auto exporter = + SVGExporter::Make(SVGStream, context, Rect::MakeWH(400, 200), + SVGExportFlags::ConvertTextToPaths | SVGExportFlags::DisablePrettyXML); + auto* canvas = exporter->getCanvas(); + + canvas->drawSimpleText("Hi", 0, 80, font, paint); + + exporter->close(); + auto SVGString = SVGStream->readString(); + ASSERT_EQ(SVGString, compareString); +} + +TGFX_TEST(SVGExportTest, EmojiText) { + std::string compareString = + "🤡👻🐠🤩😃🤪"; + + ContextScope scope; + auto* context = scope.getContext(); + ASSERT_TRUE(context != nullptr); + + auto typeface = + Typeface::MakeFromPath(ProjectPath::Absolute("resources/font/NotoColorEmoji.ttf")); + Font font(typeface, 50.f); + Paint paint; + paint.setColor(Color::Red()); + + auto SVGStream = MemoryWriteStream::Make(); + auto exporter = SVGExporter::Make(SVGStream, context, Rect::MakeWH(400, 200), + SVGExportFlags::DisablePrettyXML); + auto* canvas = exporter->getCanvas(); + + canvas->drawSimpleText("🤡👻🐠🤩😃🤪", 0, 80, font, paint); + + exporter->close(); + auto SVGString = SVGStream->readString(); + ASSERT_EQ(SVGString, compareString); +} + +TGFX_TEST(SVGExportTest, EmojiTextFile) { + std::string compareString = + "🤡👻🐠🤩😃🤪"; + + ContextScope scope; + auto* context = scope.getContext(); + ASSERT_TRUE(context != nullptr); + + auto path = ProjectPath::Absolute("test/out/FileWrite.txt"); + std::filesystem::path filePath = path; + std::filesystem::create_directories(filePath.parent_path()); + + auto typeface = + Typeface::MakeFromPath(ProjectPath::Absolute("resources/font/NotoColorEmoji.ttf")); + Font font(typeface, 50.f); + Paint paint; + paint.setColor(Color::Red()); + + auto SVGStream = WriteStream::MakeFromFile(path); + auto exporter = SVGExporter::Make(SVGStream, context, Rect::MakeWH(400, 200), + SVGExportFlags::DisablePrettyXML); + auto* canvas = exporter->getCanvas(); + + canvas->drawSimpleText("🤡👻🐠🤩😃🤪", 0, 80, font, paint); + + exporter->close(); + + auto readStream = Stream::MakeFromFile(path); + EXPECT_TRUE(readStream != nullptr); + EXPECT_EQ(readStream->size(), 346U); + Buffer buffer(readStream->size()); + readStream->read(buffer.data(), buffer.size()); + EXPECT_EQ(std::string((char*)buffer.data(), buffer.size()), compareString); + + std::filesystem::remove(path); +} + +TGFX_TEST(SVGExportTest, ClipState) { + std::string compareString = + ""; + + ContextScope scope; + auto* context = scope.getContext(); + ASSERT_TRUE(context != nullptr); + + auto SVGStream = MemoryWriteStream::Make(); + auto exporter = SVGExporter::Make(SVGStream, context, Rect::MakeWH(300, 300), + SVGExportFlags::DisablePrettyXML); + auto* canvas = exporter->getCanvas(); + + { + Paint paint; + paint.setColor(Color::Red()); + canvas->save(); + canvas->clipRect(Rect::MakeXYWH(0, 0, 100, 100)); + canvas->drawRect(Rect::MakeXYWH(0, 0, 200, 200), paint); + canvas->restore(); + + paint.setColor(Color::Green()); + canvas->save(); + Path path; + path.addOval(Rect::MakeXYWH(100, 100, 100, 100)); + canvas->clipPath(path); + canvas->drawRect(Rect::MakeXYWH(100, 100, 200, 200), paint); + canvas->restore(); + + paint.setColor(Color::Blue()); + canvas->save(); + canvas->drawRect(Rect::MakeXYWH(200, 200, 100, 100), paint); + canvas->restore(); + } + + exporter->close(); + auto SVGString = SVGStream->readString(); + ASSERT_EQ(SVGString, compareString); +} + +} // namespace tgfx \ No newline at end of file