From 26b677f6bbcdf2e8053440a5c4ee26dfee58136e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 21:18:20 +0000 Subject: [PATCH 1/3] Initial plan From ad5a279096f577396a2c873352aee83887ab9601 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 21:29:58 +0000 Subject: [PATCH 2/3] refactor: compose haru backend capabilities --- .../components/backend-integration.md | 13 +- .../contributors/components/backend.md | 14 +- doc/source/api/backend.rst | 10 +- .../backend/docraft_rendering_backend.h | 48 +- .../backend/docraft_shape_rendering_backend.h | 5 +- .../backend/docraft_text_rendering_backend.h | 6 +- .../backend/pdf/docraft_haru_backend.h | 227 +---- .../backend/pdf/docraft_haru_backend.cc | 807 ++++++++++-------- .../src/docraft/docraft_document_context.cc | 12 +- .../management/docraft_backend_cache.cc | 23 +- docraft/src/docraft/model/docraft_list.cc | 7 +- .../docraft/renderer/docraft_pdf_renderer.cc | 45 +- .../painter/docraft_circle_painter.cc | 27 +- .../renderer/painter/docraft_line_painter.cc | 17 +- .../painter/docraft_polygon_painter.cc | 27 +- .../painter/docraft_rectangle_painter.cc | 27 +- .../renderer/painter/docraft_text_painter.cc | 14 +- .../painter/docraft_triangle_painter.cc | 27 +- .../backend/docraft_haru_backend_test.cc | 118 ++- .../utils/docraft_mock_rendering_backend.h | 18 +- 20 files changed, 756 insertions(+), 736 deletions(-) diff --git a/doc/project-doc/contributors/components/backend-integration.md b/doc/project-doc/contributors/components/backend-integration.md index 72ffd06..09b1c05 100644 --- a/doc/project-doc/contributors/components/backend-integration.md +++ b/doc/project-doc/contributors/components/backend-integration.md @@ -17,17 +17,21 @@ Important implication: ## 2. Interface contract you must implement -A backend must implement `IDocraftRenderingBackend`, which aggregates: +A backend must implement `IDocraftRenderingBackend`, which exposes capability +accessors for: - text primitives, -- line/shape primitives, +- line primitives, +- shape primitives, - image primitives, - page management, - save/output extension, - metadata application, - font registration and font selection hooks. -In practice you implement one concrete class inheriting `IDocraftRenderingBackend`. +In practice you implement one concrete class inheriting +`IDocraftRenderingBackend` and return the capability objects from the root via +`() const` and `edit_()`. ## 3. External integration (recommended path) @@ -64,7 +68,8 @@ void render_with_external_backend(const std::string &craft_path) { Typical steps for a backend added directly in this repository: 1. Add backend class, for example `docraft::backend::svg::DocraftSvgBackend`. -2. Implement all methods of `IDocraftRenderingBackend`. +2. Implement all root methods of `IDocraftRenderingBackend` and wire each + capability accessor to the appropriate capability object. 3. Add new source/header files to `docraft/CMakeLists.txt`. 4. Add tests in `docraft/test/backend/` and/or rendering smoke tests. 5. Choose selection strategy: diff --git a/doc/project-doc/contributors/components/backend.md b/doc/project-doc/contributors/components/backend.md index 13bf9a7..e588ce0 100644 --- a/doc/project-doc/contributors/components/backend.md +++ b/doc/project-doc/contributors/components/backend.md @@ -4,9 +4,10 @@ The backend layer is the portability boundary for output targets. ## 1. Contract hierarchy -`IDocraftRenderingBackend` aggregates these contracts: +`IDocraftRenderingBackend` exposes these contracts via explicit accessors: - `IDocraftTextRenderingBackend` +- `IDocraftLineRenderingBackend` - `IDocraftShapeRenderingBackend` - `IDocraftImageRenderingBackend` - `IDocraftPageRenderingBackend` @@ -18,17 +19,22 @@ Plus lifecycle methods: - font registration/selection helpers - metadata application -This design lets renderers/painters consume only the primitives they need. +Capability interfaces are standalone (no inheritance chain between them), and +the root backend provides `() const` plus `edit_()` +accessors so renderers/painters can consume only the primitives they need. ## 2. Concrete implementation: Haru backend -`DocraftHaruBackend` implements all contracts over libharu. +`DocraftHaruBackend` remains the single concrete backend entry point, but it +composes smaller libharu-backed capability objects internally. Responsibilities include: - managing document/page handles, - text drawing and measurement, -- line/shape/image drawing, +- line drawing, +- shape drawing and graphics state, +- image drawing, - page navigation and page format, - metadata mapping to PDF info fields, - save to `.pdf`. diff --git a/doc/source/api/backend.rst b/doc/source/api/backend.rst index eef66b9..c4cd475 100644 --- a/doc/source/api/backend.rst +++ b/doc/source/api/backend.rst @@ -8,7 +8,9 @@ output. IDocraftRenderingBackend ------------------------ -Aggregated interface inheriting all sub-backend interfaces. +Root backend interface exposing lifecycle/font operations plus explicit +capability accessors such as ``line_rendering()`` / ``edit_line_rendering()`` +and the corresponding text, shape, image, and page accessors. .. doxygenclass:: docraft::backend::IDocraftRenderingBackend :project: docraft @@ -17,6 +19,8 @@ Aggregated interface inheriting all sub-backend interfaces. Sub-backend Interfaces ---------------------- +Capability interfaces are standalone and chain-free. + IDocraftTextRenderingBackend ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -58,9 +62,9 @@ Concrete Backends DocraftHaruBackend ^^^^^^^^^^^^^^^^^^ -PDF backend implementation using libharu. +PDF backend implementation using libharu, composed from capability-focused +internal objects behind the root backend. .. doxygenclass:: docraft::backend::pdf::DocraftHaruBackend :project: docraft :members: - diff --git a/docraft/include/docraft/backend/docraft_rendering_backend.h b/docraft/include/docraft/backend/docraft_rendering_backend.h index 87ae6e4..566060b 100644 --- a/docraft/include/docraft/backend/docraft_rendering_backend.h +++ b/docraft/include/docraft/backend/docraft_rendering_backend.h @@ -19,6 +19,7 @@ #include "docraft/docraft_lib.h" #include +#include "docraft/backend/docraft_line_rendering_backend.h" #include "docraft/backend/docraft_image_rendering_backend.h" #include "docraft/backend/docraft_page_rendering_backend.h" #include "docraft/backend/docraft_shape_rendering_backend.h" @@ -32,15 +33,52 @@ namespace docraft::backend { /** * @brief Aggregated rendering backend interface. */ - class DOCRAFT_LIB IDocraftRenderingBackend : public IDocraftTextRenderingBackend, - public IDocraftShapeRenderingBackend, - public IDocraftImageRenderingBackend, - public IDocraftPageRenderingBackend { + class DOCRAFT_LIB IDocraftRenderingBackend { public: /** * @brief Virtual destructor. */ - ~IDocraftRenderingBackend() override = default; + virtual ~IDocraftRenderingBackend() = default; + /** + * @brief Returns the line rendering capability, if available. + */ + [[nodiscard]] virtual const IDocraftLineRenderingBackend *line_rendering() const = 0; + /** + * @brief Returns the editable line rendering capability, if available. + */ + [[nodiscard]] virtual IDocraftLineRenderingBackend *edit_line_rendering() = 0; + /** + * @brief Returns the text rendering capability, if available. + */ + [[nodiscard]] virtual const IDocraftTextRenderingBackend *text_rendering() const = 0; + /** + * @brief Returns the editable text rendering capability, if available. + */ + [[nodiscard]] virtual IDocraftTextRenderingBackend *edit_text_rendering() = 0; + /** + * @brief Returns the shape rendering capability, if available. + */ + [[nodiscard]] virtual const IDocraftShapeRenderingBackend *shape_rendering() const = 0; + /** + * @brief Returns the editable shape rendering capability, if available. + */ + [[nodiscard]] virtual IDocraftShapeRenderingBackend *edit_shape_rendering() = 0; + /** + * @brief Returns the image rendering capability, if available. + */ + [[nodiscard]] virtual const IDocraftImageRenderingBackend *image_rendering() const = 0; + /** + * @brief Returns the editable image rendering capability, if available. + */ + [[nodiscard]] virtual IDocraftImageRenderingBackend *edit_image_rendering() = 0; + /** + * @brief Returns the page rendering capability, if available. + */ + [[nodiscard]] virtual const IDocraftPageRenderingBackend *page_rendering() const = 0; + /** + * @brief Returns the editable page rendering capability, if available. + */ + [[nodiscard]] virtual IDocraftPageRenderingBackend *edit_page_rendering() = 0; /** * @brief Saves the document to a file path. * @param path Output file path. diff --git a/docraft/include/docraft/backend/docraft_shape_rendering_backend.h b/docraft/include/docraft/backend/docraft_shape_rendering_backend.h index a0179cd..db83c29 100644 --- a/docraft/include/docraft/backend/docraft_shape_rendering_backend.h +++ b/docraft/include/docraft/backend/docraft_shape_rendering_backend.h @@ -20,19 +20,18 @@ #include -#include "docraft/backend/docraft_line_rendering_backend.h" #include "docraft/model/docraft_position.h" namespace docraft::backend { /** * @brief Interface for shape rendering backends used by Docraft. */ - class DOCRAFT_LIB IDocraftShapeRenderingBackend : public virtual IDocraftLineRenderingBackend { + class DOCRAFT_LIB IDocraftShapeRenderingBackend { public: /** * @brief Virtual destructor. */ - ~IDocraftShapeRenderingBackend() override = default; + virtual ~IDocraftShapeRenderingBackend() = default; /** * @brief Saves the current graphics state. */ diff --git a/docraft/include/docraft/backend/docraft_text_rendering_backend.h b/docraft/include/docraft/backend/docraft_text_rendering_backend.h index 3266440..2dc8116 100644 --- a/docraft/include/docraft/backend/docraft_text_rendering_backend.h +++ b/docraft/include/docraft/backend/docraft_text_rendering_backend.h @@ -19,18 +19,16 @@ #include "docraft/docraft_lib.h" #include -#include "docraft/backend/docraft_line_rendering_backend.h" - namespace docraft::backend { /** * @brief Interface for text rendering backends used by Docraft. */ - class DOCRAFT_LIB IDocraftTextRenderingBackend : public virtual IDocraftLineRenderingBackend { + class DOCRAFT_LIB IDocraftTextRenderingBackend { public: /** * @brief Virtual destructor. */ - ~IDocraftTextRenderingBackend() override = default; + virtual ~IDocraftTextRenderingBackend() = default; /** * @brief Initializes the text rendering context. Must be called before any text drawing operations. */ diff --git a/docraft/include/docraft/backend/pdf/docraft_haru_backend.h b/docraft/include/docraft/backend/pdf/docraft_haru_backend.h index 5dcd9a3..3d75f8d 100644 --- a/docraft/include/docraft/backend/pdf/docraft_haru_backend.h +++ b/docraft/include/docraft/backend/pdf/docraft_haru_backend.h @@ -19,6 +19,7 @@ #include "docraft/docraft_lib.h" #include #include +#include #include #include @@ -39,160 +40,17 @@ namespace docraft::backend::pdf { * @brief Releases Haru resources. */ ~DocraftHaruBackend() override; -#pragma region text rendering - /** - * @brief Begins a text object. - */ - void begin_text() const override; - /** - * @brief Ends a text object. - */ - void end_text() const override; - /** - * @brief Draws text at the given coordinates. - */ - void draw_text(const std::string& text, float x, float y) const override; - /** - * @brief Sets text fill color. - */ - void set_text_color(float r, float g, float b) const override; - /** - * @brief Draws text with a custom transformation matrix. - */ - void draw_text_matrix( - const std::string& text, - float scale_x, - float skew_x, - float skew_y, - float scale_y, - float translate_x, - float translate_y) const override; - /** - * @brief Measures text width using current font settings. - */ - float measure_text_width(const std::string& text) const override; -#pragma endregion -#pragma region line rendering - /** - * @brief Sets stroke color for lines and shapes. - */ - void set_stroke_color(float r, float g, float b) const override; - /** - * @brief Sets line width in points. - */ - void set_line_width(float thickness) const override; - /** - * @brief Draws a line between two points. - */ - void draw_line(float x1, float y1, float x2, float y2) const override; -#pragma endregion -#pragma region shape rendering - /** - * @brief Saves the current graphics state. - */ - void save_state() const override; - /** - * @brief Restores the previous graphics state. - */ - void restore_state() const override; - /** - * @brief Sets fill color for shapes. - */ - void set_fill_color(float r, float g, float b) const override; - /** - * @brief Sets fill alpha for shapes. - */ - void set_fill_alpha(float alpha) const override; - /** - * @brief Sets stroke alpha for shapes. - */ - void set_stroke_alpha(float alpha) const override; - /** - * @brief Adds a rectangle path. - */ - void draw_rectangle(float x, float y, float width, float height) const override; - /** - * @brief Adds a circle path. - */ - void draw_circle(float center_x, float center_y, float radius) const override; - /** - * @brief Adds a polygon path. - */ - void draw_polygon(const std::vector &points) const override; - /** - * @brief Fills the current path. - */ - void fill() const override; - /** - * @brief Strokes the current path. - */ - void stroke() const override; - /** - * @brief Fills and strokes the current path. - */ - void fill_stroke() const override; -#pragma endregion -#pragma region image rendering - /** - * @brief Draws a PNG image from file. - */ - void draw_png_image( - const std::string& path, - float x, - float y, - float width, - float height) const override; - /** - * @brief Draws a PNG image from memory. - */ - void draw_png_image_from_memory( - const unsigned char* data, - std::size_t size, - float x, - float y, - float width, - float height) const override; - /** - * @brief Draws a JPEG image from file. - */ - void draw_jpeg_image( - const std::string& path, - float x, - float y, - float width, - float height) const override; - /** - * @brief Draws a JPEG image from memory. - */ - void draw_jpeg_image_from_memory( - const unsigned char* data, - std::size_t size, - float x, - float y, - float width, - float height) const override; - /** - * @brief Draws a raw RGB image from file. - */ - void draw_raw_rgb_image( - const std::string& path, - int pixel_width, - int pixel_height, - float x, - float y, - float width, - float height) const override; - /** - * @brief Draws a raw RGB image from memory. - */ - void draw_raw_rgb_image_from_memory( - const unsigned char* data, - int pixel_width, - int pixel_height, - float x, - float y, - float width, - float height) const override; +#pragma region capabilities + [[nodiscard]] const docraft::backend::IDocraftLineRenderingBackend *line_rendering() const override; + [[nodiscard]] docraft::backend::IDocraftLineRenderingBackend *edit_line_rendering() override; + [[nodiscard]] const docraft::backend::IDocraftTextRenderingBackend *text_rendering() const override; + [[nodiscard]] docraft::backend::IDocraftTextRenderingBackend *edit_text_rendering() override; + [[nodiscard]] const docraft::backend::IDocraftShapeRenderingBackend *shape_rendering() const override; + [[nodiscard]] docraft::backend::IDocraftShapeRenderingBackend *edit_shape_rendering() override; + [[nodiscard]] const docraft::backend::IDocraftImageRenderingBackend *image_rendering() const override; + [[nodiscard]] docraft::backend::IDocraftImageRenderingBackend *edit_image_rendering() override; + [[nodiscard]] const docraft::backend::IDocraftPageRenderingBackend *page_rendering() const override; + [[nodiscard]] docraft::backend::IDocraftPageRenderingBackend *edit_page_rendering() override; #pragma endregion #pragma region backend lifecycle @@ -214,59 +72,13 @@ namespace docraft::backend::pdf { * @brief Applies document metadata to the PDF info dictionary. */ void set_document_metadata(const DocraftDocumentMetadata &metadata) override; -#pragma endregion -#pragma region page management -/** - * @brief Returns the current page width in points. - */ - float page_width() const override; - /** - * @brief Returns the current page height in points. - */ - float page_height() const override; - /** - * @brief Adds a new page to the document and makes it the current page. - */ - void add_new_page() override; - /** - * @brief Moves the cursor to the next page if it exists. - * @throws std::runtime_error if already at the last page. - */ - void move_to_next_page() override; - /** - * @brief Navigates to a specific page (0-based index). - * @param page_number Destination page index. - * @throws std::runtime_error if the page number is out of range. - */ - void go_to_page(std::size_t page_number) override; - /** - * @brief Navigates to the first page. - */ - void go_to_first_page() override; - /** - * @brief Navigates to the previous page. - * @throws std::runtime_error if already at the first page. - */ - void go_to_previous_page() override; - /** - * @brief Navigates to the last page. - */ - void go_to_last_page() override; - /** - * @brief Sets the page size and orientation. - */ - void set_page_format(model::DocraftPageSize size, - model::DocraftPageOrientation orientation) override; - /** - * @brief Returns the current page number (1-based index). - */ - std::size_t current_page_number() const override; - /** - * @brief Returns the total number of pages in the document. - */ - std::size_t total_page_count() const override; #pragma endregion private: + class TextHaruBackend; + class LineHaruBackend; + class ShapeHaruBackend; + class ImageHaruBackend; + class PageHaruBackend; /** * @brief Creates a new page and adds it to the document. */ @@ -285,6 +97,11 @@ namespace docraft::backend::pdf { * @brief Applies the current alpha state to the Haru graphics state. */ void apply_alpha_state() const; + std::unique_ptr text_backend_; + std::unique_ptr line_backend_; + std::unique_ptr shape_backend_; + std::unique_ptr image_backend_; + std::unique_ptr page_backend_; HPDF_Doc pdf_; std::vector pages_; size_t current_page_number_ = 0; diff --git a/docraft/src/docraft/backend/pdf/docraft_haru_backend.cc b/docraft/src/docraft/backend/pdf/docraft_haru_backend.cc index c845717..b6de1e8 100644 --- a/docraft/src/docraft/backend/pdf/docraft_haru_backend.cc +++ b/docraft/src/docraft/backend/pdf/docraft_haru_backend.cc @@ -32,116 +32,116 @@ namespace { std::unordered_map map; HPDFErrorMap() { map = { - { "1001", "Internal error. The consistency of the data was lost." }, // HPDF_ARRAY_COUNT_ERR - { "1002", "Internal error. The consistency of the data was lost." }, // HPDF_ARRAY_ITEM_NOT_FOUND - { "1003", "Internal error. The consistency of the data was lost." }, // HPDF_ARRAY_ITEM_UNEXPECTED_TYPE - { "1004", "The length of the data exceeds HPDF_LIMIT_MAX_STRING_LEN." }, // HPDF_BINARY_LENGTH_ERR - { "1005", "Cannot get a palette data from PNG image." }, // HPDF_CANNOT_GET_PALLET + { "1001", "Internal error. The consistency of the data was lost." }, + { "1002", "Internal error. The consistency of the data was lost." }, + { "1003", "Internal error. The consistency of the data was lost." }, + { "1004", "The length of the data exceeds HPDF_LIMIT_MAX_STRING_LEN." }, + { "1005", "Cannot get a palette data from PNG image." }, { "1006", "Reserved/unknown HPDF error." }, - { "1007", "The count of elements of a dictionary exceeds HPDF_LIMIT_MAX_DICT_ELEMENT." }, // HPDF_DICT_COUNT_ERR - { "1008", "Internal error. The consistency of the data was lost." }, // HPDF_DICT_ITEM_NOT_FOUND - { "1009", "Internal error. The consistency of the data was lost." }, // HPDF_DICT_ITEM_UNEXPECTED_TYPE - { "100a", "Internal error. The consistency of the data was lost." }, // HPDF_DICT_STREAM_LENGTH_NOT_FOUND - { "100b", "HPDF_SetPermission() or HPDF_SetEncryptMode() was called before a password is set." }, // HPDF_DOC_ENCRYPTDICT_NOT_FOUND - { "100c", "Internal error. The consistency of the data was lost." }, // HPDF_DOC_INVALID_OBJECT + { "1007", "The count of elements of a dictionary exceeds HPDF_LIMIT_MAX_DICT_ELEMENT." }, + { "1008", "Internal error. The consistency of the data was lost." }, + { "1009", "Internal error. The consistency of the data was lost." }, + { "100a", "Internal error. The consistency of the data was lost." }, + { "100b", "HPDF_SetPermission() or HPDF_SetEncryptMode() was called before a password is set." }, + { "100c", "Internal error. The consistency of the data was lost." }, { "100d", "Reserved/unknown HPDF error." }, - { "100e", "Tried to register a font that has been registered." }, // HPDF_DUPLICATE_REGISTRATION - { "100f", "Cannot register a character to the japanese word wrap characters list." }, // HPDF_EXCEED_JWW_CODE_NUM_LIMIT + { "100e", "Tried to register a font that has been registered." }, + { "100f", "Cannot register a character to the japanese word wrap characters list." }, { "1010", "Reserved/unknown HPDF error." }, - { "1011", "Tried to set the owner password to NULL or owner and user password are the same." }, // HPDF_ENCRYPT_INVALID_PASSWORD - { "1013", "Internal error. The consistency of the data was lost." }, // HPDF_ERR_UNKNOWN_CLASS - { "1014", "The depth of the graphics state stack exceeded HPDF_LIMIT_MAX_GSTATE." }, // HPDF_EXCEED_GSTATE_LIMIT - { "1015", "Memory allocation failed." }, // HPDF_FAILD_TO_ALLOC_MEM - { "1016", "File processing failed. (A detailed code is set.)" }, // HPDF_FILE_IO_ERROR - { "1017", "Cannot open a file. (A detailed code is set.)" }, // HPDF_FILE_OPEN_ERROR + { "1011", "Tried to set the owner password to NULL or owner and user password are the same." }, + { "1013", "Internal error. The consistency of the data was lost." }, + { "1014", "The depth of the graphics state stack exceeded HPDF_LIMIT_MAX_GSTATE." }, + { "1015", "Memory allocation failed." }, + { "1016", "File processing failed. (A detailed code is set.)" }, + { "1017", "Cannot open a file. (A detailed code is set.)" }, { "1018", "Reserved/unknown HPDF error." }, - { "1019", "Tried to load a font that has been registered." }, // HPDF_FONT_EXISTS - { "101a", "Invalid font-file format or internal consistency error." }, // HPDF_FONT_INVALID_WIDTHS_TABLE - { "101b", "Cannot recognize a header of an AFM file." }, // HPDF_INVALID_AFM_HEADER - { "101c", "The specified annotation handle is invalid." }, // HPDF_INVALID_ANNOTATION + { "1019", "Tried to load a font that has been registered." }, + { "101a", "Invalid font-file format or internal consistency error." }, + { "101b", "Cannot recognize a header of an AFM file." }, + { "101c", "The specified annotation handle is invalid." }, { "101d", "Reserved/unknown HPDF error." }, - { "101e", "Invalid bit-per-component for mask image." }, // HPDF_INVALID_BIT_PER_COMPONENT - { "101f", "Cannot recognize char-metrics data of an AFM file." }, // HPDF_INVALID_CHAR_MATRICS_DATA - { "1020", "Invalid color space or invalid operation for current color space." }, // HPDF_INVALID_COLOR_SPACE - { "1021", "Invalid compression mode value." }, // HPDF_INVALID_COMPRESSION_MODE - { "1022", "An invalid date-time value was set." }, // HPDF_INVALID_DATE_TIME - { "1023", "An invalid destination handle was set." }, // HPDF_INVALID_DESTINATION + { "101e", "Invalid bit-per-component for mask image." }, + { "101f", "Cannot recognize char-metrics data of an AFM file." }, + { "1020", "Invalid color space or invalid operation for current color space." }, + { "1021", "Invalid compression mode value." }, + { "1022", "An invalid date-time value was set." }, + { "1023", "An invalid destination handle was set." }, { "1024", "Reserved/unknown HPDF error." }, - { "1025", "An invalid document handle is set." }, // HPDF_INVALID_DOCUMENT - { "1026", "Called a function invalid in the current document state." }, // HPDF_INVALID_DOCUMENT_STATE - { "1027", "An invalid encoder handle is set." }, // HPDF_INVALID_ENCODER - { "1028", "Invalid combination between font and encoder." }, // HPDF_INVALID_ENCODER_TYPE + { "1025", "An invalid document handle is set." }, + { "1026", "Called a function invalid in the current document state." }, + { "1027", "An invalid encoder handle is set." }, + { "1028", "Invalid combination between font and encoder." }, { "1029", "Reserved/unknown HPDF error." }, { "102a", "Reserved/unknown HPDF error." }, - { "102b", "An invalid encoding name is specified." }, // HPDF_INVALID_ENCODING_NAME - { "102c", "Invalid encryption key length." }, // HPDF_INVALID_ENCRYPT_KEY_LEN - { "102d", "Invalid font definition data or unsupported font format." }, // HPDF_INVALID_FONTDEF_DATA - { "102e", "Internal error. The consistency of the data was lost." }, // HPDF_INVALID_FONTDEF_TYPE - { "102f", "A font with the specified name is not found." }, // HPDF_INVALID_FONT_NAME - { "1030", "Unsupported image format." }, // HPDF_INVALID_IMAGE - { "1031", "Unsupported JPEG data." }, // HPDF_INVALID_JPEG_DATA - { "1032", "Cannot read a postscript-name from an AFM file." }, // HPDF_INVALID_N_DATA - { "1033", "Invalid object or internal consistency error." }, // HPDF_INVALID_OBJECT - { "1034", "Internal error. The consistency of the data was lost." }, // HPDF_INVALID_OBJ_ID - { "1035", "Invalid operation (e.g. wrong use of image mask functions)." }, // HPDF_INVALID_OPERATION - { "1036", "An invalid outline handle was specified." }, // HPDF_INVALID_OUTLINE - { "1037", "An invalid page handle was specified." }, // HPDF_INVALID_PAGE - { "1038", "An invalid pages handle was specified." }, // HPDF_INVALID_PAGES - { "1039", "An invalid parameter value was set." }, // HPDF_INVALID_PARAMETER + { "102b", "An invalid encoding name is specified." }, + { "102c", "Invalid encryption key length." }, + { "102d", "Invalid font definition data or unsupported font format." }, + { "102e", "Internal error. The consistency of the data was lost." }, + { "102f", "A font with the specified name is not found." }, + { "1030", "Unsupported image format." }, + { "1031", "Unsupported JPEG data." }, + { "1032", "Cannot read a postscript-name from an AFM file." }, + { "1033", "Invalid object or internal consistency error." }, + { "1034", "Internal error. The consistency of the data was lost." }, + { "1035", "Invalid operation (e.g. wrong use of image mask functions)." }, + { "1036", "An invalid outline handle was specified." }, + { "1037", "An invalid page handle was specified." }, + { "1038", "An invalid pages handle was specified." }, + { "1039", "An invalid parameter value was set." }, { "103a", "Reserved/unknown HPDF error." }, - { "103b", "Invalid PNG image format." }, // HPDF_INVALID_PNG_IMAGE - { "103c", "Internal error. The consistency of the data was lost." }, // HPDF_INVALID_STREAM - { "103d", "Internal error. The \"_FILE_NAME\" entry for delayed loading is missing." }, // HPDF_MISSING_FILE_NAME_ENTRY + { "103b", "Invalid PNG image format." }, + { "103c", "Internal error. The consistency of the data was lost." }, + { "103d", "Internal error. The \"_FILE_NAME\" entry for delayed loading is missing." }, { "103e", "Reserved/unknown HPDF error." }, - { "103f", "Invalid .TTC file format." }, // HPDF_INVALID_TTC_FILE - { "1040", "TTC index parameter exceeded number of included fonts." }, // HPDF_INVALID_TTC_INDEX - { "1041", "Cannot read width data from an AFM file." }, // HPDF_INVALID_WX_DATA - { "1042", "Internal error. The consistency of the data was lost." }, // HPDF_ITEM_NOT_FOUND - { "1043", "An error from libpng while loading an image." }, // HPDF_LIBPNG_ERROR - { "1044", "Internal error. The consistency of the data was lost." }, // HPDF_NAME_INVALID_VALUE - { "1045", "Internal error. The consistency of the data was lost." }, // HPDF_NAME_OUT_OF_RANGE + { "103f", "Invalid .TTC file format." }, + { "1040", "TTC index parameter exceeded number of included fonts." }, + { "1041", "Cannot read width data from an AFM file." }, + { "1042", "Internal error. The consistency of the data was lost." }, + { "1043", "An error from libpng while loading an image." }, + { "1044", "Internal error. The consistency of the data was lost." }, + { "1045", "Internal error. The consistency of the data was lost." }, { "1046", "Reserved/unknown HPDF error." }, { "1047", "Reserved/unknown HPDF error." }, { "1048", "Reserved/unknown HPDF error." }, - { "1049", "Internal error. The consistency of the data was lost." }, // HPDF_PAGES_MISSING_KIDS_ENTRY - { "104a", "Internal error. The consistency of the data was lost." }, // HPDF_PAGE_CANNOT_FIND_OBJECT - { "104b", "Internal error. The consistency of the data was lost." }, // HPDF_PAGE_CANNOT_GET_ROOT_PAGES - { "104c", "There are no graphics-states to be restored." }, // HPDF_PAGE_CANNOT_RESTORE_GSTATE - { "104d", "Internal error. The consistency of the data was lost." }, // HPDF_PAGE_CANNOT_SET_PARENT - { "104e", "The current font is not set." }, // HPDF_PAGE_FONT_NOT_FOUND - { "104f", "An invalid font handle was specified." }, // HPDF_PAGE_INVALID_FONT - { "1050", "An invalid font size was set." }, // HPDF_PAGE_INVALID_FONT_SIZE - { "1051", "Invalid graphics mode." }, // HPDF_PAGE_INVALID_GMODE - { "1052", "Internal error. The consistency of the data was lost." }, // HPDF_PAGE_INVALID_INDEX - { "1053", "The specified rotate value is not a multiple of 90." }, // HPDF_PAGE_INVALID_ROTATE_VALUE - { "1054", "An invalid page size was set." }, // HPDF_PAGE_INVALID_SIZE - { "1055", "An invalid image handle was set." }, // HPDF_PAGE_INVALID_XOBJECT - { "1056", "The specified value is out of range." }, // HPDF_PAGE_OUT_OF_RANGE - { "1057", "The specified real value is out of range." }, // HPDF_REAL_OUT_OF_RANGE - { "1058", "Unexpected EOF marker was detected." }, // HPDF_STREAM_EOF - { "1059", "Internal error. The consistency of the data was lost." }, // HPDF_STREAM_READLN_CONTINUE + { "1049", "Internal error. The consistency of the data was lost." }, + { "104a", "Internal error. The consistency of the data was lost." }, + { "104b", "Internal error. The consistency of the data was lost." }, + { "104c", "There are no graphics-states to be restored." }, + { "104d", "Internal error. The consistency of the data was lost." }, + { "104e", "The current font is not set." }, + { "104f", "An invalid font handle was specified." }, + { "1050", "An invalid font size was set." }, + { "1051", "Invalid graphics mode." }, + { "1052", "Internal error. The consistency of the data was lost." }, + { "1053", "The specified rotate value is not a multiple of 90." }, + { "1054", "An invalid page size was set." }, + { "1055", "An invalid image handle was set." }, + { "1056", "The specified value is out of range." }, + { "1057", "The specified real value is out of range." }, + { "1058", "Unexpected EOF marker was detected." }, + { "1059", "Internal error. The consistency of the data was lost." }, { "105a", "Reserved/unknown HPDF error." }, - { "105b", "The length of the specified text is too long." }, // HPDF_STRING_OUT_OF_RANGE - { "105c", "The execution of a function was skipped because of other errors." }, // HPDF_THIS_FUNC_WAS_SKIPPED - { "105d", "This TrueType font cannot be embedded (restricted by license)." }, // HPDF_TTF_CANNOT_EMBEDDING_FONT - { "105e", "Unsupported TTF format (invalid cmap)." }, // HPDF_TTF_INVALID_CMAP - { "105f", "Unsupported TTF format." }, // HPDF_TTF_INVALID_FOMAT - { "1060", "Unsupported TTF format (missing table)." }, // HPDF_TTF_MISSING_TABLE - { "1061", "Internal error. The consistency of the data was lost." }, // HPDF_UNSUPPORTED_FONT_TYPE - { "1062", "Library not configured to use libpng or internal error." }, // HPDF_UNSUPPORTED_FUNC - { "1063", "Unsupported JPEG format." }, // HPDF_UNSUPPORTED_JPEG_FORMAT - { "1064", "Failed to parse .PFB file." }, // HPDF_UNSUPPORTED_TYPE1_FONT - { "1065", "Internal error. The consistency of the data was lost." }, // HPDF_XREF_COUNT_ERR - { "1066", "An error occurred while executing a function of zlib." }, // HPDF_ZLIB_ERROR - { "1067", "An error returned from zlib." }, // HPDF_INVALID_PAGE_INDEX - { "1068", "An invalid URI was set." }, // HPDF_INVALID_URI - { "1069", "An invalid page layout was set." }, // HPDF_PAGELAYOUT_OUT_OF_RANGE - { "1070", "An invalid page mode was set." }, // HPDF_PAGEMODE_OUT_OF_RANGE - { "1071", "An invalid page number style was set." }, // HPDF_PAGENUM_STYLE_OUT_OF_RANGE - { "1072", "An invalid annotation icon was set." }, // HPDF_ANNOT_INVALID_ICON - { "1073", "An invalid annotation border style was set." }, // HPDF_ANNOT_INVALID_BORDER_STYLE - { "1074", "An invalid page direction was set." }, // HPDF_PAGE_INVALID_DIRECTION - { "1075", "An invalid font handle was specified." } // HPDF_INVALID_FONT + { "105b", "The length of the specified text is too long." }, + { "105c", "The execution of a function was skipped because of other errors." }, + { "105d", "This TrueType font cannot be embedded (restricted by license)." }, + { "105e", "Unsupported TTF format (invalid cmap)." }, + { "105f", "Unsupported TTF format." }, + { "1060", "Unsupported TTF format (missing table)." }, + { "1061", "Internal error. The consistency of the data was lost." }, + { "1062", "Library not configured to use libpng or internal error." }, + { "1063", "Unsupported JPEG format." }, + { "1064", "Failed to parse .PFB file." }, + { "1065", "Internal error. The consistency of the data was lost." }, + { "1066", "An error occurred while executing a function of zlib." }, + { "1067", "An error returned from zlib." }, + { "1068", "An invalid URI was set." }, + { "1069", "An invalid page layout was set." }, + { "1070", "An invalid page mode was set." }, + { "1071", "An invalid page number style was set." }, + { "1072", "An invalid annotation icon was set." }, + { "1073", "An invalid annotation border style was set." }, + { "1074", "An invalid page direction was set." }, + { "1075", "An invalid font handle was specified." } }; } static const HPDFErrorMap& instance() { @@ -153,13 +153,13 @@ namespace { void HPDF_STDCALL error_handler(HPDF_STATUS error_no, HPDF_STATUS, void *) { std::ostringstream ss; ss << std::hex << error_no; - std::string error_no_hex = ss.str(); + const std::string error_no_hex = ss.str(); const auto &err_map = HPDFErrorMap::instance().map; - auto it = err_map.find(error_no_hex); + const auto it = err_map.find(error_no_hex); if (it != err_map.end()) { - std::cerr << "error: error_no=0x" << error_no_hex << ", message=" << it->second<< std::endl; + std::cerr << "error: error_no=0x" << error_no_hex << ", message=" << it->second << std::endl; } else { std::cerr << "error: error_no=0x" << error_no_hex << std::endl; } @@ -240,244 +240,392 @@ namespace docraft::backend::pdf { } } // namespace - void DocraftHaruBackend::create_new_page() { - HPDF_Page new_page = HPDF_AddPage(pdf_); - if (!new_page) { - throw std::runtime_error("Failed to create a new page"); + class DocraftHaruBackend::TextHaruBackend : public docraft::backend::IDocraftTextRenderingBackend { + public: + explicit TextHaruBackend(DocraftHaruBackend &owner) : owner_(owner) {} + + void begin_text() const override { + HPDF_Page_BeginText(owner_.pages_[owner_.internal_current_page_index()]); } - apply_page_format(new_page); - pages_.push_back(new_page); - current_page_number_ = pages_.size() - 1; // Move to the newly created page - } - DocraftHaruBackend::DocraftHaruBackend() { - pdf_ = HPDF_New(error_handler, NULL); - if (!pdf_) { - throw std::runtime_error("Failed to initialize Haru PDF document"); + + void end_text() const override { + HPDF_Page_EndText(owner_.pages_[owner_.internal_current_page_index()]); } - HPDF_UseUTFEncodings(pdf_); - HPDF_SetCurrentEncoder(pdf_, "UTF-8"); - HPDF_SetCompressionMode(pdf_, HPDF_COMP_ALL); - create_new_page(); - } - DocraftHaruBackend::~DocraftHaruBackend() { - if (pdf_) { - HPDF_Free(pdf_); - pdf_ = nullptr; - for (auto &p: pages_) { - // Haru automatically manages page lifetimes, so we don't need to free them individually. Just clear the vector to release references. - p = nullptr; + + void draw_text(const std::string &text, float x, float y) const override { + HPDF_Page_TextOut(owner_.pages_[owner_.internal_current_page_index()], x, y, text.c_str()); + } + + void set_text_color(float r, float g, float b) const override { + HPDF_Page_SetRGBFill(owner_.pages_[owner_.internal_current_page_index()], r, g, b); + } + + void draw_text_matrix(const std::string &text, + float scale_x, + float skew_x, + float skew_y, + float scale_y, + float translate_x, + float translate_y) const override { + HPDF_Page_SetTextMatrix( + owner_.pages_[owner_.internal_current_page_index()], + scale_x, + skew_x, + skew_y, + scale_y, + translate_x, + translate_y); + HPDF_Page_ShowText(owner_.pages_[owner_.internal_current_page_index()], text.c_str()); + } + + float measure_text_width(const std::string &text) const override { + return HPDF_Page_TextWidth(owner_.pages_[owner_.internal_current_page_index()], text.c_str()); + } + + private: + DocraftHaruBackend &owner_; + }; + + class DocraftHaruBackend::LineHaruBackend : public docraft::backend::IDocraftLineRenderingBackend { + public: + explicit LineHaruBackend(DocraftHaruBackend &owner) : owner_(owner) {} + + void set_stroke_color(float r, float g, float b) const override { + HPDF_Page_SetRGBStroke(owner_.pages_[owner_.internal_current_page_index()], r, g, b); + } + + void set_line_width(float thickness) const override { + HPDF_Page_SetLineWidth(owner_.pages_[owner_.internal_current_page_index()], thickness); + } + + void draw_line(float x1, float y1, float x2, float y2) const override { + HPDF_Page_MoveTo(owner_.pages_[owner_.internal_current_page_index()], x1, y1); + HPDF_Page_LineTo(owner_.pages_[owner_.internal_current_page_index()], x2, y2); + HPDF_Page_Stroke(owner_.pages_[owner_.internal_current_page_index()]); + } + + private: + DocraftHaruBackend &owner_; + }; + + class DocraftHaruBackend::ShapeHaruBackend : public docraft::backend::IDocraftShapeRenderingBackend { + public: + explicit ShapeHaruBackend(DocraftHaruBackend &owner) : owner_(owner) {} + + void save_state() const override { + HPDF_Page_GSave(owner_.pages_[owner_.internal_current_page_index()]); + } + + void restore_state() const override { + HPDF_Page_GRestore(owner_.pages_[owner_.internal_current_page_index()]); + } + + void set_fill_color(float r, float g, float b) const override { + HPDF_Page_SetRGBFill(owner_.pages_[owner_.internal_current_page_index()], r, g, b); + } + + void set_fill_alpha(float alpha) const override { + owner_.fill_alpha_ = alpha; + owner_.apply_alpha_state(); + } + + void set_stroke_alpha(float alpha) const override { + owner_.stroke_alpha_ = alpha; + owner_.apply_alpha_state(); + } + + void draw_rectangle(float x, float y, float width, float height) const override { + HPDF_Page_Rectangle(owner_.pages_[owner_.internal_current_page_index()], x, y, width, height); + } + + void draw_circle(float center_x, float center_y, float radius) const override { + HPDF_Page_Circle(owner_.pages_[owner_.internal_current_page_index()], center_x, center_y, radius); + } + + void draw_polygon(const std::vector &points) const override { + if (points.size() < 2U) { + return; + } + + HPDF_Page_MoveTo(owner_.pages_[owner_.internal_current_page_index()], points[0].x, points[0].y); + for (size_t i = 1; i < points.size(); ++i) { + HPDF_Page_LineTo(owner_.pages_[owner_.internal_current_page_index()], points[i].x, points[i].y); } - pages_.clear(); + HPDF_Page_ClosePath(owner_.pages_[owner_.internal_current_page_index()]); } - } - void DocraftHaruBackend::begin_text() const { - HPDF_Page_BeginText(pages_[internal_current_page_index()]); - } + void fill() const override { + HPDF_Page_Fill(owner_.pages_[owner_.internal_current_page_index()]); + } - void DocraftHaruBackend::end_text() const { - HPDF_Page_EndText(pages_[internal_current_page_index()]); - } - void DocraftHaruBackend::draw_text(const std::string &text, float x, float y) const { - HPDF_Page_TextOut(pages_[internal_current_page_index()], x, y, text.c_str()); - } + void stroke() const override { + HPDF_Page_Stroke(owner_.pages_[owner_.internal_current_page_index()]); + } - void DocraftHaruBackend::set_text_color(float r, float g, float b) const { - HPDF_Page_SetRGBFill(pages_[internal_current_page_index()], r, g, b); - } + void fill_stroke() const override { + HPDF_Page_FillStroke(owner_.pages_[owner_.internal_current_page_index()]); + } - float DocraftHaruBackend::measure_text_width(const std::string &text) const { - return HPDF_Page_TextWidth(pages_[internal_current_page_index()], text.c_str()); - } + private: + DocraftHaruBackend &owner_; + }; - void DocraftHaruBackend::set_stroke_color(float r, float g, float b) const { - HPDF_Page_SetRGBStroke(pages_[internal_current_page_index()], r, g, b); - } + class DocraftHaruBackend::ImageHaruBackend : public docraft::backend::IDocraftImageRenderingBackend { + public: + explicit ImageHaruBackend(DocraftHaruBackend &owner) : owner_(owner) {} + + void draw_png_image(const std::string& path, + float x, + float y, + float width, + float height) const override { + auto image = HPDF_LoadPngImageFromFile(owner_.pdf_, path.c_str()); + if (!image) { + throw std::runtime_error("Failed to load PNG image: " + path); + } + HPDF_Page_DrawImage(owner_.pages_[owner_.internal_current_page_index()], image, x, y, width, height); + } - void DocraftHaruBackend::set_line_width(float thickness) const { - HPDF_Page_SetLineWidth(pages_[internal_current_page_index()], thickness); - } + void draw_png_image_from_memory(const unsigned char* data, + std::size_t size, + float x, + float y, + float width, + float height) const override { + auto image = HPDF_LoadPngImageFromMem( + owner_.pdf_, + reinterpret_cast(data), + static_cast(size)); + if (!image) { + throw std::runtime_error("Failed to load PNG image from memory"); + } + HPDF_Page_DrawImage(owner_.pages_[owner_.internal_current_page_index()], image, x, y, width, height); + } - void DocraftHaruBackend::draw_line(float x1, float y1, float x2, float y2) const { - HPDF_Page_MoveTo(pages_[internal_current_page_index()], x1, y1); - HPDF_Page_LineTo(pages_[internal_current_page_index()], x2, y2); - HPDF_Page_Stroke(pages_[internal_current_page_index()]); - } + void draw_jpeg_image(const std::string& path, + float x, + float y, + float width, + float height) const override { + auto image = HPDF_LoadJpegImageFromFile(owner_.pdf_, path.c_str()); + if (!image) { + throw std::runtime_error("Failed to load JPEG image: " + path); + } + HPDF_Page_DrawImage(owner_.pages_[owner_.internal_current_page_index()], image, x, y, width, height); + } - void DocraftHaruBackend::draw_text_matrix( - const std::string &text, - float scale_x, - float skew_x, - float skew_y, - float scale_y, - float translate_x, - float translate_y) const { - HPDF_Page_SetTextMatrix(pages_[internal_current_page_index()], scale_x, skew_x, skew_y, scale_y, translate_x, translate_y); - HPDF_Page_ShowText(pages_[internal_current_page_index()], text.c_str()); - } + void draw_jpeg_image_from_memory(const unsigned char* data, + std::size_t size, + float x, + float y, + float width, + float height) const override { + auto image = HPDF_LoadJpegImageFromMem( + owner_.pdf_, + reinterpret_cast(data), + static_cast(size)); + if (!image) { + throw std::runtime_error("Failed to load JPEG image from memory"); + } + HPDF_Page_DrawImage(owner_.pages_[owner_.internal_current_page_index()], image, x, y, width, height); + } - void DocraftHaruBackend::save_state() const { - HPDF_Page_GSave(pages_[internal_current_page_index()]); - } + void draw_raw_rgb_image(const std::string& path, + int pixel_width, + int pixel_height, + float x, + float y, + float width, + float height) const override { + auto image = HPDF_LoadRawImageFromFile( + owner_.pdf_, + path.c_str(), + static_cast(pixel_width), + static_cast(pixel_height), + HPDF_CS_DEVICE_RGB); + if (!image) { + throw std::runtime_error("Failed to load raw RGB image: " + path); + } + HPDF_Page_DrawImage(owner_.pages_[owner_.internal_current_page_index()], image, x, y, width, height); + } - void DocraftHaruBackend::restore_state() const { - HPDF_Page_GRestore(pages_[internal_current_page_index()]);//restore the previous graphics state, which was saved by the last call of HPDF_Page_GSave() or HPDF_Page_SaveToStream(). - } + void draw_raw_rgb_image_from_memory(const unsigned char* data, + int pixel_width, + int pixel_height, + float x, + float y, + float width, + float height) const override { + constexpr HPDF_UINT bits_per_component = 8; + auto *image = HPDF_LoadRawImageFromMem( + owner_.pdf_, + reinterpret_cast(data), + static_cast(pixel_width), + static_cast(pixel_height), + HPDF_CS_DEVICE_RGB, + bits_per_component); + if (!image) { + throw std::runtime_error("Failed to load raw RGB image from memory"); + } + HPDF_Page_DrawImage(owner_.pages_[owner_.internal_current_page_index()], image, x, y, width, height); + } - void DocraftHaruBackend::set_fill_color(float r, float g, float b) const { - HPDF_Page_SetRGBFill(pages_[internal_current_page_index()], r, g, b); - } + private: + DocraftHaruBackend &owner_; + }; - void DocraftHaruBackend::set_fill_alpha(float alpha) const { - fill_alpha_ = alpha; - apply_alpha_state(); - } + class DocraftHaruBackend::PageHaruBackend : public docraft::backend::IDocraftPageRenderingBackend { + public: + explicit PageHaruBackend(DocraftHaruBackend &owner) : owner_(owner) {} - void DocraftHaruBackend::set_stroke_alpha(float alpha) const { - stroke_alpha_ = alpha; - apply_alpha_state(); - } + float page_width() const override { + return HPDF_Page_GetWidth(owner_.pages_[owner_.internal_current_page_index()]); + } - void DocraftHaruBackend::draw_rectangle(float x, float y, float width, float height) const { - HPDF_Page_Rectangle(pages_[internal_current_page_index()], x, y, width, height); - } + float page_height() const override { + return HPDF_Page_GetHeight(owner_.pages_[owner_.internal_current_page_index()]); + } - void DocraftHaruBackend::draw_circle(float center_x, float center_y, float radius) const { - HPDF_Page_Circle(pages_[internal_current_page_index()], center_x, center_y, radius); - } + void add_new_page() override { + owner_.create_new_page(); + } + + void move_to_next_page() override { + if (owner_.current_page_number_ + 1 < owner_.pages_.size()) { + ++owner_.current_page_number_; + return; + } + throw std::runtime_error("Already at the last page, cannot move to next page"); + } - void DocraftHaruBackend::draw_polygon(const std::vector &points) const { - if (points.size() < 2U) { - return; + void go_to_page(std::size_t page_number) override { + if (page_number < owner_.pages_.size()) { + owner_.current_page_number_ = page_number; + return; + } + throw std::runtime_error("Invalid page number: " + std::to_string(page_number)); } - HPDF_Page_MoveTo(pages_[internal_current_page_index()], points[0].x, points[0].y); - for (size_t i = 1; i < points.size(); ++i) { - HPDF_Page_LineTo(pages_[internal_current_page_index()], points[i].x, points[i].y); + void go_to_first_page() override { + if (owner_.pages_.empty()) { + throw std::runtime_error("No pages in document"); + } + owner_.current_page_number_ = 0; + } + + void go_to_previous_page() override { + if (owner_.current_page_number_ == 0) { + throw std::runtime_error("Already at the first page, cannot move to previous page"); + } + --owner_.current_page_number_; + } + + void go_to_last_page() override { + if (owner_.pages_.empty()) { + throw std::runtime_error("No pages in document"); + } + owner_.current_page_number_ = owner_.pages_.size() - 1; + } + + void set_page_format(model::DocraftPageSize size, + model::DocraftPageOrientation orientation) override { + owner_.page_size_ = to_hpdf_size(size); + owner_.page_direction_ = to_hpdf_direction(orientation); + for (auto &page : owner_.pages_) { + if (page) { + owner_.apply_page_format(page); + } + } + } + + std::size_t current_page_number() const override { + return owner_.current_page_number_ + 1; + } + + std::size_t total_page_count() const override { + return owner_.pages_.size(); + } + + private: + DocraftHaruBackend &owner_; + }; + + void DocraftHaruBackend::create_new_page() { + HPDF_Page new_page = HPDF_AddPage(pdf_); + if (!new_page) { + throw std::runtime_error("Failed to create a new page"); + } + apply_page_format(new_page); + pages_.push_back(new_page); + current_page_number_ = pages_.size() - 1; + } + + DocraftHaruBackend::DocraftHaruBackend() + : text_backend_(std::make_unique(*this)), + line_backend_(std::make_unique(*this)), + shape_backend_(std::make_unique(*this)), + image_backend_(std::make_unique(*this)), + page_backend_(std::make_unique(*this)), + pdf_(nullptr) { + pdf_ = HPDF_New(error_handler, nullptr); + if (!pdf_) { + throw std::runtime_error("Failed to initialize Haru PDF document"); } - HPDF_Page_ClosePath(pages_[internal_current_page_index()]); + HPDF_UseUTFEncodings(pdf_); + HPDF_SetCurrentEncoder(pdf_, "UTF-8"); + HPDF_SetCompressionMode(pdf_, HPDF_COMP_ALL); + create_new_page(); } - void DocraftHaruBackend::fill() const { - HPDF_Page_Fill(pages_[internal_current_page_index()]); + DocraftHaruBackend::~DocraftHaruBackend() { + if (pdf_) { + HPDF_Free(pdf_); + pdf_ = nullptr; + } + for (auto &page : pages_) { + page = nullptr; + } + pages_.clear(); } - void DocraftHaruBackend::stroke() const { - HPDF_Page_Stroke(pages_[internal_current_page_index()]); + const docraft::backend::IDocraftLineRenderingBackend *DocraftHaruBackend::line_rendering() const { + return line_backend_.get(); } - void DocraftHaruBackend::fill_stroke() const { - HPDF_Page_FillStroke(pages_[internal_current_page_index()]); + docraft::backend::IDocraftLineRenderingBackend *DocraftHaruBackend::edit_line_rendering() { + return line_backend_.get(); } - void DocraftHaruBackend::draw_png_image( - const std::string& path, - float x, - float y, - float width, - float height) const { - auto image = HPDF_LoadPngImageFromFile(pdf_, path.c_str()); - if (!image) { - throw std::runtime_error("Failed to load PNG image: " + path); - } - HPDF_Page_DrawImage(pages_[internal_current_page_index()], image, x, y, width, height); + const docraft::backend::IDocraftTextRenderingBackend *DocraftHaruBackend::text_rendering() const { + return text_backend_.get(); } - void DocraftHaruBackend::draw_png_image_from_memory( - const unsigned char* data, - std::size_t size, - float x, - float y, - float width, - float height) const { - auto image = HPDF_LoadPngImageFromMem( - pdf_, - reinterpret_cast(data), - static_cast(size)); - if (!image) { - throw std::runtime_error("Failed to load PNG image from memory"); - } - HPDF_Page_DrawImage(pages_[internal_current_page_index()], image, x, y, width, height); + docraft::backend::IDocraftTextRenderingBackend *DocraftHaruBackend::edit_text_rendering() { + return text_backend_.get(); } - void DocraftHaruBackend::draw_jpeg_image( - const std::string& path, - float x, - float y, - float width, - float height) const { - auto image = HPDF_LoadJpegImageFromFile(pdf_, path.c_str()); - if (!image) { - throw std::runtime_error("Failed to load JPEG image: " + path); - } - HPDF_Page_DrawImage(pages_[internal_current_page_index()], image, x, y, width, height); + const docraft::backend::IDocraftShapeRenderingBackend *DocraftHaruBackend::shape_rendering() const { + return shape_backend_.get(); } - void DocraftHaruBackend::draw_jpeg_image_from_memory( - const unsigned char* data, - std::size_t size, - float x, - float y, - float width, - float height) const { - auto image = HPDF_LoadJpegImageFromMem( - pdf_, - reinterpret_cast(data), - static_cast(size)); - if (!image) { - throw std::runtime_error("Failed to load JPEG image from memory"); - } - HPDF_Page_DrawImage(pages_[internal_current_page_index()], image, x, y, width, height); + docraft::backend::IDocraftShapeRenderingBackend *DocraftHaruBackend::edit_shape_rendering() { + return shape_backend_.get(); } - void DocraftHaruBackend::draw_raw_rgb_image( - const std::string& path, - int pixel_width, - int pixel_height, - float x, - float y, - float width, - float height) const { - auto image = HPDF_LoadRawImageFromFile( - pdf_, - path.c_str(), - static_cast(pixel_width), - static_cast(pixel_height), - HPDF_CS_DEVICE_RGB); - if (!image) { - throw std::runtime_error("Failed to load raw RGB image: " + path); - } - HPDF_Page_DrawImage(pages_[internal_current_page_index()], image, x, y, width, height); + const docraft::backend::IDocraftImageRenderingBackend *DocraftHaruBackend::image_rendering() const { + return image_backend_.get(); } - void DocraftHaruBackend::draw_raw_rgb_image_from_memory( - const unsigned char* data, - int pixel_width, - int pixel_height, - float x, - float y, - float width, - float height) const { - constexpr HPDF_UINT bits_per_component = 8; - auto *image = HPDF_LoadRawImageFromMem( - pdf_, - reinterpret_cast(data), - static_cast(pixel_width), - static_cast(pixel_height), - HPDF_CS_DEVICE_RGB, - bits_per_component); - if (!image) { - throw std::runtime_error("Failed to load raw RGB image from memory"); - } - HPDF_Page_DrawImage(pages_[internal_current_page_index()], image, x, y, width, height); + docraft::backend::IDocraftImageRenderingBackend *DocraftHaruBackend::edit_image_rendering() { + return image_backend_.get(); } - float DocraftHaruBackend::page_width() const { - return HPDF_Page_GetWidth(pages_[internal_current_page_index()]); + const docraft::backend::IDocraftPageRenderingBackend *DocraftHaruBackend::page_rendering() const { + return page_backend_.get(); } - float DocraftHaruBackend::page_height() const { - return HPDF_Page_GetHeight(pages_[internal_current_page_index()]); + docraft::backend::IDocraftPageRenderingBackend *DocraftHaruBackend::edit_page_rendering() { + return page_backend_.get(); } void DocraftHaruBackend::save_to_file(const std::string& path) const { @@ -488,9 +636,7 @@ namespace docraft::backend::pdf { return ".pdf"; } - const char* DocraftHaruBackend::register_ttf_font_from_file( - const std::string& path, - bool embed) const { + const char* DocraftHaruBackend::register_ttf_font_from_file(const std::string& path, bool embed) const { const char* result = HPDF_LoadTTFontFromFile(pdf_, path.c_str(), embed ? HPDF_TRUE : HPDF_FALSE); if (!result) { HPDF_ResetError(pdf_); @@ -498,9 +644,7 @@ namespace docraft::backend::pdf { return result; } - bool DocraftHaruBackend::can_use_font( - const std::string& internal_name, - const char* encoder) const { + bool DocraftHaruBackend::can_use_font(const std::string& internal_name, const char* encoder) const { HPDF_Font font = HPDF_GetFont(pdf_, internal_name.c_str(), encoder); if (!font || HPDF_GetError(pdf_) != HPDF_OK) { HPDF_ResetError(pdf_); @@ -509,10 +653,7 @@ namespace docraft::backend::pdf { return true; } - void DocraftHaruBackend::set_font( - const std::string& internal_name, - float size, - const char* encoder) const { + void DocraftHaruBackend::set_font(const std::string& internal_name, float size, const char* encoder) const { HPDF_Font font = HPDF_GetFont(pdf_, internal_name.c_str(), encoder); if (!font) { throw std::runtime_error("Failed to resolve font: " + internal_name); @@ -542,54 +683,6 @@ namespace docraft::backend::pdf { } } - void DocraftHaruBackend::add_new_page() { - create_new_page(); - } - - void DocraftHaruBackend::move_to_next_page() { - if (current_page_number_ + 1 < pages_.size()) { - ++current_page_number_; - } else { - throw std::runtime_error("Already at the last page, cannot move to next page"); - } - } - - void DocraftHaruBackend::go_to_page(std::size_t page_number) { - if (page_number < pages_.size()) { - current_page_number_ = page_number; - } else { - throw std::runtime_error("Invalid page number: " + std::to_string(page_number)); - } - } - - void DocraftHaruBackend::go_to_first_page() { - if (pages_.empty()) { - throw std::runtime_error("No pages in document"); - } - current_page_number_ = 0; - } - - void DocraftHaruBackend::go_to_previous_page() { - if (current_page_number_ == 0) { - throw std::runtime_error("Already at the first page, cannot move to previous page"); - } - --current_page_number_; - } - - void DocraftHaruBackend::go_to_last_page() { - if (pages_.empty()) { - throw std::runtime_error("No pages in document"); - } - current_page_number_ = pages_.size() - 1; - } - - std::size_t DocraftHaruBackend::current_page_number() const { - return current_page_number_+1; // Return 1-based page number for external use - } - - std::size_t DocraftHaruBackend::total_page_count() const { - return pages_.size(); - } size_t DocraftHaruBackend::internal_current_page_index() const { return current_page_number_; } @@ -597,16 +690,4 @@ namespace docraft::backend::pdf { void DocraftHaruBackend::apply_page_format(HPDF_Page page) const { HPDF_Page_SetSize(page, page_size_, page_direction_); } - - void DocraftHaruBackend::set_page_format(model::DocraftPageSize size, - model::DocraftPageOrientation orientation) { - page_size_ = to_hpdf_size(size); - page_direction_ = to_hpdf_direction(orientation); - for (auto &page : pages_) { - if (page) { - apply_page_format(page); - } - } - } - -} // docraft +} // docraft::backend::pdf diff --git a/docraft/src/docraft/docraft_document_context.cc b/docraft/src/docraft/docraft_document_context.cc index 79e5252..cb8052d 100644 --- a/docraft/src/docraft/docraft_document_context.cc +++ b/docraft/src/docraft/docraft_document_context.cc @@ -22,8 +22,8 @@ namespace docraft { DocraftDocumentContext::DocraftDocumentContext() { backend_ = std::make_shared(); - page_height_ = backend_->page_height(); - page_width_ = backend_->page_width(); + page_height_ = backend_->page_rendering()->page_height(); + page_width_ = backend_->page_rendering()->page_width(); current_rect_width_ = page_width_; refresh_backend_caches(); } @@ -31,8 +31,8 @@ namespace docraft { DocraftDocumentContext::DocraftDocumentContext( const std::shared_ptr &backend) : backend_( backend) { - page_height_ = backend_->page_height(); - page_width_ = backend_->page_width(); + page_height_ = backend_->page_rendering()->page_height(); + page_width_ = backend_->page_rendering()->page_width(); current_rect_width_ = page_width_; refresh_backend_caches(); } @@ -77,8 +77,8 @@ namespace docraft { void DocraftDocumentContext::set_backend(const std::shared_ptr &backend) { backend_ = backend ? backend : std::make_shared(); - page_height_ = backend_->page_height(); - page_width_ = backend_->page_width(); + page_height_ = backend_->page_rendering()->page_height(); + page_width_ = backend_->page_rendering()->page_width(); current_rect_width_ = page_width_; refresh_backend_caches(); } diff --git a/docraft/src/docraft/management/docraft_backend_cache.cc b/docraft/src/docraft/management/docraft_backend_cache.cc index 6b05aec..def9fc5 100644 --- a/docraft/src/docraft/management/docraft_backend_cache.cc +++ b/docraft/src/docraft/management/docraft_backend_cache.cc @@ -18,6 +18,18 @@ #include "docraft/docraft_lib.h" namespace docraft::management { + namespace { + template + std::shared_ptr alias_backend_interface( + const std::shared_ptr &backend, + InterfaceType *interface_pointer) { + if (!backend || !interface_pointer) { + return {}; + } + return std::shared_ptr(backend, interface_pointer); + } + } // namespace + void DocraftBackendCache::initialize_from_backend( const std::shared_ptr &backend) { refresh_caches(backend); @@ -64,11 +76,10 @@ namespace docraft::management { } void DocraftBackendCache::refresh_caches(const std::shared_ptr &backend) { - docraft::ensure_lazy_backend(line_backend_, backend); - docraft::ensure_lazy_backend(shape_backend_, backend); - docraft::ensure_lazy_backend(text_backend_, backend); - docraft::ensure_lazy_backend(image_backend_, backend); - docraft::ensure_lazy_backend(page_backend_, backend); + line_backend_ = alias_backend_interface(backend, backend ? backend->edit_line_rendering() : nullptr); + shape_backend_ = alias_backend_interface(backend, backend ? backend->edit_shape_rendering() : nullptr); + text_backend_ = alias_backend_interface(backend, backend ? backend->edit_text_rendering() : nullptr); + image_backend_ = alias_backend_interface(backend, backend ? backend->edit_image_rendering() : nullptr); + page_backend_ = alias_backend_interface(backend, backend ? backend->edit_page_rendering() : nullptr); } } // docraft::management - diff --git a/docraft/src/docraft/model/docraft_list.cc b/docraft/src/docraft/model/docraft_list.cc index a541121..3c6c3bb 100644 --- a/docraft/src/docraft/model/docraft_list.cc +++ b/docraft/src/docraft/model/docraft_list.cc @@ -34,15 +34,16 @@ namespace docraft::model { } const auto &marker = markers_[i]; if (marker.kind == Marker::Kind::kBox) { - if (context && context->shape_backend()) { + if (context && context->shape_backend() && context->line_backend()) { auto rgb = text_node->color().toRGB(); const float size = marker.size > 0.0F ? marker.size : (text_node->font_size() * 0.6F); const float x = marker.position.x; const float y = marker.position.y - (size * 0.2F); auto shape = context->shape_backend(); + auto line = context->line_backend(); shape->save_state(); - shape->set_stroke_color(rgb.r, rgb.g, rgb.b); - shape->set_line_width(1.0F); + line->set_stroke_color(rgb.r, rgb.g, rgb.b); + line->set_line_width(1.0F); shape->draw_rectangle(x, y, size, size); shape->stroke(); shape->restore_state(); diff --git a/docraft/src/docraft/renderer/docraft_pdf_renderer.cc b/docraft/src/docraft/renderer/docraft_pdf_renderer.cc index 9e16568..27b2a8b 100644 --- a/docraft/src/docraft/renderer/docraft_pdf_renderer.cc +++ b/docraft/src/docraft/renderer/docraft_pdf_renderer.cc @@ -35,8 +35,9 @@ namespace docraft::renderer { painter.draw(context()); } void DocraftPDFRenderer::render_section(const model::DocraftSection §ion_node) { - auto backend = context()->shape_backend(); - if (!backend) return; + auto shape_backend = context()->shape_backend(); + auto line_backend = context()->line_backend(); + if (!shape_backend || !line_backend) return; const float left = section_node.position().x; const float right = section_node.position().x + section_node.width(); @@ -54,56 +55,56 @@ namespace docraft::renderer { bool has_stroke = border_width > 0.0F && border_color.a > 0.0F; if (content_width > 0.0F && content_height > 0.0F && (has_fill || has_stroke)) { - backend->save_state(); + shape_backend->save_state(); if (bg_color.a < 1.0F || border_color.a < 1.0F) { - backend->set_fill_alpha(bg_color.a); - backend->set_stroke_alpha(border_color.a); + shape_backend->set_fill_alpha(bg_color.a); + shape_backend->set_stroke_alpha(border_color.a); } if (border_width > 0.0F) { - backend->set_line_width(border_width); + line_backend->set_line_width(border_width); } - backend->set_fill_color(bg_color.r, bg_color.g, bg_color.b); - backend->set_stroke_color(border_color.r, border_color.g, border_color.b); + shape_backend->set_fill_color(bg_color.r, bg_color.g, bg_color.b); + line_backend->set_stroke_color(border_color.r, border_color.g, border_color.b); - backend->draw_rectangle(left, bottom, content_width, content_height); + shape_backend->draw_rectangle(left, bottom, content_width, content_height); if (has_fill && has_stroke) { - backend->fill_stroke(); + shape_backend->fill_stroke(); } else if (has_fill) { - backend->fill(); + shape_backend->fill(); } else if (has_stroke) { - backend->stroke(); + shape_backend->stroke(); } - backend->restore_state(); + shape_backend->restore_state(); } const auto &stroke_color = section_node.border_color().toRGB(); const float stroke_width = section_node.border_width(); const bool has_margin_stroke = stroke_width > 0.0F && stroke_color.a > 0.0F; if (has_margin_stroke) { - backend->save_state(); + shape_backend->save_state(); if (stroke_color.a < 1.0F) { - backend->set_stroke_alpha(stroke_color.a); + shape_backend->set_stroke_alpha(stroke_color.a); } - backend->set_line_width(stroke_width); - backend->set_stroke_color(stroke_color.r, stroke_color.g, stroke_color.b); + line_backend->set_line_width(stroke_width); + line_backend->set_stroke_color(stroke_color.r, stroke_color.g, stroke_color.b); if (section_node.margin_left() > 0.0F) { - backend->draw_line(left, top, left, bottom); + line_backend->draw_line(left, top, left, bottom); } if (section_node.margin_right() > 0.0F) { - backend->draw_line(right, top, right, bottom); + line_backend->draw_line(right, top, right, bottom); } if (section_node.margin_top() > 0.0F) { - backend->draw_line(left, top, right, top); + line_backend->draw_line(left, top, right, top); } if (section_node.margin_bottom() > 0.0F) { - backend->draw_line(left, bottom, right, bottom); + line_backend->draw_line(left, bottom, right, bottom); } - backend->restore_state(); + shape_backend->restore_state(); } } void DocraftPDFRenderer::render_image(const model::DocraftImage &image_node) { diff --git a/docraft/src/docraft/renderer/painter/docraft_circle_painter.cc b/docraft/src/docraft/renderer/painter/docraft_circle_painter.cc index c669df5..486ea2d 100644 --- a/docraft/src/docraft/renderer/painter/docraft_circle_painter.cc +++ b/docraft/src/docraft/renderer/painter/docraft_circle_painter.cc @@ -26,8 +26,9 @@ namespace docraft::renderer::painter { void DocraftCirclePainter::draw(const std::shared_ptr &context) { if (!context) return; - auto backend = context->shape_backend(); - if (!backend) return; + auto shape_backend = context->shape_backend(); + auto line_backend = context->line_backend(); + if (!shape_backend || !line_backend) return; const auto &bg_color = circle_node_.background_color().toRGB(); const auto &border_color = circle_node_.border_color().toRGB(); @@ -42,33 +43,33 @@ namespace docraft::renderer::painter { return; } - backend->save_state(); + shape_backend->save_state(); if (bg_color.a < 1.0F || border_color.a < 1.0F) { - backend->set_fill_alpha(bg_color.a); - backend->set_stroke_alpha(border_color.a); + shape_backend->set_fill_alpha(bg_color.a); + shape_backend->set_stroke_alpha(border_color.a); } if (border_width > 0.0F) { - backend->set_line_width(border_width); + line_backend->set_line_width(border_width); } - backend->set_fill_color(bg_color.r, bg_color.g, bg_color.b); - backend->set_stroke_color(border_color.r, border_color.g, border_color.b); + shape_backend->set_fill_color(bg_color.r, bg_color.g, bg_color.b); + line_backend->set_stroke_color(border_color.r, border_color.g, border_color.b); - backend->draw_circle(circle_node_.center().x, circle_node_.center().y, radius); + shape_backend->draw_circle(circle_node_.center().x, circle_node_.center().y, radius); const bool has_fill = bg_color.a > 0.0F; const bool has_stroke = border_width > 0.0F && border_color.a > 0.0F; if (has_fill && has_stroke) { - backend->fill_stroke(); + shape_backend->fill_stroke(); } else if (has_fill) { - backend->fill(); + shape_backend->fill(); } else if (has_stroke) { - backend->stroke(); + shape_backend->stroke(); } - backend->restore_state(); + shape_backend->restore_state(); } } // docraft::renderer::painter diff --git a/docraft/src/docraft/renderer/painter/docraft_line_painter.cc b/docraft/src/docraft/renderer/painter/docraft_line_painter.cc index d4f3393..04cadc1 100644 --- a/docraft/src/docraft/renderer/painter/docraft_line_painter.cc +++ b/docraft/src/docraft/renderer/painter/docraft_line_painter.cc @@ -24,8 +24,9 @@ namespace docraft::renderer::painter { void DocraftLinePainter::draw(const std::shared_ptr &context) { if (!context) return; - auto backend = context->shape_backend(); - if (!backend) return; + auto shape_backend = context->shape_backend(); + auto line_backend = context->line_backend(); + if (!shape_backend || !line_backend) return; const auto &stroke_color = line_node_.border_color().toRGB(); const float stroke_width = line_node_.border_width(); @@ -33,13 +34,13 @@ namespace docraft::renderer::painter { return; } - backend->save_state(); + shape_backend->save_state(); if (stroke_color.a < 1.0F) { - backend->set_stroke_alpha(stroke_color.a); + shape_backend->set_stroke_alpha(stroke_color.a); } - backend->set_line_width(stroke_width); - backend->set_stroke_color(stroke_color.r, stroke_color.g, stroke_color.b); + line_backend->set_line_width(stroke_width); + line_backend->set_stroke_color(stroke_color.r, stroke_color.g, stroke_color.b); const auto &start = line_node_.start(); const auto &end = line_node_.end(); @@ -50,8 +51,8 @@ namespace docraft::renderer::painter { const float x2 = origin.x + end.x; const float y2 = origin.y - end.y; - backend->draw_line(x1, y1, x2, y2); + line_backend->draw_line(x1, y1, x2, y2); - backend->restore_state(); + shape_backend->restore_state(); } } // docraft::renderer::painter diff --git a/docraft/src/docraft/renderer/painter/docraft_polygon_painter.cc b/docraft/src/docraft/renderer/painter/docraft_polygon_painter.cc index d81d97b..33cb306 100644 --- a/docraft/src/docraft/renderer/painter/docraft_polygon_painter.cc +++ b/docraft/src/docraft/renderer/painter/docraft_polygon_painter.cc @@ -24,8 +24,9 @@ namespace docraft::renderer::painter { void DocraftPolygonPainter::draw(const std::shared_ptr &context) { if (!context) return; - auto backend = context->shape_backend(); - if (!backend) return; + auto shape_backend = context->shape_backend(); + auto line_backend = context->line_backend(); + if (!shape_backend || !line_backend) return; const auto &bg_color = polygon_node_.background_color().toRGB(); const auto &border_color = polygon_node_.border_color().toRGB(); @@ -47,33 +48,33 @@ namespace docraft::renderer::painter { transformed.push_back({.x = origin.x + pt.x, .y = origin.y - pt.y}); } - backend->save_state(); + shape_backend->save_state(); if (bg_color.a < 1.0F || border_color.a < 1.0F) { - backend->set_fill_alpha(bg_color.a); - backend->set_stroke_alpha(border_color.a); + shape_backend->set_fill_alpha(bg_color.a); + shape_backend->set_stroke_alpha(border_color.a); } if (border_width > 0.0F) { - backend->set_line_width(border_width); + line_backend->set_line_width(border_width); } - backend->set_fill_color(bg_color.r, bg_color.g, bg_color.b); - backend->set_stroke_color(border_color.r, border_color.g, border_color.b); + shape_backend->set_fill_color(bg_color.r, bg_color.g, bg_color.b); + line_backend->set_stroke_color(border_color.r, border_color.g, border_color.b); - backend->draw_polygon(transformed); + shape_backend->draw_polygon(transformed); const bool has_fill = bg_color.a > 0.0F; const bool has_stroke = border_width > 0.0F && border_color.a > 0.0F; if (has_fill && has_stroke) { - backend->fill_stroke(); + shape_backend->fill_stroke(); } else if (has_fill) { - backend->fill(); + shape_backend->fill(); } else if (has_stroke) { - backend->stroke(); + shape_backend->stroke(); } - backend->restore_state(); + shape_backend->restore_state(); } } // docraft::renderer::painter diff --git a/docraft/src/docraft/renderer/painter/docraft_rectangle_painter.cc b/docraft/src/docraft/renderer/painter/docraft_rectangle_painter.cc index d8fb4ba..3452dbb 100644 --- a/docraft/src/docraft/renderer/painter/docraft_rectangle_painter.cc +++ b/docraft/src/docraft/renderer/painter/docraft_rectangle_painter.cc @@ -26,8 +26,9 @@ namespace docraft::renderer::painter { void DocraftRectanglePainter::draw(const std::shared_ptr &context) { // Validate context and handles early to avoid invalid-document errors if (!context) return; - auto backend = context->shape_backend(); - if (!backend) return; + auto shape_backend = context->shape_backend(); + auto line_backend = context->line_backend(); + if (!shape_backend || !line_backend) return; // const auto& box = rectangle_node_.transform_box(); const auto& bg_color = rectangle_node_.background_color().toRGB(); @@ -40,25 +41,25 @@ namespace docraft::renderer::painter { } // Save graphics state to isolate painter changes - backend->save_state(); + shape_backend->save_state(); // 1. SET GRAPHICS STATE FIRST (Alpha, Line Width) // These MUST be set before starting a path (rectangle starts a path) if (bg_color.a < 1.0F || border_color.a < 1.0F) { - backend->set_fill_alpha(bg_color.a); - backend->set_stroke_alpha(border_color.a); + shape_backend->set_fill_alpha(bg_color.a); + shape_backend->set_stroke_alpha(border_color.a); } if (border_width > 0.0F) { - backend->set_line_width(border_width); + line_backend->set_line_width(border_width); } // 2. SET COLORS - backend->set_fill_color(bg_color.r, bg_color.g, bg_color.b); - backend->set_stroke_color(border_color.r, border_color.g, border_color.b); + shape_backend->set_fill_color(bg_color.r, bg_color.g, bg_color.b); + line_backend->set_stroke_color(border_color.r, border_color.g, border_color.b); // 3. DEFINE AND EXECUTE PATH - backend->draw_rectangle( + shape_backend->draw_rectangle( rectangle_node_.anchors().bottom_left.x, rectangle_node_.anchors().bottom_left.y, rectangle_node_.width(), @@ -69,14 +70,14 @@ namespace docraft::renderer::painter { bool has_stroke = border_width > 0.0F && border_color.a > 0.0F; if (has_fill && has_stroke) { - backend->fill_stroke(); + shape_backend->fill_stroke(); } else if (has_fill) { - backend->fill(); + shape_backend->fill(); } else if (has_stroke) { - backend->stroke(); + shape_backend->stroke(); } // Restore graphics state - backend->restore_state(); + shape_backend->restore_state(); } } // docraft diff --git a/docraft/src/docraft/renderer/painter/docraft_text_painter.cc b/docraft/src/docraft/renderer/painter/docraft_text_painter.cc index 6b760c9..92777c8 100644 --- a/docraft/src/docraft/renderer/painter/docraft_text_painter.cc +++ b/docraft/src/docraft/renderer/painter/docraft_text_painter.cc @@ -95,7 +95,13 @@ namespace docraft::renderer::painter { void DocraftTextPainter:: draw_underline(const std::shared_ptr &context, const std::string &text) { - auto backend = context->text_backend(); + if (!context->text_backend()) { + return; + } + auto line_backend = context->line_backend(); + if (!line_backend) { + return; + } // 1. Draw the text normally first auto result = draw_text(context, text); @@ -106,9 +112,9 @@ namespace docraft::renderer::painter { // 3. Draw the line auto rgb = current_line_->color().toRGB(); - backend->set_stroke_color(rgb.r, rgb.g, rgb.b); - backend->set_line_width(thickness); - backend->draw_line(result.first.first, underline_top, result.second.first, underline_top); + line_backend->set_stroke_color(rgb.r, rgb.g, rgb.b); + line_backend->set_line_width(thickness); + line_backend->draw_line(result.first.first, underline_top, result.second.first, underline_top); } void DocraftTextPainter::draw(const std::shared_ptr &context) { diff --git a/docraft/src/docraft/renderer/painter/docraft_triangle_painter.cc b/docraft/src/docraft/renderer/painter/docraft_triangle_painter.cc index f2e0766..a658471 100644 --- a/docraft/src/docraft/renderer/painter/docraft_triangle_painter.cc +++ b/docraft/src/docraft/renderer/painter/docraft_triangle_painter.cc @@ -24,8 +24,9 @@ namespace docraft::renderer::painter { void DocraftTrianglePainter::draw(const std::shared_ptr &context) { if (!context) return; - auto backend = context->shape_backend(); - if (!backend) return; + auto shape_backend = context->shape_backend(); + auto line_backend = context->line_backend(); + if (!shape_backend || !line_backend) return; const auto &bg_color = triangle_node_.background_color().toRGB(); const auto &border_color = triangle_node_.border_color().toRGB(); @@ -47,33 +48,33 @@ namespace docraft::renderer::painter { transformed.push_back({.x = origin.x + pt.x, .y = origin.y - pt.y}); } - backend->save_state(); + shape_backend->save_state(); if (bg_color.a < 1.0F || border_color.a < 1.0F) { - backend->set_fill_alpha(bg_color.a); - backend->set_stroke_alpha(border_color.a); + shape_backend->set_fill_alpha(bg_color.a); + shape_backend->set_stroke_alpha(border_color.a); } if (border_width > 0.0F) { - backend->set_line_width(border_width); + line_backend->set_line_width(border_width); } - backend->set_fill_color(bg_color.r, bg_color.g, bg_color.b); - backend->set_stroke_color(border_color.r, border_color.g, border_color.b); + shape_backend->set_fill_color(bg_color.r, bg_color.g, bg_color.b); + line_backend->set_stroke_color(border_color.r, border_color.g, border_color.b); - backend->draw_polygon(transformed); + shape_backend->draw_polygon(transformed); const bool has_fill = bg_color.a > 0.0F; const bool has_stroke = border_width > 0.0F && border_color.a > 0.0F; if (has_fill && has_stroke) { - backend->fill_stroke(); + shape_backend->fill_stroke(); } else if (has_fill) { - backend->fill(); + shape_backend->fill(); } else if (has_stroke) { - backend->stroke(); + shape_backend->stroke(); } - backend->restore_state(); + shape_backend->restore_state(); } } // docraft::renderer::painter diff --git a/docraft/test/docraft/backend/docraft_haru_backend_test.cc b/docraft/test/docraft/backend/docraft_haru_backend_test.cc index 2202b24..0409bb7 100644 --- a/docraft/test/docraft/backend/docraft_haru_backend_test.cc +++ b/docraft/test/docraft/backend/docraft_haru_backend_test.cc @@ -20,89 +20,121 @@ class DocraftHaruBackendTest : public ::testing::Test { return *backend_; } + const docraft::backend::IDocraftPageRenderingBackend& page_backend() const { + return *backend_->page_rendering(); + } + + docraft::backend::IDocraftPageRenderingBackend& edit_page_backend() { + return *backend_->edit_page_rendering(); + } + + const docraft::backend::IDocraftTextRenderingBackend& text_backend() const { + return *backend_->text_rendering(); + } + + docraft::backend::IDocraftTextRenderingBackend& edit_text_backend() { + return *backend_->edit_text_rendering(); + } + private: std::unique_ptr backend_; }; +TEST_F(DocraftHaruBackendTest, ExposesStableCapabilityAccessors) { + const auto& const_backend = backend(); + + ASSERT_NE(const_backend.line_rendering(), nullptr); + ASSERT_NE(const_backend.text_rendering(), nullptr); + ASSERT_NE(const_backend.shape_rendering(), nullptr); + ASSERT_NE(const_backend.image_rendering(), nullptr); + ASSERT_NE(const_backend.page_rendering(), nullptr); + + EXPECT_EQ(const_backend.line_rendering(), backend().edit_line_rendering()); + EXPECT_EQ(const_backend.text_rendering(), backend().edit_text_rendering()); + EXPECT_EQ(const_backend.shape_rendering(), backend().edit_shape_rendering()); + EXPECT_EQ(const_backend.image_rendering(), backend().edit_image_rendering()); + EXPECT_EQ(const_backend.page_rendering(), backend().edit_page_rendering()); +} + TEST_F(DocraftHaruBackendTest, StartsWithSinglePageAndValidDimensions) { - EXPECT_EQ(backend().total_page_count(), 1U); - EXPECT_EQ(backend().current_page_number(), 1U); - EXPECT_GT(backend().page_width(), 0.0F); - EXPECT_GT(backend().page_height(), 0.0F); + EXPECT_EQ(page_backend().total_page_count(), 1U); + EXPECT_EQ(page_backend().current_page_number(), 1U); + EXPECT_GT(page_backend().page_width(), 0.0F); + EXPECT_GT(page_backend().page_height(), 0.0F); } TEST_F(DocraftHaruBackendTest, MoveToNextPage) { - backend().add_new_page(); - EXPECT_EQ(backend().total_page_count(), 2U); - EXPECT_EQ(backend().current_page_number(), 2U); - backend().go_to_page(0);// Go back to first page - backend().move_to_next_page(); - EXPECT_EQ(backend().current_page_number(), 2U); + edit_page_backend().add_new_page(); + EXPECT_EQ(page_backend().total_page_count(), 2U); + EXPECT_EQ(page_backend().current_page_number(), 2U); + edit_page_backend().go_to_page(0);// Go back to first page + edit_page_backend().move_to_next_page(); + EXPECT_EQ(page_backend().current_page_number(), 2U); } TEST_F(DocraftHaruBackendTest, AddsAndNavigatesPages) { - backend().add_new_page(); - backend().add_new_page(); + edit_page_backend().add_new_page(); + edit_page_backend().add_new_page(); - EXPECT_EQ(backend().total_page_count(), 3U); - EXPECT_EQ(backend().current_page_number(), 3U); + EXPECT_EQ(page_backend().total_page_count(), 3U); + EXPECT_EQ(page_backend().current_page_number(), 3U); - backend().go_to_page(0); - EXPECT_EQ(backend().current_page_number(), 1U); + edit_page_backend().go_to_page(0); + EXPECT_EQ(page_backend().current_page_number(), 1U); - backend().move_to_next_page(); - EXPECT_EQ(backend().current_page_number(), 2U); + edit_page_backend().move_to_next_page(); + EXPECT_EQ(page_backend().current_page_number(), 2U); - backend().go_to_page(2); - EXPECT_EQ(backend().current_page_number(), 3U); + edit_page_backend().go_to_page(2); + EXPECT_EQ(page_backend().current_page_number(), 3U); } TEST_F(DocraftHaruBackendTest, NavigatesFirstPreviousLastPages) { - backend().add_new_page(); - backend().add_new_page(); + edit_page_backend().add_new_page(); + edit_page_backend().add_new_page(); - backend().go_to_first_page(); - EXPECT_EQ(backend().current_page_number(), 1U); + edit_page_backend().go_to_first_page(); + EXPECT_EQ(page_backend().current_page_number(), 1U); - backend().move_to_next_page(); - EXPECT_EQ(backend().current_page_number(), 2U); + edit_page_backend().move_to_next_page(); + EXPECT_EQ(page_backend().current_page_number(), 2U); - backend().go_to_last_page(); - EXPECT_EQ(backend().current_page_number(), 3U); + edit_page_backend().go_to_last_page(); + EXPECT_EQ(page_backend().current_page_number(), 3U); - backend().go_to_previous_page(); - EXPECT_EQ(backend().current_page_number(), 2U); + edit_page_backend().go_to_previous_page(); + EXPECT_EQ(page_backend().current_page_number(), 2U); } TEST_F(DocraftHaruBackendTest, ThrowsOnPreviousAtFirstPage) { - backend().go_to_first_page(); - EXPECT_THROW(backend().go_to_previous_page(), std::runtime_error); + edit_page_backend().go_to_first_page(); + EXPECT_THROW(edit_page_backend().go_to_previous_page(), std::runtime_error); } TEST_F(DocraftHaruBackendTest, SetsPageFormat) { - EXPECT_NO_THROW(backend().set_page_format(model::DocraftPageSize::kA3, - model::DocraftPageOrientation::kLandscape)); - EXPECT_GT(backend().page_width(), 0.0F); - EXPECT_GT(backend().page_height(), 0.0F); + EXPECT_NO_THROW(edit_page_backend().set_page_format(model::DocraftPageSize::kA3, + model::DocraftPageOrientation::kLandscape)); + EXPECT_GT(page_backend().page_width(), 0.0F); + EXPECT_GT(page_backend().page_height(), 0.0F); } TEST_F(DocraftHaruBackendTest, ThrowsWhenMovingPastLastPage) { - EXPECT_THROW(backend().move_to_next_page(), std::runtime_error); + EXPECT_THROW(edit_page_backend().move_to_next_page(), std::runtime_error); } TEST_F(DocraftHaruBackendTest, ThrowsOnInvalidPageNavigation) { - EXPECT_THROW(backend().go_to_page(1U), std::runtime_error); - EXPECT_THROW(backend().go_to_page(2U), std::runtime_error); + EXPECT_THROW(edit_page_backend().go_to_page(1U), std::runtime_error); + EXPECT_THROW(edit_page_backend().go_to_page(2U), std::runtime_error); } TEST_F(DocraftHaruBackendTest, SupportsBuiltInFontAndTextMeasure) { EXPECT_TRUE(backend().can_use_font("Helvetica", nullptr)); EXPECT_NO_THROW(backend().set_font("Helvetica", 12.0F, nullptr)); - backend().begin_text(); - backend().draw_text("Hello backend", 20.0F, 20.0F); - backend().end_text(); + edit_text_backend().begin_text(); + edit_text_backend().draw_text("Hello backend", 20.0F, 20.0F); + edit_text_backend().end_text(); - EXPECT_GT(backend().measure_text_width("Hello backend"), 0.0F); + EXPECT_GT(text_backend().measure_text_width("Hello backend"), 0.0F); } TEST_F(DocraftHaruBackendTest, ReportsPdfFileExtension) { diff --git a/docraft/test/docraft/utils/docraft_mock_rendering_backend.h b/docraft/test/docraft/utils/docraft_mock_rendering_backend.h index 53d7ca9..4d920b4 100644 --- a/docraft/test/docraft/utils/docraft_mock_rendering_backend.h +++ b/docraft/test/docraft/utils/docraft_mock_rendering_backend.h @@ -7,7 +7,12 @@ #include "docraft/backend/docraft_rendering_backend.h" namespace docraft::test::utils { - class MockRenderingBackend : public backend::IDocraftRenderingBackend { + class MockRenderingBackend : public backend::IDocraftRenderingBackend, + public backend::IDocraftLineRenderingBackend, + public backend::IDocraftShapeRenderingBackend, + public backend::IDocraftTextRenderingBackend, + public backend::IDocraftImageRenderingBackend, + public backend::IDocraftPageRenderingBackend { public: struct Config { float page_width = 100.0F; @@ -25,6 +30,17 @@ namespace docraft::test::utils { current_page_ = 0; } + [[nodiscard]] const backend::IDocraftLineRenderingBackend *line_rendering() const override { return this; } + [[nodiscard]] backend::IDocraftLineRenderingBackend *edit_line_rendering() override { return this; } + [[nodiscard]] const backend::IDocraftTextRenderingBackend *text_rendering() const override { return this; } + [[nodiscard]] backend::IDocraftTextRenderingBackend *edit_text_rendering() override { return this; } + [[nodiscard]] const backend::IDocraftShapeRenderingBackend *shape_rendering() const override { return this; } + [[nodiscard]] backend::IDocraftShapeRenderingBackend *edit_shape_rendering() override { return this; } + [[nodiscard]] const backend::IDocraftImageRenderingBackend *image_rendering() const override { return this; } + [[nodiscard]] backend::IDocraftImageRenderingBackend *edit_image_rendering() override { return this; } + [[nodiscard]] const backend::IDocraftPageRenderingBackend *page_rendering() const override { return this; } + [[nodiscard]] backend::IDocraftPageRenderingBackend *edit_page_rendering() override { return this; } + void begin_text() const override {} void end_text() const override {} void draw_text(const std::string &, float, float) const override {} From b95e349b9f104ca6fb86452b41d4f6c53a687d42 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 21:33:57 +0000 Subject: [PATCH 3/3] refactor: finalize backend capability composition --- .../docraft/backend/pdf/docraft_haru_backend.cc | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/docraft/src/docraft/backend/pdf/docraft_haru_backend.cc b/docraft/src/docraft/backend/pdf/docraft_haru_backend.cc index b6de1e8..27651b7 100644 --- a/docraft/src/docraft/backend/pdf/docraft_haru_backend.cc +++ b/docraft/src/docraft/backend/pdf/docraft_haru_backend.cc @@ -238,6 +238,14 @@ namespace docraft::backend::pdf { const HPDF_STATUS status = HPDF_SetInfoDateAttr(pdf, type, to_hpdf_date(*value)); throw_if_hpdf_error(status, "Failed to set PDF metadata '" + field_name + "'"); } + + HPDF_Doc create_hpdf_document() { + HPDF_Doc pdf = HPDF_New(error_handler, nullptr); + if (!pdf) { + throw std::runtime_error("Failed to initialize Haru PDF document"); + } + return pdf; + } } // namespace class DocraftHaruBackend::TextHaruBackend : public docraft::backend::IDocraftTextRenderingBackend { @@ -566,11 +574,7 @@ namespace docraft::backend::pdf { shape_backend_(std::make_unique(*this)), image_backend_(std::make_unique(*this)), page_backend_(std::make_unique(*this)), - pdf_(nullptr) { - pdf_ = HPDF_New(error_handler, nullptr); - if (!pdf_) { - throw std::runtime_error("Failed to initialize Haru PDF document"); - } + pdf_(create_hpdf_document()) { HPDF_UseUTFEncodings(pdf_); HPDF_SetCurrentEncoder(pdf_, "UTF-8"); HPDF_SetCompressionMode(pdf_, HPDF_COMP_ALL);