diff --git a/.github/agents/docraft.agent.md b/.github/agents/docraft.agent.md new file mode 100644 index 0000000..095c89c --- /dev/null +++ b/.github/agents/docraft.agent.md @@ -0,0 +1,15 @@ +--- +description: >- + Docraft - C++ declarative PDF generation library. Context for working with the + codebase. +tools: ['insert_edit_into_file', 'replace_string_in_file', 'create_file', 'apply_patch', 'get_terminal_output', 'open_file', 'run_in_terminal', 'get_errors', 'list_dir', 'read_file', 'file_search', 'grep_search', 'validate_cves', 'run_subagent', 'semantic_search'] +--- + +# Docraft Agent Context + +**Docraft** is a C++ library for generating PDF documents declaratively using the **Craft Language** (XML markup). +Use this document to understand the architecture, structure, and workflow for contributing or working with the codebase. + +--- + +See `doc/contributors/components/` for deep dives. \ No newline at end of file diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..1bd2643 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,274 @@ +## Quick Facts + +- **Type**: C++ Library (header-only or compiled) +- **Main API**: `DocraftDocument`, `DocraftCraftLanguageParser` +- **Backend**: libharu (PDF generation) +- **Language**: XML-based markup (Craft Language) +- **Templating**: Variable substitution + Foreach loops via JSON +- **Output**: Single-file PDF with metadata support + +--- + +## Architecture Overview + +``` +XML (Craft Language) + ↓ +Parser (pugixml) + ↓ +DOM AST (DocraftNode tree) + ↓ +Template Engine (substitute ${var}, expand Foreach) + ↓ +Layout Engine (compute positions, pagination) + ↓ +Renderer (visitor pattern → painter calls) + ↓ +Capability Provider Interfaces + ↓ +PDF Implementation (Haru) + ↓ +PDF Output +``` + +**Key insight**: Each layer is loosely coupled via interfaces. Only Parser and Document are tightly bound. + +--- + +## Folder Structure + +``` +docraft/ +├── include/docraft/ +│ ├── docraft_document.h ← Main API +│ ├── docraft_document_context.h ← Shared render state +│ ├── backend/ +│ │ ├── docraft_rendering_backend.h ← Capability provider interfaces +│ │ ├── docraft_*_rendering_backend.h ← Capability interfaces +│ │ └── pdf/ +│ │ ├── docraft_haru_backend.h ← PDF impl +│ │ └── docraft_haru_*.h ← Inner classes +│ ├── layout/ +│ │ ├── docraft_layout_engine.h +│ │ └── handler/ ← Chain of Responsibility +│ ├── renderer/ ← Visitor pattern +│ ├── craft/ ← Parser +│ ├── model/ ← Node types +│ ├── templating/ ← Variable + Foreach +│ └── utils/ ← Font, keyword, logger +├── src/docraft/ ← Implementations +└── test/docraft/ ← Unit tests +``` + +--- + +## Core Components + +### 1. Document & Context + +- **DocraftDocument**: Entry point, orchestrates pipeline +- **DocraftDocumentContext**: Shared render state +- ⚠️ Service Locator anti-pattern (20+ getters) + +### 2. Parser + +- **DocraftCraftLanguageParser**: XML → typed AST +- Uses **pugixml** +- Case-sensitive tags +- Unknown tags = parse error + +### 3. Backend Interface + +Capability provider contracts are split by domain: + +- Line, Text, Shape, Image, Page rendering +- Output, Font, Metadata backends +- Implementation: `DocraftHaruBackend` (libharu) + +### 4. Layout Engine + +- Computes x, y, width, height +- Handles automatic page breaking +- **Chain of Responsibility** pattern +- Specialized handlers per node type + +### 5. Renderer + +- **Visitor pattern** on AST +- **Painters** call backend primitives +- Node knows WHAT, Painter knows HOW + +### 6. Model / DOM + +- **DocraftNode**: Base class +- **50+ node types**: Text, Table, List, Shape, etc. +- Each has: parser, layout handler, draw method + +### 7. Template Engine + +- `${variable}` substitution +- `` expansion +- Runs before layout +- No-op if not set + +--- + +## Data Flow + +``` +Parser → DocraftDocument → render() + → configure_settings() + → template_document() + → layout() + → render() + → save_to_file() +``` + +--- + +## Common Tasks + +### Add a node type + +1. Model: `model/docraft_my_node.h` +2. Parser: Register tag +3. Handler: Layout logic +4. Renderer: Painter calls +5. Tests: All subsystems + +### Fix a bug + +1. Locate node type +2. Breakpoint in layout handler +3. Breakpoint in painter +4. Breakpoint in backend + +### Add backend capability + +1. Interface: `backend/docraft_*.h` +2. Extend: capability provider interfaces (Rendering/Resource/Lifecycle) +3. Implement: Haru backend +4. Add to: `DocraftHaruBackend` +5. Use: In painters + +--- + +## Critical Issues + +**10 architectural issues in `.local/ARCHITETTURA_CRITICITA.md`:** + +1. **SRP** - historical aggregated facade removed; capability providers are split by domain +2. **Circular deps** - PageHaruBackend ↔ DocraftHaruBackend +3. **Ownership** - State scattered (pdf_, cursor, colors) +4. **Interfaces** - Missing coordinate, color space abstractions +5. **Mock** - Doesn't reflect reality +6. **Accessors** - nullptr not compile-time safe +7. **Service Locator** - 20+ getters in Context +8. **Fallback** - No unsupported capability strategy +9. **Conceptual** - Font/Metadata/Output mixed with rendering +10. **Versioning** - No interface evolution support + +Severity: 🔴 High (1,2,5,7) | 🟠 Medium (3,4,8,9) | 🟡 Low (6,10) + +--- + +## Templating + +```xml + +Invoice ${invoice_number} + + + +${data("name")} x ${data("qty")} + + + + +``` + +--- + +## Node Attributes + +**Common**: `name`, `x`, `y`, `width`, `height`, `padding`, `weight`, `z_index`, `position`, `visible` + +**Text**: `font_name`, `font_size`, `style`, `color`, `alignment`, `underline` + +**Colors**: `#RRGGBB`, named colors (`red`, `blue`, etc.) + +--- + +## File Organization + +- Headers: `include/docraft/` (parallel `src/`) +- Implementations: `src/docraft/` (`.cpp`) +- Tests: `test/docraft/` (by subsystem) +- No circular includes +- Forward declarations for pointer types + +--- + +## Testing + +- Unit tests by subsystem +- Mock backend: `test/docraft/utils/` +- Parser: schema validation +- Layout: positioning +- Renderer: painter calls +- Backend: Haru-specific + +--- + +## Craft Language + +**Tags**: Document, Settings, Metadata, Header, Body, Footer, Text, Table, List, Image, Shape, Layout, Foreach + +**Rules**: + +- `` and `` required +- Unknown tags = error +- Invalid enums = error +- Text can't contain Text + +--- + +## Dependencies + +| Library | Purpose | License | +|---------------|------------|---------| +| libharu | PDF gen | ZLIB | +| pugixml | XML parse | MIT | +| nlohmann-json | JSON parse | MIT | +| GoogleTest | Testing | BSD | + +**Build**: CMake ≥ 3.16, C++17, macOS/Linux/Windows + +--- + +## Commands + +```bash +# Build +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build -j + +# Test +ctest --test-dir build -C Release --output-on-failure + +# Render +./build/artifacts/bin/docraft_tool input.craft output.pdf + +# Docs +cd doc && make html +``` + +--- + +## Next Steps + +1. **Read `.local/ARCHITETTURA_CRITICITA.md`** - Architectural analysis +2. **Read `.local/PROGETTO_CONTESTO.md`** - Deep context +3. **Pick a task** from Common Tasks above +4. **Write tests** for features +5. **Run full build + tests** before PR diff --git a/.gitignore b/.gitignore index 0c3e52d..1c09d18 100644 --- a/.gitignore +++ b/.gitignore @@ -63,4 +63,7 @@ Thumbs.db .dockerignore #CMake presets -CMakeUserPresets.json \ No newline at end of file +CMakeUserPresets.json + +# Local documentation and analysis (for Copilot/internal use) +.local/ 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/project-doc/contributors/components/layout-and-pagination.md b/doc/project-doc/contributors/components/layout-and-pagination.md index d0ad18b..8a56c0c 100644 --- a/doc/project-doc/contributors/components/layout-and-pagination.md +++ b/doc/project-doc/contributors/components/layout-and-pagination.md @@ -2,17 +2,49 @@ Layout computes physical geometry and page assignment for every visible node. -## 1. Core architecture +## 1. Engine file structure (PImpl) + +The layout engine is intentionally split across files: + +- Public facade: `docraft/src/docraft/layout/docraft_layout_engine.cc` +- Private implementation declaration: `docraft/src/docraft/layout/docraft_layout_engine_impl.h` +- Private implementation definition: `docraft/src/docraft/layout/docraft_layout_engine_impl.cc` +- Public API: `docraft/include/docraft/layout/docraft_layout_engine.h` + +The facade only forwards calls to `DocraftLayoutEngine::Impl`. +All layout internals (handlers, section planning, pagination, node traversal) live in `Impl`. + +## 2. Core architecture Main pieces: -- `DocraftLayoutEngine`: orchestration + section planning. -- `DocraftCursor`: current flow position and direction stack. +- `DocraftLayoutEngine`: stable public entry point. +- `DocraftLayoutEngine::Impl`: full orchestration logic. +- `DocraftCursor`: flow position and direction stack. - Handler chain for node-specific computations. Handlers currently include dedicated logic for text, lists, tables, blank lines, layouts, and generic nodes. -## 2. Section-based layout strategy +## 3. Execution flow + +At a high level: + +1. `compute_document_layout(...)` splits nodes into Header/Body/Footer. +2. A `SectionPlan` is computed from navigation ratios and section visibility. +3. Header is laid out first (if visible). +4. Body is laid out with pagination rules. +5. Footer is laid out last (if visible). + +For single-node layout: + +1. `compute_layout(node, cursor)` checks visibility. +2. It selects flow vs absolute positioning mode. +3. It configures local cursor scope for text/list/rect containers. +4. Child nodes are recursively laid out. +5. The handler chain computes the current node box. +6. Cursor advances using horizontal/vertical spacing rules. + +## 4. Section strategy and ratios Documents are laid out as section blocks: @@ -20,11 +52,10 @@ Documents are laid out as section blocks: - Body - Footer -The engine computes a section plan from configured ratios and visibility. - -If header/footer are hidden or absent, their ratio is reassigned to body. +If header/footer are hidden or absent, their ratio is re-assigned to body. +This keeps total vertical allocation stable for each page. -## 3. Cursor and flow model +## 5. Cursor and flow model `DocraftCursor` tracks: @@ -34,10 +65,13 @@ If header/footer are hidden or absent, their ratio is reassigned to body. Nodes can be: -- `block`: participate in flow; +- `block`: participate in cursor flow, - `absolute`: positioned independently from flow cursor. -## 4. Pagination behavior +Inside horizontal layouts, children receive width slices based on `weight`. +If weight is unspecified (`-1`), equal weights are assigned before layout. + +## 6. Pagination behavior Body layout includes pagination rules: @@ -45,19 +79,29 @@ Body layout includes pagination rules: - overflowing non-absolute nodes are moved/re-laid out on next page, - tables can be split across pages when partial row ranges fit. +Table overflow handling: + +1. Count how many rows still fit in body bounds. +2. Split table at that row using `split_after_row(...)`. +3. Reinsert the remainder into body children after the current table. +4. Assign page ownership for both fragments. + After pagination decisions, `page_owner` is assigned recursively to node subtrees. -## 5. Why handlers are chained +## 7. Why handlers are chained The chain-of-responsibility pattern keeps layout logic modular. -Adding a new node type usually means adding a dedicated handler and placing it at the correct precedence point in the chain. +Adding a new node type usually means: -Handler order matters because first match wins. +1. implement a dedicated layout handler, +2. register it in `Impl::configure_handlers()` with correct priority, +3. verify precedence (first match wins), +4. add tests for layout + pagination interactions. -## 6. Practical contributor notes +## 8. Practical contributor notes -- Preserve layout determinism: avoid random or order-sensitive side effects. -- Keep text measurement backend-driven (`measure_text_width`) for consistency. -- Be explicit with section bounds and cursor resets when adding pagination behavior. -- Cover overflow + page owner edge cases in tests. +- Keep text measurement backend-driven for deterministic widths. +- Preserve stable ordering in recursive layout to avoid visual regressions. +- Be explicit with cursor resets when changing pagination rules. +- Cover overflow and `page_owner` propagation in tests. diff --git a/doc/source/api/backend.rst b/doc/source/api/backend.rst index eef66b9..dd1d211 100644 --- a/doc/source/api/backend.rst +++ b/doc/source/api/backend.rst @@ -1,21 +1,47 @@ Rendering Backend ================= -The backend layer provides abstract interfaces for drawing primitives (text, -shapes, images, pages) and a concrete implementation using libharu for PDF -output. +The backend layer defines the low-level contracts used by Docraft to render documents. It is organized around small capability interfaces for drawing text, +shapes, lines, images, and pages, plus provider interfaces that group related capabilities. -IDocraftRenderingBackend ------------------------- +Concrete backends, such as the libharu-based PDF backend, implement these contracts and expose them through capability providers. -Aggregated interface inheriting all sub-backend interfaces. +Capability Providers +-------------------- -.. doxygenclass:: docraft::backend::IDocraftRenderingBackend +Capability providers group backend functionality by responsibility. They allow Docraft services to request only the capabilities they need while keeping the individual rendering interfaces independent and focused. + +IDocraftRenderingCapabilityProvider +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Provides access to rendering capabilities for lines, text, shapes, images, and pages. + +.. doxygenclass:: docraft::backend::IDocraftRenderingCapabilityProvider + :project: docraft + :members: + +IDocraftResourceCapabilityProvider +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Provides access to resource-related capabilities, such as font handling. + +.. doxygenclass:: docraft::backend::IDocraftResourceCapabilityProvider + :project: docraft + :members: + +IDocraftLifecycleCapabilityProvider +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Provides access to document lifecycle capabilities, such as output persistence and metadata handling. + +.. doxygenclass:: docraft::backend::IDocraftLifecycleCapabilityProvider :project: docraft :members: -Sub-backend Interfaces ----------------------- +Rendering Capability Interfaces +------------------------------- + +Rendering capability interfaces are standalone and chain-free. Each interface covers one drawing responsibility. IDocraftTextRenderingBackend ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -52,15 +78,59 @@ IDocraftPageRenderingBackend :project: docraft :members: +Resource and Lifecycle Interfaces +--------------------------------- + +These interfaces define supporting backend responsibilities used by document rendering and export. + +IDocraftFontBackend +^^^^^^^^^^^^^^^^^^^ + +.. doxygenclass:: docraft::backend::IDocraftFontBackend + :project: docraft + :members: + +IDocraftOutputBackend +^^^^^^^^^^^^^^^^^^^^^ + +.. doxygenclass:: docraft::backend::IDocraftOutputBackend + :project: docraft + :members: + +IDocraftMetadataBackend +^^^^^^^^^^^^^^^^^^^^^^^ + +.. doxygenclass:: docraft::backend::IDocraftMetadataBackend + :project: docraft + :members: + +Provider Factories +------------------ + +Factories create the provider set used by the rendering service. + +DocraftCapabilityProviders +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. doxygenstruct:: docraft::backend::DocraftCapabilityProviders + :project: docraft + :members: + +IDocraftCapabilityProvidersFactory +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. doxygenclass:: docraft::backend::IDocraftCapabilityProvidersFactory + :project: docraft + :members: + Concrete Backends ----------------- DocraftHaruBackend ^^^^^^^^^^^^^^^^^^ -PDF backend implementation using libharu. +PDF backend implementation using libharu. It composes capability-focused internal objects and exposes them through the backend provider model. .. doxygenclass:: docraft::backend::pdf::DocraftHaruBackend :project: docraft :members: - diff --git a/doc/source/api/exceptions.rst b/doc/source/api/exceptions.rst new file mode 100644 index 0000000..657c9cd --- /dev/null +++ b/doc/source/api/exceptions.rst @@ -0,0 +1,184 @@ +Exceptions +========== + +Docraft provides a domain-based exception system under ``docraft::exception``. + +The exception model is designed to: + +- keep error types aligned with functional domains, +- avoid direct use of ``std::runtime_error`` and similar standard exception types, +- make API and test expectations explicit and stable. + +Base Exception +-------------- + +All Docraft exceptions derive from ``DocraftException``. + +.. doxygenclass:: docraft::exception::DocraftException + :project: docraft + :members: + +Domain Overview +--------------- + +- **Configuration**: invalid command-line/configuration state. +- **FileSystem**: missing or unreadable files. +- **DataFormat**: invalid JSON/XML/data structures. +- **Input**: invalid values provided by caller or parsed attributes. +- **Template**: template variables and template image data errors. +- **Layout**: layout pipeline failures and configuration issues. +- **Document**: document/model lifecycle and state errors. +- **Backend**: backend capability/page/runtime state errors. +- **Rendering**: rendering-specific failures. +- **Implementation**: intentionally unimplemented features. + +Domain Classes +-------------- + +Configuration +~~~~~~~~~~~~~ + +.. doxygenclass:: docraft::exception::ConfigurationException + :project: docraft + :members: + +.. doxygenclass:: docraft::exception::MissingArgumentException + :project: docraft + :members: + +FileSystem +~~~~~~~~~~ + +.. doxygenclass:: docraft::exception::FileSystemException + :project: docraft + :members: + +.. doxygenclass:: docraft::exception::FileNotFoundException + :project: docraft + :members: + +.. doxygenclass:: docraft::exception::CannotOpenFileException + :project: docraft + :members: + +DataFormat +~~~~~~~~~~ + +.. doxygenclass:: docraft::exception::DataFormatException + :project: docraft + :members: + +.. doxygenclass:: docraft::exception::InvalidJSONException + :project: docraft + :members: + +Input +~~~~~ + +.. doxygenclass:: docraft::exception::InvalidInputException + :project: docraft + :members: + +Template +~~~~~~~~ + +.. doxygenclass:: docraft::exception::TemplateException + :project: docraft + :members: + +.. doxygenclass:: docraft::exception::TemplateVariableExistsException + :project: docraft + :members: + +.. doxygenclass:: docraft::exception::TemplateVariableNotFoundException + :project: docraft + :members: + +.. doxygenclass:: docraft::exception::TemplateImageDataException + :project: docraft + :members: + +Layout +~~~~~~ + +.. doxygenclass:: docraft::exception::LayoutException + :project: docraft + :members: + +.. doxygenclass:: docraft::exception::LayoutConfigurationException + :project: docraft + :members: + +Document +~~~~~~~~ + +.. doxygenclass:: docraft::exception::DocumentException + :project: docraft + :members: + +.. doxygenclass:: docraft::exception::DocumentStateException + :project: docraft + :members: + +.. doxygenclass:: docraft::exception::ModelException + :project: docraft + :members: + +Backend +~~~~~~~ + +.. doxygenclass:: docraft::exception::BackendStateException + :project: docraft + :members: + +.. doxygenclass:: docraft::exception::CapabilityUnavailableException + :project: docraft + :members: + +.. doxygenclass:: docraft::exception::PageStateException + :project: docraft + :members: + +Rendering +~~~~~~~~~ + +.. doxygenclass:: docraft::exception::RenderingException + :project: docraft + :members: + +.. doxygenclass:: docraft::exception::RenderingFailedException + :project: docraft + :members: + +Implementation +~~~~~~~~~~~~~~ + +.. doxygenclass:: docraft::exception::NotImplementedException + :project: docraft + :members: + +Usage Example +------------- + +.. code-block:: cpp + + #include "docraft/exception/docraft_exception.h" + + void use_template(const docraft::templating::DocraftTemplateEngine& engine) { + try { + auto value = engine.find_template_variable("invoice_number"); + (void)value; + } catch (const docraft::exception::TemplateVariableNotFoundException& ex) { + // Handle only missing-variable errors + } catch (const docraft::exception::DocraftException& ex) { + // Fallback for all Docraft domain exceptions + } + } + +Headers +------- + +- ``docraft/exception/docraft_exception.h``: compatibility include for the full exception set. +- ``docraft/exception/docraft_exceptions.h``: master include for all exception domains. +- ``docraft/exception/docraft_*_exceptions.h``: domain-specific headers. + diff --git a/doc/source/api/index.rst b/doc/source/api/index.rst index c92a901..1a4cc40 100644 --- a/doc/source/api/index.rst +++ b/doc/source/api/index.rst @@ -11,6 +11,7 @@ Explore the library's modules below to understand how to build, layout, and rend document context + exceptions model_nodes model_shapes model_containers @@ -43,6 +44,13 @@ Core Components Runtime context and cursor management — tracks the current rendering position, page boundaries, and layout state. + .. grid-item-card:: ⚠️ Exceptions + :link: exceptions + :link-type: doc + + Domain-driven exception hierarchy used across parser, layout, rendering, + backend, and templating subsystems. + Document Model -------------- diff --git a/doc/source/api/layout.rst b/doc/source/api/layout.rst index e10f930..4355483 100644 --- a/doc/source/api/layout.rst +++ b/doc/source/api/layout.rst @@ -5,6 +5,42 @@ The layout engine computes position and size for every node in the document tree. It uses a chain-of-responsibility pattern: specialized handlers claim specific node types, while a fallback handler covers generic nodes. +How It Works +------------ + +The public class ``DocraftLayoutEngine`` is a facade over an internal +``DocraftLayoutEngine::Impl``. The split keeps the public header stable while +hiding implementation details (handlers, pagination internals, section plan, +helper state). + +Execution model: + +1. ``compute_document_layout(...)`` splits top-level sections (Header/Body/Footer). +2. A section plan is computed from navigation ratios and section visibility. +3. Header is laid out (if visible), then Body, then Footer. +4. Body layout applies pagination rules and assigns ``page_owner`` recursively. + +Node-level model: + +- ``compute_layout(node, cursor)`` evaluates visibility and positioning mode. +- Flow nodes advance the cursor; absolute nodes are laid out independently. +- Container nodes recursively layout children, then handlers compute final box. +- Cursor spacing is adjusted with fixed horizontal/vertical spacing rules. + +Pagination model in body: + +- ``NewPage`` forces page advance. +- Overflowing non-absolute nodes are re-laid on a new page. +- Tables are split when only a subset of rows fits in remaining body space. + +Implementation files +-------------------- + +- Public API: ``docraft/include/docraft/layout/docraft_layout_engine.h`` +- Facade: ``docraft/src/docraft/layout/docraft_layout_engine.cc`` +- Private impl declaration: ``docraft/src/docraft/layout/docraft_layout_engine_impl.h`` +- Private impl definition: ``docraft/src/docraft/layout/docraft_layout_engine_impl.cc`` + DocraftLayoutEngine ------------------- @@ -76,4 +112,3 @@ Advances the cursor for blank-line spacing. .. doxygenclass:: docraft::layout::handler::DocraftLayoutBlankLine :project: docraft :members: - diff --git a/docraft/CMakeLists.txt b/docraft/CMakeLists.txt index 92f2a34..4615c3b 100644 --- a/docraft/CMakeLists.txt +++ b/docraft/CMakeLists.txt @@ -22,6 +22,14 @@ set(DOCRAFT_SOURCES include/docraft/docraft_document_metadata.h src/docraft/docraft_document_context.cc include/docraft/docraft_document_context.h + src/docraft/services/docraft_rendering_service.cc + include/docraft/services/docraft_rendering_service.h + src/docraft/services/docraft_layout_service.cc + include/docraft/services/docraft_layout_service.h + src/docraft/services/docraft_typography_service.cc + include/docraft/services/docraft_typography_service.h + src/docraft/services/docraft_navigation_service.cc + include/docraft/services/docraft_navigation_service.h src/docraft/docraft_cursor.cc include/docraft/docraft_cursor.h src/docraft/docraft_color.cc @@ -33,6 +41,8 @@ set(DOCRAFT_SOURCES include/docraft/renderer/painter/docraft_text_painter.h include/docraft/renderer/painter/i_painter.h src/docraft/layout/docraft_layout_engine.cc + src/docraft/layout/docraft_layout_engine_impl.cc + src/docraft/layout/docraft_layout_engine_impl.h include/docraft/layout/docraft_layout_engine.h src/docraft/layout/handler/docraft_layout_text_handler.cc include/docraft/layout/handler/docraft_layout_text_handler.h @@ -60,7 +70,11 @@ set(DOCRAFT_SOURCES include/docraft/backend/docraft_shape_rendering_backend.h include/docraft/backend/docraft_image_rendering_backend.h include/docraft/backend/docraft_page_rendering_backend.h + include/docraft/backend/docraft_backend_providers_factory.h include/docraft/backend/docraft_rendering_backend.h + include/docraft/backend/docraft_output_backend.h + include/docraft/backend/docraft_font_backend.h + include/docraft/backend/docraft_metadata_backend.h src/docraft/model/docraft_image.cc include/docraft/model/docraft_image.h src/docraft/renderer/painter/docraft_image_painter.cc @@ -75,6 +89,10 @@ set(DOCRAFT_SOURCES include/docraft/renderer/painter/docraft_table_painter.h src/docraft/layout/handler/docraft_layout_table_handler.cc include/docraft/layout/handler/docraft_layout_table_handler.h + src/docraft/layout/handler/docraft_layout_horizontal_table_handler.cc + include/docraft/layout/handler/docraft_layout_horizontal_table_handler.h + src/docraft/layout/handler/docraft_layout_vertical_table_handler.cc + include/docraft/layout/handler/docraft_layout_vertical_table_handler.h include/docraft/utils/docraft_font_registry.h src/docraft/model/docraft_rectangle.cc include/docraft/model/docraft_rectangle.h @@ -143,6 +161,25 @@ set(DOCRAFT_SOURCES src/docraft/model/docraft_position.cc src/docraft/backend/pdf/docraft_haru_backend.cc include/docraft/backend/pdf/docraft_haru_backend.h + src/docraft/backend/pdf/docraft_haru_backend_providers_factory.cc + include/docraft/backend/pdf/docraft_haru_backend_providers_factory.h + src/docraft/backend/pdf/docraft_haru_output_backend.cc + include/docraft/backend/pdf/docraft_haru_output_backend.h + src/docraft/backend/pdf/docraft_haru_font_backend.cc + include/docraft/backend/pdf/docraft_haru_font_backend.h + src/docraft/backend/pdf/docraft_haru_metadata_backend.cc + include/docraft/backend/pdf/docraft_haru_metadata_backend.h + src/docraft/backend/pdf/docraft_haru_text_backend.cc + include/docraft/backend/pdf/docraft_haru_text_backend.h + src/docraft/backend/pdf/docraft_haru_line_backend.cc + include/docraft/backend/pdf/docraft_haru_line_backend.h + src/docraft/backend/pdf/docraft_haru_shape_backend.cc + include/docraft/backend/pdf/docraft_haru_shape_backend.h + src/docraft/backend/pdf/docraft_haru_image_backend.cc + include/docraft/backend/pdf/docraft_haru_image_backend.h + src/docraft/backend/pdf/docraft_haru_page_backend.cc + include/docraft/backend/pdf/docraft_haru_page_backend.h + include/docraft/backend/pdf/docraft_haru_shared_state.h src/docraft/model/docraft_settings.cc include/docraft/model/docraft_settings.h include/docraft/model/docraft_page_format.h @@ -163,7 +200,8 @@ set(DOCRAFT_SOURCES ) set(DOCRAFT_LIBRARY_TYPE STATIC) if (BUILD_SHARED_LIBS) - set(DOCRAFT_LIBRARY_TYPE SHARED) + set(DOCRAFT_LIBRARY_TYPE SHARED + include/docraft/exception/docraft_exceptions.h) endif () add_library(docraft ${DOCRAFT_LIBRARY_TYPE} ${DOCRAFT_SOURCES} diff --git a/docraft/include/docraft/backend/docraft_backend_providers_factory.h b/docraft/include/docraft/backend/docraft_backend_providers_factory.h new file mode 100644 index 0000000..6223ef9 --- /dev/null +++ b/docraft/include/docraft/backend/docraft_backend_providers_factory.h @@ -0,0 +1,42 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 "docraft/backend/docraft_rendering_backend.h" +#include "docraft/docraft_lib.h" + +namespace docraft::backend { + struct DOCRAFT_LIB DocraftCapabilityProviders { + std::shared_ptr rendering_provider; + std::shared_ptr resource_provider; + std::shared_ptr lifecycle_provider; + }; + + class DOCRAFT_LIB IDocraftCapabilityProvidersFactory { + public: + virtual ~IDocraftCapabilityProvidersFactory() = default; + + [[nodiscard]] virtual DocraftCapabilityProviders create_capability_providers() const = 0; + }; + + // Backward-compatibility aliases for existing integrations. + using DocraftBackendProviders = DocraftCapabilityProviders; + using IDocraftBackendProvidersFactory = IDocraftCapabilityProvidersFactory; +} // namespace docraft::backend + diff --git a/docraft/include/docraft/backend/docraft_font_backend.h b/docraft/include/docraft/backend/docraft_font_backend.h new file mode 100644 index 0000000..c7b2cfc --- /dev/null +++ b/docraft/include/docraft/backend/docraft_font_backend.h @@ -0,0 +1,56 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 "docraft/docraft_lib.h" + +#include + +namespace docraft::backend { + /** + * @brief Capability interface for backend font registration and selection. + */ + class DOCRAFT_LIB IDocraftFontBackend { + public: + virtual ~IDocraftFontBackend() = default; + + /** + * @brief Registers a TTF font and returns the internal name. + * @param path Font file path. + * @param embed Whether to embed the font in the document. + * @return Backend internal font name. + */ + virtual const char *register_ttf_font_from_file(const std::string &path, bool embed) const = 0; + + /** + * @brief Checks whether a font can be used with the given encoder. + * @param internal_name Backend internal font name. + * @param encoder Backend encoder name. + * @return true if the font can be used. + */ + virtual bool can_use_font(const std::string &internal_name, const char *encoder) const = 0; + + /** + * @brief Sets the current font and size. + * @param internal_name Backend internal font name. + * @param size Font size in points. + * @param encoder Backend encoder name. + */ + virtual void set_font(const std::string &internal_name, float size, const char *encoder) const = 0; + }; +} // namespace docraft::backend + diff --git a/docraft/include/docraft/backend/docraft_image_rendering_backend.h b/docraft/include/docraft/backend/docraft_image_rendering_backend.h index 9006d1b..c7b4cd0 100644 --- a/docraft/include/docraft/backend/docraft_image_rendering_backend.h +++ b/docraft/include/docraft/backend/docraft_image_rendering_backend.h @@ -30,60 +30,66 @@ namespace docraft::backend { * @brief Virtual destructor. */ virtual ~IDocraftImageRenderingBackend() = default; + /** * @brief Draws a PNG image from file. */ virtual void draw_png_image( - const std::string& path, + const std::string &path, float x, float y, float width, float height) const = 0; + /** * @brief Draws a PNG image from memory. */ virtual void draw_png_image_from_memory( - const unsigned char* data, + const unsigned char *data, std::size_t size, float x, float y, float width, float height) const = 0; + /** * @brief Draws a JPEG image from file. */ virtual void draw_jpeg_image( - const std::string& path, + const std::string &path, float x, float y, float width, float height) const = 0; + /** * @brief Draws a JPEG image from memory. */ virtual void draw_jpeg_image_from_memory( - const unsigned char* data, + const unsigned char *data, std::size_t size, float x, float y, float width, float height) const = 0; + /** * @brief Draws a raw RGB image from file. */ virtual void draw_raw_rgb_image( - const std::string& path, + const std::string &path, int pixel_width, int pixel_height, float x, float y, float width, float height) const = 0; + /** * @brief Draws a raw RGB image from memory. */ virtual void draw_raw_rgb_image_from_memory( - const unsigned char* data, + const unsigned char *data, int pixel_width, int pixel_height, float x, diff --git a/docraft/include/docraft/backend/docraft_line_rendering_backend.h b/docraft/include/docraft/backend/docraft_line_rendering_backend.h index 96173b3..acdb71e 100644 --- a/docraft/include/docraft/backend/docraft_line_rendering_backend.h +++ b/docraft/include/docraft/backend/docraft_line_rendering_backend.h @@ -28,6 +28,7 @@ namespace docraft::backend { * @brief Virtual destructor. */ virtual ~IDocraftLineRenderingBackend() = default; + /** * @brief Sets the stroke color used for subsequent line drawing. * @param r Red component in [0,1]. @@ -35,11 +36,13 @@ namespace docraft::backend { * @param b Blue component in [0,1]. */ virtual void set_stroke_color(float r, float g, float b) const = 0; + /** * @brief Sets the line width used for subsequent line drawing. * @param thickness Line width in points. */ virtual void set_line_width(float thickness) const = 0; + /** * @brief Draws a line between two points using the current stroke settings. * @param x1 The x-coordinate of the line start. diff --git a/docraft/include/docraft/backend/docraft_metadata_backend.h b/docraft/include/docraft/backend/docraft_metadata_backend.h new file mode 100644 index 0000000..ba9b99e --- /dev/null +++ b/docraft/include/docraft/backend/docraft_metadata_backend.h @@ -0,0 +1,40 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 "docraft/docraft_lib.h" + +namespace docraft { + class DocraftDocumentMetadata; +} + +namespace docraft::backend { + /** + * @brief Capability interface for document metadata support. + */ + class DOCRAFT_LIB IDocraftMetadataBackend { + public: + virtual ~IDocraftMetadataBackend() = default; + + /** + * @brief Applies document metadata to the backend document. + * @param metadata Metadata values to apply. + */ + virtual void set_document_metadata(const DocraftDocumentMetadata &metadata) = 0; + }; +} // namespace docraft::backend + diff --git a/docraft/include/docraft/backend/docraft_output_backend.h b/docraft/include/docraft/backend/docraft_output_backend.h new file mode 100644 index 0000000..eaaf8bc --- /dev/null +++ b/docraft/include/docraft/backend/docraft_output_backend.h @@ -0,0 +1,44 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 "docraft/docraft_lib.h" + +#include + +namespace docraft::backend { + /** + * @brief Capability interface for document output operations. + */ + class DOCRAFT_LIB IDocraftOutputBackend { + public: + virtual ~IDocraftOutputBackend() = default; + + /** + * @brief Saves the document to a file path. + * @param path Output file path. + */ + virtual void save_to_file(const std::string &path) const = 0; + + /** + * @brief Returns the preferred file extension for this backend. + * @return Extension with or without leading dot (e.g. ".pdf" or "pdf"). + */ + [[nodiscard]] virtual std::string file_extension() const = 0; + }; +} // namespace docraft::backend + diff --git a/docraft/include/docraft/backend/docraft_page_rendering_backend.h b/docraft/include/docraft/backend/docraft_page_rendering_backend.h index 90962e3..5138f54 100644 --- a/docraft/include/docraft/backend/docraft_page_rendering_backend.h +++ b/docraft/include/docraft/backend/docraft_page_rendering_backend.h @@ -31,41 +31,50 @@ namespace docraft::backend { * @brief Virtual destructor. */ virtual ~IDocraftPageRenderingBackend() = default; + /** * @brief Returns the current page width in points. * @return Page width in points. */ virtual float page_width() const = 0; + /** * @brief Returns the current page height in points. * @return Page height in points. */ virtual float page_height() const = 0; + /** * @brief Adds a new page to the document. */ virtual void add_new_page() = 0; + /** * @brief Moves the cursor to the next page. */ virtual void move_to_next_page() = 0; + /** * @brief Navigates to a specific page (0-based index). * @param page_number Destination page index. */ virtual void go_to_page(std::size_t page_number) = 0; + /** * @brief Navigates to the first page. */ virtual void go_to_first_page() = 0; + /** * @brief Navigates to the previous page. */ virtual void go_to_previous_page() = 0; + /** * @brief Navigates to the last page. */ virtual void go_to_last_page() = 0; + /** * @brief Sets the page size and orientation. * @param size Page size. @@ -73,11 +82,13 @@ namespace docraft::backend { */ virtual void set_page_format(model::DocraftPageSize size, model::DocraftPageOrientation orientation) = 0; + /** * @brief Returns the current page number. * @return Current page number. */ virtual std::size_t current_page_number() const = 0; + /** * @brief Returns the total number of pages in the document. * @return Total page count. diff --git a/docraft/include/docraft/backend/docraft_pdf_backend.h b/docraft/include/docraft/backend/docraft_pdf_backend.h index 66f38fb..07c8600 100644 --- a/docraft/include/docraft/backend/docraft_pdf_backend.h +++ b/docraft/include/docraft/backend/docraft_pdf_backend.h @@ -20,22 +20,22 @@ #include namespace docraft::backend { - /** + /** * This is the interface for the PDF backend of Docraft. */ - class DOCRAFT_LIB IDocraftPDFBackend { - public: - /** + class DOCRAFT_LIB IDocraftPDFBackend { + public: + /** * @brief Measures the width of text using the current font state. * @param text Text string. * @return Text width in points. */ - virtual float measure_text(const std::string& text)=0; - /** + virtual float measure_text(const std::string &text) =0; + + /** * @brief Draws text using the current font state. * @param text Text string. */ - virtual void draw_text(const std::string& text)=0; - - }; + virtual void draw_text(const std::string &text) =0; + }; } // docraft diff --git a/docraft/include/docraft/backend/docraft_rendering_backend.h b/docraft/include/docraft/backend/docraft_rendering_backend.h index 87ae6e4..b22cd53 100644 --- a/docraft/include/docraft/backend/docraft_rendering_backend.h +++ b/docraft/include/docraft/backend/docraft_rendering_backend.h @@ -17,65 +17,71 @@ #pragma once #include "docraft/docraft_lib.h" -#include +#include "docraft/backend/docraft_font_backend.h" #include "docraft/backend/docraft_image_rendering_backend.h" +#include "docraft/backend/docraft_line_rendering_backend.h" +#include "docraft/backend/docraft_metadata_backend.h" +#include "docraft/backend/docraft_output_backend.h" #include "docraft/backend/docraft_page_rendering_backend.h" #include "docraft/backend/docraft_shape_rendering_backend.h" #include "docraft/backend/docraft_text_rendering_backend.h" -namespace docraft { - class DocraftDocumentMetadata; -} - namespace docraft::backend { /** - * @brief Aggregated rendering backend interface. + * @brief Rendering capability contract (geometry, text, images, pages). + */ + class DOCRAFT_LIB IDocraftRenderingCapabilityProvider { + public: + virtual ~IDocraftRenderingCapabilityProvider() = default; + + [[nodiscard]] virtual const IDocraftLineRenderingBackend *line_rendering() const = 0; + + [[nodiscard]] virtual IDocraftLineRenderingBackend *edit_line_rendering() = 0; + + [[nodiscard]] virtual const IDocraftTextRenderingBackend *text_rendering() const = 0; + + [[nodiscard]] virtual IDocraftTextRenderingBackend *edit_text_rendering() = 0; + + [[nodiscard]] virtual const IDocraftShapeRenderingBackend *shape_rendering() const = 0; + + [[nodiscard]] virtual IDocraftShapeRenderingBackend *edit_shape_rendering() = 0; + + [[nodiscard]] virtual const IDocraftImageRenderingBackend *image_rendering() const = 0; + + [[nodiscard]] virtual IDocraftImageRenderingBackend *edit_image_rendering() = 0; + + [[nodiscard]] virtual const IDocraftPageRenderingBackend *page_rendering() const = 0; + + [[nodiscard]] virtual IDocraftPageRenderingBackend *edit_page_rendering() = 0; + }; + + /** + * @brief Resource capability contract (fonts and related resources). + */ + class DOCRAFT_LIB IDocraftResourceCapabilityProvider { + public: + virtual ~IDocraftResourceCapabilityProvider() = default; + + [[nodiscard]] virtual const IDocraftFontBackend *font_backend() const = 0; + + [[nodiscard]] virtual IDocraftFontBackend *edit_font_backend() = 0; + }; + + /** + * @brief Document lifecycle capability contract (metadata and persistence). */ - class DOCRAFT_LIB IDocraftRenderingBackend : public IDocraftTextRenderingBackend, - public IDocraftShapeRenderingBackend, - public IDocraftImageRenderingBackend, - public IDocraftPageRenderingBackend { + class DOCRAFT_LIB IDocraftLifecycleCapabilityProvider { public: - /** - * @brief Virtual destructor. - */ - ~IDocraftRenderingBackend() override = default; - /** - * @brief Saves the document to a file path. - * @param path Output file path. - */ - virtual void save_to_file(const std::string& path) const = 0; - /** - * @brief Returns the preferred file extension for this backend. - * @return Extension with or without leading dot (e.g. ".pdf" or "pdf"). - */ - [[nodiscard]] virtual std::string file_extension() const = 0; - /** - * @brief Registers a TTF font and returns the internal name. - * @param path Font file path. - * @param embed Whether to embed the font in the document. - * @return Backend internal font name. - */ - virtual const char* register_ttf_font_from_file(const std::string& path, bool embed) const = 0; - /** - * @brief Checks whether a font can be used with the given encoder. - * @param internal_name Backend internal font name. - * @param encoder Backend encoder name. - * @return true if the font can be used. - */ - virtual bool can_use_font(const std::string& internal_name, const char* encoder) const = 0; - /** - * @brief Sets the current font and size. - * @param internal_name Backend internal font name. - * @param size Font size in points. - * @param encoder Backend encoder name. - */ - virtual void set_font(const std::string& internal_name, float size, const char* encoder) const = 0; - /** - * @brief Applies document metadata to the backend document. - * @param metadata Metadata values to apply. - */ - virtual void set_document_metadata(const DocraftDocumentMetadata &metadata) = 0; + virtual ~IDocraftLifecycleCapabilityProvider() = default; + + [[nodiscard]] virtual const IDocraftOutputBackend *output_backend() const = 0; + + [[nodiscard]] virtual IDocraftOutputBackend *edit_output_backend() = 0; + + [[nodiscard]] virtual const IDocraftMetadataBackend *metadata_backend() const = 0; + + [[nodiscard]] virtual IDocraftMetadataBackend *edit_metadata_backend() = 0; }; + } // docraft::backend 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..fa9802a 100644 --- a/docraft/include/docraft/backend/docraft_text_rendering_backend.h +++ b/docraft/include/docraft/backend/docraft_text_rendering_backend.h @@ -19,33 +19,35 @@ #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. */ virtual void begin_text() const = 0; + /** * @brief Finalizes the text rendering context. Must be called after all text drawing operations are completed. */ virtual void end_text() const = 0; + /** * @brief Draws the specified text at the given coordinates on the PDF page. * @param text The text to be drawn. * @param x The x-coordinate where the text should start. * @param y The y-coordinate where the text should start. */ - virtual void draw_text(const std::string& text, float x, float y) const = 0; + virtual void draw_text(const std::string &text, float x, float y) const = 0; + /** * @brief Sets the fill color used for subsequent text drawing. * @param r Red component in [0,1]. @@ -53,6 +55,7 @@ namespace docraft::backend { * @param b Blue component in [0,1]. */ virtual void set_text_color(float r, float g, float b) const = 0; + /** * @brief Draws the specified text using a custom transformation matrix. * @param text The text to be drawn. @@ -64,18 +67,19 @@ namespace docraft::backend { * @param translate_y The vertical translation (movement) in points. */ virtual void draw_text_matrix( - const std::string& text, + const std::string &text, float scale_x, float skew_x, float skew_y, float scale_y, float translate_x, float translate_y) const = 0; + /** * @brief Measures the width of the specified text using the current font settings. * @param text The text to be measured. * @return The width of the text in points. */ - virtual float measure_text_width(const std::string& text) const = 0; + virtual float measure_text_width(const std::string &text) const = 0; }; } // docraft::backend diff --git a/docraft/include/docraft/backend/pdf/docraft_haru_backend.h b/docraft/include/docraft/backend/pdf/docraft_haru_backend.h index 5dcd9a3..7bbca60 100644 --- a/docraft/include/docraft/backend/pdf/docraft_haru_backend.h +++ b/docraft/include/docraft/backend/pdf/docraft_haru_backend.h @@ -18,277 +18,85 @@ #include "docraft/docraft_lib.h" #include -#include -#include -#include +#include +#include "docraft/backend/docraft_font_backend.h" +#include "docraft/backend/docraft_metadata_backend.h" +#include "docraft/backend/docraft_output_backend.h" #include "docraft/backend/docraft_rendering_backend.h" -#include "docraft/model/docraft_position.h" +#include "docraft/backend/pdf/docraft_haru_shared_state.h" namespace docraft::backend::pdf { + class DocraftHaruTextBackend; + class DocraftHaruLineBackend; + class DocraftHaruShapeBackend; + class DocraftHaruImageBackend; + class DocraftHaruPageBackend; + class DocraftHaruOutputBackend; + class DocraftHaruFontBackend; + class DocraftHaruMetadataBackend; + /** * @brief This class is responsible for managing the Haru PDF document and providing an interface for rendering operations. */ - class DOCRAFT_LIB DocraftHaruBackend : public docraft::backend::IDocraftRenderingBackend { + class DOCRAFT_LIB DocraftHaruBackend : public docraft::backend::IDocraftRenderingCapabilityProvider, + public docraft::backend::IDocraftResourceCapabilityProvider, + public docraft::backend::IDocraftLifecycleCapabilityProvider { public: /** * @brief Creates a Haru PDF backend with a new document and page. */ DocraftHaruBackend(); + /** * @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 endregion -#pragma region backend lifecycle +#pragma region capabilities + [[nodiscard]] const docraft::backend::IDocraftLineRenderingBackend *line_rendering() const override; - void save_to_file(const std::string& path) const override; - [[nodiscard]] std::string file_extension() const override; - /** - * @brief Registers a TTF font and returns the internal name. - */ - const char* register_ttf_font_from_file(const std::string& path, bool embed) const override; - /** - * @brief Returns whether the backend can use a font with the given encoder. - */ - bool can_use_font(const std::string& internal_name, const char* encoder) const override; - /** - * @brief Sets the current font and size. - */ - void set_font(const std::string& internal_name, float size, const char* encoder) const override; - /** - * @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; + [[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; + + [[nodiscard]] const docraft::backend::IDocraftOutputBackend *output_backend() const override; + + [[nodiscard]] docraft::backend::IDocraftOutputBackend *edit_output_backend() override; + + [[nodiscard]] const docraft::backend::IDocraftFontBackend *font_backend() const override; + + [[nodiscard]] docraft::backend::IDocraftFontBackend *edit_font_backend() override; + + [[nodiscard]] const docraft::backend::IDocraftMetadataBackend *metadata_backend() const override; + + [[nodiscard]] docraft::backend::IDocraftMetadataBackend *edit_metadata_backend() override; #pragma endregion + + [[nodiscard]] HPDF_Doc pdf_document() const; + private: - /** - * @brief Creates a new page and adds it to the document. - */ - void create_new_page(); - /** - * @brief Returns the current page index (0-based) for internal use. - */ - size_t internal_current_page_index() const; - /** - * @brief Applies the current page format to a page handle. - */ - void apply_page_format(HPDF_Page page) const; - HPDF_PageSizes page_size_ = HPDF_PAGE_SIZE_A4; - HPDF_PageDirection page_direction_ = HPDF_PAGE_PORTRAIT; - /** - * @brief Applies the current alpha state to the Haru graphics state. - */ - void apply_alpha_state() const; - HPDF_Doc pdf_; - std::vector pages_; - size_t current_page_number_ = 0; - mutable float fill_alpha_ = 1.0F; - mutable float stroke_alpha_ = 1.0F; + std::shared_ptr state_; + std::unique_ptr output_backend_; + std::unique_ptr font_backend_; + std::unique_ptr metadata_backend_; + 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_; }; } // docraft diff --git a/docraft/include/docraft/backend/pdf/docraft_haru_backend_providers_factory.h b/docraft/include/docraft/backend/pdf/docraft_haru_backend_providers_factory.h new file mode 100644 index 0000000..37a1e6a --- /dev/null +++ b/docraft/include/docraft/backend/pdf/docraft_haru_backend_providers_factory.h @@ -0,0 +1,30 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 "docraft/backend/docraft_backend_providers_factory.h" + +namespace docraft::backend::pdf { + class DOCRAFT_LIB DocraftHaruCapabilityProvidersFactory : public backend::IDocraftCapabilityProvidersFactory { + public: + [[nodiscard]] backend::DocraftCapabilityProviders create_capability_providers() const override; + }; + + // Backward-compatibility alias. + using DocraftHaruBackendProvidersFactory = DocraftHaruCapabilityProvidersFactory; +} // namespace docraft::backend::pdf + diff --git a/docraft/include/docraft/backend/pdf/docraft_haru_font_backend.h b/docraft/include/docraft/backend/pdf/docraft_haru_font_backend.h new file mode 100644 index 0000000..bf4988a --- /dev/null +++ b/docraft/include/docraft/backend/pdf/docraft_haru_font_backend.h @@ -0,0 +1,45 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 "docraft/backend/docraft_font_backend.h" +#include "docraft/backend/pdf/docraft_haru_shared_state.h" + +#include + +namespace docraft::backend::pdf { + /** + * @brief Haru implementation of font operations. + * + * LIFETIME: The shared state must have a registered page operations provider. + * This is guaranteed if the device is used through DocraftHaruBackend. + */ + class DocraftHaruFontBackend : public docraft::backend::IDocraftFontBackend { + public: + explicit DocraftHaruFontBackend(const std::shared_ptr &state); + + const char *register_ttf_font_from_file(const std::string &path, bool embed) const override; + + bool can_use_font(const std::string &internal_name, const char *encoder) const override; + + void set_font(const std::string &internal_name, float size, const char *encoder) const override; + + private: + std::shared_ptr state_; + }; +} // namespace docraft::backend::pdf + diff --git a/docraft/include/docraft/backend/pdf/docraft_haru_image_backend.h b/docraft/include/docraft/backend/pdf/docraft_haru_image_backend.h new file mode 100644 index 0000000..83c07d2 --- /dev/null +++ b/docraft/include/docraft/backend/pdf/docraft_haru_image_backend.h @@ -0,0 +1,102 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 "docraft/backend/docraft_image_rendering_backend.h" +#include "docraft/backend/pdf/docraft_haru_shared_state.h" + +#include + +namespace docraft::backend::pdf { + /** + * @brief Haru implementation of image rendering operations. + */ + class DocraftHaruImageBackend : public docraft::backend::IDocraftImageRenderingBackend { + public: + /** + * @brief Creates an image backend bound to a Haru document owner. + * + * LIFETIME: The shared state must have a registered page operations provider. + * This is guaranteed if the device is used through DocraftHaruBackend. + */ + explicit DocraftHaruImageBackend(const std::shared_ptr &state); + + /** + * @brief Loads and draws a PNG image from a file path. + */ + void draw_png_image(const std::string &path, + float x, + float y, + float width, + float height) const override; + + /** + * @brief Loads and draws a PNG image from an in-memory buffer. + */ + 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 Loads and draws a JPEG image from a file path. + */ + void draw_jpeg_image(const std::string &path, + float x, + float y, + float width, + float height) const override; + + /** + * @brief Loads and draws a JPEG image from an in-memory buffer. + */ + 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 Loads and draws a raw RGB image from a 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 Loads and draws a raw RGB image from an in-memory buffer. + */ + 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; + + private: + std::shared_ptr state_; + }; +} // namespace docraft::backend::pdf + diff --git a/docraft/include/docraft/backend/pdf/docraft_haru_line_backend.h b/docraft/include/docraft/backend/pdf/docraft_haru_line_backend.h new file mode 100644 index 0000000..40a7a12 --- /dev/null +++ b/docraft/include/docraft/backend/pdf/docraft_haru_line_backend.h @@ -0,0 +1,57 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 "docraft/backend/docraft_line_rendering_backend.h" +#include "docraft/backend/pdf/docraft_haru_shared_state.h" + +#include + +namespace docraft::backend::pdf { + /** + * @brief Haru implementation of line rendering operations. + */ + class DocraftHaruLineBackend : public docraft::backend::IDocraftLineRenderingBackend { + public: + /** + * @brief Creates a line backend bound to a Haru document owner. + * + * LIFETIME: The shared state must have a registered page operations provider. + * This is guaranteed if the device is used through DocraftHaruBackend. + */ + explicit DocraftHaruLineBackend(const std::shared_ptr &state); + + /** + * @brief Sets the stroke color used for line drawing. + */ + void set_stroke_color(float r, float g, float b) const override; + + /** + * @brief Sets the line width used for line drawing. + */ + void set_line_width(float thickness) const override; + + /** + * @brief Draws a line segment between two points. + */ + void draw_line(float x1, float y1, float x2, float y2) const override; + + private: + std::shared_ptr state_; + }; +} // namespace docraft::backend::pdf + diff --git a/docraft/include/docraft/backend/pdf/docraft_haru_metadata_backend.h b/docraft/include/docraft/backend/pdf/docraft_haru_metadata_backend.h new file mode 100644 index 0000000..976e9d1 --- /dev/null +++ b/docraft/include/docraft/backend/pdf/docraft_haru_metadata_backend.h @@ -0,0 +1,38 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 "docraft/backend/docraft_metadata_backend.h" +#include "docraft/backend/pdf/docraft_haru_shared_state.h" + +#include + +namespace docraft::backend::pdf { + /** + * @brief Haru implementation of metadata operations. + */ + class DocraftHaruMetadataBackend : public docraft::backend::IDocraftMetadataBackend { + public: + explicit DocraftHaruMetadataBackend(const std::shared_ptr &state); + + void set_document_metadata(const DocraftDocumentMetadata &metadata) override; + + private: + std::shared_ptr state_; + }; +} // namespace docraft::backend::pdf + diff --git a/docraft/include/docraft/backend/pdf/docraft_haru_output_backend.h b/docraft/include/docraft/backend/pdf/docraft_haru_output_backend.h new file mode 100644 index 0000000..3f3f66b --- /dev/null +++ b/docraft/include/docraft/backend/pdf/docraft_haru_output_backend.h @@ -0,0 +1,40 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 "docraft/backend/docraft_output_backend.h" +#include "docraft/backend/pdf/docraft_haru_shared_state.h" + +#include + +namespace docraft::backend::pdf { + /** + * @brief Haru implementation of output operations. + */ + class DocraftHaruOutputBackend : public docraft::backend::IDocraftOutputBackend { + public: + explicit DocraftHaruOutputBackend(const std::shared_ptr &state); + + void save_to_file(const std::string &path) const override; + + [[nodiscard]] std::string file_extension() const override; + + private: + std::shared_ptr state_; + }; +} // namespace docraft::backend::pdf + diff --git a/docraft/include/docraft/backend/pdf/docraft_haru_page_backend.h b/docraft/include/docraft/backend/pdf/docraft_haru_page_backend.h new file mode 100644 index 0000000..17880ac --- /dev/null +++ b/docraft/include/docraft/backend/pdf/docraft_haru_page_backend.h @@ -0,0 +1,127 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 "docraft/backend/docraft_page_rendering_backend.h" +#include "docraft/backend/pdf/docraft_haru_shared_state.h" + +#include +#include + +namespace docraft::backend::pdf { + /** + * @brief Haru implementation of page management operations. + * + * Implements both IDocraftPageRenderingBackend (for public page operations) + * and IPageOperationsProvider (for internal capability backend access). + * + * Page state is centralized in DocraftHaruSharedState (pages_, current_page_index_, + * page_size_, page_direction_) to avoid state distribution. + * + * LIFETIME: Must register itself as the page operations provider with the + * shared state immediately upon construction, and be destroyed only after + * all capability backends that depend on it are destroyed. + */ + class DocraftHaruPageBackend : public docraft::backend::IDocraftPageRenderingBackend, + public IPageOperationsProvider { + public: + /** + * @brief Creates a page backend bound to a Haru document owner. + * + * Automatically registers itself as the page operations provider. + * Uses state's centralized page_state for all page management. + */ + explicit DocraftHaruPageBackend(const std::shared_ptr &state); + + /** + * @brief Unregisters itself from shared state if still the active provider. + */ + ~DocraftHaruPageBackend() override; + + /** + * @brief Returns the width of the current page. + */ + float page_width() const override; + + /** + * @brief Returns the height of the current page. + */ + float page_height() const override; + + /** + * @brief Appends a new page and moves the cursor to it. + */ + void add_new_page() override; + + /** + * @brief Moves to the next page if available. + */ + void move_to_next_page() override; + + /** + * @brief Moves to the page identified by zero-based index. + */ + void go_to_page(std::size_t page_number) override; + + /** + * @brief Moves to the first page. + */ + void go_to_first_page() override; + + /** + * @brief Moves to the previous page. + */ + void go_to_previous_page() override; + + /** + * @brief Moves to the last page. + */ + void go_to_last_page() override; + + /** + * @brief Applies the page size and orientation to all existing pages. + */ + void set_page_format(model::DocraftPageSize size, + model::DocraftPageOrientation orientation) override; + + /** + * @brief Returns the current page number (1-based). + */ + 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; + + /** + * @brief Returns the current page handle for internal capability backends. + */ + [[nodiscard]] HPDF_Page current_page() const override; + + /** + * @brief Returns the current page index (0-based) for internal capability backends. + */ + [[nodiscard]] std::size_t current_page_index() const override; + + private: + void apply_page_format(HPDF_Page page) const; + + std::shared_ptr state_; + }; +} // namespace docraft::backend::pdf + diff --git a/docraft/include/docraft/backend/pdf/docraft_haru_shape_backend.h b/docraft/include/docraft/backend/pdf/docraft_haru_shape_backend.h new file mode 100644 index 0000000..fc7f83d --- /dev/null +++ b/docraft/include/docraft/backend/pdf/docraft_haru_shape_backend.h @@ -0,0 +1,108 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 "docraft/backend/docraft_shape_rendering_backend.h" +#include "docraft/backend/pdf/docraft_haru_shared_state.h" + +#include + +namespace docraft::backend::pdf { + /** + * @brief Haru implementation of shape rendering operations. + * + * Implements IDocraftShapeRenderingBackend for shape drawing capabilities. + * + * Graphics state (fill_alpha, stroke_alpha) is centralized in DocraftHaruSharedState + * to avoid state distribution across backends. + * + * LIFETIME: The shared state must have a registered page operations provider. + * This is guaranteed if the device is used through DocraftHaruBackend. + */ + class DocraftHaruShapeBackend : public docraft::backend::IDocraftShapeRenderingBackend { + public: + /** + * @brief Creates a shape backend bound to a Haru document owner. + * + * LIFETIME: The shared state must have a registered page operations provider. + * This is guaranteed if the device is used through DocraftHaruBackend. + * Uses state's centralized graphics_state for alpha management. + */ + explicit DocraftHaruShapeBackend(const std::shared_ptr &state); + + /** + * @brief Saves the current graphics state. + */ + void save_state() const override; + + /** + * @brief Restores the previously saved graphics state. + */ + void restore_state() const override; + + /** + * @brief Sets the fill color used for shape painting. + */ + void set_fill_color(float r, float g, float b) const override; + + /** + * @brief Sets the fill alpha used for subsequent shape operations. + */ + void set_fill_alpha(float alpha) const override; + + /** + * @brief Sets the stroke alpha used for subsequent shape operations. + */ + void set_stroke_alpha(float alpha) const override; + + /** + * @brief Appends a rectangle path to the current page path. + */ + void draw_rectangle(float x, float y, float width, float height) const override; + + /** + * @brief Appends a circle path to the current page path. + */ + void draw_circle(float center_x, float center_y, float radius) const override; + + /** + * @brief Appends a closed polygon path from the provided points. + */ + 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 in one operation. + */ + void fill_stroke() const override; + + private: + void apply_alpha_state() const; + + std::shared_ptr state_; + }; +} // namespace docraft::backend::pdf + diff --git a/docraft/include/docraft/backend/pdf/docraft_haru_shared_state.h b/docraft/include/docraft/backend/pdf/docraft_haru_shared_state.h new file mode 100644 index 0000000..0a567b1 --- /dev/null +++ b/docraft/include/docraft/backend/pdf/docraft_haru_shared_state.h @@ -0,0 +1,217 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 + +#include "docraft/exception/docraft_exceptions.h" + +namespace docraft::backend::pdf { + /** + * @brief Observer interface for page operations. + * + * This interface allows capability backends to access page operations without + * maintaining direct raw pointers to DocraftHaruPageBackend. The lifetime contract + * is: the provider MUST remain valid for the entire lifetime of the shared state + * that holds it. This is guaranteed by keeping the provider registration and + * shared state lifecycle in sync within DocraftHaruBackend. + */ + class IPageOperationsProvider { + public: + virtual ~IPageOperationsProvider() = default; + + /** + * @brief Returns the current page handle. + * @note Must not be nullptr while provider is registered. + */ + virtual HPDF_Page current_page() const = 0; + + /** + * @brief Returns the width of the current page in points. + */ + virtual float page_width() const = 0; + + /** + * @brief Returns the height of the current page in points. + */ + virtual float page_height() const = 0; + + /** + * @brief Returns the current page index (0-based). + */ + virtual std::size_t current_page_index() const = 0; + }; + + /** + * @brief Centralized graphics state (color, alpha). + * + * Prevents state distribution across multiple backends by centralizing + * all graphics state in one place. Shape and other backends query/modify + * this state through the shared state. + */ + struct GraphicsState { + float fill_alpha = 1.0F; + float stroke_alpha = 1.0F; + // Future: fill_color, stroke_color can be added here + }; + + /** + * @brief Centralized page state (pages, current page index, format). + * + * Centralizes all page-related state that was previously scattered + * between DocraftHaruPageBackend and other backends. + */ + struct PageState { + std::vector pages; + std::size_t current_page_index = 0; + HPDF_PageSizes page_size = HPDF_PAGE_SIZE_A4; + HPDF_PageDirection page_direction = HPDF_PAGE_PORTRAIT; + }; + + /** + * @brief Shared Haru document state used by capability backends. + * + * LIFETIME & OWNERSHIP CONTRACT: + * - `pdf`: Owned by this struct (created/destroyed by DocraftHaruBackend) + * - `page_operations_provider`: Non-owning observer, registered during init + * - `page_state`: Owned by this struct (all page management centralized) + * - `graphics_state`: Owned by this struct (all graphics state centralized) + * + * INVARIANTS: + * 1. page_operations_provider MUST be set before any operation uses it + * 2. page_state must be valid after initialization + * 3. graphics_state must be valid after initialization + * 4. All state changes go through this struct's methods for consistency + */ + struct DocraftHaruSharedState { + // PDF document owner - lifetime managed by DocraftHaruBackend + HPDF_Doc pdf = nullptr; + + // Centralized page state (was scattered in DocraftHaruPageBackend) + PageState page_state; + + // Centralized graphics state (was scattered in DocraftHaruShapeBackend) + GraphicsState graphics_state; + + // Non-owning observer for page operations + IPageOperationsProvider *page_operations_provider = nullptr; + + /** + * @brief Registers the page operations provider. + * + * CONTRACT: Must be called exactly once during initialization, + * before any capability backend accesses page operations. + */ + void set_page_operations_provider(IPageOperationsProvider *provider) { + page_operations_provider = provider; + } + + /** + * @brief Clears the page operations provider. + * + * CONTRACT: Must be called exactly once during destruction, + * after all capability backends are destroyed. + */ + void clear_page_operations_provider() { + page_operations_provider = nullptr; + } + + /** + * @brief Ensures the provider is available. + * + * @throws docraft::exception::BackendStateException if provider is nullptr. + */ + IPageOperationsProvider *ensure_page_provider() const { + if (!page_operations_provider) { + throw docraft::exception::BackendStateException( + "Page operations provider not set. " + "Capability backend accessed before initialization." + ); + } + return page_operations_provider; + } + + [[nodiscard]] float fill_alpha() const { + return graphics_state.fill_alpha; + } + + [[nodiscard]] float &edit_fill_alpha() { + return graphics_state.fill_alpha; + } + + [[nodiscard]] float stroke_alpha() const { + return graphics_state.stroke_alpha; + } + + [[nodiscard]] float &edit_stroke_alpha() { + return graphics_state.stroke_alpha; + } + + [[nodiscard]] HPDF_PageSizes page_size() const { + return page_state.page_size; + } + + [[nodiscard]] HPDF_PageSizes &edit_page_size() { + return page_state.page_size; + } + + [[nodiscard]] HPDF_PageDirection page_direction() const { + return page_state.page_direction; + } + + [[nodiscard]] HPDF_PageDirection &edit_page_direction() { + return page_state.page_direction; + } + + /** + * @brief Adds a page to the internal pages vector and returns it. + */ + HPDF_Page add_page(HPDF_Page page) { + page_state.pages.push_back(page); + return page; + } + + /** + * @brief Returns the total number of pages. + */ + std::size_t page_count() const { + return page_state.pages.size(); + } + + /** + * @brief Returns the current page index (0-based). + */ + [[nodiscard]] std::size_t current_page_index() const { + return page_state.current_page_index; + } + + [[nodiscard]] std::size_t &edit_current_page_index() { + return page_state.current_page_index; + } + + [[nodiscard]] std::vector &edit_pages() { + return page_state.pages; + } + + [[nodiscard]] const std::vector &pages() const { + return page_state.pages; + } + }; +} // namespace docraft::backend::pdf + diff --git a/docraft/include/docraft/backend/pdf/docraft_haru_text_backend.h b/docraft/include/docraft/backend/pdf/docraft_haru_text_backend.h new file mode 100644 index 0000000..fbf5b0e --- /dev/null +++ b/docraft/include/docraft/backend/pdf/docraft_haru_text_backend.h @@ -0,0 +1,78 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 "docraft/backend/docraft_text_rendering_backend.h" +#include "docraft/backend/pdf/docraft_haru_shared_state.h" + +#include + +namespace docraft::backend::pdf { + /** + * @brief Haru implementation of text rendering operations. + */ + class DocraftHaruTextBackend : public docraft::backend::IDocraftTextRenderingBackend { + public: + /** + * @brief Creates a text backend bound to a Haru document owner. + * + * LIFETIME: The shared state must have a registered page operations provider. + * This is guaranteed if the device is used through DocraftHaruBackend. + */ + explicit DocraftHaruTextBackend(const std::shared_ptr &state); + + /** + * @brief Begins a text object on the current page. + */ + void begin_text() const override; + + /** + * @brief Ends the current text object on the current page. + */ + void end_text() const override; + + /** + * @brief Draws plain text at the given page coordinates. + */ + void draw_text(const std::string &text, float x, float y) const override; + + /** + * @brief Sets the text fill color for subsequent text drawing. + */ + void set_text_color(float r, float g, float b) const override; + + /** + * @brief Draws text using an explicit text 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 the width of a text string using current font settings. + */ + float measure_text_width(const std::string &text) const override; + + private: + std::shared_ptr state_; + }; +} // namespace docraft::backend::pdf + diff --git a/docraft/include/docraft/craft/parser/docraft_parser_helpers.h b/docraft/include/docraft/craft/parser/docraft_parser_helpers.h index edec663..2bc76d4 100644 --- a/docraft/include/docraft/craft/parser/docraft_parser_helpers.h +++ b/docraft/include/docraft/craft/parser/docraft_parser_helpers.h @@ -38,7 +38,7 @@ namespace docraft::craft::parser::detail { * Supports hex colors (e.g., \#RRGGBB or \#RRGGBBAA) and named colors (e.g., "red", "blue"). * @param color_attr XML attribute containing the color value. * @return A DocraftColor object representing the parsed color. - * @throws std::invalid_argument if the color string is not in a valid format or is an unsupported named color. + * @throws docraft::exception::InvalidInputException if the color string is not in a valid format or is an unsupported named color. */ DocraftColor get_docraft_color(const pugi::xml_attribute &color_attr); diff --git a/docraft/include/docraft/docraft_backend_cache.h b/docraft/include/docraft/docraft_backend_cache.h index aae71c8..4dbd9a4 100644 --- a/docraft/include/docraft/docraft_backend_cache.h +++ b/docraft/include/docraft/docraft_backend_cache.h @@ -20,7 +20,7 @@ #include namespace docraft::backend { - class IDocraftRenderingBackend; + class IDocraftRenderingCapabilityProvider; class IDocraftLineRenderingBackend; class IDocraftShapeRenderingBackend; class IDocraftTextRenderingBackend; @@ -37,11 +37,8 @@ namespace docraft { */ class DOCRAFT_LIB DocraftBackendCache { public: - /** - * @brief Initializes the backend cache from a main rendering backend. - * @param backend The main rendering backend. - */ - void initialize_from_backend(const std::shared_ptr &backend); + void initialize_from_provider( + const std::shared_ptr &rendering_provider); /** * @brief Returns the line backend (cached). @@ -88,9 +85,9 @@ namespace docraft { /** * @brief Refreshes all cached backend interfaces (called internally). - * @param backend The main rendering backend. + * @param rendering_provider Rendering capability provider. */ - void refresh_caches(const std::shared_ptr &backend); + void refresh_caches(const std::shared_ptr &rendering_provider); std::shared_ptr line_backend_; std::shared_ptr shape_backend_; diff --git a/docraft/include/docraft/docraft_document.h b/docraft/include/docraft/docraft_document.h index 51ee473..33663f1 100644 --- a/docraft/include/docraft/docraft_document.h +++ b/docraft/include/docraft/docraft_document.h @@ -24,7 +24,9 @@ #include "docraft/docraft_document_context.h" #include "docraft/docraft_document_metadata.h" +#include "docraft/backend/docraft_backend_providers_factory.h" #include "docraft/management/docraft_document_config.h" +#include "docraft/management/docraft_dom_facade.h" #include "docraft/management/docraft_document_query.h" #include "docraft/model/docraft_node.h" #include "docraft/model/docraft_settings.h" @@ -45,7 +47,8 @@ namespace docraft { * and invoking rendering. Configuration (metadata, settings, keywords) is delegated * to DocraftDocumentConfig for single responsibility. */ - class DOCRAFT_LIB DocraftDocument { + class DOCRAFT_LIB DocraftDocument + : public management::DocraftDOMFacade { public: /** * @brief Creates a document with an optional title. @@ -73,18 +76,25 @@ namespace docraft { * @brief Applies template processing to the document DOM using the configured template engine. */ void template_document(); + /** * @brief Renders the document using the configured context and renderer. */ void render(); /** - * @brief Overrides the rendering backend used during render. + * @brief Sets a factory used to create backend providers. * - * Passing nullptr resets to the default backend. - * @param backend Rendering backend implementation. + * Passing nullptr restores the default Haru providers factory. + */ + void set_capability_providers_factory( + const std::shared_ptr &capability_providers_factory); + + /** + * @brief Backward-compatible alias for set_capability_providers_factory. */ - void set_backend(const std::shared_ptr &backend); + void set_backend_providers_factory( + const std::shared_ptr &backend_providers_factory); /** * @brief Returns the document DOM nodes. @@ -118,58 +128,7 @@ namespace docraft { [[nodiscard]] std::shared_ptr context() const; - // Backward compatibility delegates to config_ - void set_document_title(const std::string &document_title); - - [[nodiscard]] const std::string &document_title() const; - - void set_document_path(const std::string &document_path); - - [[nodiscard]] const std::string &document_path() const; - - void set_settings(const std::shared_ptr &settings); - - [[nodiscard]] std::shared_ptr settings() const; - - void set_document_metadata(const DocraftDocumentMetadata &metadata); - - [[nodiscard]] const DocraftDocumentMetadata &document_metadata() const; - - // Backward compatibility: DOM query delegates - [[nodiscard]] std::vector > find_by_name( - const std::string &name) const; - - [[nodiscard]] std::vector > take_by_name(const std::string &name); - - [[nodiscard]] std::shared_ptr find_first_by_name(const std::string &name) const; - - [[nodiscard]] std::shared_ptr take_first_by_name(const std::string &name); - - [[nodiscard]] std::shared_ptr find_last_by_name(const std::string &name) const; - - [[nodiscard]] std::shared_ptr take_last_by_name(const std::string &name); - - template - [[nodiscard]] std::vector > find_by_type() const; - - template - [[nodiscard]] std::vector > take_by_type(); - - // Backward compatibility: config shortcuts - void enable_auto_keywords(bool enabled = true); - - [[nodiscard]] bool auto_keywords_enabled() const; - - void set_auto_keywords_config(const utils::DocraftKeywordExtractor::Config &config); - - [[nodiscard]] const utils::DocraftKeywordExtractor::Config &auto_keywords_config() const; - - void set_document_template_engine(const std::shared_ptr &template_engine); - - [[nodiscard]] std::shared_ptr document_template_engine() const; - - [[nodiscard]] std::shared_ptr edit_document_template_engine(); - + // Backward compatibility: non-trivial shortcut preserved on the main class. void refresh_auto_keywords(); private: @@ -187,7 +146,7 @@ namespace docraft { std::shared_ptr context_; std::vector > dom_; management::DocraftDocumentConfig config_; + std::shared_ptr capability_providers_factory_; }; } -#include "docraft_document.hpp" diff --git a/docraft/include/docraft/docraft_document_context.h b/docraft/include/docraft/docraft_document_context.h index 75bbcb8..e58ab05 100644 --- a/docraft/include/docraft/docraft_document_context.h +++ b/docraft/include/docraft/docraft_document_context.h @@ -17,188 +17,104 @@ #pragma once #include "docraft/docraft_lib.h" -#include - -#include "docraft_cursor.h" #include "docraft/backend/docraft_rendering_backend.h" -#include "docraft/generic/docraft_font_applier.h" -#include "docraft/model/docraft_page_format.h" -#include "docraft/management/docraft_backend_cache.h" -#include "docraft/management/docraft_document_section_manager.h" +#include "docraft/services/docraft_rendering_service.h" +#include "docraft/services/docraft_layout_service.h" +#include "docraft/services/docraft_typography_service.h" +#include "docraft/services/docraft_navigation_service.h" +#include namespace docraft { namespace renderer { class DocraftAbstractRenderer; } - namespace model { - class DocraftHeader; - class DocraftBody; - class DocraftFooter; - } /** - * @brief Shared rendering and layout state for a document. + * @brief Context Facade: Orchestrates document rendering and layout services. + * + * This is the primary entry point for the document rendering pipeline. + * It composes four services: + * - RenderingService: backend + capability caching + * - LayoutService: cursor + page metrics + * - TypographyService: font management + * - NavigationService: document structure (header/body/footer) + page nav * - * The context holds the active rendering backend, page metrics, cursors, and delegates - * section management and backend caching to specialized helper classes. + * Responsibilities: + * - Wiring and lifetime management of services + * - Renderer setup and delegation + * - Direct access to four service bundles */ class DOCRAFT_LIB DocraftDocumentContext { - public: + public: /** - * @brief Constructs a context with a default backend. + * @brief Constructs a context with a default PDF backend. */ DocraftDocumentContext(); - /** - * @brief Constructs a context with the provided rendering backend. - * @param backend Rendering backend to use. - */ - explicit DocraftDocumentContext(const std::shared_ptr& backend); - /** - * @brief Releases context resources. - */ - ~DocraftDocumentContext(); /** - * @brief Returns the active rendering backend. - * @return Shared pointer to the rendering backend. + * @brief Constructs a context with backend providers factory. */ - [[nodiscard]] std::shared_ptr rendering_backend() const; - [[nodiscard]] std::shared_ptr edit_rendering_backend(); + explicit DocraftDocumentContext( + const std::shared_ptr &capability_providers_factory); /** - * @brief Returns the layout cursor. - * @return Reference to the cursor. + * @brief Destructor. */ - DocraftCursor& cursor(); + ~DocraftDocumentContext(); - /** - * @brief Returns remaining vertical space on the current page section. - * @return Available vertical space in points. - */ - float available_space() const; + // ===== Service Accessors ===== /** - * @brief Sets the renderer responsible for translating nodes to backend calls. - * @param renderer Renderer instance. + * @brief Returns the rendering service (backend + capability caching). */ - void set_renderer(const std::shared_ptr &renderer); + services::RenderingService &edit_rendering(); - /** - * @brief Returns the current renderer. - * @return Shared pointer to the renderer (may be nullptr). - */ - std::shared_ptr renderer(); + [[nodiscard]] const services::RenderingService &rendering() const; /** - * @brief Sets the width of the current layout rectangle. - * @param x Width in points. + * @brief Returns the layout service (cursor + page metrics). */ - void set_current_rect_width(float x); + services::LayoutService &edit_layout(); - /** - * @brief Returns the page width in points. - * @return Page width in points. - */ - [[nodiscard]] float page_width() const; + [[nodiscard]] const services::LayoutService &layout() const; /** - * @brief Returns the page height in points. - * @return Page height in points. + * @brief Returns the typography service (font management). */ - [[nodiscard]] float page_height() const; + services::TypographyService &edit_typography(); - /** - * @brief Returns the font applier instance. - * @return Font applier (may be nullptr). - */ - [[nodiscard]] std::shared_ptr font_applier() const; - [[nodiscard]] std::shared_ptr edit_font_applier(); - - /** - * @brief Sets the font applier used for text nodes. - * @param font_applier Font applier instance. - */ - void set_font_applier(const std::shared_ptr& font_applier); + [[nodiscard]] const services::TypographyService &typography() const; /** - * @brief Replaces the underlying rendering backend. - * @param backend New rendering backend. Pass nullptr to restore the default backend. + * @brief Returns the navigation service (sections + page navigation). */ - void set_backend(const std::shared_ptr& backend); + services::NavigationService &edit_navigation(); - /** - * @brief Sets the page format for the backend and updates cached size. - */ - void set_page_format(model::DocraftPageSize size, model::DocraftPageOrientation orientation); - - /** - * @brief Moves to the first page (index 0). - */ - void go_to_first_page(); + [[nodiscard]] const services::NavigationService &navigation() const; - /** - * @brief Moves to the previous page. - */ - void go_to_previous_page(); + // ===== Renderer Management ===== /** - * @brief Moves to the last page. + * @brief Sets the renderer responsible for translating document nodes to backend calls. + * @param renderer Renderer instance. */ - void go_to_last_page(); + void set_renderer(const std::shared_ptr &renderer); /** - * @brief Returns the section manager for header/body/footer. + * @brief Returns the current renderer. + * @return Shared pointer to the renderer (may be nullptr). */ - management::DocraftDocumentSectionManager §ion_manager(); - [[nodiscard]] const management::DocraftDocumentSectionManager §ion_manager() const; + [[nodiscard]] std::shared_ptr renderer() const; - /** - * @brief Returns the backend cache manager. - */ - management::DocraftBackendCache &backend_cache(); - [[nodiscard]] const management::DocraftBackendCache &backend_cache() const; - - // Backward compatibility: delegate to backend_cache() - [[nodiscard]] std::shared_ptr line_backend() const; - [[nodiscard]] std::shared_ptr edit_line_backend(); - [[nodiscard]] std::shared_ptr shape_backend() const; - [[nodiscard]] std::shared_ptr edit_shape_backend(); - [[nodiscard]] std::shared_ptr text_backend() const; - [[nodiscard]] std::shared_ptr edit_text_backend(); - [[nodiscard]] std::shared_ptr image_backend() const; - [[nodiscard]] std::shared_ptr edit_image_backend(); - [[nodiscard]] std::shared_ptr page_backend() const; - [[nodiscard]] std::shared_ptr edit_page_backend(); - - // Backward compatibility: delegate to section_manager() - void set_header(const std::shared_ptr &header); - [[nodiscard]] std::shared_ptr header() const; - [[nodiscard]] std::shared_ptr edit_header(); - void set_body(const std::shared_ptr &body); - [[nodiscard]] std::shared_ptr body() const; - [[nodiscard]] std::shared_ptr edit_body(); - void set_footer(const std::shared_ptr &footer); - [[nodiscard]] std::shared_ptr footer() const; - [[nodiscard]] std::shared_ptr edit_footer(); - void set_section_ratios(float header_ratio, float body_ratio, float footer_ratio); - [[nodiscard]] float header_ratio() const; - [[nodiscard]] float body_ratio() const; - [[nodiscard]] float footer_ratio() const; + [[nodiscard]] std::shared_ptr edit_renderer(); private: - /** - * @brief Refreshes all backend caches (called after backend changes). - */ - void refresh_backend_caches(); + void sync_layout_page_dimensions_from_backend(); - DocraftCursor cursor_; - float current_rect_width_=0; + std::unique_ptr rendering_; + std::unique_ptr layout_; + std::unique_ptr typography_; + std::unique_ptr navigation_; std::shared_ptr renderer_; - float page_width_; - float page_height_; - std::shared_ptr font_applier_; - std::shared_ptr backend_; - management::DocraftDocumentSectionManager section_manager_; - management::DocraftBackendCache backend_cache_; }; -} // docraft +} // namespace docraft diff --git a/docraft/include/docraft/exception/docraft_backend_exceptions.h b/docraft/include/docraft/exception/docraft_backend_exceptions.h new file mode 100644 index 0000000..2e4f878 --- /dev/null +++ b/docraft/include/docraft/exception/docraft_backend_exceptions.h @@ -0,0 +1,30 @@ +#pragma once + +#include "docraft/exception/docraft_exception_base.h" + +namespace docraft::exception { + /** + * @brief Base exception for backend-related errors. + */ + class BackendStateException : public DocraftException { + public: + using DocraftException::DocraftException; + }; + + /** + * @brief Exception thrown when a required capability is unavailable. + */ + class CapabilityUnavailableException : public BackendStateException { + public: + using BackendStateException::BackendStateException; + }; + + /** + * @brief Exception thrown for page state errors. + */ + class PageStateException : public BackendStateException { + public: + using BackendStateException::BackendStateException; + }; +} // namespace docraft::exception + diff --git a/docraft/include/docraft/exception/docraft_configuration_exceptions.h b/docraft/include/docraft/exception/docraft_configuration_exceptions.h new file mode 100644 index 0000000..aa15709 --- /dev/null +++ b/docraft/include/docraft/exception/docraft_configuration_exceptions.h @@ -0,0 +1,22 @@ +#pragma once + +#include "docraft/exception/docraft_exception_base.h" + +namespace docraft::exception { + /** + * @brief Exception thrown for configuration-related errors. + */ + class ConfigurationException : public DocraftException { + public: + using DocraftException::DocraftException; + }; + + /** + * @brief Exception thrown when a required argument is missing. + */ + class MissingArgumentException : public ConfigurationException { + public: + using ConfigurationException::ConfigurationException; + }; +} // namespace docraft::exception + diff --git a/docraft/include/docraft/exception/docraft_data_exceptions.h b/docraft/include/docraft/exception/docraft_data_exceptions.h new file mode 100644 index 0000000..8cfe10c --- /dev/null +++ b/docraft/include/docraft/exception/docraft_data_exceptions.h @@ -0,0 +1,22 @@ +#pragma once + +#include "docraft/exception/docraft_exception_base.h" + +namespace docraft::exception { + /** + * @brief Base exception for data format-related errors. + */ + class DataFormatException : public DocraftException { + public: + using DocraftException::DocraftException; + }; + + /** + * @brief Exception thrown when JSON parsing fails. + */ + class InvalidJSONException : public DataFormatException { + public: + using DataFormatException::DataFormatException; + }; +} // namespace docraft::exception + diff --git a/docraft/include/docraft/exception/docraft_document_exceptions.h b/docraft/include/docraft/exception/docraft_document_exceptions.h new file mode 100644 index 0000000..871ad65 --- /dev/null +++ b/docraft/include/docraft/exception/docraft_document_exceptions.h @@ -0,0 +1,30 @@ +#pragma once + +#include "docraft/exception/docraft_exception_base.h" + +namespace docraft::exception { + /** + * @brief Base exception for document-related errors. + */ + class DocumentException : public DocraftException { + public: + using DocraftException::DocraftException; + }; + + /** + * @brief Exception thrown when document state is invalid. + */ + class DocumentStateException : public DocumentException { + public: + using DocumentException::DocumentException; + }; + + /** + * @brief Exception thrown for model-related document errors. + */ + class ModelException : public DocumentException { + public: + using DocumentException::DocumentException; + }; +} // namespace docraft::exception + diff --git a/docraft/include/docraft/exception/docraft_exception_base.h b/docraft/include/docraft/exception/docraft_exception_base.h new file mode 100644 index 0000000..1108c4a --- /dev/null +++ b/docraft/include/docraft/exception/docraft_exception_base.h @@ -0,0 +1,56 @@ +#pragma once + +#include +#include + +namespace docraft::exception { + /** + * @brief Base exception class for the Docraft library. + * + * All Docraft-specific exceptions inherit from this class. + * This class intentionally does not inherit from standard exception types. + */ + class DocraftException { + private: + mutable std::string message_; + + public: + /** + * @brief Constructs a DocraftException with a message. + * @param message The error message. + */ + explicit DocraftException(const std::string &message) : message_(message) { + } + + /** + * @brief Returns the exception message as a C-string. + * @return A C-string containing the error message. + * + * This method follows a classic what()-style API. + */ + [[nodiscard]] const char *what() const noexcept { + return message_.c_str(); + } + + /** + * @brief Returns the message as a std::string. + * @return The error message. + */ + [[nodiscard]] const std::string &message() const noexcept { + return message_; + } + + // Explicit copy and move constructors/assignment for completeness + DocraftException(const DocraftException &other) noexcept = default; + + DocraftException &operator=(const DocraftException &other) noexcept = default; + + DocraftException(DocraftException &&other) noexcept = default; + + DocraftException &operator=(DocraftException &&other) noexcept = default; + + virtual ~DocraftException() noexcept = default; + }; +} // namespace docraft::exception + + diff --git a/docraft/include/docraft/exception/docraft_exceptions.h b/docraft/include/docraft/exception/docraft_exceptions.h new file mode 100644 index 0000000..5887b94 --- /dev/null +++ b/docraft/include/docraft/exception/docraft_exceptions.h @@ -0,0 +1,21 @@ +#pragma once + +/** + * @brief Master exception header for Docraft. + * + * This header includes all domain-specific exception classes organized + * by functional domain. + */ + +#include "docraft/exception/docraft_exception_base.h" +#include "docraft/exception/docraft_configuration_exceptions.h" +#include "docraft/exception/docraft_filesystem_exceptions.h" +#include "docraft/exception/docraft_data_exceptions.h" +#include "docraft/exception/docraft_input_exceptions.h" +#include "docraft/exception/docraft_template_exceptions.h" +#include "docraft/exception/docraft_layout_exceptions.h" +#include "docraft/exception/docraft_document_exceptions.h" +#include "docraft/exception/docraft_backend_exceptions.h" +#include "docraft/exception/docraft_rendering_exceptions.h" +#include "docraft/exception/docraft_implementation_exceptions.h" + diff --git a/docraft/include/docraft/exception/docraft_filesystem_exceptions.h b/docraft/include/docraft/exception/docraft_filesystem_exceptions.h new file mode 100644 index 0000000..7fa2d1d --- /dev/null +++ b/docraft/include/docraft/exception/docraft_filesystem_exceptions.h @@ -0,0 +1,30 @@ +#pragma once + +#include "docraft/exception/docraft_exception_base.h" + +namespace docraft::exception { + /** + * @brief Base exception for file system-related errors. + */ + class FileSystemException : public DocraftException { + public: + using DocraftException::DocraftException; + }; + + /** + * @brief Exception thrown when a file cannot be found. + */ + class FileNotFoundException : public FileSystemException { + public: + using FileSystemException::FileSystemException; + }; + + /** + * @brief Exception thrown when a file cannot be opened. + */ + class CannotOpenFileException : public FileSystemException { + public: + using FileSystemException::FileSystemException; + }; +} // namespace docraft::exception + diff --git a/docraft/include/docraft/exception/docraft_implementation_exceptions.h b/docraft/include/docraft/exception/docraft_implementation_exceptions.h new file mode 100644 index 0000000..1472721 --- /dev/null +++ b/docraft/include/docraft/exception/docraft_implementation_exceptions.h @@ -0,0 +1,14 @@ +#pragma once + +#include "docraft/exception/docraft_exception_base.h" + +namespace docraft::exception { + /** + * @brief Exception thrown when a feature is not yet implemented. + */ + class NotImplementedException : public DocraftException { + public: + using DocraftException::DocraftException; + }; +} // namespace docraft::exception + diff --git a/docraft/include/docraft/exception/docraft_input_exceptions.h b/docraft/include/docraft/exception/docraft_input_exceptions.h new file mode 100644 index 0000000..07b76e3 --- /dev/null +++ b/docraft/include/docraft/exception/docraft_input_exceptions.h @@ -0,0 +1,14 @@ +#pragma once + +#include "docraft/exception/docraft_exception_base.h" + +namespace docraft::exception { + /** + * @brief Exception thrown for invalid input validation errors. + */ + class InvalidInputException : public DocraftException { + public: + using DocraftException::DocraftException; + }; +} // namespace docraft::exception + diff --git a/docraft/include/docraft/exception/docraft_layout_exceptions.h b/docraft/include/docraft/exception/docraft_layout_exceptions.h new file mode 100644 index 0000000..cf963c8 --- /dev/null +++ b/docraft/include/docraft/exception/docraft_layout_exceptions.h @@ -0,0 +1,22 @@ +#pragma once + +#include "docraft/exception/docraft_exception_base.h" + +namespace docraft::exception { + /** + * @brief Base exception for layout-related errors. + */ + class LayoutException : public DocraftException { + public: + using DocraftException::DocraftException; + }; + + /** + * @brief Exception thrown for layout configuration errors. + */ + class LayoutConfigurationException : public LayoutException { + public: + using LayoutException::LayoutException; + }; +} // namespace docraft::exception + diff --git a/docraft/include/docraft/exception/docraft_rendering_exceptions.h b/docraft/include/docraft/exception/docraft_rendering_exceptions.h new file mode 100644 index 0000000..dd0b620 --- /dev/null +++ b/docraft/include/docraft/exception/docraft_rendering_exceptions.h @@ -0,0 +1,22 @@ +#pragma once + +#include "docraft/exception/docraft_exception_base.h" + +namespace docraft::exception { + /** + * @brief Base exception for rendering-related errors. + */ + class RenderingException : public DocraftException { + public: + using DocraftException::DocraftException; + }; + + /** + * @brief Exception thrown when rendering fails. + */ + class RenderingFailedException : public RenderingException { + public: + using RenderingException::RenderingException; + }; +} // namespace docraft::exception + diff --git a/docraft/include/docraft/exception/docraft_template_exceptions.h b/docraft/include/docraft/exception/docraft_template_exceptions.h new file mode 100644 index 0000000..be3c616 --- /dev/null +++ b/docraft/include/docraft/exception/docraft_template_exceptions.h @@ -0,0 +1,38 @@ +#pragma once + +#include "docraft/exception/docraft_exception_base.h" + +namespace docraft::exception { + /** + * @brief Base exception for template-related errors. + */ + class TemplateException : public DocraftException { + public: + using DocraftException::DocraftException; + }; + + /** + * @brief Exception thrown when a template variable already exists. + */ + class TemplateVariableExistsException : public TemplateException { + public: + using TemplateException::TemplateException; + }; + + /** + * @brief Exception thrown when a template variable is not found. + */ + class TemplateVariableNotFoundException : public TemplateException { + public: + using TemplateException::TemplateException; + }; + + /** + * @brief Exception thrown when template image data is invalid or missing. + */ + class TemplateImageDataException : public TemplateException { + public: + using TemplateException::TemplateException; + }; +} // namespace docraft::exception + diff --git a/docraft/include/docraft/layout/docraft_layout_engine.h b/docraft/include/docraft/layout/docraft_layout_engine.h index 87f7e97..a3875f6 100644 --- a/docraft/include/docraft/layout/docraft_layout_engine.h +++ b/docraft/include/docraft/layout/docraft_layout_engine.h @@ -22,16 +22,13 @@ #include #include "docraft/docraft_cursor.h" -#include "docraft/generic/chain_of_responsibility_handler.h" #include "docraft/model/docraft_node.h" -#include "docraft/model/docraft_section.h" -namespace docraft::layout { - namespace handler { - class DocraftLayoutListHandler; - } +namespace docraft { + class DocraftDocumentContext; +} - using Handlers = std::vector>>; +namespace docraft::layout { /** * @brief Computes layout boxes for document nodes using a chain of handlers. * @@ -48,10 +45,15 @@ namespace docraft::layout { explicit DocraftLayoutEngine(const std::shared_ptr& context, bool reset_cursor = true); DocraftLayoutEngine(const DocraftLayoutEngine&) = delete; DocraftLayoutEngine& operator=(const DocraftLayoutEngine&) = delete; + + DocraftLayoutEngine(DocraftLayoutEngine &&) noexcept; + + DocraftLayoutEngine &operator=(DocraftLayoutEngine &&) noexcept; + /** * @brief Destructor. */ - ~DocraftLayoutEngine() = default; + ~DocraftLayoutEngine(); /** * @brief Computes the layout for a single node tree. @@ -85,61 +87,7 @@ namespace docraft::layout { static model::DocraftTransform compute_max_rect(const std::vector& boxes) ; private: - struct Sections { - std::shared_ptr header; - std::shared_ptr body; - std::shared_ptr footer; - }; - struct SectionPlan { - bool header_to_render = false; - bool body_to_render = false; - bool footer_to_render = false; - float header_ratio = 0.0F; - float body_ratio = 0.0F; - float footer_ratio = 0.0F; - }; - - /** - * @brief Configures the handler chain for the current context. - */ - void configure_handlers(const std::shared_ptr& context); - Handlers handlers_; - std::shared_ptr context_; - const float kHeaderHeightRatio_ = 0.06F; - const float kBodyHeightRatio_ = 0.88F; - const float kFooterHeightRatio_ = 0.06F; - const float kVerticalSpacing_ = 4.0F; - const float kHorizontalSpacing_ = 4.0F; - handler::DocraftLayoutListHandler* list_handler_ = nullptr; - - /** - * @brief Execute the correct handler to compute the layout for the given node. - * @param node - * @param box Output transform. - * @param cursor Cursor used for layout. - * @return true if a handler processed the node. - */ - bool compute_node(const std::shared_ptr& node, model::DocraftTransform* box, DocraftCursor& cursor) const; - /** - * @brief Computes the layout width available to a section node. - * @param node Section node. - * @return Available width in points. - */ - float compute_width(const std::shared_ptr &node) const; - /** - * @brief Assigns page owner to a node and its children. - * @param node Target node. - * @param page Page number (1-based). - */ - void assign_page_owner_recursive(const std::shared_ptr& node, int page) const; - Sections split_sections(const std::vector>& nodes) const; - SectionPlan build_section_plan(const Sections& sections) const; - void layout_header_section(const std::shared_ptr& header, float header_ratio); - void layout_body_section(const std::shared_ptr& body, - const std::shared_ptr& header, - const SectionPlan& plan); - void layout_footer_section(const std::shared_ptr& footer, - const std::shared_ptr& body, - const SectionPlan& plan); + class Impl; + std::unique_ptr impl_; }; } // layout diff --git a/docraft/include/docraft/layout/handler/abstract_docraft_layout_handler.h b/docraft/include/docraft/layout/handler/abstract_docraft_layout_handler.h index 446211d..c9833b2 100644 --- a/docraft/include/docraft/layout/handler/abstract_docraft_layout_handler.h +++ b/docraft/include/docraft/layout/handler/abstract_docraft_layout_handler.h @@ -20,12 +20,13 @@ #include #include "docraft/docraft_document_context.h" +#include "docraft/exception/docraft_exceptions.h" #include "docraft/model/docraft_node.h" #include "docraft/generic/chain_of_responsibility_handler.h" namespace docraft::layout::handler { template - requires std::derived_from + requires std::derived_from /** * @brief Base class for layout handlers in the chain of responsibility. * @@ -34,27 +35,32 @@ namespace docraft::layout::handler { * * @tparam T Node type handled by this handler. */ - class AbstractDocraftLayoutHandler :public generic::DocraftChainOfResponsibilityHandler { + class AbstractDocraftLayoutHandler : public generic::DocraftChainOfResponsibilityHandler { public: using DocraftChainOfResponsibilityHandler::DocraftChainOfResponsibilityHandler; + ~AbstractDocraftLayoutHandler() override = default; /** * @brief Creates a handler bound to a document context. - * @throws std::invalid_argument if context is null. + * @throws docraft::exception::InvalidInputException if context is null. */ - explicit AbstractDocraftLayoutHandler(const std::shared_ptr &context) : context_(context) { + explicit AbstractDocraftLayoutHandler(const std::shared_ptr &context) : context_( + context) { if (!context_) { - throw std::invalid_argument("context is null"); + throw docraft::exception::InvalidInputException("context is null"); } } + /** * @brief Computes the layout box for a concrete node type. * @param node Node to layout. * @param box Output transform. * @param cursor Layout cursor. */ - virtual void compute(const std::shared_ptr& node, model::DocraftTransform* box, DocraftCursor& cursor) =0; + virtual void compute(const std::shared_ptr &node, model::DocraftTransform *box, DocraftCursor &cursor) =0; + /** * @brief Returns the bound document context. * @return Document context. @@ -70,7 +76,8 @@ namespace docraft::layout::handler { std::shared_ptr context() const { return context_; } - protected: + + private: std::shared_ptr context_; }; } diff --git a/docraft/include/docraft/layout/handler/docraft_layout_horizontal_table_handler.h b/docraft/include/docraft/layout/handler/docraft_layout_horizontal_table_handler.h new file mode 100644 index 0000000..c0da500 --- /dev/null +++ b/docraft/include/docraft/layout/handler/docraft_layout_horizontal_table_handler.h @@ -0,0 +1,116 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 + +#include "docraft/docraft_document_context.h" +#include "docraft/docraft_lib.h" +#include "docraft/layout/docraft_layout_engine.h" +#include "docraft/layout/handler/docraft_layout_table_handler.h" + +namespace docraft::layout::handler { + /** + * @brief Layout handler for horizontal tables. + * + * Handles DocraftTable nodes whose orientation is kHorizontal. + * Columns are placed left-to-right; a header row precedes the data rows. + * + * Inherits common helpers and context access from DocraftLayoutTableHandler. + */ + class DOCRAFT_LIB DocraftLayoutHorizontalTableHandler : public DocraftLayoutTableHandler { + public: + using DocraftLayoutTableHandler::DocraftLayoutTableHandler; + + /** + * @brief Computes the layout for a horizontal table node. + * @param node Horizontal table node. + * @param box Output transform (position + size). + * @param cursor Layout cursor; not advanced (table manages its own cursor internally). + */ + void compute(const std::shared_ptr &node, + model::DocraftTransform *box, + DocraftCursor &cursor) override; + + private: + struct TableContent { + std::vector > title_nodes; + std::vector > > rows; + }; + + struct WidthPlan { + float table_width = 0.0F; + std::vector col_widths; + + WidthPlan() = default; + + WidthPlan(float width, std::vector widths) + : table_width(width), col_widths(std::move(widths)) { + } + }; + + /** Initialize per-compute state and cursor anchoring. */ + void setup_compute_state(const std::shared_ptr &node, DocraftCursor &cursor); + + /** Collect normalized title/content node vectors used by all compute phases. */ + static TableContent collect_table_content(const std::shared_ptr &node); + + /** Compute final column widths and natural title-row height. */ + WidthPlan compute_width_plan(const std::shared_ptr &node, + const TableContent &content); + + /** Apply resolved width plan to class state consumed by row layout helpers. */ + void apply_width_plan(const WidthPlan &plan); + + /** Layout table title row and all body rows, returning body total height. */ + float layout_body_rows(const TableContent &content, float start_y, float min_row_height); + + /** Write final node/box geometry and restore layout service width state. */ + void finalize_output(const std::shared_ptr &node, + model::DocraftTransform *box, + float table_width, + float table_height); + + /** Emit debug logs for computed cell geometry. */ + static void log_cells(const std::shared_ptr &node); + + /** Release temporary compute resources kept in class state. */ + void clear_compute_state(); + + float layout_row(const std::vector > &row_nodes, + float row_top_y, + float min_row_height); + + std::shared_ptr context_; + std::optional engine_; + DocraftCursor table_cursor_; + std::size_t cols_ = 0; + std::vector col_widths_; + std::vector col_lefts_; + float offset_x_ = 0.0F; + float offset_y_ = 0.0F; + float cell_padding_x_ = 0.0F; + float cell_padding_y_ = 0.0F; + float baseline_offset_ = 0.0F; + float fixed_x_ = 0.0F; + float fixed_y_ = 0.0F; + float saved_available_space_ = 0.0F; + }; +} // namespace docraft::layout::handler + diff --git a/docraft/include/docraft/layout/handler/docraft_layout_table_handler.h b/docraft/include/docraft/layout/handler/docraft_layout_table_handler.h index 9b2e5e9..4f419dd 100644 --- a/docraft/include/docraft/layout/handler/docraft_layout_table_handler.h +++ b/docraft/include/docraft/layout/handler/docraft_layout_table_handler.h @@ -23,9 +23,14 @@ namespace docraft::layout::handler { /** - * @brief Layout handler for table nodes. + * @brief Base layout handler for table nodes. * - * Calculates cell boxes and header/content areas based on weights. + * Handles the degenerate case of tables with no titles (zero-size output). + * Orientation-specific layout is delegated to the sub-handlers: + * - DocraftLayoutHorizontalTableHandler (kHorizontal) + * - DocraftLayoutVerticalTableHandler (kVertical) + * + * Register all three in the handler chain so each handles its own case. */ class DOCRAFT_LIB DocraftLayoutTableHandler : public AbstractDocraftLayoutHandler { public: @@ -36,7 +41,8 @@ namespace docraft::layout::handler { * @param box Output transform. * @param cursor Layout cursor. */ - void compute(const std::shared_ptr& node, model::DocraftTransform* box, DocraftCursor& cursor) override; + void compute(const std::shared_ptr &node, model::DocraftTransform *box, + DocraftCursor &cursor) override; /** * @brief Handles a node if it is a DocraftTable. @@ -45,6 +51,7 @@ namespace docraft::layout::handler { * @param cursor Layout cursor. * @return true if handled. */ - bool handle(const std::shared_ptr &request, model::DocraftTransform *result, DocraftCursor& cursor) override; + bool handle(const std::shared_ptr &request, model::DocraftTransform *result, + DocraftCursor &cursor) override; }; } // docraft diff --git a/docraft/include/docraft/layout/handler/docraft_layout_vertical_table_handler.h b/docraft/include/docraft/layout/handler/docraft_layout_vertical_table_handler.h new file mode 100644 index 0000000..5f1f5f5 --- /dev/null +++ b/docraft/include/docraft/layout/handler/docraft_layout_vertical_table_handler.h @@ -0,0 +1,124 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 "docraft/docraft_document_context.h" +#include "docraft/docraft_lib.h" +#include "docraft/layout/docraft_layout_engine.h" +#include "docraft/layout/handler/docraft_layout_table_handler.h" + +namespace docraft::layout::handler { + /** + * @brief Layout handler for vertical tables. + * + * Handles DocraftTable nodes whose orientation is kVertical. + * Row labels occupy the first column; value columns follow to the right. + * An optional header row (htitle_nodes) sits above all data rows. + * + * Inherits common helpers and context access from DocraftLayoutTableHandler. + */ + class DOCRAFT_LIB DocraftLayoutVerticalTableHandler : public DocraftLayoutTableHandler { + public: + using DocraftLayoutTableHandler::DocraftLayoutTableHandler; + + /** + * @brief Computes the layout for a vertical table node. + * @param node Vertical table node. + * @param box Output transform (position + size). + * @param cursor Layout cursor; not advanced (table manages its own cursor internally). + */ + void compute(const std::shared_ptr &node, + model::DocraftTransform *box, + DocraftCursor &cursor) override; + + private: + struct TableData { + std::vector > title_nodes; + std::vector > flat_values; + std::size_t row_count = 0; + std::size_t value_cols = 0; + }; + + struct ColumnPlan { + float title_col_width = 0.0F; + float value_col_width = 0.0F; + std::size_t value_cols = 0; + }; + + struct BandView { + const std::vector > *nodes = nullptr; + const std::vector *lefts = nullptr; + const std::vector *widths = nullptr; + }; + + /** Initialize per-compute state and cursor anchoring. */ + void setup_compute_state(const std::shared_ptr &node, DocraftCursor &cursor); + + /** Collect normalized title/value node vectors used by layout phases. */ + static TableData collect_table_data(const std::shared_ptr &node); + + /** Compute natural width required by the title column. */ + float compute_title_natural_width(const TableData &data); + + /** Resolve title/value column widths using weights and explicit value widths. */ + ColumnPlan resolve_column_plan(const std::shared_ptr &node, + const TableData &data, + float title_natural_width) const; + + /** Layout optional header row and return consumed height. */ + float layout_header_row(const std::shared_ptr &node, + const ColumnPlan &plan, + float row_top_y); + + /** Layout all table body rows and return consumed height. */ + float layout_body_rows(const TableData &data, + const ColumnPlan &plan, + float row_top_y); + + /** Write final node/box geometry and restore layout service width state. */ + void finalize_output(const std::shared_ptr &node, + model::DocraftTransform *box, + float table_width, + float table_height); + + /** Emit debug logs for computed cell geometry. */ + static void log_cells(const std::shared_ptr &node); + + /** Release temporary compute resources kept in class state. */ + void clear_compute_state(); + + float compute_band_height(const BandView &band, float row_top_y); + + void place_band(const BandView &band, float row_top_y, float row_height) const; + + std::shared_ptr context_; + std::optional engine_; + DocraftCursor table_cursor_; + float padding_x_ = 0.0F; + float padding_y_ = 0.0F; + float cell_padding_x_ = 0.0F; + float cell_padding_y_ = 0.0F; + float baseline_offset_ = 0.0F; + float fixed_x_ = 0.0F; + float fixed_y_ = 0.0F; + float saved_available_space_ = 0.0F; + }; +} // namespace docraft::layout::handler + diff --git a/docraft/include/docraft/management/docraft_backend_cache.h b/docraft/include/docraft/management/docraft_backend_cache.h index 5188815..8b9c36d 100644 --- a/docraft/include/docraft/management/docraft_backend_cache.h +++ b/docraft/include/docraft/management/docraft_backend_cache.h @@ -31,16 +31,18 @@ namespace docraft::management { class DOCRAFT_LIB DocraftBackendCache { public: /** - * @brief Initializes the backend cache from a main rendering backend. - * @param backend The main rendering backend. + * @brief Initializes the backend cache from rendering capability provider. + * @param rendering_provider Rendering capability provider. */ - void initialize_from_backend(const std::shared_ptr& backend); + void initialize_from_provider( + const std::shared_ptr &rendering_provider); /** * @brief Returns the line backend (cached). * @return Line rendering backend. */ [[nodiscard]] std::shared_ptr line_backend() const; + [[nodiscard]] std::shared_ptr edit_line_backend(); /** @@ -48,6 +50,7 @@ namespace docraft::management { * @return Shape rendering backend. */ [[nodiscard]] std::shared_ptr shape_backend() const; + [[nodiscard]] std::shared_ptr edit_shape_backend(); /** @@ -55,6 +58,7 @@ namespace docraft::management { * @return Text rendering backend. */ [[nodiscard]] std::shared_ptr text_backend() const; + [[nodiscard]] std::shared_ptr edit_text_backend(); /** @@ -62,6 +66,7 @@ namespace docraft::management { * @return Image rendering backend. */ [[nodiscard]] std::shared_ptr image_backend() const; + [[nodiscard]] std::shared_ptr edit_image_backend(); /** @@ -69,6 +74,7 @@ namespace docraft::management { * @return Page rendering backend. */ [[nodiscard]] std::shared_ptr page_backend() const; + [[nodiscard]] std::shared_ptr edit_page_backend(); private: @@ -76,9 +82,9 @@ namespace docraft::management { /** * @brief Refreshes all cached backend interfaces (called internally). - * @param backend The main rendering backend. + * @param rendering_provider Rendering capability provider. */ - void refresh_caches(const std::shared_ptr& backend); + void refresh_caches(const std::shared_ptr &rendering_provider); std::shared_ptr line_backend_; std::shared_ptr shape_backend_; diff --git a/docraft/include/docraft/management/docraft_document_config.h b/docraft/include/docraft/management/docraft_document_config.h index 28494ba..a36ab97 100644 --- a/docraft/include/docraft/management/docraft_document_config.h +++ b/docraft/include/docraft/management/docraft_document_config.h @@ -117,14 +117,6 @@ namespace docraft::management { */ [[nodiscard]] const utils::DocraftKeywordExtractor::Config &auto_keywords_config() const; - /** - * @brief Extracts keywords from a set of nodes and merges them into metadata. - * @param nodes The nodes to extract keywords from (typically document DOM). - * - * No-op when auto-keyword extraction is disabled. - */ - void refresh_auto_keywords(const std::vector > &nodes); - void set_document_template_engine(const std::shared_ptr &template_engine); [[nodiscard]] std::shared_ptr document_template_engine() const; diff --git a/docraft/include/docraft/management/docraft_dom_facade.h b/docraft/include/docraft/management/docraft_dom_facade.h new file mode 100644 index 0000000..fec1e74 --- /dev/null +++ b/docraft/include/docraft/management/docraft_dom_facade.h @@ -0,0 +1,59 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 + +#include "docraft/management/docraft_document_config.h" +#include "docraft/model/docraft_node.h" + +namespace docraft::management { + /** + * @brief CRTP facade exposing DOM query shortcuts for DocraftDocument-like classes. + */ + template + class DocraftDOMFacade { + public: + [[nodiscard]] std::vector > find_by_name( + const std::string &name) const; + + [[nodiscard]] std::vector > take_by_name(const std::string &name) const; + + [[nodiscard]] std::shared_ptr find_first_by_name(const std::string &name) const; + + [[nodiscard]] std::shared_ptr take_first_by_name(const std::string &name); + + [[nodiscard]] std::shared_ptr find_last_by_name(const std::string &name) const; + + [[nodiscard]] std::shared_ptr take_last_by_name(const std::string &name); + + template + [[nodiscard]] std::vector > find_by_type() const; + + template + [[nodiscard]] std::vector > take_by_type(); + + private: + [[nodiscard]] Derived &self(); + + [[nodiscard]] const Derived &self() const; + }; +} // namespace docraft::management + +#include "docraft_dom_facade.hpp" diff --git a/docraft/include/docraft/management/docraft_dom_facade.hpp b/docraft/include/docraft/management/docraft_dom_facade.hpp new file mode 100644 index 0000000..e0a206d --- /dev/null +++ b/docraft/include/docraft/management/docraft_dom_facade.hpp @@ -0,0 +1,150 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 + +namespace docraft::management { + template + std::vector > + DocraftDOMFacade::find_by_name(const std::string &name) const { + std::vector > result; + self().traverse_dom([&result, &name](const std::shared_ptr &node, auto op) { + if (op != decltype(op)::kEnter || !node) { + return; + } + if (node->node_name() == name) { + result.push_back(node); + } + }); + return result; + } + + template + std::vector > + DocraftDOMFacade::take_by_name(const std::string &name) const { + std::vector > result; + self().traverse_dom([&result, &name](const std::shared_ptr &node, auto op) { + if (op != decltype(op)::kEnter || !node) { + return; + } + if (node->node_name() == name) { + result.push_back(node); + } + }); + return result; + } + + template + std::shared_ptr + DocraftDOMFacade::find_first_by_name(const std::string &name) const { + std::shared_ptr result; + self().traverse_dom([&result, &name](const std::shared_ptr &node, auto op) { + if (result || op != decltype(op)::kEnter || !node) { + return; + } + if (node->node_name() == name) { + result = node; + } + }); + return result; + } + + template + std::shared_ptr + DocraftDOMFacade::take_first_by_name(const std::string &name) { + std::shared_ptr result; + self().traverse_dom([&result, &name](const std::shared_ptr &node, auto op) { + if (result || op != decltype(op)::kEnter || !node) { + return; + } + if (node->node_name() == name) { + result = node; + } + }); + return result; + } + + template + std::shared_ptr + DocraftDOMFacade::find_last_by_name(const std::string &name) const { + std::shared_ptr result; + self().traverse_dom([&result, &name](const std::shared_ptr &node, auto op) { + if (op != decltype(op)::kEnter || !node) { + return; + } + if (node->node_name() == name) { + result = node; + } + }); + return result; + } + + template + std::shared_ptr + DocraftDOMFacade::take_last_by_name(const std::string &name) { + std::shared_ptr result; + self().traverse_dom([&result, &name](const std::shared_ptr &node, auto op) { + if (op != decltype(op)::kEnter || !node) { + return; + } + if (node->node_name() == name) { + result = node; + } + }); + return result; + } + + template + template + std::vector > DocraftDOMFacade::find_by_type() const { + std::vector > result; + self().traverse_dom([&result](const std::shared_ptr &node, auto op) { + if (op != decltype(op)::kEnter) { + return; + } + if (auto casted = std::dynamic_pointer_cast(node)) { + result.push_back(casted); + } + }); + return result; + } + + template + template + std::vector > DocraftDOMFacade::take_by_type() { + std::vector > result; + self().traverse_dom([&result](const std::shared_ptr &node, auto op) { + if (op != decltype(op)::kEnter) { + return; + } + if (auto casted = std::dynamic_pointer_cast(node)) { + result.push_back(casted); + } + }); + return result; + } + + template + Derived &DocraftDOMFacade::self() { + return static_cast(*this); + } + + template + const Derived &DocraftDOMFacade::self() const { + return static_cast(*this); + } +} // namespace docraft::management + diff --git a/docraft/include/docraft/model/docraft_table.h b/docraft/include/docraft/model/docraft_table.h index 61924b6..ed7d7dc 100644 --- a/docraft/include/docraft/model/docraft_table.h +++ b/docraft/include/docraft/model/docraft_table.h @@ -169,6 +169,8 @@ namespace docraft::model { */ void add_htitle_node(const std::shared_ptr &node, std::optional background = std::nullopt); + + /** * @brief Adds a content node. * @param node Content node. @@ -299,7 +301,10 @@ namespace docraft::model { * @brief Returns title nodes. * @return Vector of title nodes. */ - [[nodiscard]] const std::vector> &title_nodes() const; + [[nodiscard]] const std::vector > title_nodes() const; + + const std::vector > &title_text_nodes() const; + /** * @brief Returns header title nodes (vertical tables only). * @return Vector of header title nodes. @@ -362,6 +367,16 @@ namespace docraft::model { void set_model_type(DocraftModelType model_type); static DocraftModelType identify_model_type(const std::string &model_str); + + protected: + /** + * @brief Determines if a node is allowed as content in the table + * @note Only allows DocraftText and DocraftImage nodes for now, but can be extended in the future. + * @param node + * @return + */ + static bool is_content_allowed(const std::shared_ptr &node); + private: int rows_; int cols_; diff --git a/docraft/include/docraft/services/docraft_layout_service.h b/docraft/include/docraft/services/docraft_layout_service.h new file mode 100644 index 0000000..5049ccc --- /dev/null +++ b/docraft/include/docraft/services/docraft_layout_service.h @@ -0,0 +1,86 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 "docraft/docraft_lib.h" +#include "docraft/docraft_cursor.h" +#include "docraft/model/docraft_page_format.h" +#include + +namespace docraft::services { + /** + * @brief Manages document layout state: cursor position and page metrics. + * + * Responsible for: + * - Tracking current cursor position during layout + * - Storing page width/height + * - Computing available vertical space + * - Handling page format changes + */ + class DOCRAFT_LIB LayoutService { + public: + LayoutService(); + + ~LayoutService(); + + /** + * @brief Returns the layout cursor. + */ + DocraftCursor &cursor(); + + [[nodiscard]] const DocraftCursor &cursor() const; + + /** + * @brief Returns page width in points. + */ + [[nodiscard]] float page_width() const; + + /** + * @brief Returns page height in points. + */ + [[nodiscard]] float page_height() const; + + /** + * @brief Returns remaining vertical space on current section. + */ + [[nodiscard]] float available_space() const; + + /** + * @brief Sets the current layout rectangle width. + */ + void set_current_rect_width(float width); + + [[nodiscard]] float current_rect_width() const; + + /** + * @brief Overrides page dimensions directly (used when backend exposes concrete page size). + */ + void set_page_dimensions(float width, float height); + + /** + * @brief Sets page format and updates cached dimensions. + */ + void set_page_format(model::DocraftPageOrientation orientation); + + private: + std::unique_ptr cursor_; + float page_width_ = 0.0F; + float page_height_ = 0.0F; + float current_rect_width_ = 0.0F; + }; +} // namespace docraft::services + diff --git a/docraft/include/docraft/services/docraft_navigation_service.h b/docraft/include/docraft/services/docraft_navigation_service.h new file mode 100644 index 0000000..60ceb2e --- /dev/null +++ b/docraft/include/docraft/services/docraft_navigation_service.h @@ -0,0 +1,102 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 "docraft/docraft_lib.h" +#include "docraft/management/docraft_document_section_manager.h" +#include + +namespace docraft::model { + class DocraftHeader; + class DocraftBody; + class DocraftFooter; +} + +namespace docraft::services { + /** + * @brief Manages document structure: header, body, footer, and page navigation. + * + * Responsible for: + * - Managing document sections (header, body, footer) + * - Handling section ratios + * - Navigating pages + */ + class DOCRAFT_LIB NavigationService { + public: + NavigationService(); + + ~NavigationService(); + + /** + * @brief Returns the section manager. + */ + management::DocraftDocumentSectionManager §ion_manager(); + + [[nodiscard]] const management::DocraftDocumentSectionManager §ion_manager() const; + + /** + * @brief Sets the header section. + */ + void set_header(const std::shared_ptr &header); + + /** + * @brief Returns the header. + */ + [[nodiscard]] std::shared_ptr header() const; + + [[nodiscard]] std::shared_ptr edit_header(); + + /** + * @brief Sets the body section. + */ + void set_body(const std::shared_ptr &body); + + /** + * @brief Returns the body. + */ + [[nodiscard]] std::shared_ptr body() const; + + [[nodiscard]] std::shared_ptr edit_body(); + + /** + * @brief Sets the footer section. + */ + void set_footer(const std::shared_ptr &footer); + + /** + * @brief Returns the footer. + */ + [[nodiscard]] std::shared_ptr footer() const; + + [[nodiscard]] std::shared_ptr edit_footer(); + + /** + * @brief Sets section height ratios (header:body:footer). + */ + void set_section_ratios(float header_ratio, float body_ratio, float footer_ratio); + + [[nodiscard]] float header_ratio() const; + + [[nodiscard]] float body_ratio() const; + + [[nodiscard]] float footer_ratio() const; + + private: + std::unique_ptr section_manager_; + }; +} // namespace docraft::services + diff --git a/docraft/include/docraft/services/docraft_rendering_service.h b/docraft/include/docraft/services/docraft_rendering_service.h new file mode 100644 index 0000000..b55d229 --- /dev/null +++ b/docraft/include/docraft/services/docraft_rendering_service.h @@ -0,0 +1,124 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 "docraft/docraft_lib.h" +#include "docraft/backend/docraft_backend_providers_factory.h" +#include "docraft/backend/docraft_rendering_backend.h" +#include + +namespace docraft::services { + enum class MissingCapabilityPolicy { + kFail, + kWarn, + kIgnore + }; + + struct CapabilityRequirements { + bool line_rendering = false; + bool text_rendering = false; + bool shape_rendering = false; + bool image_rendering = false; + bool page_rendering = false; + bool font_backend = false; + bool output_backend = false; + bool metadata_backend = false; + }; + + /** + * @brief Manages the rendering backend and capability caching. + * + * Responsible for: + * - Holding the active rendering backend + * - Caching backend capabilities + * - Providing typed access to backend capabilities + */ + class DOCRAFT_LIB RenderingService { + public: + explicit RenderingService( + const std::shared_ptr &capability_providers_factory); + + RenderingService(); + + ~RenderingService(); + + /** + * @brief Sets backend providers factory. + */ + void set_capability_providers_factory( + const std::shared_ptr &capability_providers_factory); + + // Backward-compatible API. + void set_backend_providers_factory( + const std::shared_ptr &backend_providers_factory); + + void set_missing_capability_policy(MissingCapabilityPolicy policy); + + [[nodiscard]] MissingCapabilityPolicy missing_capability_policy() const; + + void validate_capabilities(const CapabilityRequirements &requirements) const; + + /** + * @brief Returns cached line rendering capability. + */ + [[nodiscard]] std::shared_ptr line_rendering() const; + + [[nodiscard]] std::shared_ptr edit_line_rendering(); + + /** + * @brief Returns cached text rendering capability. + */ + [[nodiscard]] std::shared_ptr text_rendering() const; + + [[nodiscard]] std::shared_ptr edit_text_rendering(); + + /** + * @brief Returns cached shape rendering capability. + */ + [[nodiscard]] std::shared_ptr shape_rendering() const; + + [[nodiscard]] std::shared_ptr edit_shape_rendering(); + + /** + * @brief Returns cached image rendering capability. + */ + [[nodiscard]] std::shared_ptr image_rendering() const; + + [[nodiscard]] std::shared_ptr edit_image_rendering(); + + /** + * @brief Returns cached page rendering capability. + */ + [[nodiscard]] std::shared_ptr page_rendering() const; + + [[nodiscard]] std::shared_ptr edit_page_rendering(); + + [[nodiscard]] std::shared_ptr font_backend() const; + + [[nodiscard]] std::shared_ptr output_backend() const; + + [[nodiscard]] std::shared_ptr metadata_backend() const; + + [[nodiscard]] std::shared_ptr edit_metadata_backend(); + + private: + backend::DocraftCapabilityProviders capability_providers_; + std::shared_ptr capability_providers_factory_; + MissingCapabilityPolicy missing_capability_policy_ = MissingCapabilityPolicy::kFail; + }; +} // namespace docraft::services + diff --git a/docraft/include/docraft/services/docraft_typography_service.h b/docraft/include/docraft/services/docraft_typography_service.h new file mode 100644 index 0000000..24f9ee4 --- /dev/null +++ b/docraft/include/docraft/services/docraft_typography_service.h @@ -0,0 +1,54 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 "docraft/docraft_lib.h" +#include "docraft/generic/docraft_font_applier.h" +#include + +namespace docraft::services { + /** + * @brief Manages typography and font handling. + * + * Responsible for: + * - Holding the font applier instance + * - Applying font to text nodes + */ + class DOCRAFT_LIB TypographyService { + public: + TypographyService(); + + ~TypographyService(); + + /** + * @brief Returns the font applier instance. + */ + [[nodiscard]] std::shared_ptr font_applier() const; + + [[nodiscard]] std::shared_ptr edit_font_applier(); + + /** + * @brief Sets the font applier. + */ + void set_font_applier(const std::shared_ptr &applier); + + private: + std::shared_ptr font_applier_ = nullptr; + }; +} // namespace docraft::services + + diff --git a/docraft/include/docraft/templating/docraft_template_engine.h b/docraft/include/docraft/templating/docraft_template_engine.h index c4b9ca0..caab94c 100644 --- a/docraft/include/docraft/templating/docraft_template_engine.h +++ b/docraft/include/docraft/templating/docraft_template_engine.h @@ -51,7 +51,7 @@ namespace docraft::templating { * @brief Retrieves a template variable value. * @param name Variable name (case-insensitive). * @return Stored value. - * @throws std::runtime_error if not found. + * @throws docraft::exception::DocraftException if not found. */ std::string find_template_variable(const std::string &name) const; /** @@ -61,7 +61,7 @@ namespace docraft::templating { /** * @brief Removes a template variable by name. * @param name Variable name (case-insensitive). - * @throws std::runtime_error if not found. + * @throws docraft::exception::DocraftException if not found. */ void remove_template_variable(const std::string &name); /** @@ -100,7 +100,7 @@ namespace docraft::templating { * @param base64 Base64 string with raw RGB bytes (no data URI prefix). * @param width Pixel width. * @param height Pixel height. - * @throws std::runtime_error if decoded size does not match width*height*3. + * @throws docraft::exception::DataFormatException if decoded size does not match width*height*3. */ void add_base64_image_data(const std::string &image_id, std::string_view base64, @@ -110,7 +110,7 @@ namespace docraft::templating { * @brief Retrieves raw image data by id. * @param image_id Image id (case-insensitive). * @return RawImageData reference. - * @throws std::runtime_error if not found. + * @throws docraft::exception::DocraftException if not found. */ const RawImageData &get_image_data(const std::string &image_id) const; diff --git a/docraft/include/docraft/utils/docraft_base64.h b/docraft/include/docraft/utils/docraft_base64.h index 035508e..b45ec71 100644 --- a/docraft/include/docraft/utils/docraft_base64.h +++ b/docraft/include/docraft/utils/docraft_base64.h @@ -18,12 +18,14 @@ #include #include +#include "docraft/exception/docraft_exceptions.h" + namespace docraft::utils { /** * @brief Decodes a base64 string into raw bytes. * @param input Base64 string (no data URI prefix). * @return Decoded bytes. - * @throws std::invalid_argument if the input is not valid base64. + * @throws docraft::exception::InvalidInputException if the input is not valid base64. */ std::vector decode_base64(std::string_view input); } diff --git a/docraft/src/docraft/backend/pdf/docraft_haru_backend.cc b/docraft/src/docraft/backend/pdf/docraft_haru_backend.cc index c845717..a14fcfa 100644 --- a/docraft/src/docraft/backend/pdf/docraft_haru_backend.cc +++ b/docraft/src/docraft/backend/pdf/docraft_haru_backend.cc @@ -15,136 +15,143 @@ */ #include "docraft/backend/pdf/docraft_haru_backend.h" +#include "docraft/backend/pdf/docraft_haru_font_backend.h" +#include "docraft/backend/pdf/docraft_haru_image_backend.h" +#include "docraft/backend/pdf/docraft_haru_line_backend.h" +#include "docraft/backend/pdf/docraft_haru_metadata_backend.h" +#include "docraft/backend/pdf/docraft_haru_output_backend.h" +#include "docraft/backend/pdf/docraft_haru_page_backend.h" +#include "docraft/backend/pdf/docraft_haru_shape_backend.h" +#include "docraft/backend/pdf/docraft_haru_text_backend.h" #include -#include #include -#include #include -#include #include -#include "docraft/docraft_document_metadata.h" +#include "docraft/exception/docraft_exceptions.h" namespace { struct HPDFErrorMap { 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 - { "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 - { "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 - { "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 - { "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 - { "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 - { "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 - { "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 - { "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 - { "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 - { "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 - { "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 + {"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."}, + {"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."}, + {"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."}, + {"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."}, + {"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."}, + {"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."}, + {"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."}, + {"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."}, + {"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."}, + {"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."}, + {"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."}, + {"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() { + + static const HPDFErrorMap &instance() { static HPDFErrorMap inst; return inst; } @@ -153,13 +160,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; } @@ -168,445 +175,115 @@ namespace { namespace docraft::backend::pdf { namespace { - HPDF_PageSizes to_hpdf_size(model::DocraftPageSize size) { - switch (size) { - case model::DocraftPageSize::kA3: - return HPDF_PAGE_SIZE_A3; - case model::DocraftPageSize::kA5: - return HPDF_PAGE_SIZE_A5; - case model::DocraftPageSize::kLetter: - return HPDF_PAGE_SIZE_LETTER; - case model::DocraftPageSize::kLegal: - return HPDF_PAGE_SIZE_LEGAL; - case model::DocraftPageSize::kA4: - default: - return HPDF_PAGE_SIZE_A4; + HPDF_Doc create_hpdf_document() { + HPDF_Doc pdf = HPDF_New(error_handler, nullptr); + if (!pdf) { + throw docraft::exception::BackendStateException("Failed to initialize Haru PDF document"); } - } - - HPDF_PageDirection to_hpdf_direction(model::DocraftPageOrientation orientation) { - switch (orientation) { - case model::DocraftPageOrientation::kLandscape: - return HPDF_PAGE_LANDSCAPE; - case model::DocraftPageOrientation::kPortrait: - default: - return HPDF_PAGE_PORTRAIT; - } - } - - void throw_if_hpdf_error(HPDF_STATUS status, const std::string &operation) { - if (status == HPDF_OK) { - return; - } - std::ostringstream stream; - stream << operation << " (HPDF status 0x" << std::hex << status << ")"; - throw std::runtime_error(stream.str()); - } - - HPDF_Date to_hpdf_date(const DocraftDocumentMetadata::DateTime &date) { - return HPDF_Date{ - .year = date.year, - .month = date.month, - .day = date.day, - .hour = date.hour, - .minutes = date.minutes, - .seconds = date.seconds, - .ind = date.ind, - .off_hour = date.off_hour, - .off_minutes = date.off_minutes - }; - } - - void set_info_attr_if_present(HPDF_Doc pdf, - HPDF_InfoType type, - const std::optional &value, - const std::string &field_name) { - if (!value || value->empty()) { - return; - } - const HPDF_STATUS status = HPDF_SetInfoAttr(pdf, type, value->c_str()); - throw_if_hpdf_error(status, "Failed to set PDF metadata '" + field_name + "'"); - } - - void set_info_date_attr_if_present(HPDF_Doc pdf, - HPDF_InfoType type, - const std::optional &value, - const std::string &field_name) { - if (!value) { - return; - } - const HPDF_STATUS status = HPDF_SetInfoDateAttr(pdf, type, to_hpdf_date(*value)); - throw_if_hpdf_error(status, "Failed to set PDF metadata '" + field_name + "'"); + return 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"); - } - 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"); - } - 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; - } - pages_.clear(); - } - } - - void DocraftHaruBackend::begin_text() const { - HPDF_Page_BeginText(pages_[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 DocraftHaruBackend::set_text_color(float r, float g, float b) const { - HPDF_Page_SetRGBFill(pages_[internal_current_page_index()], r, g, b); - } - - float DocraftHaruBackend::measure_text_width(const std::string &text) const { - return HPDF_Page_TextWidth(pages_[internal_current_page_index()], text.c_str()); - } - - void DocraftHaruBackend::set_stroke_color(float r, float g, float b) const { - HPDF_Page_SetRGBStroke(pages_[internal_current_page_index()], r, g, b); - } - - void DocraftHaruBackend::set_line_width(float thickness) const { - HPDF_Page_SetLineWidth(pages_[internal_current_page_index()], thickness); - } - - 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 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 DocraftHaruBackend::save_state() const { - HPDF_Page_GSave(pages_[internal_current_page_index()]); - } - - 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 DocraftHaruBackend::set_fill_color(float r, float g, float b) const { - HPDF_Page_SetRGBFill(pages_[internal_current_page_index()], r, g, b); - } - - void DocraftHaruBackend::set_fill_alpha(float alpha) const { - fill_alpha_ = alpha; - apply_alpha_state(); - } - - void DocraftHaruBackend::set_stroke_alpha(float alpha) const { - stroke_alpha_ = alpha; - apply_alpha_state(); - } - - 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); - } - - 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 DocraftHaruBackend::draw_polygon(const std::vector &points) const { - if (points.size() < 2U) { - return; - } - - 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); - } - HPDF_Page_ClosePath(pages_[internal_current_page_index()]); - } - - void DocraftHaruBackend::fill() const { - HPDF_Page_Fill(pages_[internal_current_page_index()]); - } - - void DocraftHaruBackend::stroke() const { - HPDF_Page_Stroke(pages_[internal_current_page_index()]); - } - - void DocraftHaruBackend::fill_stroke() const { - HPDF_Page_FillStroke(pages_[internal_current_page_index()]); - } - - 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); - } - - 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); - } - - 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); + state_ = std::make_shared(); + state_->pdf = create_hpdf_document(); + output_backend_ = std::make_unique(state_); + page_backend_ = std::make_unique(state_); + font_backend_ = std::make_unique(state_); + metadata_backend_ = std::make_unique(state_); + text_backend_ = std::make_unique(state_); + line_backend_ = std::make_unique(state_); + shape_backend_ = std::make_unique(state_); + image_backend_ = std::make_unique(state_); + + HPDF_UseUTFEncodings(state_->pdf); + HPDF_SetCurrentEncoder(state_->pdf, "UTF-8"); + HPDF_SetCompressionMode(state_->pdf, HPDF_COMP_ALL); + page_backend_->add_new_page(); } - 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"); + DocraftHaruBackend::~DocraftHaruBackend() { + // Destroy capability backends explicitly to enforce teardown order. + image_backend_.reset(); + shape_backend_.reset(); + line_backend_.reset(); + text_backend_.reset(); + page_backend_.reset(); + metadata_backend_.reset(); + font_backend_.reset(); + output_backend_.reset(); + + if (state_ && state_->pdf) { + HPDF_Free(state_->pdf); + state_->pdf = nullptr; } - HPDF_Page_DrawImage(pages_[internal_current_page_index()], image, x, y, width, height); } - 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::IDocraftLineRenderingBackend *DocraftHaruBackend::line_rendering() const { + return line_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::IDocraftLineRenderingBackend *DocraftHaruBackend::edit_line_rendering() { + return line_backend_.get(); } - float DocraftHaruBackend::page_width() const { - return HPDF_Page_GetWidth(pages_[internal_current_page_index()]); + const docraft::backend::IDocraftTextRenderingBackend *DocraftHaruBackend::text_rendering() const { + return text_backend_.get(); } - float DocraftHaruBackend::page_height() const { - return HPDF_Page_GetHeight(pages_[internal_current_page_index()]); + docraft::backend::IDocraftTextRenderingBackend *DocraftHaruBackend::edit_text_rendering() { + return text_backend_.get(); } - void DocraftHaruBackend::save_to_file(const std::string& path) const { - HPDF_SaveToFile(pdf_, path.c_str()); + const docraft::backend::IDocraftShapeRenderingBackend *DocraftHaruBackend::shape_rendering() const { + return shape_backend_.get(); } - std::string DocraftHaruBackend::file_extension() const { - return ".pdf"; + docraft::backend::IDocraftShapeRenderingBackend *DocraftHaruBackend::edit_shape_rendering() { + return shape_backend_.get(); } - 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_); - } - return result; + const docraft::backend::IDocraftImageRenderingBackend *DocraftHaruBackend::image_rendering() const { + return image_backend_.get(); } - 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_); - return false; - } - return true; + docraft::backend::IDocraftImageRenderingBackend *DocraftHaruBackend::edit_image_rendering() { + return image_backend_.get(); } - 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); - } - HPDF_Page_SetFontAndSize(pages_[internal_current_page_index()], font, size); + const docraft::backend::IDocraftPageRenderingBackend *DocraftHaruBackend::page_rendering() const { + return page_backend_.get(); } - void DocraftHaruBackend::set_document_metadata(const DocraftDocumentMetadata &metadata) { - set_info_date_attr_if_present(pdf_, HPDF_INFO_CREATION_DATE, metadata.creation_date(), "creation_date"); - set_info_date_attr_if_present(pdf_, HPDF_INFO_MOD_DATE, metadata.modification_date(), "modification_date"); - set_info_attr_if_present(pdf_, HPDF_INFO_AUTHOR, metadata.author(), "author"); - set_info_attr_if_present(pdf_, HPDF_INFO_CREATOR, metadata.creator(), "creator"); - set_info_attr_if_present(pdf_, HPDF_INFO_PRODUCER, metadata.producer(), "producer"); - set_info_attr_if_present(pdf_, HPDF_INFO_TITLE, metadata.title(), "title"); - set_info_attr_if_present(pdf_, HPDF_INFO_SUBJECT, metadata.subject(), "subject"); - set_info_attr_if_present(pdf_, HPDF_INFO_KEYWORDS, metadata.keywords(), "keywords"); - set_info_attr_if_present(pdf_, HPDF_INFO_TRAPPED, metadata.trapped(), "trapped"); - set_info_attr_if_present(pdf_, HPDF_INFO_GTS_PDFX, metadata.gts_pdfx(), "gts_pdfx"); + docraft::backend::IDocraftPageRenderingBackend *DocraftHaruBackend::edit_page_rendering() { + return page_backend_.get(); } - void DocraftHaruBackend::apply_alpha_state() const { - auto *ext = HPDF_CreateExtGState(pdf_); - if (ext) { - HPDF_ExtGState_SetAlphaFill(ext, fill_alpha_); - HPDF_ExtGState_SetAlphaStroke(ext, stroke_alpha_); - HPDF_Page_SetExtGState(pages_[internal_current_page_index()], ext); - } + const docraft::backend::IDocraftOutputBackend *DocraftHaruBackend::output_backend() const { + return output_backend_.get(); } - void DocraftHaruBackend::add_new_page() { - create_new_page(); + docraft::backend::IDocraftOutputBackend *DocraftHaruBackend::edit_output_backend() { + return output_backend_.get(); } - 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"); - } + const docraft::backend::IDocraftFontBackend *DocraftHaruBackend::font_backend() const { + return font_backend_.get(); } - 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)); - } + docraft::backend::IDocraftFontBackend *DocraftHaruBackend::edit_font_backend() { + return font_backend_.get(); } - void DocraftHaruBackend::go_to_first_page() { - if (pages_.empty()) { - throw std::runtime_error("No pages in document"); - } - current_page_number_ = 0; + const docraft::backend::IDocraftMetadataBackend *DocraftHaruBackend::metadata_backend() const { + return metadata_backend_.get(); } - 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_; + docraft::backend::IDocraftMetadataBackend *DocraftHaruBackend::edit_metadata_backend() { + return metadata_backend_.get(); } - void DocraftHaruBackend::go_to_last_page() { - if (pages_.empty()) { - throw std::runtime_error("No pages in document"); - } - current_page_number_ = pages_.size() - 1; + HPDF_Doc DocraftHaruBackend::pdf_document() const { + return state_ ? state_->pdf : nullptr; } - - 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_; - } - - 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/backend/pdf/docraft_haru_backend_providers_factory.cc b/docraft/src/docraft/backend/pdf/docraft_haru_backend_providers_factory.cc new file mode 100644 index 0000000..99a1447 --- /dev/null +++ b/docraft/src/docraft/backend/pdf/docraft_haru_backend_providers_factory.cc @@ -0,0 +1,31 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 "docraft/backend/pdf/docraft_haru_backend_providers_factory.h" + +#include "docraft/backend/pdf/docraft_haru_backend.h" + +namespace docraft::backend::pdf { + backend::DocraftCapabilityProviders DocraftHaruCapabilityProvidersFactory::create_capability_providers() const { + auto backend = std::make_shared(); + return { + .rendering_provider = backend, + .resource_provider = backend, + .lifecycle_provider = backend + }; + } +} // namespace docraft::backend::pdf + diff --git a/docraft/src/docraft/backend/pdf/docraft_haru_font_backend.cc b/docraft/src/docraft/backend/pdf/docraft_haru_font_backend.cc new file mode 100644 index 0000000..c7a9fa1 --- /dev/null +++ b/docraft/src/docraft/backend/pdf/docraft_haru_font_backend.cc @@ -0,0 +1,73 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 "docraft/backend/pdf/docraft_haru_font_backend.h" + +#include + +#include + +namespace docraft::backend::pdf { + using exception::BackendStateException; + + DocraftHaruFontBackend::DocraftHaruFontBackend(const std::shared_ptr &state) + : state_(state) { + } + + const char *DocraftHaruFontBackend::register_ttf_font_from_file(const std::string &path, + bool embed) const { + auto *const pdf = state_ ? state_->pdf : nullptr; + if (!pdf) { + return nullptr; + } + const char *result = HPDF_LoadTTFontFromFile(pdf, + path.c_str(), + embed ? HPDF_TRUE : HPDF_FALSE); + if (!result) { + HPDF_ResetError(pdf); + } + return result; + } + + bool DocraftHaruFontBackend::can_use_font(const std::string &internal_name, + const char *encoder) const { + auto *const pdf = state_ ? state_->pdf : nullptr; + if (!pdf) { + return false; + } + HPDF_Font font = HPDF_GetFont(pdf, internal_name.c_str(), encoder); + if (!font || HPDF_GetError(pdf) != HPDF_OK) { + HPDF_ResetError(pdf); + return false; + } + return true; + } + + void DocraftHaruFontBackend::set_font(const std::string &internal_name, + float size, + const char *encoder) const { + auto *const pdf = state_ ? state_->pdf : nullptr; + if (!pdf) { + throw BackendStateException("Haru document is not initialized"); + } + HPDF_Font font = HPDF_GetFont(pdf, internal_name.c_str(), encoder); + if (!font) { + throw BackendStateException("Failed to resolve font: " + internal_name); + } + const auto *provider = state_->ensure_page_provider(); + HPDF_Page_SetFontAndSize(provider->current_page(), font, size); + } +} // namespace docraft::backend::pdf diff --git a/docraft/src/docraft/backend/pdf/docraft_haru_image_backend.cc b/docraft/src/docraft/backend/pdf/docraft_haru_image_backend.cc new file mode 100644 index 0000000..36367d2 --- /dev/null +++ b/docraft/src/docraft/backend/pdf/docraft_haru_image_backend.cc @@ -0,0 +1,129 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 "docraft/backend/pdf/docraft_haru_image_backend.h" + +#include + +#include "docraft/exception/docraft_exceptions.h" + +namespace docraft::backend::pdf { + DocraftHaruImageBackend::DocraftHaruImageBackend(const std::shared_ptr &state) + : state_(state) { + } + + void DocraftHaruImageBackend::draw_png_image(const std::string &path, + float x, + float y, + float width, + float height) const { + auto *image = HPDF_LoadPngImageFromFile(state_ ? state_->pdf : nullptr, path.c_str()); + if (!image) { + throw docraft::exception::RenderingFailedException("Failed to load PNG image: " + path); + } + auto *provider = state_->ensure_page_provider(); + HPDF_Page_DrawImage(provider->current_page(), image, x, y, width, height); + } + + void DocraftHaruImageBackend::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( + state_ ? state_->pdf : nullptr, + reinterpret_cast(data), + static_cast(size)); + if (!image) { + throw docraft::exception::RenderingFailedException("Failed to load PNG image from memory"); + } + auto *provider = state_->ensure_page_provider(); + HPDF_Page_DrawImage(provider->current_page(), image, x, y, width, height); + } + + void DocraftHaruImageBackend::draw_jpeg_image(const std::string &path, + float x, + float y, + float width, + float height) const { + auto *image = HPDF_LoadJpegImageFromFile(state_ ? state_->pdf : nullptr, path.c_str()); + if (!image) { + throw docraft::exception::RenderingFailedException("Failed to load JPEG image: " + path); + } + auto *provider = state_->ensure_page_provider(); + HPDF_Page_DrawImage(provider->current_page(), image, x, y, width, height); + } + + void DocraftHaruImageBackend::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( + state_ ? state_->pdf : nullptr, + data, + static_cast(size)); + if (!image) { + throw docraft::exception::RenderingFailedException("Failed to load JPEG image from memory"); + } + auto *provider = state_->ensure_page_provider(); + HPDF_Page_DrawImage(provider->current_page(), image, x, y, width, height); + } + + void DocraftHaruImageBackend::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( + state_ ? state_->pdf : nullptr, + path.c_str(), + static_cast(pixel_width), + static_cast(pixel_height), + HPDF_CS_DEVICE_RGB); + if (!image) { + throw docraft::exception::RenderingFailedException("Failed to load raw RGB image: " + path); + } + auto *provider = state_->ensure_page_provider(); + HPDF_Page_DrawImage(provider->current_page(), image, x, y, width, height); + } + + void DocraftHaruImageBackend::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 kBitsPerComponent = 8; + auto *image = HPDF_LoadRawImageFromMem( + state_ ? state_->pdf : nullptr, + data, + static_cast(pixel_width), + static_cast(pixel_height), + HPDF_CS_DEVICE_RGB, + kBitsPerComponent); + if (!image) { + throw docraft::exception::RenderingFailedException("Failed to load raw RGB image from memory"); + } + auto *provider = state_->ensure_page_provider(); + HPDF_Page_DrawImage(provider->current_page(), image, x, y, width, height); + } +} // namespace docraft::backend::pdf diff --git a/docraft/src/docraft/backend/pdf/docraft_haru_line_backend.cc b/docraft/src/docraft/backend/pdf/docraft_haru_line_backend.cc new file mode 100644 index 0000000..20e5bd1 --- /dev/null +++ b/docraft/src/docraft/backend/pdf/docraft_haru_line_backend.cc @@ -0,0 +1,44 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 "docraft/backend/pdf/docraft_haru_line_backend.h" + +#include + +namespace docraft::backend::pdf { + DocraftHaruLineBackend::DocraftHaruLineBackend(const std::shared_ptr &state) + : state_(state) { + } + + void DocraftHaruLineBackend::set_stroke_color(float r, float g, float b) const { + auto *provider = state_->ensure_page_provider(); + HPDF_Page_SetRGBStroke(provider->current_page(), r, g, b); + } + + void DocraftHaruLineBackend::set_line_width(float thickness) const { + auto *provider = state_->ensure_page_provider(); + HPDF_Page_SetLineWidth(provider->current_page(), thickness); + } + + void DocraftHaruLineBackend::draw_line(float x1, float y1, float x2, float y2) const { + auto *provider = state_->ensure_page_provider(); + HPDF_Page page = provider->current_page(); + HPDF_Page_MoveTo(page, x1, y1); + HPDF_Page_LineTo(page, x2, y2); + HPDF_Page_Stroke(page); + } +} // namespace docraft::backend::pdf + diff --git a/docraft/src/docraft/backend/pdf/docraft_haru_metadata_backend.cc b/docraft/src/docraft/backend/pdf/docraft_haru_metadata_backend.cc new file mode 100644 index 0000000..3f18f97 --- /dev/null +++ b/docraft/src/docraft/backend/pdf/docraft_haru_metadata_backend.cc @@ -0,0 +1,97 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 "docraft/backend/pdf/docraft_haru_metadata_backend.h" + +#include +#include +#include + +#include + +#include "docraft/docraft_document_metadata.h" +#include "docraft/exception/docraft_exceptions.h" + +namespace docraft::backend::pdf { + namespace { + void throw_if_hpdf_error(HPDF_STATUS status, const std::string &operation) { + if (status == HPDF_OK) { + return; + } + std::ostringstream stream; + stream << operation << " (HPDF status 0x" << std::hex << status << ")"; + throw docraft::exception::BackendStateException(stream.str()); + } + + HPDF_Date to_hpdf_date(const DocraftDocumentMetadata::DateTime &date) { + return HPDF_Date{ + .year = date.year, + .month = date.month, + .day = date.day, + .hour = date.hour, + .minutes = date.minutes, + .seconds = date.seconds, + .ind = date.ind, + .off_hour = date.off_hour, + .off_minutes = date.off_minutes + }; + } + + void set_info_attr_if_present(HPDF_Doc pdf, + HPDF_InfoType type, + const std::optional &value, + const std::string &field_name) { + if (!value || value->empty()) { + return; + } + const HPDF_STATUS status = HPDF_SetInfoAttr(pdf, type, value->c_str()); + throw_if_hpdf_error(status, "Failed to set PDF metadata '" + field_name + "'"); + } + + void set_info_date_attr_if_present(HPDF_Doc pdf, + HPDF_InfoType type, + const std::optional &value, + const std::string &field_name) { + if (!value) { + return; + } + const HPDF_STATUS status = HPDF_SetInfoDateAttr(pdf, type, to_hpdf_date(*value)); + throw_if_hpdf_error(status, "Failed to set PDF metadata '" + field_name + "'"); + } + } // namespace + + DocraftHaruMetadataBackend::DocraftHaruMetadataBackend(const std::shared_ptr &state) + : state_(state) { + } + + void DocraftHaruMetadataBackend::set_document_metadata(const DocraftDocumentMetadata &metadata) { + const auto pdf = state_ ? state_->pdf : nullptr; + if (!pdf) { + throw docraft::exception::BackendStateException("Haru document is not initialized"); + } + set_info_date_attr_if_present(pdf, HPDF_INFO_CREATION_DATE, metadata.creation_date(), "creation_date"); + set_info_date_attr_if_present(pdf, HPDF_INFO_MOD_DATE, metadata.modification_date(), + "modification_date"); + set_info_attr_if_present(pdf, HPDF_INFO_AUTHOR, metadata.author(), "author"); + set_info_attr_if_present(pdf, HPDF_INFO_CREATOR, metadata.creator(), "creator"); + set_info_attr_if_present(pdf, HPDF_INFO_PRODUCER, metadata.producer(), "producer"); + set_info_attr_if_present(pdf, HPDF_INFO_TITLE, metadata.title(), "title"); + set_info_attr_if_present(pdf, HPDF_INFO_SUBJECT, metadata.subject(), "subject"); + set_info_attr_if_present(pdf, HPDF_INFO_KEYWORDS, metadata.keywords(), "keywords"); + set_info_attr_if_present(pdf, HPDF_INFO_TRAPPED, metadata.trapped(), "trapped"); + set_info_attr_if_present(pdf, HPDF_INFO_GTS_PDFX, metadata.gts_pdfx(), "gts_pdfx"); + } +} // namespace docraft::backend::pdf diff --git a/docraft/src/docraft/backend/pdf/docraft_haru_output_backend.cc b/docraft/src/docraft/backend/pdf/docraft_haru_output_backend.cc new file mode 100644 index 0000000..9bb161f --- /dev/null +++ b/docraft/src/docraft/backend/pdf/docraft_haru_output_backend.cc @@ -0,0 +1,39 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 "docraft/backend/pdf/docraft_haru_output_backend.h" + +#include + +#include "docraft/exception/docraft_exceptions.h" + +namespace docraft::backend::pdf { + DocraftHaruOutputBackend::DocraftHaruOutputBackend(const std::shared_ptr &state) + : state_(state) { + } + + void DocraftHaruOutputBackend::save_to_file(const std::string &path) const { + const auto pdf = state_ ? state_->pdf : nullptr; + if (!pdf) { + throw docraft::exception::BackendStateException("Haru document is not initialized"); + } + HPDF_SaveToFile(pdf, path.c_str()); + } + + std::string DocraftHaruOutputBackend::file_extension() const { + return ".pdf"; + } +} // namespace docraft::backend::pdf diff --git a/docraft/src/docraft/backend/pdf/docraft_haru_page_backend.cc b/docraft/src/docraft/backend/pdf/docraft_haru_page_backend.cc new file mode 100644 index 0000000..b8e8b7c --- /dev/null +++ b/docraft/src/docraft/backend/pdf/docraft_haru_page_backend.cc @@ -0,0 +1,167 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 "docraft/backend/pdf/docraft_haru_page_backend.h" + +#include +#include + +#include + +#include "docraft/exception/docraft_exceptions.h" + +namespace docraft::backend::pdf { + namespace { + HPDF_PageSizes to_hpdf_size(model::DocraftPageSize size) { + switch (size) { + case model::DocraftPageSize::kA3: + return HPDF_PAGE_SIZE_A3; + case model::DocraftPageSize::kA5: + return HPDF_PAGE_SIZE_A5; + case model::DocraftPageSize::kLetter: + return HPDF_PAGE_SIZE_LETTER; + case model::DocraftPageSize::kLegal: + return HPDF_PAGE_SIZE_LEGAL; + case model::DocraftPageSize::kA4: + default: + return HPDF_PAGE_SIZE_A4; + } + } + + HPDF_PageDirection to_hpdf_direction(model::DocraftPageOrientation orientation) { + switch (orientation) { + case model::DocraftPageOrientation::kLandscape: + return HPDF_PAGE_LANDSCAPE; + case model::DocraftPageOrientation::kPortrait: + default: + return HPDF_PAGE_PORTRAIT; + } + } + } // namespace + + DocraftHaruPageBackend::DocraftHaruPageBackend( + const std::shared_ptr &state) : state_(state) { + if (state_) { + // Register self as the page operations provider for capability backends. + // LIFETIME CONTRACT: This provider remains valid for the entire lifetime of state_. + state_->set_page_operations_provider(this); + } + } + + DocraftHaruPageBackend::~DocraftHaruPageBackend() { + if (state_ && state_->page_operations_provider == this) { + state_->clear_page_operations_provider(); + } + } + + float DocraftHaruPageBackend::page_width() const { + return HPDF_Page_GetWidth(current_page()); + } + + float DocraftHaruPageBackend::page_height() const { + return HPDF_Page_GetHeight(current_page()); + } + + void DocraftHaruPageBackend::add_new_page() { + HPDF_Page new_page = HPDF_AddPage(state_ ? state_->pdf : nullptr); + if (!new_page) { + throw docraft::exception::PageStateException("Failed to create a new page"); + } + apply_page_format(new_page); + state_->add_page(new_page); + state_->edit_current_page_index() = state_->page_count() - 1; + } + + void DocraftHaruPageBackend::move_to_next_page() { + if (state_->current_page_index() + 1 < state_->page_count()) { + state_->edit_current_page_index() = state_->current_page_index() + 1; + return; + } + throw docraft::exception::PageStateException("Already at the last page, cannot move to next page"); + } + + void DocraftHaruPageBackend::go_to_page(std::size_t page_number) { + if (page_number < state_->page_count()) { + state_->edit_current_page_index() = page_number; + return; + } + throw docraft::exception::PageStateException(std::format("Invalid page number: {}. Total pages: {}", + page_number + 1, + state_->page_count())); + } + + void DocraftHaruPageBackend::go_to_first_page() { + if (state_->page_count() == 0) { + throw docraft::exception::PageStateException("No pages in document"); + } + state_->edit_current_page_index() = 0; + } + + void DocraftHaruPageBackend::go_to_previous_page() { + if (state_->current_page_index() == 0) { + throw docraft::exception::PageStateException("Already at the first page, cannot move to previous page"); + } + state_->edit_current_page_index() = state_->current_page_index() - 1; + } + + void DocraftHaruPageBackend::go_to_last_page() { + if (state_->page_count() == 0) { + throw docraft::exception::PageStateException("No pages in document"); + } + state_->edit_current_page_index() = state_->page_count() - 1; + } + + void DocraftHaruPageBackend::set_page_format(model::DocraftPageSize size, + model::DocraftPageOrientation orientation) { + state_->edit_page_size() = to_hpdf_size(size); + state_->edit_page_direction() = to_hpdf_direction(orientation); + for (auto &page: state_->edit_pages()) { + if (page) { + apply_page_format(page); + } + } + } + + std::size_t DocraftHaruPageBackend::current_page_number() const { + return state_->current_page_index() + 1; + } + + std::size_t DocraftHaruPageBackend::total_page_count() const { + return state_->page_count(); + } + + HPDF_Page DocraftHaruPageBackend::current_page() const { + if (state_->page_count() == 0) { + throw docraft::exception::PageStateException("No pages in document"); + } + if (state_->current_page_index() >= state_->page_count()) { + throw docraft::exception::PageStateException("Current page index is out of bounds"); + } + return state_->pages()[state_->current_page_index()]; + } + + std::size_t DocraftHaruPageBackend::current_page_index() const { + if (state_->page_count() == 0) { + throw docraft::exception::PageStateException("No pages in document"); + } + return state_->current_page_index(); + } + + void DocraftHaruPageBackend::apply_page_format(HPDF_Page page) const { + HPDF_Page_SetSize(page, state_->page_size(), state_->page_direction()); + } +} // namespace docraft::backend::pdf + diff --git a/docraft/src/docraft/backend/pdf/docraft_haru_shape_backend.cc b/docraft/src/docraft/backend/pdf/docraft_haru_shape_backend.cc new file mode 100644 index 0000000..3e20af3 --- /dev/null +++ b/docraft/src/docraft/backend/pdf/docraft_haru_shape_backend.cc @@ -0,0 +1,99 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 "docraft/backend/pdf/docraft_haru_shape_backend.h" + +#include + +namespace docraft::backend::pdf { + DocraftHaruShapeBackend::DocraftHaruShapeBackend(const std::shared_ptr &state) + : state_(state) { + } + + void DocraftHaruShapeBackend::save_state() const { + auto *provider = state_->ensure_page_provider(); + HPDF_Page_GSave(provider->current_page()); + } + + void DocraftHaruShapeBackend::restore_state() const { + auto *provider = state_->ensure_page_provider(); + HPDF_Page_GRestore(provider->current_page()); + } + + void DocraftHaruShapeBackend::set_fill_color(float r, float g, float b) const { + auto *provider = state_->ensure_page_provider(); + HPDF_Page_SetRGBFill(provider->current_page(), r, g, b); + } + + void DocraftHaruShapeBackend::set_fill_alpha(float alpha) const { + state_->edit_fill_alpha() = alpha; + apply_alpha_state(); + } + + void DocraftHaruShapeBackend::set_stroke_alpha(float alpha) const { + state_->edit_stroke_alpha() = alpha; + apply_alpha_state(); + } + + void DocraftHaruShapeBackend::draw_rectangle(float x, float y, float width, float height) const { + auto *provider = state_->ensure_page_provider(); + HPDF_Page_Rectangle(provider->current_page(), x, y, width, height); + } + + void DocraftHaruShapeBackend::draw_circle(float center_x, float center_y, float radius) const { + auto *provider = state_->ensure_page_provider(); + HPDF_Page_Circle(provider->current_page(), center_x, center_y, radius); + } + + void DocraftHaruShapeBackend::draw_polygon(const std::vector &points) const { + if (points.size() < 2U) { + return; + } + + auto *provider = state_->ensure_page_provider(); + HPDF_Page page = provider->current_page(); + HPDF_Page_MoveTo(page, points[0].x, points[0].y); + for (size_t i = 1; i < points.size(); ++i) { + HPDF_Page_LineTo(page, points[i].x, points[i].y); + } + HPDF_Page_ClosePath(page); + } + + void DocraftHaruShapeBackend::fill() const { + auto *provider = state_->ensure_page_provider(); + HPDF_Page_Fill(provider->current_page()); + } + + void DocraftHaruShapeBackend::stroke() const { + auto *provider = state_->ensure_page_provider(); + HPDF_Page_Stroke(provider->current_page()); + } + + void DocraftHaruShapeBackend::fill_stroke() const { + auto *provider = state_->ensure_page_provider(); + HPDF_Page_FillStroke(provider->current_page()); + } + + void DocraftHaruShapeBackend::apply_alpha_state() const { + auto *ext = HPDF_CreateExtGState(state_ ? state_->pdf : nullptr); + if (ext) { + auto *provider = state_->ensure_page_provider(); + HPDF_ExtGState_SetAlphaFill(ext, state_->fill_alpha()); + HPDF_ExtGState_SetAlphaStroke(ext, state_->stroke_alpha()); + HPDF_Page_SetExtGState(provider->current_page(), ext); + } + } +} // namespace docraft::backend::pdf diff --git a/docraft/src/docraft/backend/pdf/docraft_haru_text_backend.cc b/docraft/src/docraft/backend/pdf/docraft_haru_text_backend.cc new file mode 100644 index 0000000..e7680a6 --- /dev/null +++ b/docraft/src/docraft/backend/pdf/docraft_haru_text_backend.cc @@ -0,0 +1,71 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 "docraft/backend/pdf/docraft_haru_text_backend.h" + +#include + +namespace docraft::backend::pdf { + DocraftHaruTextBackend::DocraftHaruTextBackend(const std::shared_ptr &state) + : state_(state) { + } + + void DocraftHaruTextBackend::begin_text() const { + auto *provider = state_->ensure_page_provider(); + HPDF_Page_BeginText(provider->current_page()); + } + + void DocraftHaruTextBackend::end_text() const { + auto *provider = state_->ensure_page_provider(); + HPDF_Page_EndText(provider->current_page()); + } + + void DocraftHaruTextBackend::draw_text(const std::string &text, float x, float y) const { + auto *provider = state_->ensure_page_provider(); + HPDF_Page_TextOut(provider->current_page(), x, y, text.c_str()); + } + + void DocraftHaruTextBackend::set_text_color(float r, float g, float b) const { + auto *provider = state_->ensure_page_provider(); + HPDF_Page_SetRGBFill(provider->current_page(), r, g, b); + } + + void DocraftHaruTextBackend::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 { + auto provider = state_->ensure_page_provider(); + HPDF_Page page = provider->current_page(); + HPDF_Page_SetTextMatrix( + page, + scale_x, + skew_x, + skew_y, + scale_y, + translate_x, + translate_y); + HPDF_Page_ShowText(page, text.c_str()); + } + + float DocraftHaruTextBackend::measure_text_width(const std::string &text) const { + auto *provider = state_->ensure_page_provider(); + return HPDF_Page_TextWidth(provider->current_page(), text.c_str()); + } +} // namespace docraft::backend::pdf + diff --git a/docraft/src/docraft/craft/docraft_craft_language_parser.cc b/docraft/src/docraft/craft/docraft_craft_language_parser.cc index 379717d..4a08527 100644 --- a/docraft/src/docraft/craft/docraft_craft_language_parser.cc +++ b/docraft/src/docraft/craft/docraft_craft_language_parser.cc @@ -18,11 +18,11 @@ #include "docraft/craft/docraft_craft_language_parser.h" #include +#include #include #include #include #include -#include #include #include #include @@ -31,6 +31,7 @@ #include "docraft/craft/docraft_craft_language_tokens.h" #include "docraft/craft/parser/docraft_parser.h" +#include "docraft/exception/docraft_exceptions.h" #include "docraft/model/docraft_header.h" #include "docraft/model/docraft_body.h" #include "docraft/model/docraft_footer.h" @@ -130,7 +131,8 @@ namespace { if (value == "false" || value == "0" || value == "no" || value == "off") { return false; } - throw std::invalid_argument("Invalid boolean value '" + raw_value + "' for " + error_context); + throw docraft::exception::InvalidInputException( + "Invalid boolean value '" + raw_value + "' for " + error_context); } int parse_int_in_range(const std::optional &value, @@ -141,27 +143,29 @@ namespace { int default_value = 0) { if (!value) { if (required) { - throw std::invalid_argument("Missing required metadata date field '" + field_name + "'"); + throw docraft::exception::InvalidInputException( + "Missing required metadata date field '" + field_name + "'"); } return default_value; } - try { - const int parsed = std::stoi(*value); - if (parsed < min_value || parsed > max_value) { - throw std::invalid_argument("Metadata date field '" + field_name + "' out of range"); - } - return parsed; - } catch (const std::invalid_argument &) { - throw std::invalid_argument("Invalid integer metadata date field '" + field_name + "': " + *value); - } catch (const std::out_of_range &) { - throw std::invalid_argument("Out-of-range metadata date field '" + field_name + "': " + *value); + int parsed = 0; + const char *begin = value->data(); + const char *end = value->data() + value->size(); + const auto [ptr, ec] = std::from_chars(begin, end, parsed); + if (ec != std::errc() || ptr != end) { + throw docraft::exception::InvalidInputException( + "Invalid integer metadata date field '" + field_name + "': " + *value); + } + if (parsed < min_value || parsed > max_value) { + throw docraft::exception::InvalidInputException("Metadata date field '" + field_name + "' out of range"); } + return parsed; } docraft::DocraftDocumentMetadata::DateTime parse_metadata_date(const pugi::xml_node &date_node, const std::string &tag_name) { if (!date_node) { - throw std::invalid_argument("Missing metadata date node '" + tag_name + "'"); + throw docraft::exception::InvalidInputException("Missing metadata date node '" + tag_name + "'"); } const std::optional year_value = read_attr_or_child_text( @@ -212,7 +216,8 @@ namespace { char timezone_indicator = '+'; if (ind_value) { if (ind_value->size() != 1U) { - throw std::invalid_argument("Metadata date field '" + tag_name + ".ind' must be a single character"); + throw docraft::exception::InvalidInputException( + "Metadata date field '" + tag_name + ".ind' must be a single character"); } timezone_indicator = (*ind_value)[0]; } @@ -224,7 +229,8 @@ namespace { return date_time; } if (timezone_indicator != '+' && timezone_indicator != '-') { - throw std::invalid_argument("Metadata date field '" + tag_name + ".ind' must be '+' or '-'"); + throw docraft::exception::InvalidInputException( + "Metadata date field '" + tag_name + ".ind' must be '+' or '-'"); } date_time.ind = timezone_indicator; date_time.off_hour = parse_int_in_range(off_hour_value, tag_name + ".off_hour", 0, 23, false, 0); @@ -403,7 +409,8 @@ namespace { docraft::craft::elements::metadata::auto_keywords::attribute::kMaxKeywords}.c_str())) { const int max_keywords = max_keywords_attr.as_int(); if (max_keywords <= 0) { - throw std::invalid_argument("Metadata AutoKeywords max_keywords must be greater than 0"); + throw docraft::exception::InvalidInputException( + "Metadata AutoKeywords max_keywords must be greater than 0"); } outcome.auto_keyword_config.max_keywords = static_cast(max_keywords); } @@ -413,7 +420,8 @@ namespace { docraft::craft::elements::metadata::auto_keywords::attribute::kMinLength}.c_str())) { const int min_length = min_length_attr.as_int(); if (min_length <= 0) { - throw std::invalid_argument("Metadata AutoKeywords min_length must be greater than 0"); + throw docraft::exception::InvalidInputException( + "Metadata AutoKeywords min_length must be greater than 0"); } outcome.auto_keyword_config.min_length = static_cast(min_length); } @@ -423,7 +431,7 @@ namespace { docraft::craft::elements::metadata::auto_keywords::attribute::kLanguage}.c_str())) { const auto parsed_languages = split_languages(language_attr.as_string()); if (parsed_languages.empty()) { - throw std::invalid_argument("Metadata AutoKeywords language cannot be empty"); + throw docraft::exception::InvalidInputException("Metadata AutoKeywords language cannot be empty"); } std::vector validated_languages; for (const auto &language: parsed_languages) { @@ -432,7 +440,7 @@ namespace { ch = static_cast(std::tolower(static_cast(ch))); } if (!is_supported_stopword_language(normalized)) { - throw std::invalid_argument( + throw docraft::exception::InvalidInputException( "Unsupported AutoKeywords language '" + language + "'. Supported: it,en,fr,de,es"); } validated_languages.push_back(normalized); @@ -477,7 +485,8 @@ void DocraftCraftLanguageParser::parse(const std::string &craft_language_source) xml_doc_ = pugi::xml_document(); pugi::xml_parse_result result = xml_doc_.load_string(craft_language_source.c_str()); if (!result) { - throw std::runtime_error("Error parsing .craft content: " + std::string(result.description())); + throw docraft::exception::DataFormatException( + "Error parsing .craft content: " + std::string(result.description())); } load_document(); } @@ -486,7 +495,8 @@ void DocraftCraftLanguageParser::load_from_file(const std::string &file_path) { xml_doc_ = pugi::xml_document(); pugi::xml_parse_result result = xml_doc_.load_file(file_path.c_str()); if (!result) { - throw std::runtime_error("Error loading .craft file: " + std::string(result.description())); + throw docraft::exception::DataFormatException( + "Error loading .craft file: " + std::string(result.description())); } load_document(); } @@ -545,7 +555,7 @@ std::shared_ptr DocraftCraftLanguageParser::parse_node(const std::string node_name = xml_node.name(); auto it = parsers_.find(node_name); if (it == parsers_.end()) { - throw std::runtime_error("No parser registered for node: " + node_name); + throw docraft::exception::DataFormatException("No parser registered for node: " + node_name); } auto result = it->second->parse(xml_node); @@ -561,7 +571,8 @@ std::shared_ptr DocraftCraftLanguageParser::parse_node(const } if (auto list_node = std::dynamic_pointer_cast(result)) { if (child.name() != std::string{elements::kText}) { - throw std::invalid_argument(std::string(child.name()) + " cannot be placed in a list"); + throw docraft::exception::InvalidInputException( + std::string(child.name()) + " cannot be placed in a list"); } } @@ -572,7 +583,7 @@ std::shared_ptr DocraftCraftLanguageParser::parse_node(const child_name == std::string{elements::kTitle} || child_name == std::string{elements::kSubtitle} || child_name == std::string{elements::kPageNumber}) { - throw std::invalid_argument( + throw docraft::exception::InvalidInputException( "Text nodes cannot contain child <" + child_name + "> nodes. " + "Text is a leaf node and only accepts text content. " + "Use as a container for multiple text elements instead."); @@ -592,19 +603,19 @@ void DocraftCraftLanguageParser::load_document() { const std::string root_tag = tag_formatter(section::kDocument.data()); pugi::xml_node root = xml_doc_.child(root_tag.c_str()); if (!root) { - throw std::runtime_error("Invalid .craft file: missing root element"); + throw docraft::exception::DataFormatException("Invalid .craft file: missing root element"); } pugi::xml_node document = root; if (const pugi::xml_attribute path_attr = document.attribute(section::attribute::kPath.data())) { const std::string output_path = trim_copy(path_attr.as_string()); if (!output_path.empty()) { - document_->set_document_path(output_path); + document_->edit_config().set_document_path(output_path); } } const DocraftMetadataParseOutcome metadata_parse_outcome = parse_metadata_node(document); if (metadata_parse_outcome.has_metadata) { - document_->set_document_metadata(metadata_parse_outcome.metadata); + document_->edit_config().set_document_metadata(metadata_parse_outcome.metadata); } //Settings @@ -614,7 +625,7 @@ void DocraftCraftLanguageParser::load_document() { auto it = parsers_.find(settings_tag); if (it != parsers_.end()) { if (auto settings = std::dynamic_pointer_cast(it->second->parse(settings_node))) { - document_->set_settings(settings); + document_->edit_config().set_settings(settings); } } } @@ -641,7 +652,7 @@ void DocraftCraftLanguageParser::load_document() { } } } else { - throw std::runtime_error("Invalid .craft file: missing element"); + throw docraft::exception::DataFormatException("Invalid .craft file: missing element"); } // Footer (optional) @@ -657,7 +668,7 @@ void DocraftCraftLanguageParser::load_document() { } if (metadata_parse_outcome.auto_keyword_config.enabled) { - DocraftDocumentMetadata metadata = document_->document_metadata(); + DocraftDocumentMetadata metadata = document_->config().document_metadata(); utils::DocraftKeywordExtractor extractor({ .max_keywords = metadata_parse_outcome.auto_keyword_config.max_keywords, .min_length = metadata_parse_outcome.auto_keyword_config.min_length, @@ -666,11 +677,11 @@ void DocraftCraftLanguageParser::load_document() { const std::vector extracted_keywords = extractor.extract(*document_); if (!extracted_keywords.empty()) { metadata.set_keywords(merge_keywords(metadata.keywords(), extracted_keywords)); - document_->set_document_metadata(metadata); + document_->edit_config().set_document_metadata(metadata); } } - LOG_INFO("Document loaded successfully with title: " + document_->document_title()); + LOG_INFO("Document loaded successfully with title: " + document_->config().document_title()); } } // namespace docraft::craft diff --git a/docraft/src/docraft/craft/parser/docraft_foreach_parser.cc b/docraft/src/docraft/craft/parser/docraft_foreach_parser.cc index c9ac65d..d77ddfe 100644 --- a/docraft/src/docraft/craft/parser/docraft_foreach_parser.cc +++ b/docraft/src/docraft/craft/parser/docraft_foreach_parser.cc @@ -20,6 +20,7 @@ #include #include "docraft/craft/parser/docraft_parser.h" +#include "docraft/exception/docraft_exceptions.h" #include "docraft/model/docraft_foreach.h" #include "docraft/craft/docraft_craft_language_tokens.h" @@ -119,6 +120,6 @@ namespace docraft::craft::parser { foreach_node->set_template_nodes(template_nodes); return foreach_node; } - throw std::runtime_error("Missing 'model' attribute in foreach node."); + throw docraft::exception::InvalidInputException("Missing 'model' attribute in foreach node."); } } // namespace docraft::craft::parser diff --git a/docraft/src/docraft/craft/parser/docraft_image_parser.cc b/docraft/src/docraft/craft/parser/docraft_image_parser.cc index 6256b2d..95d40df 100644 --- a/docraft/src/docraft/craft/parser/docraft_image_parser.cc +++ b/docraft/src/docraft/craft/parser/docraft_image_parser.cc @@ -22,6 +22,8 @@ #include +#include "docraft/exception/docraft_exceptions.h" + namespace docraft::craft::parser { namespace { bool extract_base64_payload(std::string_view value, std::string_view &payload) { @@ -45,7 +47,7 @@ namespace docraft::craft::parser { //image cannot have path and data at the same time if (auto src_attr = (craft_language_source.attribute(elements::image::attribute::kSrc.data()) != nullptr) && (craft_language_source.attribute(elements::image::attribute::kData.data()) != nullptr)) { - throw std::invalid_argument("Image node cannot have both 'src' and 'data' attributes."); + throw docraft::exception::InvalidInputException("Image node cannot have both 'src' and 'data' attributes."); } if (auto src_attr = craft_language_source.attribute(elements::image::attribute::kSrc.data())) { image_node->set_path(src_attr.as_string()); @@ -57,18 +59,20 @@ namespace docraft::craft::parser { const auto width_attr = craft_language_source.attribute(elements::image::attribute::kDataWidth.data()); const auto height_attr = craft_language_source.attribute(elements::image::attribute::kDataHeight.data()); if (!width_attr || !height_attr) { - throw std::invalid_argument("Base64 image data requires data_width and data_height."); + throw docraft::exception::InvalidInputException( + "Base64 image data requires data_width and data_height."); } const int pixel_width = width_attr.as_int(); const int pixel_height = height_attr.as_int(); if (pixel_width <= 0 || pixel_height <= 0) { - throw std::invalid_argument("Base64 image data has invalid dimensions."); + throw docraft::exception::InvalidInputException("Base64 image data has invalid dimensions."); } auto decoded = utils::decode_base64(payload); const auto expected_size = static_cast(pixel_width) * static_cast(pixel_height) * 3U; if (decoded.size() != expected_size) { - throw std::invalid_argument("Base64 image data size does not match dimensions (RGB expected)."); + throw docraft::exception::InvalidInputException( + "Base64 image data size does not match dimensions (RGB expected)."); } image_node->set_raw_data(decoded, pixel_width, pixel_height); } else { diff --git a/docraft/src/docraft/craft/parser/docraft_layout_parser.cc b/docraft/src/docraft/craft/parser/docraft_layout_parser.cc index b674b5d..c94dcc8 100644 --- a/docraft/src/docraft/craft/parser/docraft_layout_parser.cc +++ b/docraft/src/docraft/craft/parser/docraft_layout_parser.cc @@ -17,6 +17,7 @@ #include "docraft/craft/parser/docraft_parser.h" #include "docraft/craft/parser/docraft_parser_helpers.h" +#include "docraft/exception/docraft_exceptions.h" #include "docraft/model/docraft_layout.h" namespace docraft::craft::parser { @@ -29,7 +30,7 @@ namespace docraft::craft::parser { } else if (orientation_str == std::string{orientation::kVertical}) { layout_node->set_orientation(model::LayoutOrientation::kVertical); } else { - throw std::invalid_argument("Invalid layout orientation: " + orientation_str); + throw docraft::exception::InvalidInputException("Invalid layout orientation: " + orientation_str); } } detail::configure_docraft_node_attributes(layout_node, craft_language_source); diff --git a/docraft/src/docraft/craft/parser/docraft_list_parser.cc b/docraft/src/docraft/craft/parser/docraft_list_parser.cc index 203a412..23a0e57 100644 --- a/docraft/src/docraft/craft/parser/docraft_list_parser.cc +++ b/docraft/src/docraft/craft/parser/docraft_list_parser.cc @@ -17,6 +17,7 @@ #include "docraft/craft/parser/docraft_parser.h" #include "docraft/craft/parser/docraft_parser_helpers.h" +#include "docraft/exception/docraft_exceptions.h" #include "docraft/model/docraft_list.h" namespace docraft::craft::parser { @@ -30,7 +31,7 @@ namespace docraft::craft::parser { } else if (style_str == std::string{elements::list::style::kRoman}) { list_node->set_ordered_style(model::OrderedListStyle::kRoman); } else { - throw std::invalid_argument("Invalid list style: " + style_str); + throw docraft::exception::InvalidInputException("Invalid list style: " + style_str); } } @@ -52,7 +53,7 @@ namespace docraft::craft::parser { } else if (dot_str == std::string{elements::ulist::dot::kBox}) { list_node->set_unordered_dot(model::UnorderedListDot::kBox); } else { - throw std::invalid_argument("Invalid unordered list dot: " + dot_str); + throw docraft::exception::InvalidInputException("Invalid unordered list dot: " + dot_str); } } diff --git a/docraft/src/docraft/craft/parser/docraft_page_number_parser.cc b/docraft/src/docraft/craft/parser/docraft_page_number_parser.cc index 04c410e..4eca779 100644 --- a/docraft/src/docraft/craft/parser/docraft_page_number_parser.cc +++ b/docraft/src/docraft/craft/parser/docraft_page_number_parser.cc @@ -17,6 +17,7 @@ #include "docraft/craft/parser/docraft_parser.h" #include "docraft/craft/parser/docraft_parser_helpers.h" +#include "docraft/exception/docraft_exceptions.h" #include "docraft/model/docraft_page_number.h" namespace docraft::craft::parser { @@ -43,7 +44,7 @@ namespace docraft::craft::parser { } else if (style_str == std::string{style::kNormal}) { page_number_node->set_style(model::TextStyle::kNormal); } else { - throw std::invalid_argument("Invalid text style: " + style_str); + throw docraft::exception::InvalidInputException("Invalid text style: " + style_str); } } if (auto alignment_attr = craft_language_source.attribute(elements::text::attribute::kAlignment.data())) { @@ -57,7 +58,7 @@ namespace docraft::craft::parser { } else if (alignment_str == std::string{alignment::kLeft}) { page_number_node->set_alignment(model::TextAlignment::kLeft); } else { - throw std::invalid_argument("Invalid text alignment: " + alignment_str); + throw docraft::exception::InvalidInputException("Invalid text alignment: " + alignment_str); } } if (auto underline_attr = craft_language_source.attribute(elements::text::attribute::kUnderline.data())) { diff --git a/docraft/src/docraft/craft/parser/docraft_parser_helpers.cc b/docraft/src/docraft/craft/parser/docraft_parser_helpers.cc index aa9a575..2fdf258 100644 --- a/docraft/src/docraft/craft/parser/docraft_parser_helpers.cc +++ b/docraft/src/docraft/craft/parser/docraft_parser_helpers.cc @@ -19,6 +19,8 @@ #include #include +#include "docraft/exception/docraft_exceptions.h" + namespace docraft::craft::parser::detail { bool is_hex_color(const std::string &color) { if (color.size() != 7 && color.size() != 9) { @@ -38,7 +40,7 @@ namespace docraft::craft::parser::detail { DocraftColor get_docraft_color(const pugi::xml_attribute &color_attr) { std::string color_name_str = color_attr.as_string(); if (color_name_str.empty()) { - throw std::invalid_argument("Color attribute cannot be empty"); + throw docraft::exception::InvalidInputException("Color attribute cannot be empty"); } // Support template expressions: ${data("fieldname")} or ${variable_name} @@ -51,7 +53,7 @@ namespace docraft::craft::parser::detail { if (color_name_str[0] == '#') { if (!is_hex_color(color_name_str)) { - throw std::invalid_argument("Invalid hex color: " + color_name_str); + throw docraft::exception::InvalidInputException("Invalid hex color: " + color_name_str); } return DocraftColor(color_name_str); } @@ -76,7 +78,7 @@ namespace docraft::craft::parser::detail { return DocraftColor(ColorName::kPurple); } - throw std::invalid_argument("Unknown color: " + color_name_str); + throw docraft::exception::InvalidInputException("Unknown color: " + color_name_str); } void add_external_fonts_from_node(const pugi::xml_node &font_node, @@ -140,7 +142,7 @@ namespace docraft::craft::parser::detail { } else if (position_str == std::string{basic::attribute::position_type::kAbsolute}) { node->set_position_mode(model::DocraftPositionType::kAbsolute); } else { - throw std::invalid_argument("Invalid position value: " + position_str); + throw docraft::exception::InvalidInputException("Invalid position value: " + position_str); } } if (has_xy && !has_position_attr) { diff --git a/docraft/src/docraft/craft/parser/docraft_settings_parser.cc b/docraft/src/docraft/craft/parser/docraft_settings_parser.cc index f74e20f..977da19 100644 --- a/docraft/src/docraft/craft/parser/docraft_settings_parser.cc +++ b/docraft/src/docraft/craft/parser/docraft_settings_parser.cc @@ -17,6 +17,7 @@ #include "docraft/craft/parser/docraft_parser.h" #include "docraft/craft/parser/docraft_parser_helpers.h" +#include "docraft/exception/docraft_exceptions.h" #include "docraft/model/docraft_settings.h" namespace docraft::craft::parser { @@ -37,7 +38,7 @@ namespace docraft::craft::parser { if (size_str == "A4" || size_str == "a4") { return model::DocraftPageSize::kA4; } - throw std::invalid_argument("Invalid page_size: " + size_str); + throw docraft::exception::InvalidInputException("Invalid page_size: " + size_str); } model::DocraftPageOrientation parse_page_orientation(const std::string &orientation_str) { @@ -47,7 +48,7 @@ namespace docraft::craft::parser { if (orientation_str == "portrait") { return model::DocraftPageOrientation::kPortrait; } - throw std::invalid_argument("Invalid page_orientation: " + orientation_str); + throw docraft::exception::InvalidInputException("Invalid page_orientation: " + orientation_str); } void parse_page_format(const pugi::xml_node &craft_language_source, @@ -87,10 +88,10 @@ namespace docraft::craft::parser { const float footer_ratio = footer_attr ? footer_attr.as_float() : 0.06F; if (header_ratio < 0.0F || body_ratio < 0.0F || footer_ratio < 0.0F) { - throw std::invalid_argument("Section ratios must be non-negative"); + throw docraft::exception::InvalidInputException("Section ratios must be non-negative"); } if (header_ratio + body_ratio + footer_ratio > 1.0F + 1e-6F) { - throw std::invalid_argument("Section ratios must sum to 1.0 or less"); + throw docraft::exception::InvalidInputException("Section ratios must sum to 1.0 or less"); } settings_node->set_section_ratios(header_ratio, body_ratio, footer_ratio); @@ -105,7 +106,7 @@ namespace docraft::craft::parser { if (auto name_attr = font.attribute(elements::settings::fonts::attribute::kName.data())) { docraft_font.name = name_attr.as_string(); } else { - throw std::invalid_argument( + throw docraft::exception::InvalidInputException( std::string{elements::settings::fonts::attribute::kName.data()} + " attribute is required for a font"); } @@ -123,7 +124,8 @@ namespace docraft::craft::parser { settings_node->add_font(docraft_font); } } else { - throw std::invalid_argument(std::string(font_node.name()) + " cannot be placed in settings"); + throw docraft::exception::InvalidInputException( + std::string(font_node.name()) + " cannot be placed in settings"); } } } diff --git a/docraft/src/docraft/craft/parser/docraft_shape_parser_utils.cc b/docraft/src/docraft/craft/parser/docraft_shape_parser_utils.cc index 779db34..34aa3f7 100644 --- a/docraft/src/docraft/craft/parser/docraft_shape_parser_utils.cc +++ b/docraft/src/docraft/craft/parser/docraft_shape_parser_utils.cc @@ -20,6 +20,8 @@ #include #include +#include "docraft/exception/docraft_exceptions.h" + namespace docraft::craft::parser::detail { std::vector parse_points_attribute(const pugi::xml_node &node, const char *attr_name) { std::vector points; @@ -44,12 +46,12 @@ namespace docraft::craft::parser::detail { } auto comma_pos = token.find(','); if (comma_pos == std::string::npos) { - throw std::invalid_argument("Invalid point token: " + token); + throw docraft::exception::InvalidInputException("Invalid point token: " + token); } std::string x_str = token.substr(0, comma_pos); std::string y_str = token.substr(comma_pos + 1); if (x_str.empty() || y_str.empty()) { - throw std::invalid_argument("Invalid point token: " + token); + throw docraft::exception::InvalidInputException("Invalid point token: " + token); } float x = std::stof(x_str); float y = std::stof(y_str); diff --git a/docraft/src/docraft/craft/parser/docraft_table_parser.cc b/docraft/src/docraft/craft/parser/docraft_table_parser.cc index 098c18a..3962a26 100644 --- a/docraft/src/docraft/craft/parser/docraft_table_parser.cc +++ b/docraft/src/docraft/craft/parser/docraft_table_parser.cc @@ -19,114 +19,74 @@ #include #include "docraft/craft/parser/docraft_parser_helpers.h" +#include "docraft/exception/docraft_exceptions.h" #include "docraft/model/docraft_image.h" #include "docraft/model/docraft_table.h" #include "docraft/model/docraft_text.h" namespace docraft::craft::parser { - std::shared_ptr DocraftTableParser::parse(const pugi::xml_node &craft_language_source) { - auto table_node = std::make_shared(); - - // Baseline tweak for text vertical alignment inside cells. - if (auto baseline_attr = craft_language_source.attribute(elements::table::attribute::kBaselineOffset.data())) { - table_node->set_baseline_offset(baseline_attr.as_float()); - } - - // `model` is overloaded: - // - "horizontal"/"vertical" -> orientation only - // - JSON matrix -> data rows (horizontal only) - // - ${var} -> deferred JSON rows (templated) - bool has_model_json = false; - bool has_model_template = false; - bool has_header_json = false; - bool has_header_template = false; - bool require_body = false; - if (auto model_attr = craft_language_source.attribute(elements::table::attribute::kModel.data())) { - std::string model_str = model_attr.as_string(); - if (model_str == std::string{orientation::kVertical}) { - table_node->set_orientation(model::LayoutOrientation::kVertical); - } else if (model_str == std::string{orientation::kHorizontal}) { - table_node->set_orientation(model::LayoutOrientation::kHorizontal); - } else { - if (model_str.contains("${")) { - has_model_template = true; - } else { - auto model_type = model::DocraftTable::identify_model_type(model_str); - has_model_json = true; - if (model_type == model::DocraftModelType::kStringMatrix) { - table_node->set_model_type(model::DocraftModelType::kStringMatrix); - table_node->apply_json_rows(model_str); - } else if (model_type == model::DocraftModelType::kJsonObject) { - table_node->set_model_type(model::DocraftModelType::kJsonObject); - require_body = true; - } - } - table_node->set_model_template(model_str); - - } - } - // Optional `header` attribute for column titles: - // - JSON array -> titles - // - ${var} -> deferred JSON titles (templated) - if (auto header_attr = craft_language_source.attribute("header")) { - std::string header_str = header_attr.as_string(); - if (header_str.contains("${")) { - table_node->set_header_template(header_str); - has_header_template = true; - } else { - table_node->apply_json_header(header_str); - has_header_json = true; - } - } - - if (auto tile_attr = craft_language_source.attribute(elements::table::attribute::kTableTile.data())) { - table_node->set_default_cell_background(detail::get_docraft_color(tile_attr)); - } + namespace { + struct TableParseState { + bool has_model_json = false; + bool has_model_template = false; + bool has_header_json = false; + bool has_header_template = false; + bool require_body = false; + }; - auto parse_title_node = [](const pugi::xml_node &title) -> std::shared_ptr { - auto title_node = std::make_shared(title.child_value()); + model::TextAlignment parse_table_title_alignment(const pugi::xml_node &title) { if (auto alignment_attr = title.attribute(elements::table_title::attribute::kAlignment.data())) { - std::string alignment_str = alignment_attr.as_string(); + const std::string alignment_str = alignment_attr.as_string(); if (alignment_str == std::string{alignment::kLeft}) { - title_node->set_alignment(model::TextAlignment::kLeft); - } else if (alignment_str == std::string{alignment::kRight}) { - title_node->set_alignment(model::TextAlignment::kRight); - } else if (alignment_str == std::string{alignment::kJustified}) { - title_node->set_alignment(model::TextAlignment::kJustified); - } else if (alignment_str == std::string{alignment::kCenter}) { - title_node->set_alignment(model::TextAlignment::kCenter); - } else { - throw std::invalid_argument("Invalid table title alignment: " + alignment_str); + return model::TextAlignment::kLeft; } - } else { - title_node->set_alignment(model::TextAlignment::kCenter); + if (alignment_str == std::string{alignment::kRight}) { + return model::TextAlignment::kRight; + } + if (alignment_str == std::string{alignment::kJustified}) { + return model::TextAlignment::kJustified; + } + if (alignment_str == std::string{alignment::kCenter}) { + return model::TextAlignment::kCenter; + } + throw docraft::exception::InvalidInputException("Invalid table title alignment: " + alignment_str); } + return model::TextAlignment::kCenter; + } + + model::TextStyle parse_table_title_style(const pugi::xml_node &title) { if (auto style_attr = title.attribute(elements::table_title::attribute::kStyle.data())) { - std::string style_str = style_attr.as_string(); + const std::string style_str = style_attr.as_string(); if (style_str == std::string{style::kBold}) { - title_node->set_style(model::TextStyle::kBold); - } else if (style_str == std::string{style::kItalic}) { - title_node->set_style(model::TextStyle::kItalic); - } else if (style_str == std::string{style::kBoldItalic}) { - title_node->set_style(model::TextStyle::kBoldItalic); - } else if (style_str == std::string{style::kNormal}) { - title_node->set_style(model::TextStyle::kNormal); - } else { - throw std::invalid_argument("Invalid table title style: " + style_str); + return model::TextStyle::kBold; } - } else { - title_node->set_style(model::TextStyle::kBold); + if (style_str == std::string{style::kItalic}) { + return model::TextStyle::kItalic; + } + if (style_str == std::string{style::kBoldItalic}) { + return model::TextStyle::kBoldItalic; + } + if (style_str == std::string{style::kNormal}) { + return model::TextStyle::kNormal; + } + throw docraft::exception::InvalidInputException("Invalid table title style: " + style_str); } + return model::TextStyle::kBold; + } + + std::shared_ptr parse_table_title_node(const pugi::xml_node &title) { + auto title_node = std::make_shared(title.child_value()); + title_node->set_alignment(parse_table_title_alignment(title)); + title_node->set_style(parse_table_title_style(title)); if (auto color_attr = title.attribute(elements::table_title::attribute::kColor.data())) { title_node->set_color(detail::get_docraft_color(color_attr)); } return title_node; - }; + } - auto parse_background_color = [](const pugi::xml_node &node, - const char *primary_attr, - const char *alt_attr = nullptr) - -> std::optional { + std::optional parse_background_color(const pugi::xml_node &node, + const char *primary_attr, + const char *alt_attr = nullptr) { if (auto attr = node.attribute(primary_attr)) { return detail::get_docraft_color(attr); } @@ -136,91 +96,200 @@ namespace docraft::craft::parser { } } return std::nullopt; - }; + } - const bool is_vertical = table_node->orientation() == model::LayoutOrientation::kVertical; - auto table_header = craft_language_source.child(elements::kTHead.data()); - auto table_body = craft_language_source.child(elements::kTBody.data()); + TableParseState parse_table_model_and_header_attributes(const pugi::xml_node &craft_language_source, + const std::shared_ptr & + table_node) { + TableParseState state; - // JSON/templated data is mutually exclusive with explicit body nodes. - // `THead` can be used instead of `header`, but not together. - if (has_model_json || has_model_template || has_header_json || has_header_template) { - if (table_body) { - //Tbody is template for each item + // `model` is overloaded: + // - "horizontal"/"vertical" -> orientation only + // - JSON matrix -> data rows (horizontal only) + // - ${var} -> deferred JSON rows (templated) + if (auto model_attr = craft_language_source.attribute(elements::table::attribute::kModel.data())) { + std::string model_str = model_attr.as_string(); + if (model_str == std::string{orientation::kVertical}) { + table_node->set_orientation(model::LayoutOrientation::kVertical); + } else if (model_str == std::string{orientation::kHorizontal}) { + table_node->set_orientation(model::LayoutOrientation::kHorizontal); + } else { + if (model_str.contains("${")) { + state.has_model_template = true; + } else { + auto model_type = model::DocraftTable::identify_model_type(model_str); + state.has_model_json = true; + if (model_type == model::DocraftModelType::kStringMatrix) { + table_node->set_model_type(model::DocraftModelType::kStringMatrix); + table_node->apply_json_rows(model_str); + } else if (model_type == model::DocraftModelType::kJsonObject) { + table_node->set_model_type(model::DocraftModelType::kJsonObject); + state.require_body = true; + } + } + table_node->set_model_template(model_str); + } } - if ((has_header_json || has_header_template) && table_header) { - throw std::invalid_argument("Table JSON header cannot be combined with THead"); + + // Optional `header` attribute for column titles: + // - JSON array -> titles + // - ${var} -> deferred JSON titles (templated) + if (auto header_attr = craft_language_source.attribute("header")) { + std::string header_str = header_attr.as_string(); + if (header_str.contains("${")) { + table_node->set_header_template(header_str); + state.has_header_template = true; + } else { + table_node->apply_json_header(header_str); + state.has_header_json = true; + } } - if (table_node->orientation() == model::LayoutOrientation::kVertical) { - throw std::invalid_argument("Table JSON model does not support vertical model"); + + return state; + } + + void validate_table_model_header_constraints(const TableParseState &state, + const std::shared_ptr &table_node, + const pugi::xml_node &table_header, + const pugi::xml_node &table_body) { + // JSON/templated data is mutually exclusive with explicit body nodes. + // `THead` can be used instead of `header`, but not together. + if (state.has_model_json || state.has_model_template || state.has_header_json || state. + has_header_template) { + if (table_body) { + //Tbody is template for each item + } + if ((state.has_header_json || state.has_header_template) && table_header) { + throw docraft::exception::InvalidInputException("Table JSON header cannot be combined with THead"); + } + if (table_node->orientation() == model::LayoutOrientation::kVertical) { + throw docraft::exception::InvalidInputException("Table JSON model does not support vertical model"); + } } } - // Parse explicit THead (static titles). - if (table_header) { + void parse_explicit_table_header(const pugi::xml_node &table_header, + const std::shared_ptr &table_node, + const TableParseState &state, + const bool is_vertical) { + // Parse explicit THead (static titles). if (is_vertical) { int header_cols = 0; for (auto title: table_header.children()) { if (title.name() == std::string{elements::kHTitle}) { header_cols++; - auto title_node = parse_title_node(title); + auto title_node = parse_table_title_node(title); const auto bg = parse_background_color( title, elements::table_htitle::attribute::kBackgroundColor.data()); table_node->add_htitle_node(title_node, bg); } else if (title.name() == std::string{elements::kTitle}) { - throw std::invalid_argument("Title is reserved for text headings; use HTitle in table headers"); + throw docraft::exception::InvalidInputException( + "Title is reserved for text headings; use HTitle in table headers"); } else { - throw std::invalid_argument(std::string(title.name()) + " cannot be placed in a table header"); + throw docraft::exception::InvalidInputException( + std::string(title.name()) + " cannot be placed in a table header"); } } if (header_cols > 0) { table_node->set_content_cols(header_cols); table_node->set_cols(header_cols + 1); } - } else { - int col_number = 0; - const int existing_cols = table_node->content_cols(); - std::vector titles; - for (auto title: table_header.children()) { - if (title.name() == std::string{elements::kHTitle}) { - col_number++; - auto title_node = parse_title_node(title); - const auto bg = parse_background_color( - title, - elements::table_htitle::attribute::kBackgroundColor.data()); - table_node->add_title_node(title_node, bg); - titles.emplace_back(title.child_value()); - } else if (title.name() == std::string{elements::kTitle} || - title.name() == std::string{elements::kVTitle}) { - throw std::invalid_argument( - "Use HTitle in table headers (VTitle is only for vertical row labels)"); - } else { - throw std::invalid_argument(std::string(title.name()) + " cannot be placed in a table header"); + return; + } + + int col_number = 0; + const int existing_cols = table_node->content_cols(); + std::vector titles; + for (auto title: table_header.children()) { + if (title.name() == std::string{elements::kHTitle}) { + col_number++; + auto title_node = parse_table_title_node(title); + const auto bg = parse_background_color( + title, + elements::table_htitle::attribute::kBackgroundColor.data()); + table_node->add_title_node(title_node, bg); + titles.emplace_back(title.child_value()); + } else if (title.name() == std::string{elements::kTitle} || + title.name() == std::string{elements::kVTitle}) { + throw docraft::exception::InvalidInputException( + "Use HTitle in table headers (VTitle is only for vertical row labels)"); + } else { + throw docraft::exception::InvalidInputException( + std::string(title.name()) + " cannot be placed in a table header"); + } + } + // If JSON rows were already provided, header must match column count. + if ((state.has_model_json || state.has_model_template) && existing_cols > 0 && existing_cols != + col_number) { + throw docraft::exception::InvalidInputException("Table header columns do not match model columns"); + } + table_node->set_titles(titles); + table_node->set_cols(col_number); + table_node->set_content_cols(col_number); + } + + void parse_table_cell_content(const pugi::xml_node &col, + const std::shared_ptr &table_node, + int &row_value_cols) { + if (col.children().empty()) { + return; + } + + auto child = col.first_child(); + if (child.name() == std::string{elements::kText}) { + DocraftTextParser text_parser; + auto text_node = text_parser.parse(child); + if (auto width_attr = col.attribute(basic::attribute::kWidth.data())) { + const float explicit_width = width_attr.as_float(); + if (explicit_width <= 0.0F) { + throw docraft::exception::InvalidInputException("Cell width must be > 0"); } + text_node->set_width(explicit_width); } - // If JSON rows were already provided, header must match column count. - if ((has_model_json || has_model_template) && existing_cols > 0 && existing_cols != col_number) { - throw std::invalid_argument("Table header columns do not match model columns"); + const auto cell_bg = parse_background_color( + col, + elements::table_column::attribute::kBackgroundColor.data(), + elements::table_column::attribute::kTableTile.data()); + table_node->add_content_node(text_node, cell_bg); + row_value_cols++; + return; + } + + if (child.name() == std::string{elements::kImage}) { + DocraftImageParser image_parser; + auto image = image_parser.parse(child); + if (auto width_attr = col.attribute(basic::attribute::kWidth.data())) { + const float explicit_width = width_attr.as_float(); + if (explicit_width <= 0.0F) { + throw docraft::exception::InvalidInputException("Cell width must be > 0"); + } + image->set_width(explicit_width); } - table_node->set_titles(titles); - table_node->set_cols(col_number); - table_node->set_content_cols(col_number); + const auto cell_bg = parse_background_color( + col, + elements::table_column::attribute::kBackgroundColor.data(), + elements::table_column::attribute::kTableTile.data()); + table_node->add_content_node(image, cell_bg); + row_value_cols++; + return; } - } else if (!is_vertical && !has_model_json && !has_model_template && !has_header_json && !has_header_template) { - throw std::invalid_argument(std::string(elements::kTHead.data()) + - " tag not found, it is mandatory"); + + throw docraft::exception::InvalidInputException(std::string(child.name()) + + " is not supported in the table column"); } - // Parse body rows when using explicit TBody. - table_body = craft_language_source.child(elements::kTBody.data()); - if (table_body) { + void parse_explicit_table_body(const pugi::xml_node &table_body, + const std::shared_ptr &table_node, + const bool is_vertical) { + // Parse body rows when using explicit TBody. int row_count = 0; int max_value_cols = 0; std::vector v_titles; for (auto row: table_body.children()) { if (row.name() != std::string{elements::kRow}) { - throw std::invalid_argument(std::string(row.name()) + " cannot be placed in a table body"); + throw docraft::exception::InvalidInputException( + std::string(row.name()) + " cannot be placed in a table body"); } const auto row_bg = parse_background_color( @@ -235,10 +304,10 @@ namespace docraft::craft::parser { const std::string col_name = col.name(); if (is_vertical && col_name == std::string{elements::kVTitle}) { if (found_vtitle) { - throw std::invalid_argument("Only one VTitle is allowed per Row"); + throw docraft::exception::InvalidInputException("Only one VTitle is allowed per Row"); } found_vtitle = true; - auto title_node = parse_title_node(col); + auto title_node = parse_table_title_node(col); const auto bg = parse_background_color( col, elements::table_vtitle::attribute::kBackgroundColor.data()); @@ -248,55 +317,17 @@ namespace docraft::craft::parser { } if (col_name == std::string{elements::kCell}) { - if (!col.children().empty()) { - auto child = col.first_child(); - if (child.name() == std::string{elements::kText}) { - DocraftTextParser text_parser; - auto text_node = text_parser.parse(child); - if (auto width_attr = col.attribute(basic::attribute::kWidth.data())) { - const float explicit_width = width_attr.as_float(); - if (explicit_width <= 0.0F) { - throw std::invalid_argument("Cell width must be > 0"); - } - text_node->set_width(explicit_width); - } - const auto cell_bg = parse_background_color( - col, - elements::table_column::attribute::kBackgroundColor.data(), - elements::table_column::attribute::kTableTile.data()); - table_node->add_content_node(text_node, cell_bg); - row_value_cols++; - } else if (child.name() == std::string{elements::kImage}) { - DocraftImageParser image_parser; - auto image = image_parser.parse(child); - if (auto width_attr = col.attribute(basic::attribute::kWidth.data())) { - const float explicit_width = width_attr.as_float(); - if (explicit_width <= 0.0F) { - throw std::invalid_argument("Cell width must be > 0"); - } - image->set_width(explicit_width); - } - const auto cell_bg = parse_background_color( - col, - elements::table_column::attribute::kBackgroundColor.data(), - elements::table_column::attribute::kTableTile.data()); - table_node->add_content_node(image, cell_bg); - row_value_cols++; - } else { - throw std::runtime_error(std::string(child.name()) + - " is not supported in the table column"); - } - } + parse_table_cell_content(col, table_node, row_value_cols); } else if (!is_vertical && col_name == std::string{elements::kVTitle}) { - throw std::invalid_argument("VTitle is only allowed for vertical tables"); + throw docraft::exception::InvalidInputException("VTitle is only allowed for vertical tables"); } else { - throw std::runtime_error(std::string(col.name()) + - " is not supported in the table body"); + throw docraft::exception::InvalidInputException(std::string(col.name()) + + " is not supported in the table body"); } } if (is_vertical && !found_vtitle) { - throw std::invalid_argument("VTitle is mandatory for vertical table rows"); + throw docraft::exception::InvalidInputException("VTitle is mandatory for vertical table rows"); } max_value_cols = std::max(max_value_cols, row_value_cols); } @@ -316,7 +347,40 @@ namespace docraft::craft::parser { table_node->set_rows(row_count); } } - if (require_body) { + } // namespace + + std::shared_ptr DocraftTableParser::parse(const pugi::xml_node &craft_language_source) { + auto table_node = std::make_shared(); + + // Baseline tweak for text vertical alignment inside cells. + if (auto baseline_attr = craft_language_source.attribute(elements::table::attribute::kBaselineOffset.data())) { + table_node->set_baseline_offset(baseline_attr.as_float()); + } + + const TableParseState state = parse_table_model_and_header_attributes(craft_language_source, table_node); + + if (auto tile_attr = craft_language_source.attribute(elements::table::attribute::kTableTile.data())) { + table_node->set_default_cell_background(detail::get_docraft_color(tile_attr)); + } + + const bool is_vertical = table_node->orientation() == model::LayoutOrientation::kVertical; + auto table_header = craft_language_source.child(elements::kTHead.data()); + auto table_body = craft_language_source.child(elements::kTBody.data()); + + validate_table_model_header_constraints(state, table_node, table_header, table_body); + + if (table_header) { + parse_explicit_table_header(table_header, table_node, state, is_vertical); + } else if (!is_vertical && !state.has_model_json && !state.has_model_template && !state.has_header_json && ! + state.has_header_template) { + throw exception::InvalidInputException(std::string(elements::kTHead) + + " tag not found, it is mandatory"); + } + + if (table_body) { + parse_explicit_table_body(table_body, table_node, is_vertical); + } + if (state.require_body) { table_node->apply_json_rows(table_node->model_template()); } detail::configure_docraft_node_attributes(table_node, craft_language_source); diff --git a/docraft/src/docraft/craft/parser/docraft_text_parser.cc b/docraft/src/docraft/craft/parser/docraft_text_parser.cc index ff5a237..50c402b 100644 --- a/docraft/src/docraft/craft/parser/docraft_text_parser.cc +++ b/docraft/src/docraft/craft/parser/docraft_text_parser.cc @@ -17,6 +17,7 @@ #include "docraft/craft/parser/docraft_parser.h" #include "docraft/craft/parser/docraft_parser_helpers.h" +#include "docraft/exception/docraft_exceptions.h" #include "docraft/model/docraft_text.h" namespace docraft::craft::parser { @@ -62,7 +63,7 @@ namespace docraft::craft::parser { } else if (style_str == std::string{style::kNormal}) { text_node->set_style(model::TextStyle::kNormal); } else { - throw std::invalid_argument("Invalid text style: " + style_str); + throw docraft::exception::InvalidInputException("Invalid text style: " + style_str); } } if (auto alignment_attr = craft_language_source.attribute(elements::text::attribute::kAlignment.data())) { @@ -76,7 +77,7 @@ namespace docraft::craft::parser { } else if (alignment_str == std::string{alignment::kLeft}) { text_node->set_alignment(model::TextAlignment::kLeft); } else { - throw std::invalid_argument("Invalid text alignment: " + alignment_str); + throw docraft::exception::InvalidInputException("Invalid text alignment: " + alignment_str); } } if (auto underline_attr = craft_language_source.attribute(elements::text::attribute::kUnderline.data())) { diff --git a/docraft/src/docraft/craft/parser/docraft_triangle_parser.cc b/docraft/src/docraft/craft/parser/docraft_triangle_parser.cc index b64f350..613ea8d 100644 --- a/docraft/src/docraft/craft/parser/docraft_triangle_parser.cc +++ b/docraft/src/docraft/craft/parser/docraft_triangle_parser.cc @@ -21,6 +21,7 @@ #include "docraft/craft/docraft_craft_language_tokens.h" #include "docraft/craft/parser/docraft_parser_helpers.h" #include "docraft/craft/parser/docraft_shape_parser_utils.h" +#include "docraft/exception/docraft_exceptions.h" #include "docraft/model/docraft_triangle.h" namespace docraft::craft::parser { @@ -43,7 +44,7 @@ namespace docraft::craft::parser { elements::triangle::attribute::kPoints.data()); if (!points.empty()) { if (points.size() != 3U) { - throw std::invalid_argument("Triangle requires exactly 3 points"); + throw docraft::exception::InvalidInputException("Triangle requires exactly 3 points"); } triangle->set_points(points); } diff --git a/docraft/src/docraft/docraft_color.cc b/docraft/src/docraft/docraft_color.cc index ff78341..9ad4506 100644 --- a/docraft/src/docraft/docraft_color.cc +++ b/docraft/src/docraft/docraft_color.cc @@ -21,6 +21,7 @@ #include #include +#include "docraft/exception/docraft_exceptions.h" #include "docraft/utils/docraft_parser_utilis.h" namespace docraft { @@ -65,11 +66,11 @@ namespace docraft { } if ((hex_code.size() != 7 && hex_code.size() != 9) || hex_code[0] != '#') { - throw std::invalid_argument("Invalid hex code: " + hex_code); + throw docraft::exception::InvalidInputException("Invalid hex code: " + hex_code); } for (size_t i = 1; i < hex_code.size(); ++i) { if (!std::isxdigit(static_cast(hex_code[i]))) { - throw std::invalid_argument("Invalid hex code: " + hex_code); + throw docraft::exception::InvalidInputException("Invalid hex code: " + hex_code); } } //example of color: #RRGGBB or #RRGGBBAA diff --git a/docraft/src/docraft/docraft_document.cc b/docraft/src/docraft/docraft_document.cc index ee3168a..589f398 100644 --- a/docraft/src/docraft/docraft_document.cc +++ b/docraft/src/docraft/docraft_document.cc @@ -24,6 +24,7 @@ #include "docraft/layout/docraft_layout_engine.h" #include "docraft/layout/handler/docraft_layout_handler.h" +#include "docraft/backend/pdf/docraft_haru_backend_providers_factory.h" #include "docraft/renderer/docraft_pdf_renderer.h" #include "docraft/utils/docraft_font_registry.h" #include "docraft/utils/docraft_logger.h" @@ -31,7 +32,24 @@ #include "docraft/templating/docraft_template_engine.h" namespace docraft { + using exception::DocumentStateException; + using exception::RenderingFailedException; + namespace { + void apply_capability_providers_from_factory(const std::shared_ptr &context, + const std::shared_ptr + &factory) { + if (!context || !factory) { + return; + } + auto &rendering_service = context->edit_rendering(); + auto &layout_service = context->edit_layout(); + rendering_service.set_capability_providers_factory(factory); + if (const auto page_backend = rendering_service.page_rendering()) { + layout_service.set_page_dimensions(page_backend->page_width(), page_backend->page_height()); + } + } + std::string trim_copy(std::string_view source) { std::size_t start = 0; std::size_t end = source.size(); @@ -133,7 +151,14 @@ namespace docraft { return; } if (settings->has_page_format()) { - context->set_page_format(settings->page_size(), settings->page_orientation()); + auto &rendering_service = context->edit_rendering(); + auto &layout_service = context->edit_layout(); + if (const auto page_backend = rendering_service.edit_page_rendering()) { + page_backend->set_page_format(settings->page_size(), settings->page_orientation()); + layout_service.set_page_dimensions(page_backend->page_width(), page_backend->page_height()); + } else { + layout_service.set_page_format(settings->page_orientation()); + } } } @@ -143,10 +168,30 @@ namespace docraft { return; } if (settings->has_section_ratios()) { - context->set_section_ratios(settings->header_ratio(), - settings->body_ratio(), - settings->footer_ratio()); + context->edit_navigation().set_section_ratios(settings->header_ratio(), + settings->body_ratio(), + settings->footer_ratio()); + } + } + + std::filesystem::path resolve_font_path(const std::string &font_path) { + std::filesystem::path resolved_path(font_path); + if (resolved_path.is_absolute()) { + return resolved_path; } + + const auto current_directory = std::filesystem::current_path(); + auto current_directory_candidate = current_directory / resolved_path; + if (std::filesystem::exists(current_directory_candidate)) { + return current_directory_candidate; + } + + auto parent_directory_candidate = current_directory.parent_path() / resolved_path; + if (std::filesystem::exists(parent_directory_candidate)) { + return parent_directory_candidate; + } + + return resolved_path; } void apply_font_settings(const std::shared_ptr &settings) { @@ -158,29 +203,20 @@ namespace docraft { } for (const auto &font: settings->fonts()) { for (const auto &external_font: font.external_fonts) { - auto font_path = external_font.path; - std::filesystem::path resolved_path(font_path); - if (!resolved_path.is_absolute()) { - std::filesystem::path tried1 = std::filesystem::current_path() / resolved_path; - if (std::filesystem::exists(tried1)) { - resolved_path = tried1; - } else { - std::filesystem::path tried2 = - std::filesystem::current_path().parent_path() / resolved_path; - if (std::filesystem::exists(tried2)) { - resolved_path = tried2; - } - } - } - bool ok = utils::DocraftFontRegistry::instance().register_font( + const std::string font_path = external_font.path; + const std::filesystem::path resolved_path = resolve_font_path(font_path); + + const bool ok = utils::DocraftFontRegistry::instance().register_font( external_font.name, resolved_path.string()); + if (!ok) { LOG_ERROR("Failed to register font '" + external_font.name + "' from path '" + font_path + "' (resolved: '" + resolved_path.string() + "')"); - } else { - LOG_DEBUG( - "Registered font '" + external_font.name + "' from path '" + resolved_path.string() + "'"); + continue; } + + LOG_DEBUG( + "Registered font '" + external_font.name + "' from path '" + resolved_path.string() + "'"); } } } @@ -189,11 +225,13 @@ namespace docraft { DocraftDocument::DocraftDocument(const std::string &document_title) { config_.set_document_title(document_title); context_ = std::make_shared(); + capability_providers_factory_ = std::make_shared(); + apply_capability_providers_from_factory(context_, capability_providers_factory_); } void DocraftDocument::add_node(const std::shared_ptr &node) { if (!node) { - throw std::runtime_error("Null node cannot be added to the document"); + throw DocumentStateException("Null node cannot be added to the document"); } dom_.emplace_back(node); } @@ -212,7 +250,8 @@ namespace docraft { } void DocraftDocument::handle_node_rendering(const std::shared_ptr &node) { - auto page_backend = context_->edit_page_backend(); + auto &rendering_service = context_->edit_rendering(); + auto page_backend = rendering_service.edit_page_rendering(); const std::size_t page_count = page_backend ? page_backend->total_page_count() : 1; if (node->page_owner() == -1 && page_backend) { for (std::size_t i = 0; i < page_count; ++i) { @@ -230,8 +269,9 @@ namespace docraft { } void DocraftDocument::render() { + apply_capability_providers_from_factory(context_, capability_providers_factory_); context_->set_renderer(std::make_shared(context_)); - context_->set_font_applier(std::make_shared(context_)); + context_->edit_typography().set_font_applier(std::make_shared(context_)); LOG_DEBUG("Rendering document: " + config_.document_title()); configure_document_settings(); @@ -250,7 +290,7 @@ namespace docraft { layout::DocraftLayoutEngine layout_engine(context_); layout_engine.compute_document_layout(dom_); - const auto page_backend = context_->edit_page_backend(); + const auto page_backend = context_->edit_rendering().edit_page_rendering(); if (page_backend) { page_backend->go_to_first_page(); } @@ -264,14 +304,39 @@ namespace docraft { } } - context_->edit_rendering_backend()->set_document_metadata(config_.document_metadata()); - const std::string output_file_name = with_extension( - config_.document_title(), context_->rendering_backend()->file_extension()); - context_->rendering_backend()->save_to_file(with_directory(config_.document_path(), output_file_name)); + auto &rendering_service = context_->edit_rendering(); + services::CapabilityRequirements required_capabilities; + required_capabilities.metadata_backend = true; + required_capabilities.output_backend = true; + rendering_service.validate_capabilities(required_capabilities); + + const auto metadata_backend = rendering_service.edit_metadata_backend(); + if (!metadata_backend) { + throw exception::CapabilityUnavailableException("Metadata backend capability is not available"); + } + metadata_backend->set_document_metadata(config_.document_metadata()); + + const auto output_backend = rendering_service.output_backend(); + if (!output_backend) { + throw exception::CapabilityUnavailableException("Output backend capability is not available"); + } + const std::string extension = output_backend->file_extension(); + const std::string output_file_name = with_extension(config_.document_title(), extension); + const std::string output_path = with_directory(config_.document_path(), output_file_name); + output_backend->save_to_file(output_path); + } + + void DocraftDocument::set_capability_providers_factory( + const std::shared_ptr &capability_providers_factory) { + capability_providers_factory_ = capability_providers_factory + ? capability_providers_factory + : std::make_shared(); + apply_capability_providers_from_factory(context_, capability_providers_factory_); } - void DocraftDocument::set_backend(const std::shared_ptr &backend) { - context_->set_backend(backend); + void DocraftDocument::set_backend_providers_factory( + const std::shared_ptr &backend_providers_factory) { + set_capability_providers_factory(backend_providers_factory); } std::vector > DocraftDocument::nodes() const { @@ -320,97 +385,6 @@ namespace docraft { return context_; } - // Backward compatibility delegates to config_ - void DocraftDocument::set_document_title(const std::string &document_title) { - config_.set_document_title(document_title); - } - - const std::string &DocraftDocument::document_title() const { - return config_.document_title(); - } - - void DocraftDocument::set_document_path(const std::string &document_path) { - config_.set_document_path(document_path); - } - - const std::string &DocraftDocument::document_path() const { - return config_.document_path(); - } - - void DocraftDocument::set_settings(const std::shared_ptr &settings) { - config_.set_settings(settings); - } - - std::shared_ptr DocraftDocument::settings() const { - return config_.settings(); - } - - void DocraftDocument::set_document_metadata(const DocraftDocumentMetadata &metadata) { - config_.set_document_metadata(metadata); - } - - const DocraftDocumentMetadata &DocraftDocument::document_metadata() const { - return config_.document_metadata(); - } - - // Backward compatibility: DOM query methods - std::vector > - DocraftDocument::find_by_name(const std::string &name) const { - return management::DocraftDocumentQuery::find_by_name( - dom_, name); - } - - std::vector > DocraftDocument::take_by_name(const std::string &name) { - return management::DocraftDocumentQuery::take_by_name(dom_, name); - } - - std::shared_ptr DocraftDocument::find_first_by_name(const std::string &name) const { - return management::DocraftDocumentQuery::find_first_by_name( - dom_, name); - } - - std::shared_ptr DocraftDocument::take_first_by_name(const std::string &name) { - return management::DocraftDocumentQuery::take_first_by_name(dom_, name); - } - - std::shared_ptr DocraftDocument::find_last_by_name(const std::string &name) const { - return management::DocraftDocumentQuery::find_last_by_name( - dom_, name); - } - - std::shared_ptr DocraftDocument::take_last_by_name(const std::string &name) { - return management::DocraftDocumentQuery::take_last_by_name(dom_, name); - } - - // Backward compatibility: config shortcuts - void DocraftDocument::enable_auto_keywords(bool enabled) { - config_.enable_auto_keywords(enabled); - } - - bool DocraftDocument::auto_keywords_enabled() const { - return config_.auto_keywords_enabled(); - } - - void DocraftDocument::set_auto_keywords_config(const utils::DocraftKeywordExtractor::Config &config) { - config_.set_auto_keywords_config(config); - } - - const utils::DocraftKeywordExtractor::Config &DocraftDocument::auto_keywords_config() const { - return config_.auto_keywords_config(); - } - - void DocraftDocument::set_document_template_engine( - const std::shared_ptr &template_engine) { - config_.set_document_template_engine(template_engine); - } - - std::shared_ptr DocraftDocument::document_template_engine() const { - return config_.document_template_engine(); - } - - std::shared_ptr DocraftDocument::edit_document_template_engine() { - return config_.edit_document_template_engine(); - } void DocraftDocument::refresh_auto_keywords() { if (!config_.auto_keywords_enabled()) { diff --git a/docraft/src/docraft/docraft_document_context.cc b/docraft/src/docraft/docraft_document_context.cc index 79e5252..382e7f9 100644 --- a/docraft/src/docraft/docraft_document_context.cc +++ b/docraft/src/docraft/docraft_document_context.cc @@ -15,234 +15,81 @@ */ #include "docraft/docraft_document_context.h" -#include "docraft/backend/pdf/docraft_haru_backend.h" -#include "docraft/management/docraft_backend_cache.h" -#include "docraft/management/docraft_document_section_manager.h" +#include "docraft/exception/docraft_exceptions.h" +#include "docraft/renderer/docraft_renderer.h" namespace docraft { DocraftDocumentContext::DocraftDocumentContext() { - backend_ = std::make_shared(); - page_height_ = backend_->page_height(); - page_width_ = backend_->page_width(); - current_rect_width_ = page_width_; - refresh_backend_caches(); + rendering_ = std::make_unique(); + layout_ = std::make_unique(); + typography_ = std::make_unique(); + navigation_ = std::make_unique(); + sync_layout_page_dimensions_from_backend(); } DocraftDocumentContext::DocraftDocumentContext( - const std::shared_ptr &backend) : backend_( - backend) { - page_height_ = backend_->page_height(); - page_width_ = backend_->page_width(); - current_rect_width_ = page_width_; - refresh_backend_caches(); + const std::shared_ptr &capability_providers_factory) { + rendering_ = std::make_unique(capability_providers_factory); + layout_ = std::make_unique(); + typography_ = std::make_unique(); + navigation_ = std::make_unique(); + sync_layout_page_dimensions_from_backend(); } DocraftDocumentContext::~DocraftDocumentContext() = default; -#pragma region setter - void DocraftDocumentContext::set_renderer(const std::shared_ptr &renderer) { - renderer_ = renderer; - } - - void DocraftDocumentContext::set_current_rect_width(float current_rect_width) { - current_rect_width_ = current_rect_width; - } - - std::shared_ptr DocraftDocumentContext::font_applier() const { - return font_applier_; - } - - std::shared_ptr DocraftDocumentContext::edit_font_applier() { - return font_applier_; - } - void DocraftDocumentContext::refresh_backend_caches() { - backend_cache_.initialize_from_backend(backend_); + services::RenderingService &DocraftDocumentContext::edit_rendering() { + return *rendering_; } - management::DocraftDocumentSectionManager &DocraftDocumentContext::section_manager() { - return section_manager_; + const services::RenderingService &DocraftDocumentContext::rendering() const { + return *rendering_; } - const management::DocraftDocumentSectionManager &DocraftDocumentContext::section_manager() const { - return section_manager_; + services::LayoutService &DocraftDocumentContext::edit_layout() { + return *layout_; } - management::DocraftBackendCache &DocraftDocumentContext::backend_cache() { - return backend_cache_; + const services::LayoutService &DocraftDocumentContext::layout() const { + return *layout_; } - const management::DocraftBackendCache &DocraftDocumentContext::backend_cache() const { - return backend_cache_; + services::TypographyService &DocraftDocumentContext::edit_typography() { + return *typography_; } - 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(); - current_rect_width_ = page_width_; - refresh_backend_caches(); + const services::TypographyService &DocraftDocumentContext::typography() const { + return *typography_; } - void DocraftDocumentContext::set_page_format(model::DocraftPageSize size, - model::DocraftPageOrientation orientation) { - const auto backend = backend_cache_.edit_page_backend(); - if (backend) { - backend->set_page_format(size, orientation); - page_height_ = backend->page_height(); - page_width_ = backend->page_width(); - current_rect_width_ = page_width_; - } + services::NavigationService &DocraftDocumentContext::edit_navigation() { + return *navigation_; } - void DocraftDocumentContext::set_font_applier(const std::shared_ptr &font_applier) { - font_applier_ = font_applier; - } -#pragma endregion -#pragma region getter - std::shared_ptr DocraftDocumentContext::rendering_backend() const { - return backend_; + const services::NavigationService &DocraftDocumentContext::navigation() const { + return *navigation_; } - std::shared_ptr DocraftDocumentContext::edit_rendering_backend() { - return backend_; - } - - DocraftCursor &DocraftDocumentContext::cursor() { - return cursor_; - } - - float DocraftDocumentContext::available_space() const { - return current_rect_width_; + void DocraftDocumentContext::set_renderer(const std::shared_ptr &renderer) { + renderer_ = renderer; } - std::shared_ptr DocraftDocumentContext::renderer() { + std::shared_ptr DocraftDocumentContext::renderer() const { if (!renderer_) { - throw std::runtime_error("docraft/renderer not set in DocraftPDFContext"); + throw docraft::exception::DocumentStateException("Renderer not set in DocraftDocumentContext"); } return renderer_; } - float DocraftDocumentContext::page_height() const { - return page_height_; - } - - float DocraftDocumentContext::page_width() const { - return page_width_; - } - - void DocraftDocumentContext::go_to_first_page() { - const auto backend = backend_cache_.edit_page_backend(); - if (backend) { - backend->go_to_first_page(); - } - } - - void DocraftDocumentContext::go_to_previous_page() { - const auto backend = backend_cache_.edit_page_backend(); - if (backend) { - backend->go_to_previous_page(); - } + std::shared_ptr DocraftDocumentContext::edit_renderer() { + return renderer_; } - void DocraftDocumentContext::go_to_last_page() { - const auto backend = backend_cache_.edit_page_backend(); - if (backend) { - backend->go_to_last_page(); + void DocraftDocumentContext::sync_layout_page_dimensions_from_backend() { + auto &rendering_service = *rendering_; + auto &layout_service = *layout_; + if (const auto page_backend = rendering_service.page_rendering()) { + layout_service.set_page_dimensions(page_backend->page_width(), page_backend->page_height()); } } - - // Backward compatibility delegates to backend_cache() - std::shared_ptr DocraftDocumentContext::line_backend() const { - return backend_cache_.line_backend(); - } - - std::shared_ptr DocraftDocumentContext::edit_line_backend() { - return backend_cache_.edit_line_backend(); - } - - std::shared_ptr DocraftDocumentContext::shape_backend() const { - return backend_cache_.shape_backend(); - } - - std::shared_ptr DocraftDocumentContext::edit_shape_backend() { - return backend_cache_.edit_shape_backend(); - } - - std::shared_ptr DocraftDocumentContext::text_backend() const { - return backend_cache_.text_backend(); - } - - std::shared_ptr DocraftDocumentContext::edit_text_backend() { - return backend_cache_.edit_text_backend(); - } - - std::shared_ptr DocraftDocumentContext::image_backend() const { - return backend_cache_.image_backend(); - } - - std::shared_ptr DocraftDocumentContext::edit_image_backend() { - return backend_cache_.edit_image_backend(); - } - - std::shared_ptr DocraftDocumentContext::page_backend() const { - return backend_cache_.page_backend(); - } - - std::shared_ptr DocraftDocumentContext::edit_page_backend() { - return backend_cache_.edit_page_backend(); - } - - // Backward compatibility delegates to section_manager() - void DocraftDocumentContext::set_header(const std::shared_ptr &header) { - section_manager_.set_header(header); - } - - std::shared_ptr DocraftDocumentContext::header() const { - return section_manager_.header(); - } - - std::shared_ptr DocraftDocumentContext::edit_header() { - return section_manager_.edit_header(); - } - - void DocraftDocumentContext::set_body(const std::shared_ptr &body) { - section_manager_.set_body(body); - } - - std::shared_ptr DocraftDocumentContext::body() const { - return section_manager_.body(); - } - - std::shared_ptr DocraftDocumentContext::edit_body() { - return section_manager_.edit_body(); - } - - void DocraftDocumentContext::set_footer(const std::shared_ptr &footer) { - section_manager_.set_footer(footer); - } - - std::shared_ptr DocraftDocumentContext::footer() const { - return section_manager_.footer(); - } - - std::shared_ptr DocraftDocumentContext::edit_footer() { - return section_manager_.edit_footer(); - } - - void DocraftDocumentContext::set_section_ratios(float header_ratio, float body_ratio, float footer_ratio) { - section_manager_.set_section_ratios(header_ratio, body_ratio, footer_ratio); - } - - float DocraftDocumentContext::header_ratio() const { - return section_manager_.header_ratio(); - } - - float DocraftDocumentContext::body_ratio() const { - return section_manager_.body_ratio(); - } - - float DocraftDocumentContext::footer_ratio() const { - return section_manager_.footer_ratio(); - } -#pragma endregion -} // docraft +} // namespace docraft diff --git a/docraft/src/docraft/generic/docraft_font_applier.cc b/docraft/src/docraft/generic/docraft_font_applier.cc index adbbda8..e992f42 100644 --- a/docraft/src/docraft/generic/docraft_font_applier.cc +++ b/docraft/src/docraft/generic/docraft_font_applier.cc @@ -115,8 +115,8 @@ namespace docraft::generic { } bool DocraftFontApplier::is_font_supported(const std::string &name, const char *encoder) const { - auto backend = context_->rendering_backend(); - if (!backend) { + const auto font_backend = context_->rendering().font_backend(); + if (!font_backend) { return false; } auto ®istry = utils::DocraftFontRegistry::instance(); @@ -130,7 +130,7 @@ namespace docraft::generic { if (it == fonts_.end() || it->second.empty()) { return false; } - return backend->can_use_font(it->second, encoder); + return font_backend->can_use_font(it->second, encoder); } const char *DocraftFontApplier::load_font_data(const std::string &name) const { @@ -199,11 +199,11 @@ namespace docraft::generic { } // IMPORTANT: Always load/register into THIS backend (even if file already existed) - auto backend = context_->rendering_backend(); - if (!backend) { + const auto font_backend = context_->rendering().font_backend(); + if (!font_backend) { return nullptr; } - const char *internal_name = backend->register_ttf_font_from_file(temp_path.string(), true); + const char *internal_name = font_backend->register_ttf_font_from_file(temp_path.string(), true); if (!internal_name) { LOG_ERROR("Cannot load internal font file: " + temp_path.string()); return nullptr; @@ -218,8 +218,8 @@ namespace docraft::generic { void DocraftFontApplier::apply_font( const std::shared_ptr &node) const { - auto backend = context_->rendering_backend(); - if (!backend) { + const auto font_backend = context_->rendering().font_backend(); + if (!font_backend) { return; } const std::string base_name = node->font_name(); @@ -251,7 +251,7 @@ namespace docraft::generic { if (it == fonts_.end() || it->second.empty()) { return; } - backend->set_font(it->second, node->font_size(), encoder); + font_backend->set_font(it->second, node->font_size(), encoder); } void DocraftFontApplier::set_font_encoding(const std::string &font_name, bool utf8) { diff --git a/docraft/src/docraft/layout/docraft_layout_engine.cc b/docraft/src/docraft/layout/docraft_layout_engine.cc index a7daf4f..b022abb 100644 --- a/docraft/src/docraft/layout/docraft_layout_engine.cc +++ b/docraft/src/docraft/layout/docraft_layout_engine.cc @@ -16,596 +16,41 @@ #include "docraft/layout/docraft_layout_engine.h" -#include -#include -#include +#include +#include -#include "docraft/layout/handler/docraft_basic_layout_handler.h" -#include "docraft/layout/handler/docraft_layout_blank_line.h" -#include "docraft/layout/handler/docraft_layout_handler.h" -#include "docraft/layout/handler/docraft_layout_list_handler.h" -#include "docraft/layout/handler/docraft_layout_table_handler.h" -#include "docraft/layout/handler/docraft_layout_text_handler.h" - -#include "docraft/model/docraft_header.h" -#include "docraft/model/docraft_body.h" -#include "docraft/model/docraft_footer.h" -#include "docraft/model/docraft_list.h" -#include "docraft/model/docraft_table.h" -#include "docraft/model/docraft_foreach.h" -#include "docraft/model/docraft_new_page.h" - -namespace { - /** - * Counts how many rows of the table can fit in the remaining vertical space of the body section. - * It checks the bottom y-coordinate of each row's cells against the body bottom y-coordinate. - * If a row has no cells, it is considered not to fit. - * @param table The table to check. - * @param body_bottom_y The y-coordinate of the bottom of the body section. - * @return The number of rows that can fit in the remaining space. - */ - std::size_t count_rows_fit_horizontal(const docraft::model::DocraftTable &table, float body_bottom_y) { - std::size_t fit = 0; - const auto grid = table.content_nodes(); - for (const auto &row: grid) { - float row_bottom = std::numeric_limits::infinity(); - bool found = false; - for (const auto &cell: row) { - if (cell) { - //check all cells in the row and find the lowest bottom y-coordinate - row_bottom = cell->anchors().bottom_left.y; - found = true; - break; - } - } - //if no cells found in the row, consider it not to fit - if (!found) { - return fit; - } - //if the lowest bottom y-coordinate of the row is above the body bottom y-coordinate, it fits - if (row_bottom < body_bottom_y) { - return fit; - } - ++fit; //otherwise, it fits and we check the next row - } - return fit; - } - - /** - * Similar to count_rows_fit_horizontal but checks the title rows of the table instead. - * If a title row has no cells, it is considered not to fit. - */ - std::size_t count_rows_fit_vertical(const docraft::model::DocraftTable &table, float body_bottom_y) { - std::size_t fit = 0; - const auto &titles = table.title_nodes(); - for (const auto &title: titles) { - if (!title) { - return fit; - } - const float row_bottom = title->anchors().bottom_left.y; - if (row_bottom < body_bottom_y) { - return fit; - } - ++fit; - } - return fit; - } -} // namespace +#include "docraft_layout_engine_impl.h" namespace docraft::layout { DocraftLayoutEngine::DocraftLayoutEngine(const std::shared_ptr &context, const bool reset_cursor) - : context_(context) { - configure_handlers(context); - if (reset_cursor && context) { - context->cursor().move_to(0, context->page_height()); - } + : impl_(std::make_unique(context, reset_cursor)) { } - void DocraftLayoutEngine:: - configure_handlers(const std::shared_ptr &context) { - list_handler_ = nullptr; - handlers_.emplace_back(std::make_unique(context)); - handlers_.emplace_back(std::make_unique(context)); - handlers_.emplace_back(std::make_unique(context)); - handlers_.emplace_back(std::make_unique(context)); - auto list_handler = std::make_unique(context); - list_handler_ = list_handler.get(); - handlers_.emplace_back(std::move(list_handler)); - handlers_.emplace_back(std::make_unique(context)); - } - - const std::shared_ptr &DocraftLayoutEngine::context() const { - return context_; - } - - model::DocraftTransform DocraftLayoutEngine::compute_max_rect(const std::vector &boxes) { - if (boxes.empty()) { - return model::DocraftTransform{}; - } - - float min_x = boxes[0].anchors().top_left.x; - float max_x = boxes[0].anchors().top_right.x; - float min_y = boxes[0].anchors().bottom_left.y; - float max_y = boxes[0].anchors().top_left.y; + DocraftLayoutEngine::DocraftLayoutEngine(DocraftLayoutEngine &&) noexcept = default; - for (const auto &box: boxes) { - min_x = std::min(min_x, box.anchors().top_left.x); - max_x = std::max(max_x, box.anchors().top_right.x); - min_y = std::min(min_y, box.anchors().bottom_left.y); - max_y = std::max(max_y, box.anchors().top_left.y); - } + DocraftLayoutEngine &DocraftLayoutEngine::operator=(DocraftLayoutEngine &&) noexcept = default; - float width = max_x - min_x; - float height = max_y - min_y; + DocraftLayoutEngine::~DocraftLayoutEngine() = default; - return model::DocraftTransform({.x = min_x, .y = max_y}, width, height); + const std::shared_ptr &DocraftLayoutEngine::context() const { + return impl_->context(); } - bool DocraftLayoutEngine::compute_node(const std::shared_ptr &node, - model::DocraftTransform *box, - DocraftCursor &cursor) const { - for (const auto &handler: handlers_) { - if (handler->handle(node, box, cursor)) { - return true; - } - } - return false; + model::DocraftTransform DocraftLayoutEngine::compute_max_rect(const std::vector &boxes) { + return Impl::compute_max_rect(boxes); } - model::DocraftTransform DocraftLayoutEngine::compute_layout(const std::shared_ptr &node) { - if (!node->visible()) { - return model::DocraftTransform{}; - } - auto &cursor = context()->cursor(); - return compute_layout(node, cursor); + return impl_->compute_layout(node); } model::DocraftTransform DocraftLayoutEngine::compute_layout(const std::shared_ptr &node, DocraftCursor &cursor) { - if (!node->visible()) { - return model::DocraftTransform{}; - } - std::vector child_boxes; - float max_width = context()->available_space(); - const float flow_origin_x = cursor.x(); - const float flow_origin_y = cursor.y(); - const bool is_absolute = (node->position_mode() == model::DocraftPositionType::kAbsolute); - DocraftCursor local_cursor = cursor; - DocraftCursor &active_cursor = is_absolute ? local_cursor : cursor; - if (is_absolute) { - active_cursor.move_to(flow_origin_x + node->position().x, flow_origin_y - node->position().y); - } - DocraftCursor local_node_cursor = active_cursor; - DocraftCursor *layout_cursor = &active_cursor; - const bool layout_text_flow = !is_absolute && - (std::dynamic_pointer_cast(node) || - std::dynamic_pointer_cast(node)); - if (layout_text_flow) { - local_node_cursor.move_to(active_cursor.x(), active_cursor.y()); - layout_cursor = &local_node_cursor; - } - bool rect_uses_origin_cursor = false; - DocraftCursor rect_origin_cursor = active_cursor; - if (auto rect_container = std::dynamic_pointer_cast(node)) { - if (std::dynamic_pointer_cast(node)) { - // Sections handle their own padding/margins; don't override cursor here. - } else if (!rect_container->children().empty()) { - DocraftCursor rect_cursor = active_cursor; - if (rect_container->position_mode() == model::DocraftPositionType::kAbsolute) { - rect_cursor.move_to(flow_origin_x + rect_container->position().x, - flow_origin_y - rect_container->position().y); - } else { - rect_cursor.move_to(active_cursor.x(), active_cursor.y()); - } - // Ensure children are laid out relative to the rectangle's top-left. - rect_container->set_position({.x = rect_cursor.x(), .y = rect_cursor.y()}); - rect_origin_cursor = rect_cursor; - rect_uses_origin_cursor = true; - local_node_cursor = rect_cursor; - layout_cursor = &local_node_cursor; - if (rect_container->width() > 0.0F) { - max_width = rect_container->width(); - } - context()->set_current_rect_width(max_width); - } - } - std::shared_ptr section_node = nullptr; - float section_content_bottom = 0.0F; - bool section_has_bounds = false; - if (auto section = std::dynamic_pointer_cast(node)) { - section_node = section; - const float left_margin = section_node->margin_left(); - const float right_margin = section_node->margin_right(); - const float top_margin = section_node->margin_top(); - const float padding = std::max(0.0F, section_node->padding()); - float base_x = section_node->position().x; - if (base_x == 0.0F && left_margin > 0.0F) { - base_x = left_margin; - } - active_cursor.move_to(base_x, active_cursor.y() - top_margin - padding); // vertical inset only - if (section_node->width() > 0.0F) { - max_width = section_node->width(); - } else { - max_width = max_width - left_margin - right_margin; // width from left margin to right margin - } - context()->set_current_rect_width(max_width); - if (section_node->height() > 0.0F) { - section_content_bottom = section_node->position().y - section_node->height() + section_node-> - margin_bottom() + padding; - section_has_bounds = true; - } - } - if (std::dynamic_pointer_cast(node)) { - //Move the cursor direction based on layout orientation to layout children correctly - auto layout_node = std::dynamic_pointer_cast(node); - if (layout_node->orientation() == model::LayoutOrientation::kHorizontal) { - layout_cursor->push_direction(DocraftCursorDirection::kHorizontal); - } else { - layout_cursor->push_direction(DocraftCursorDirection::kVertical); - } - } - //Process nodes from here - if (auto list_node = std::dynamic_pointer_cast(node)) { - if (!list_handler_) { - throw std::runtime_error("DocraftLayoutListHandler not configured"); - } - DocraftCursor list_cursor = *layout_cursor; - list_handler_->compute_children( - list_node, - list_cursor, - child_boxes, - [this](const std::shared_ptr &child, DocraftCursor &child_cursor) { - return compute_layout(child, child_cursor); - }, - max_width); - } else if (std::dynamic_pointer_cast(node)) { - auto container_node = std::dynamic_pointer_cast(node); - if (!container_node->children().empty()) { - bool has_variable_weight = false; - for (const auto &child: container_node->children()) { - if (child->weight() == -1.0F) { - has_variable_weight = true; - break; - } - } - if (has_variable_weight) { - // If no weight is assigned to children, distribute weights equally - - const float equal_weight = 1.0F / static_cast(container_node->children().size()); - for (const auto &child: container_node->children()) { - child->set_weight(equal_weight); - } - } - } - const float saved_available_space = context()->available_space(); - const bool is_horizontal = (layout_cursor->direction() == DocraftCursorDirection::kHorizontal); - const std::size_t child_count = container_node->children().size(); - float available_width_for_children = max_width; - if (is_horizontal && child_count > 1) { - // Reserve fixed gaps between columns, then distribute the remaining width by weight. - const float total_spacing = kHorizontalSpacing_ * static_cast(child_count - 1); - available_width_for_children = std::max(0.0F, max_width - total_spacing); - } - for (const auto &child: container_node->children()) { - if (child->z_index() == node->z_index()) { - if (is_horizontal) { - context()->set_current_rect_width(available_width_for_children * child->weight()); - } else { - context()->set_current_rect_width(max_width); - } - const float start_x = layout_cursor->x(); - const float start_y = layout_cursor->y(); - const float allocated_width = is_horizontal - ? available_width_for_children * child->weight() - : max_width; - auto child_box = compute_layout(child, *layout_cursor); - - - child_boxes.emplace_back(child_box); - if (is_horizontal) { - // Advance to next column using allocated width, not the rendered width. - layout_cursor->move_to(start_x + allocated_width + kHorizontalSpacing_, start_y); - } - if (section_has_bounds && layout_cursor->y() < section_content_bottom) { - layout_cursor->set_y(section_content_bottom); - } - } - } - context()->set_current_rect_width(saved_available_space); - } - - auto max_rect = compute_max_rect(child_boxes); - - if (rect_uses_origin_cursor) { - if (!compute_node(node, &max_rect, rect_origin_cursor)) { - throw std::runtime_error("compute node failed"); - } - } else if (!compute_node(node, &max_rect, *layout_cursor)) { - throw std::runtime_error("compute node failed"); - } - node->set_position(max_rect.position()); - node->set_width(max_rect.width()); - node->set_height(max_rect.height()); - if (!is_absolute && active_cursor.direction() == DocraftCursorDirection::kHorizontal) { - // Advance cursor to the next column start, keeping a fixed horizontal gap. - cursor.move_to(max_rect.anchors().top_right.x + kHorizontalSpacing_, max_rect.anchors().top_right.y); - } else if (!is_absolute) { - // Advance cursor down with a fixed vertical gap or per-node padding (whichever is larger). - const float spacing = std::max(kVerticalSpacing_, node->padding()); - float next_y = max_rect.anchors().bottom_left.y - spacing; - if (next_y < 0.0F) { - next_y = 0.0F; - } - cursor.move_to(flow_origin_x, next_y); - } - return max_rect; - } - - float DocraftLayoutEngine::compute_width(const std::shared_ptr &node) const { - float margin_left = node->margin_left(); - float margin_right = node->margin_right(); - return context()->page_width() - (margin_left + margin_right); - } - - void DocraftLayoutEngine::assign_page_owner_recursive(const std::shared_ptr &node, - int page) const { - if (!node) { - return; - } - node->set_page_owner(page); - // Recursively assign page owner to children if it's a container - if (auto container = std::dynamic_pointer_cast(node)) { - for (const auto &child: container->children()) { - assign_page_owner_recursive(child, page); - } - } - //handle table children separately since they are not in the normal children list - if (auto table = std::dynamic_pointer_cast(node)) { - for (const auto &title: table->title_nodes()) { - assign_page_owner_recursive(title, page); - } - for (const auto &title: table->htitle_nodes()) { - assign_page_owner_recursive(title, page); - } - for (const auto &row: table->content_nodes()) { - for (const auto &cell: row) { - assign_page_owner_recursive(cell, page); - } - } - } + return impl_->compute_layout(node, cursor); } void DocraftLayoutEngine::compute_document_layout(const std::vector > &nodes) { - const Sections sections = split_sections(nodes); - if (!sections.body) { - throw std::runtime_error("Document must have a body section"); - } - if (const auto page_backend = context()->edit_page_backend()) { - page_backend->go_to_first_page(); - } - const SectionPlan plan = build_section_plan(sections); - if (plan.header_to_render) { - layout_header_section(sections.header, plan.header_ratio); - } - if (plan.body_to_render) { - layout_body_section(sections.body, sections.header, plan); - } - if (plan.footer_to_render) { - layout_footer_section(sections.footer, sections.body, plan); - } - } - - DocraftLayoutEngine::Sections DocraftLayoutEngine::split_sections( - const std::vector > &nodes) const { - Sections sections; - for (const auto &node: nodes) { - if (const auto header_node = std::dynamic_pointer_cast(node)) { - sections.header = header_node; - } else if (const auto body_node = std::dynamic_pointer_cast(node)) { - sections.body = body_node; - } else if (const auto footer_node = std::dynamic_pointer_cast(node)) { - sections.footer = footer_node; - } - } - return sections; - } - - DocraftLayoutEngine::SectionPlan DocraftLayoutEngine::build_section_plan(const Sections §ions) const { - SectionPlan plan; - const float base_header_ratio = context()->header_ratio(); - const float base_body_ratio = context()->body_ratio(); - const float base_footer_ratio = context()->footer_ratio(); - - plan.header_ratio = sections.header ? base_header_ratio : 0.0F; - plan.footer_ratio = sections.footer ? base_footer_ratio : 0.0F; - plan.body_ratio = base_body_ratio; - plan.header_to_render = sections.header && sections.header->visible(); - plan.body_to_render = sections.body && sections.body->visible(); - plan.footer_to_render = sections.footer && sections.footer->visible(); - - if (!plan.header_to_render) { - plan.body_ratio += base_header_ratio; - } - if (!plan.footer_to_render) { - plan.body_ratio += base_footer_ratio; - } - return plan; - } - - void DocraftLayoutEngine::layout_header_section( - const std::shared_ptr &header, - const float header_ratio) { - header->set_position({.x = header->margin_left(), .y = context()->page_height()}); - header->set_width(compute_width(header)); - header->set_height(context()->page_height() * header_ratio); - context()->cursor().move_to(header->position().x, header->position().y); - (void) compute_layout(header, context()->cursor()); - header->set_position({.x = header->margin_left(), .y = context()->page_height()}); - header->set_width(compute_width(header)); - header->set_height(context()->page_height() * header_ratio); - assign_page_owner_recursive(header, -1); //always - } - - void DocraftLayoutEngine::layout_body_section( - const std::shared_ptr &body, - const std::shared_ptr &header, - const SectionPlan &plan) { - float body_start_y = context()->page_height(); - if (plan.header_to_render) { - body_start_y = header->anchors().bottom_left.y; - } - const float body_height = context()->page_height() * plan.body_ratio; - - body->set_position({.x = body->margin_left(), .y = body_start_y}); - body->set_width(compute_width(body)); - body->set_height(body_height); - const float footer_height = plan.footer_to_render ? context()->page_height() * plan.footer_ratio : 0.0F; - const float body_bottom_y = (body_start_y - body_height) + body->margin_bottom() + footer_height; - // The y-coordinate of the bottom of the body content area, accounting for footer if present - context()->set_current_rect_width(body->width()); - - DocraftCursor body_cursor; //Use a custom cursor to not affect the main one - body_cursor.move_to(body->position().x, body_start_y); - - int current_page = 1; - const auto page_backend = context()->edit_page_backend(); - if (page_backend) { - current_page = static_cast(page_backend->current_page_number()); - } - - // Layout body children and handle pagination if needed. - if (auto body_container = std::dynamic_pointer_cast(body)) { - /** - * This lambda recursively processes body children, handling pagination logic in place. It checks for explicit new page directives, - * handles foreach loops by processing their children in place, and checks for overflow after layout to determine if a new page is needed. By using a lambda, - * we maintain access to local variables like body_cursor and current_page without needing to pass them through multiple function parameters. - * @param child The current child node being processed. - * @param index_ptr Optional pointer to the current index in the container's child list, used for inserting new nodes if a table is split across pages. If nullptr, - * it indicates that we're processing children of a foreach loop and should not modify the container's child list directly. - */ - std::function &, std::size_t *)> process_child; - // Use a lambda to allow recursion while maintaining access to local variables like body_cursor and current_page - process_child = [this,page_backend,¤t_page,&body_cursor,&body,body_start_y, process_child, - body_bottom_y, body_container](const std::shared_ptr &child, - const std::size_t *index_ptr) { - if (!child) { - return; - } - if (std::dynamic_pointer_cast(child)) { - // Handle explicit new page directive - if (page_backend) { - page_backend->add_new_page(); - ++current_page; - } - // Move cursor to top of new page - body_cursor.reset_direction(); - body_cursor.move_to(body->position().x, body_start_y); - return; - } - // Handle foreach loops by processing their children in place, ensuring they inherit the correct page owner and layout context - if (auto foreach_node = std::dynamic_pointer_cast(child)) { - foreach_node->set_page_owner(-1); - const auto &foreach_children = foreach_node->children(); - for (std::size_t i = 0; i < foreach_children.size(); ++i) { - const auto &foreach_child = foreach_children[i]; - if (std::dynamic_pointer_cast(foreach_child) && - i + 1 == foreach_children.size()) { - // Skip trailing NewPage to avoid an extra blank page. - continue; - } - process_child(foreach_child, nullptr); - // Process foreach children with the same lambda, but pass nullptr for index since we're not modifying the container's child list here. - } - return; - } - - assign_page_owner_recursive(child, current_page); - // Ensure layout sees the correct page owner, especially for new nodes created by foreach processing. - DocraftCursor child_start_cursor = body_cursor; - const auto child_box = compute_layout(child, body_cursor); // Updates body_cursor - const bool overflows_body = - child->position_mode() != model::DocraftPositionType::kAbsolute && - child_box.anchors().bottom_left.y < body_bottom_y; - // Check if the child overflows the body section (only for non-absolute positioned nodes) - if (!overflows_body) { - return; - } - //Handle table splitting across pages if the overflowing child is a table. If it can be split, we split it and insert the remainder - //back into the container to be processed on the next page. If it can't be split, we just move it to the next page. - if (auto table = std::dynamic_pointer_cast(child)) { - const auto total_rows = static_cast(table->rows()); - const auto fit_rows = table->orientation() == model::LayoutOrientation::kVertical - ? count_rows_fit_vertical(*table, body_bottom_y) - : count_rows_fit_horizontal(*table, body_bottom_y); - if (fit_rows > 0 && fit_rows < total_rows) { - // Split the table and insert the remainder back into the container for the next page. - // We only do this if at least one row can fit on the current page to avoid creating empty tables. - if (auto remainder = table->split_after_row(fit_rows, true)) { - if (index_ptr) { - // Only modify the container's child list if we're processing top-level children of the body, not foreach children. - body_container->insert_child(*index_ptr + 1, remainder); - // Insert after original - } - body_cursor = child_start_cursor; - assign_page_owner_recursive(child, current_page); - // Re-assign page owner to the original table before re-layout in case it has changed due to foreach processing. - (void) compute_layout(child, body_cursor); - //Re-layout the original table with just the fitting rows on the current page. - if (page_backend) { - // Move to next page and update current_page for the remainder - page_backend->add_new_page(); - ++current_page; - } - body_cursor.reset_direction(); - body_cursor.move_to(body->position().x, body_start_y); - assign_page_owner_recursive(remainder, current_page); - return; - } - } - } - if (page_backend) { - // Move to next page and update current_page - page_backend->add_new_page(); - ++current_page; - } - body_cursor.reset_direction(); - body_cursor.move_to(body->position().x, body_start_y); // Move cursor to top for new page - assign_page_owner_recursive(child, current_page); // Update page owner before re-layout - (void) compute_layout(child, body_cursor); // Re-layout the child on the new page - }; - - // Start processing body children with the lambda, passing the index pointer for top-level children to allow table splitting logic to modify the container's child list if needed. - std::size_t index = 0; - while (index < body_container->children().size()) { - auto child = body_container->children()[index]; - process_child(child, &index); - ++index; - } - } - - body->set_position({.x = body->margin_left(), .y = body_start_y}); - body->set_width(compute_width(body)); - body->set_height(body_height); - } - - void DocraftLayoutEngine::layout_footer_section( - const std::shared_ptr &footer, - const std::shared_ptr &body, - const SectionPlan &plan) { - float footer_start_y = 0.0F; - if (plan.body_to_render) { - footer_start_y = body->anchors().bottom_left.y; - } - footer->set_position({.x = footer->margin_left(), .y = footer_start_y}); - footer->set_width(compute_width(footer)); - footer->set_height(context()->page_height() * plan.footer_ratio); - context()->cursor().move_to(footer->position().x, footer_start_y); - compute_layout(footer, context()->cursor()); - footer->set_position({.x = footer->margin_left(), .y = footer_start_y}); - footer->set_width(compute_width(footer)); - footer->set_height(context()->page_height() * plan.footer_ratio); - assign_page_owner_recursive(footer, -1); //always + impl_->compute_document_layout(nodes); } -} // docraft +} // namespace docraft::layout diff --git a/docraft/src/docraft/layout/docraft_layout_engine_impl.cc b/docraft/src/docraft/layout/docraft_layout_engine_impl.cc new file mode 100644 index 0000000..4aaffd6 --- /dev/null +++ b/docraft/src/docraft/layout/docraft_layout_engine_impl.cc @@ -0,0 +1,668 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 "docraft_layout_engine_impl.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "docraft/backend/docraft_page_rendering_backend.h" +#include "docraft/docraft_document_context.h" +#include "docraft/exception/docraft_exceptions.h" +#include "docraft/layout/handler/docraft_basic_layout_handler.h" +#include "docraft/layout/handler/docraft_layout_blank_line.h" +#include "docraft/layout/handler/docraft_layout_handler.h" +#include "docraft/layout/handler/docraft_layout_list_handler.h" +#include "docraft/layout/handler/docraft_layout_table_handler.h" +#include "docraft/layout/handler/docraft_layout_text_handler.h" +#include "docraft/model/docraft_body.h" +#include "docraft/model/docraft_footer.h" +#include "docraft/model/docraft_foreach.h" +#include "docraft/model/docraft_header.h" +#include "docraft/model/docraft_list.h" +#include "docraft/model/docraft_new_page.h" +#include "docraft/model/docraft_table.h" + +namespace { + std::size_t count_rows_fit_horizontal(const docraft::model::DocraftTable &table, const float body_bottom_y) { + std::size_t fit = 0; + const auto grid = table.content_nodes(); + for (const auto &row: grid) { + float row_bottom = std::numeric_limits::infinity(); + bool found = false; + for (const auto &cell: row) { + if (cell) { + // Use the lowest bottom among all row cells to avoid optimistic fit decisions. + row_bottom = std::min(row_bottom, cell->anchors().bottom_left.y); + found = true; + } + } + if (!found) { + return fit; + } + if (row_bottom < body_bottom_y) { + return fit; + } + ++fit; + } + return fit; + } + + std::size_t count_rows_fit_vertical(const docraft::model::DocraftTable &table, const float body_bottom_y) { + std::size_t fit = 0; + const auto &titles = table.title_nodes(); + for (const auto &title: titles) { + if (!title) { + return fit; + } + const float row_bottom = title->anchors().bottom_left.y; + if (row_bottom < body_bottom_y) { + return fit; + } + ++fit; + } + return fit; + } +} + +namespace docraft::layout { + // Layout engine workflow: + // 1) Measure constraints and establish the active cursor/available width for the current node. + // 2) Layout children (if any), collecting child boxes. + // 3) Compute and place the node itself, then advance cursor and handle page ownership/pagination. + + DocraftLayoutEngine::Impl::Impl(std::shared_ptr context, const bool reset_cursor) + : context_(std::move(context)) { + configure_handlers(); + if (reset_cursor && context_) { + auto &layout_service = context_->edit_layout(); + layout_service.cursor().move_to(0, layout_service.page_height()); + } + } + + const std::shared_ptr &DocraftLayoutEngine::Impl::context() const { + return context_; + } + + model::DocraftTransform DocraftLayoutEngine::Impl::compute_max_rect( + const std::vector &boxes) { + if (boxes.empty()) { + return model::DocraftTransform{}; + } + + float min_x = boxes[0].anchors().top_left.x; + float max_x = boxes[0].anchors().top_right.x; + float min_y = boxes[0].anchors().bottom_left.y; + float max_y = boxes[0].anchors().top_left.y; + + for (const auto &box: boxes) { + min_x = std::min(min_x, box.anchors().top_left.x); + max_x = std::max(max_x, box.anchors().top_right.x); + min_y = std::min(min_y, box.anchors().bottom_left.y); + max_y = std::max(max_y, box.anchors().top_left.y); + } + + const float width = max_x - min_x; + const float height = max_y - min_y; + + return model::DocraftTransform({.x = min_x, .y = max_y}, width, height); + } + + model::DocraftTransform DocraftLayoutEngine::Impl::compute_layout(const std::shared_ptr &node) { + if (!node->visible()) { + return model::DocraftTransform{}; + } + auto &cursor = context_->edit_layout().cursor(); + return compute_layout(node, cursor); + } + + model::DocraftTransform DocraftLayoutEngine::Impl::compute_layout(const std::shared_ptr &node, + DocraftCursor &cursor) { + if (!node->visible()) { + return model::DocraftTransform{}; + } + + // Phase 1: derive measurement constraints and choose the cursor used by this node. + std::vector child_boxes; + auto &layout_service = context_->edit_layout(); + float max_width = layout_service.available_space(); + const float flow_origin_x = cursor.x(); + const float flow_origin_y = cursor.y(); + const bool is_absolute = (node->position_mode() == model::DocraftPositionType::kAbsolute); + DocraftCursor local_cursor = cursor; + DocraftCursor &active_cursor = is_absolute ? local_cursor : cursor; + if (is_absolute) { + active_cursor.move_to(flow_origin_x + node->position().x, flow_origin_y - node->position().y); + } + DocraftCursor local_node_cursor = active_cursor; + DocraftCursor *layout_cursor = &active_cursor; + const bool layout_text_flow = !is_absolute && + (std::dynamic_pointer_cast(node) || + std::dynamic_pointer_cast(node)); + if (layout_text_flow) { + local_node_cursor.move_to(active_cursor.x(), active_cursor.y()); + layout_cursor = &local_node_cursor; + } + bool rect_uses_origin_cursor = false; + DocraftCursor rect_origin_cursor = active_cursor; + if (auto rect_container = std::dynamic_pointer_cast(node)) { + if (std::dynamic_pointer_cast(node)) { + // Sections handle their own padding/margins; don't override cursor here. + } else if (!rect_container->children().empty()) { + DocraftCursor rect_cursor = active_cursor; + if (rect_container->position_mode() == model::DocraftPositionType::kAbsolute) { + rect_cursor.move_to(flow_origin_x + rect_container->position().x, + flow_origin_y - rect_container->position().y); + } else { + rect_cursor.move_to(active_cursor.x(), active_cursor.y()); + } + rect_container->set_position({.x = rect_cursor.x(), .y = rect_cursor.y()}); + rect_origin_cursor = rect_cursor; + rect_uses_origin_cursor = true; + local_node_cursor = rect_cursor; + layout_cursor = &local_node_cursor; + if (rect_container->width() > 0.0F) { + max_width = rect_container->width(); + } + layout_service.set_current_rect_width(max_width); + } + } + std::shared_ptr section_node = nullptr; + float section_content_bottom = 0.0F; + bool section_has_bounds = false; + if (auto section = std::dynamic_pointer_cast(node)) { + section_node = section; + const float left_margin = section_node->margin_left(); + const float right_margin = section_node->margin_right(); + const float top_margin = section_node->margin_top(); + const float padding = std::max(0.0F, section_node->padding()); + float base_x = section_node->position().x; + if (base_x == 0.0F && left_margin > 0.0F) { + base_x = left_margin; + } + active_cursor.move_to(base_x, active_cursor.y() - top_margin - padding); + if (section_node->width() > 0.0F) { + max_width = section_node->width(); + } else { + max_width = max_width - left_margin - right_margin; + } + layout_service.set_current_rect_width(max_width); + if (section_node->height() > 0.0F) { + section_content_bottom = section_node->position().y - section_node->height() + + section_node->margin_bottom() + padding; + section_has_bounds = true; + } + } + if (std::dynamic_pointer_cast(node)) { + auto layout_node = std::dynamic_pointer_cast(node); + if (layout_node->orientation() == model::LayoutOrientation::kHorizontal) { + layout_cursor->push_direction(DocraftCursorDirection::kHorizontal); + } else { + layout_cursor->push_direction(DocraftCursorDirection::kVertical); + } + } + + // Phase 2: layout children (lists/containers) and collect their boxes. + if (auto list_node = std::dynamic_pointer_cast(node)) { + if (!list_handler_) { + throw docraft::exception::LayoutConfigurationException("DocraftLayoutListHandler not configured"); + } + DocraftCursor list_cursor = *layout_cursor; + list_handler_->compute_children( + list_node, + list_cursor, + child_boxes, + [this](const std::shared_ptr &child, DocraftCursor &child_cursor) { + return compute_layout(child, child_cursor); + }, + max_width); + } else if (std::dynamic_pointer_cast(node)) { + auto container_node = std::dynamic_pointer_cast(node); + normalize_child_weights(*container_node); + const float saved_available_space = layout_service.available_space(); + const bool is_horizontal = (layout_cursor->direction() == DocraftCursorDirection::kHorizontal); + if (is_horizontal) { + layout_children_horizontal(container_node, node->z_index(), max_width, + *layout_cursor, child_boxes, + section_has_bounds, section_content_bottom); + } else { + layout_children_vertical(container_node, node->z_index(), max_width, + *layout_cursor, child_boxes, + section_has_bounds, section_content_bottom); + } + layout_service.set_current_rect_width(saved_available_space); + } + + auto max_rect = compute_max_rect(child_boxes); + + // Phase 3: compute this node box, place it, then update flow cursor for the next sibling. + if (rect_uses_origin_cursor) { + if (!compute_node(node, &max_rect, rect_origin_cursor)) { + throw docraft::exception::LayoutException("compute node failed"); + } + } else if (!compute_node(node, &max_rect, *layout_cursor)) { + throw docraft::exception::LayoutException("compute node failed"); + } + node->set_position(max_rect.position()); + node->set_width(max_rect.width()); + node->set_height(max_rect.height()); + if (!is_absolute && active_cursor.direction() == DocraftCursorDirection::kHorizontal) { + cursor.move_to(max_rect.anchors().top_right.x + kHorizontalSpacing, max_rect.anchors().top_right.y); + } else if (!is_absolute) { + const float spacing = std::max(kVerticalSpacing, node->padding()); + float next_y = max_rect.anchors().bottom_left.y - spacing; + if (next_y < 0.0F) { + next_y = 0.0F; + } + cursor.move_to(flow_origin_x, next_y); + } + return max_rect; + } + + void DocraftLayoutEngine::Impl::compute_document_layout( + const std::vector > &nodes) { + // Document pass: split header/body/footer, layout each visible section, then paginate body content. + const Sections sections = split_sections(nodes); + if (!sections.body) { + throw docraft::exception::DocumentStateException("Document must have a body section"); + } + if (const auto page_backend = context_->edit_rendering().edit_page_rendering()) { + page_backend->go_to_first_page(); + } + const SectionPlan plan = build_section_plan(sections); + if (plan.header_to_render) { + layout_header_section(sections.header, plan.header_ratio); + } + if (plan.body_to_render) { + layout_body_section(sections.body, sections.header, plan); + } + if (plan.footer_to_render) { + layout_footer_section(sections.footer, sections.body, plan); + } + } + + void DocraftLayoutEngine::Impl::configure_handlers() { + list_handler_ = nullptr; + handlers_.clear(); + handlers_.emplace_back(std::make_unique(context_)); + handlers_.emplace_back(std::make_unique(context_)); + handlers_.emplace_back(std::make_unique(context_)); + // dispatches internally to horizontal/vertical sub-handlers + handlers_.emplace_back(std::make_unique(context_)); + auto list_handler = std::make_unique(context_); + list_handler_ = list_handler.get(); + handlers_.emplace_back(std::move(list_handler)); + handlers_.emplace_back(std::make_unique(context_)); + } + + bool DocraftLayoutEngine::Impl::compute_node(const std::shared_ptr &node, + model::DocraftTransform *box, + DocraftCursor &cursor) const { + for (const auto &handler: handlers_) { + if (handler->handle(node, box, cursor)) { + return true; + } + } + return false; + } + + float DocraftLayoutEngine::Impl::compute_width(const std::shared_ptr &node) const { + const float margin_left = node->margin_left(); + const float margin_right = node->margin_right(); + return context_->layout().page_width() - (margin_left + margin_right); + } + + void DocraftLayoutEngine::Impl::assign_page_owner_recursive(const std::shared_ptr &node, + const int page) const { + if (!node) { + return; + } + node->set_page_owner(page); + if (auto container = std::dynamic_pointer_cast(node)) { + for (const auto &child: container->children()) { + assign_page_owner_recursive(child, page); + } + } + if (auto table = std::dynamic_pointer_cast(node)) { + for (const auto &title: table->title_nodes()) { + assign_page_owner_recursive(title, page); + } + for (const auto &title: table->htitle_nodes()) { + assign_page_owner_recursive(title, page); + } + for (const auto &row: table->content_nodes()) { + for (const auto &cell: row) { + assign_page_owner_recursive(cell, page); + } + } + } + } + + // ── Layout orientation helpers ────────────────────────────────────────────── + + void DocraftLayoutEngine::Impl::normalize_child_weights( + model::DocraftChildrenContainerNode &container) { + if (container.children().empty()) { + return; + } + bool has_variable = false; + for (const auto &child: container.children()) { + if (child->weight() == -1.0F) { + has_variable = true; + break; + } + } + if (has_variable) { + const float equal_weight = 1.0F / static_cast(container.children().size()); + for (const auto &child: container.children()) { + child->set_weight(equal_weight); + } + } + } + + float DocraftLayoutEngine::Impl::compute_horizontal_available_width( + const float max_width, const std::size_t child_count) { + if (child_count <= 1) { + return max_width; + } + const float total_spacing = kHorizontalSpacing * static_cast(child_count - 1); + return std::max(0.0F, max_width - total_spacing); + } + + void DocraftLayoutEngine::Impl::process_child_layout( + const std::shared_ptr &child, + DocraftCursor &cursor, + std::vector &out_boxes, + const bool section_has_bounds, + const float section_content_bottom) { + const auto box = compute_layout(child, cursor); + out_boxes.emplace_back(box); + if (section_has_bounds && cursor.y() < section_content_bottom) { + cursor.set_y(section_content_bottom); + } + } + + void DocraftLayoutEngine::Impl::layout_children_vertical( + const std::shared_ptr &container, + const int parent_z_index, + const float max_width, + DocraftCursor &cursor, + std::vector &out_boxes, + const bool section_has_bounds, + const float section_content_bottom) { + auto &layout_service = context_->edit_layout(); + for (const auto &child: container->children()) { + if (child->z_index() != parent_z_index) { + continue; + } + layout_service.set_current_rect_width(max_width); + process_child_layout(child, cursor, out_boxes, section_has_bounds, section_content_bottom); + } + } + + void DocraftLayoutEngine::Impl::layout_children_horizontal( + const std::shared_ptr &container, + const int parent_z_index, + const float max_width, + DocraftCursor &cursor, + std::vector &out_boxes, + const bool section_has_bounds, + const float section_content_bottom) { + auto &layout_service = context_->edit_layout(); + const float available = compute_horizontal_available_width(max_width, container->children().size()); + for (const auto &child: container->children()) { + if (child->z_index() != parent_z_index) { + continue; + } + const float child_width = available * child->weight(); + layout_service.set_current_rect_width(child_width); + const float start_x = cursor.x(); + const float start_y = cursor.y(); + process_child_layout(child, cursor, out_boxes, section_has_bounds, section_content_bottom); + // Advance the cursor to the next horizontal slot. + cursor.move_to(start_x + child_width + kHorizontalSpacing, start_y); + } + } + + // ─────────────────────────────────────────────────────────────────────────── + + void DocraftLayoutEngine::Impl::advance_to_next_body_page(BodyLayoutState &state) { + if (state.page_backend) { + state.page_backend->add_new_page(); + ++state.current_page; + } + state.body_cursor.reset_direction(); + state.body_cursor.move_to(state.body->position().x, state.body_start_y); + } + + bool DocraftLayoutEngine::Impl::handle_table_overflow_on_body_page(const std::shared_ptr &child, + const std::size_t *index_ptr, + const DocraftCursor &child_start_cursor, + BodyLayoutState &state) { + auto table = std::dynamic_pointer_cast(child); + if (!table) { + return false; + } + + const auto total_rows = static_cast(table->rows()); + const auto fit_rows = table->orientation() == model::LayoutOrientation::kVertical + ? count_rows_fit_vertical(*table, state.body_bottom_y) + : count_rows_fit_horizontal(*table, state.body_bottom_y); + if (fit_rows == 0 || fit_rows >= total_rows) { + return false; + } + + auto remainder = table->split_after_row(fit_rows, true); + if (!remainder) { + return false; + } + + if (index_ptr) { + state.body_container->insert_child(*index_ptr + 1, remainder); + } + + state.body_cursor = child_start_cursor; + assign_page_owner_recursive(child, state.current_page); + (void) compute_layout(child, state.body_cursor); + + advance_to_next_body_page(state); + assign_page_owner_recursive(remainder, state.current_page); + return true; + } + + void DocraftLayoutEngine::Impl::process_body_child_with_pagination(const std::shared_ptr &child, + const std::size_t *index_ptr, + BodyLayoutState &state) { + if (!child) { + return; + } + + if (std::dynamic_pointer_cast(child)) { + advance_to_next_body_page(state); + return; + } + + if (auto foreach_node = std::dynamic_pointer_cast(child)) { + foreach_node->set_page_owner(-1); + const auto &foreach_children = foreach_node->children(); + for (std::size_t i = 0; i < foreach_children.size(); ++i) { + const auto &foreach_child = foreach_children[i]; + if (std::dynamic_pointer_cast(foreach_child) && i + 1 == + foreach_children.size()) { + continue; + } + process_body_child_with_pagination(foreach_child, nullptr, state); + } + return; + } + + assign_page_owner_recursive(child, state.current_page); + DocraftCursor child_start_cursor = state.body_cursor; + const auto child_box = compute_layout(child, state.body_cursor); + const bool overflows_body = + child->position_mode() != model::DocraftPositionType::kAbsolute && + child_box.anchors().bottom_left.y < state.body_bottom_y; + if (!overflows_body) { + return; + } + + if (handle_table_overflow_on_body_page(child, index_ptr, child_start_cursor, state)) { + return; + } + + if (const bool starts_at_fresh_page_top = std::abs(child_start_cursor.y() - state.body_start_y) <= 0.01F) { + // Node cannot fit even on a fresh page; avoid generating pointless blank pages. + return; + } + + advance_to_next_body_page(state); + assign_page_owner_recursive(child, state.current_page); + (void) compute_layout(child, state.body_cursor); + } + + DocraftLayoutEngine::Impl::Sections DocraftLayoutEngine::Impl::split_sections( + const std::vector > &nodes) const { + Sections sections; + for (const auto &node: nodes) { + if (const auto header_node = std::dynamic_pointer_cast(node)) { + sections.header = header_node; + } else if (const auto body_node = std::dynamic_pointer_cast(node)) { + sections.body = body_node; + } else if (const auto footer_node = std::dynamic_pointer_cast(node)) { + sections.footer = footer_node; + } + } + return sections; + } + + DocraftLayoutEngine::Impl::SectionPlan + DocraftLayoutEngine::Impl::build_section_plan(const Sections §ions) const { + SectionPlan plan; + const auto &navigation_service = context_->navigation(); + const float base_header_ratio = navigation_service.header_ratio(); + const float base_body_ratio = navigation_service.body_ratio(); + const float base_footer_ratio = navigation_service.footer_ratio(); + + plan.header_ratio = sections.header ? base_header_ratio : 0.0F; + plan.footer_ratio = sections.footer ? base_footer_ratio : 0.0F; + plan.body_ratio = base_body_ratio; + plan.header_to_render = sections.header && sections.header->visible(); + plan.body_to_render = sections.body && sections.body->visible(); + plan.footer_to_render = sections.footer && sections.footer->visible(); + + if (!plan.header_to_render) { + plan.body_ratio += base_header_ratio; + } + if (!plan.footer_to_render) { + plan.body_ratio += base_footer_ratio; + } + return plan; + } + + void DocraftLayoutEngine::Impl::layout_header_section(const std::shared_ptr &header, + const float header_ratio) { + auto &layout_service = context_->edit_layout(); + auto &layout_cursor = layout_service.cursor(); + const float page_height = layout_service.page_height(); + header->set_position({.x = header->margin_left(), .y = page_height}); + header->set_width(compute_width(header)); + header->set_height(page_height * header_ratio); + layout_cursor.move_to(header->position().x, header->position().y); + (void) compute_layout(header, layout_cursor); + header->set_position({.x = header->margin_left(), .y = page_height}); + header->set_width(compute_width(header)); + header->set_height(page_height * header_ratio); + assign_page_owner_recursive(header, -1); + } + + void DocraftLayoutEngine::Impl::layout_body_section(const std::shared_ptr &body, + const std::shared_ptr &header, + const SectionPlan &plan) { + auto &layout_service = context_->edit_layout(); + auto &rendering_service = context_->edit_rendering(); + const float page_height = layout_service.page_height(); + float body_start_y = page_height; + if (plan.header_to_render) { + body_start_y = header->anchors().bottom_left.y; + } + const float body_height = page_height * plan.body_ratio; + + body->set_position({.x = body->margin_left(), .y = body_start_y}); + body->set_width(compute_width(body)); + body->set_height(body_height); + const float footer_height = plan.footer_to_render ? page_height * plan.footer_ratio : 0.0F; + const float body_bottom_y = (body_start_y - body_height) + body->margin_bottom() + footer_height; + layout_service.set_current_rect_width(body->width()); + + DocraftCursor body_cursor; + body_cursor.move_to(body->position().x, body_start_y); + + int current_page = 1; + const auto page_backend = rendering_service.edit_page_rendering(); + if (page_backend) { + current_page = static_cast(page_backend->current_page_number()); + } + + if (auto body_container = std::dynamic_pointer_cast(body)) { + BodyLayoutState body_layout_state{ + .body = body, + .body_container = body_container, + .page_backend = page_backend, + .body_cursor = body_cursor, + .body_start_y = body_start_y, + .body_bottom_y = body_bottom_y, + .current_page = current_page + }; + + std::size_t index = 0; + while (index < body_container->children().size()) { + auto child = body_container->children()[index]; + process_body_child_with_pagination(child, &index, body_layout_state); + ++index; + } + } + + body->set_position({.x = body->margin_left(), .y = body_start_y}); + body->set_width(compute_width(body)); + body->set_height(body_height); + } + + void DocraftLayoutEngine::Impl::layout_footer_section(const std::shared_ptr &footer, + const std::shared_ptr &body, + const SectionPlan &plan) { + auto &layout_service = context_->edit_layout(); + auto &layout_cursor = layout_service.cursor(); + float footer_start_y = 0.0F; + if (plan.body_to_render) { + footer_start_y = body->anchors().bottom_left.y; + } + footer->set_position({.x = footer->margin_left(), .y = footer_start_y}); + footer->set_width(compute_width(footer)); + footer->set_height(layout_service.page_height() * plan.footer_ratio); + layout_cursor.move_to(footer->position().x, footer_start_y); + (void) compute_layout(footer, layout_cursor); + footer->set_position({.x = footer->margin_left(), .y = footer_start_y}); + footer->set_width(compute_width(footer)); + footer->set_height(layout_service.page_height() * plan.footer_ratio); + assign_page_owner_recursive(footer, -1); + } +} // namespace docraft::layout + diff --git a/docraft/src/docraft/layout/docraft_layout_engine_impl.h b/docraft/src/docraft/layout/docraft_layout_engine_impl.h new file mode 100644 index 0000000..f585aee --- /dev/null +++ b/docraft/src/docraft/layout/docraft_layout_engine_impl.h @@ -0,0 +1,184 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 "docraft/generic/chain_of_responsibility_handler.h" +#include "docraft/layout/docraft_layout_engine.h" + +namespace docraft { + class DocraftDocumentContext; + + namespace backend { + class IDocraftPageRenderingBackend; + } + + namespace model { + class DocraftBody; + class DocraftChildrenContainerNode; + class DocraftFooter; + class DocraftHeader; + class DocraftNode; + class DocraftSection; + } + + namespace layout::handler { + class DocraftLayoutListHandler; + } +} + +namespace docraft::layout { + class DocraftLayoutEngine::Impl { + public: + explicit Impl(std::shared_ptr context, bool reset_cursor); + + const std::shared_ptr &context() const; + + static model::DocraftTransform compute_max_rect(const std::vector &boxes); + + model::DocraftTransform compute_layout(const std::shared_ptr &node); + + model::DocraftTransform compute_layout(const std::shared_ptr &node, DocraftCursor &cursor); + + void compute_document_layout(const std::vector > &nodes); + + private: + static constexpr float kHorizontalSpacing = 4.0F; + static constexpr float kVerticalSpacing = 4.0F; + + struct Sections { + std::shared_ptr header; + std::shared_ptr body; + std::shared_ptr footer; + }; + + struct SectionPlan { + float header_ratio = 0.0F; + float body_ratio = 0.0F; + float footer_ratio = 0.0F; + bool header_to_render = false; + bool body_to_render = false; + bool footer_to_render = false; + }; + + struct BodyLayoutState { + std::shared_ptr body; + std::shared_ptr body_container; + std::shared_ptr page_backend; + DocraftCursor body_cursor; + float body_start_y = 0.0F; + float body_bottom_y = 0.0F; + int current_page = 1; + }; + + void configure_handlers(); + + bool compute_node(const std::shared_ptr &node, + model::DocraftTransform *box, + DocraftCursor &cursor) const; + + float compute_width(const std::shared_ptr &node) const; + + void assign_page_owner_recursive(const std::shared_ptr &node, int page) const; + + // ── Layout orientation helpers ────────────────────────────────────────── + + /** + * @brief Normalises child weights: when any child carries the sentinel + * value -1 every child is assigned an equal share (1/N). + */ + static void normalize_child_weights(model::DocraftChildrenContainerNode &container); + + /** + * @brief Returns the width available to horizontal children after + * subtracting the inter-child gap for all N-1 gaps. + */ + static float compute_horizontal_available_width(float max_width, std::size_t child_count); + + /** + * @brief Calls compute_layout for @p child, appends the resulting box to + * @p out_boxes, and clamps the cursor to the section bottom when + * applicable. Used by both orientation helpers. + */ + void process_child_layout(const std::shared_ptr &child, + DocraftCursor &cursor, + std::vector &out_boxes, + bool section_has_bounds, + float section_content_bottom); + + /** + * @brief Lays out children stacked top-to-bottom (vertical orientation). + * Each child receives the full @p max_width. + */ + void layout_children_vertical(const std::shared_ptr &container, + int parent_z_index, + float max_width, + DocraftCursor &cursor, + std::vector &out_boxes, + bool section_has_bounds, + float section_content_bottom); + + /** + * @brief Lays out children side-by-side (horizontal orientation). + * Each child's width is proportional to its weight. The cursor is + * advanced horizontally by the allocated slot width plus spacing + * after every child. + */ + void layout_children_horizontal(const std::shared_ptr &container, + int parent_z_index, + float max_width, + DocraftCursor &cursor, + std::vector &out_boxes, + bool section_has_bounds, + float section_content_bottom); + + // ─────────────────────────────────────────────────────────────────────── + + void advance_to_next_body_page(BodyLayoutState &state); + + bool handle_table_overflow_on_body_page(const std::shared_ptr &child, + const std::size_t *index_ptr, + const DocraftCursor &child_start_cursor, + BodyLayoutState &state); + + void process_body_child_with_pagination(const std::shared_ptr &child, + const std::size_t *index_ptr, + BodyLayoutState &state); + + Sections split_sections(const std::vector > &nodes) const; + + SectionPlan build_section_plan(const Sections §ions) const; + + void layout_header_section(const std::shared_ptr &header, float header_ratio); + + void layout_body_section(const std::shared_ptr &body, + const std::shared_ptr &header, + const SectionPlan &plan); + + void layout_footer_section(const std::shared_ptr &footer, + const std::shared_ptr &body, + const SectionPlan &plan); + + std::shared_ptr context_; + std::vector > > handlers_; + handler::DocraftLayoutListHandler *list_handler_ = nullptr; + }; +} // namespace docraft::layout + diff --git a/docraft/src/docraft/layout/handler/docraft_basic_layout_handler.cc b/docraft/src/docraft/layout/handler/docraft_basic_layout_handler.cc index 08ece11..100727b 100644 --- a/docraft/src/docraft/layout/handler/docraft_basic_layout_handler.cc +++ b/docraft/src/docraft/layout/handler/docraft_basic_layout_handler.cc @@ -27,7 +27,7 @@ namespace docraft::layout::handler { model::DocraftTransform *box, DocraftCursor& cursor) { if (box == nullptr) { - throw std::invalid_argument("box is null"); + throw docraft::exception::InvalidInputException("box is null"); } if (node->position_mode()==model::DocraftPositionType::kBlock) { @@ -39,16 +39,17 @@ namespace docraft::layout::handler { const bool is_rectangle = static_cast(std::dynamic_pointer_cast(node)); const float child_width = box->width(); const float child_height = box->height(); + const float available_width = edit_context()->layout().available_space(); if (node->position_mode() == model::DocraftPositionType::kBlock) { if (node->width() > 0.0F) { box->set_width(node->width()); } else if (node->auto_fill_width()) { - box->set_width(std::max(edit_context()->available_space(), child_width)); + box->set_width(std::max(available_width, child_width)); } else if (is_rectangle) { box->set_width(child_width); - } else if (edit_context()->available_space() < node->width() || node->width() == 0.0F) { - box->set_width(edit_context()->available_space()); + } else if (available_width < node->width() || node->width() == 0.0F) { + box->set_width(available_width); } else { box->set_width(node->width()); } diff --git a/docraft/src/docraft/layout/handler/docraft_layout_blank_line.cc b/docraft/src/docraft/layout/handler/docraft_layout_blank_line.cc index a102f23..2634def 100644 --- a/docraft/src/docraft/layout/handler/docraft_layout_blank_line.cc +++ b/docraft/src/docraft/layout/handler/docraft_layout_blank_line.cc @@ -21,10 +21,10 @@ namespace docraft::layout::handler { model::DocraftTransform* box, DocraftCursor& cursor) { if (box == nullptr) { - throw std::invalid_argument("box is null"); + throw docraft::exception::InvalidInputException("box is null"); } node->set_weight(1.0F); //blank line takes full width - box->set_width(edit_context()->available_space()); //get full available width + box->set_width(edit_context()->layout().available_space()); //get full available width if (node->height() > 0.0F) { box->set_height(node->height()); } else { diff --git a/docraft/src/docraft/layout/handler/docraft_layout_handler.cc b/docraft/src/docraft/layout/handler/docraft_layout_handler.cc index 5edd845..84296f1 100644 --- a/docraft/src/docraft/layout/handler/docraft_layout_handler.cc +++ b/docraft/src/docraft/layout/handler/docraft_layout_handler.cc @@ -24,11 +24,11 @@ namespace docraft::layout::handler { model::DocraftTransform *box, DocraftCursor& cursor) { if (box == nullptr) { - throw std::invalid_argument("box is null"); + throw docraft::exception::InvalidInputException("box is null"); } // If the layout has a weight, the parent already scoped available_space to that share. if (node->weight()!=-1.0F) { - node->set_width(edit_context()->available_space()); + node->set_width(edit_context()->layout().available_space()); box->set_width(node->width()); } cursor.pop_direction(); //remove layout direction diff --git a/docraft/src/docraft/layout/handler/docraft_layout_horizontal_table_handler.cc b/docraft/src/docraft/layout/handler/docraft_layout_horizontal_table_handler.cc new file mode 100644 index 0000000..fbbb1b7 --- /dev/null +++ b/docraft/src/docraft/layout/handler/docraft_layout_horizontal_table_handler.cc @@ -0,0 +1,226 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 "docraft/layout/handler/docraft_layout_horizontal_table_handler.h" + +#include +#include +#include +#include + +#include "docraft_layout_table_handler_impl.h" +#include "docraft/layout/docraft_layout_engine.h" +#include "docraft/utils/docraft_logger.h" + +namespace docraft::layout::handler { + void DocraftLayoutHorizontalTableHandler::setup_compute_state(const std::shared_ptr &node, + DocraftCursor &cursor) { + auto &layout_service = edit_context()->edit_layout(); + table_cursor_ = cursor; + table_impl::configure_cursor_position(node, table_cursor_); + + fixed_x_ = table_cursor_.x(); + fixed_y_ = table_cursor_.y() - node->padding(); + saved_available_space_ = layout_service.available_space(); + + context_ = edit_context(); + engine_.emplace(context_, false); + + constexpr float kCellPaddingY = table_impl::kHorizontalCellPaddingY; + constexpr float kCellPaddingX = table_impl::kHorizontalCellPaddingX; + baseline_offset_ = node->baseline_offset(); + cell_padding_x_ = kCellPaddingX; + cell_padding_y_ = kCellPaddingY; + offset_y_ = 2.0F * kCellPaddingY; + offset_x_ = 2.0F * kCellPaddingX; + } + + DocraftLayoutHorizontalTableHandler::TableContent DocraftLayoutHorizontalTableHandler::collect_table_content( + const std::shared_ptr &node) { + table_impl::ensure_title_nodes(node); + return TableContent{ + .title_nodes = node->title_nodes(), + .rows = node->content_nodes() + }; + } + + DocraftLayoutHorizontalTableHandler::WidthPlan DocraftLayoutHorizontalTableHandler::compute_width_plan( + const std::shared_ptr &node, + const TableContent &content) { + const std::size_t cols = content.title_nodes.size(); + std::vector natural_widths(cols, 0.0F); + + auto &layout_service = edit_context()->edit_layout(); + for (std::size_t i = 0; i < cols; ++i) { + const auto &title_node = content.title_nodes[i]; + const float saved_x = table_cursor_.x(); + const float saved_y = table_cursor_.y(); + table_cursor_.move_to(0.0F, layout_service.page_height()); + (void) engine_->compute_layout(title_node, table_cursor_); + table_cursor_.move_to(saved_x, saved_y); + + natural_widths[i] = title_node->width(); + } + + const float natural_sum = std::accumulate(natural_widths.begin(), natural_widths.end(), 0.0F); + const float available_width = table_impl::available_width_for(node, context_, natural_sum); + + std::vector explicit_col_widths(cols, 0.0F); + for (const auto &row: content.rows) { + for (std::size_t c = 0; c < std::min(row.size(), cols); ++c) { + const auto &cell = row[c]; + if (cell && cell->width() > 0.0F) { + explicit_col_widths[c] = std::max(explicit_col_widths[c], cell->width()); + } + } + } + + const bool has_explicit_col_width = std::ranges::any_of(explicit_col_widths, [](const float w) { + return w > 0.0F; + }); + + const std::vector weights = table_impl::assign_weights(node->column_weights(), cols); + const float total_weight = std::accumulate(weights.begin(), weights.end(), 0.0F); + + std::vector col_widths(cols, 0.0F); + float widths_sum = 0.0F; + for (std::size_t i = 0; i < cols; ++i) { + if (explicit_col_widths[i] > 0.0F) { + col_widths[i] = explicit_col_widths[i]; + } else { + const float target = available_width * (weights[i] / total_weight); + col_widths[i] = std::max(natural_widths[i], target); + } + widths_sum += col_widths[i]; + } + + if (!has_explicit_col_width && available_width > 0.0F && widths_sum > 0.0F && widths_sum != available_width) { + const float scale = available_width / widths_sum; + for (auto &w: col_widths) { + w *= scale; + } + widths_sum = available_width; + } + + return WidthPlan(widths_sum, col_widths); + } + + void DocraftLayoutHorizontalTableHandler::apply_width_plan(const WidthPlan &plan) { + col_widths_.assign(plan.col_widths.begin(), plan.col_widths.end()); + const auto lefts = table_impl::build_column_lefts(fixed_x_, col_widths_); + col_lefts_.assign(lefts.begin(), lefts.end()); + cols_ = col_widths_.size(); + } + + float DocraftLayoutHorizontalTableHandler::layout_body_rows(const TableContent &content, + const float start_y, + const float min_row_height) { + float y = start_y; + float total_content_height = 0.0F; + for (const auto &row: content.rows) { + const float row_height = layout_row(row, y, min_row_height); + total_content_height += row_height; + y -= row_height; + } + return total_content_height; + } + + void DocraftLayoutHorizontalTableHandler::finalize_output(const std::shared_ptr &node, + model::DocraftTransform *box, + const float table_width, + const float table_height) { + auto &layout_service = edit_context()->edit_layout(); + node->set_position({.x = fixed_x_, .y = fixed_y_}); + node->set_width(table_width); + node->set_height(table_height); + + if (box) { + box->set_position(node->position()); + box->set_width(node->width()); + box->set_height(node->height()); + } + layout_service.set_current_rect_width(saved_available_space_); + } + + void DocraftLayoutHorizontalTableHandler::log_cells(const std::shared_ptr &node) { + for (const auto &row: node->content_nodes()) { + for (const auto &cell: row) { + if (cell) { + LOG_DEBUG(fmt::format("Cell at ({}, {}) with size ({}, {})", cell->position().x, + cell->position().y, cell->width(), cell->height())); + } + } + } + } + + void DocraftLayoutHorizontalTableHandler::clear_compute_state() { + engine_.reset(); + } + + float DocraftLayoutHorizontalTableHandler::layout_row( + const std::vector > &row_nodes, + const float row_top_y, + const float min_row_height) { + float row_height = min_row_height; + for (std::size_t c = 0; c < std::min(row_nodes.size(), cols_); ++c) { + const auto &cell_node = row_nodes[c]; + if (!cell_node) { + continue; + } + table_impl::compute_cell_layout({ + .node = cell_node, + .engine = *engine_, + .context = context_, + .table_cursor = table_cursor_, + .inner_width = std::max(0.0F, col_widths_[c] - offset_x_), + .x = col_lefts_[c] + cell_padding_x_, + .y = row_top_y - cell_padding_y_ + }); + row_height = std::max(row_height, cell_node->height()); + } + + for (std::size_t c = 0; c < std::min(row_nodes.size(), cols_); ++c) { + const auto &cell_node = row_nodes[c]; + if (!cell_node) { + continue; + } + table_impl::center_text_in_row(cell_node, row_top_y, row_height + (2.0F * offset_y_), baseline_offset_); + cell_node->set_position({.x = col_lefts_[c], .y = row_top_y}); + cell_node->set_width(col_widths_[c]); + cell_node->set_height(row_height); + } + + return row_height; + } + + void DocraftLayoutHorizontalTableHandler::compute(const std::shared_ptr &node, + model::DocraftTransform *box, + DocraftCursor &cursor) { + setup_compute_state(node, cursor); + const TableContent content = collect_table_content(node); + const WidthPlan width_plan = compute_width_plan(node, content); + apply_width_plan(width_plan); + + const float min_row_height = 20.0F + offset_y_; + const float title_row_height = layout_row(content.title_nodes, fixed_y_, min_row_height); + const float body_start_y = fixed_y_ - title_row_height; + const float body_height = layout_body_rows(content, body_start_y, min_row_height); + + finalize_output(node, box, width_plan.table_width, title_row_height + body_height); + log_cells(node); + clear_compute_state(); + } +} // namespace docraft::layout::handler diff --git a/docraft/src/docraft/layout/handler/docraft_layout_list_handler.cc b/docraft/src/docraft/layout/handler/docraft_layout_list_handler.cc index 1b6dbcb..8c5d684 100644 --- a/docraft/src/docraft/layout/handler/docraft_layout_list_handler.cc +++ b/docraft/src/docraft/layout/handler/docraft_layout_list_handler.cc @@ -27,7 +27,7 @@ namespace docraft::layout::handler { model::DocraftTransform *box, DocraftCursor &cursor) { if (box == nullptr) { - throw std::invalid_argument("box is null"); + throw docraft::exception::InvalidInputException("box is null"); } if (node->position_mode() == model::DocraftPositionType::kBlock) { @@ -46,16 +46,18 @@ namespace docraft::layout::handler { const std::shared_ptr &, DocraftCursor &)> &layout_child, float max_width) const { if (!node) { - throw std::invalid_argument("list node is null"); + throw docraft::exception::InvalidInputException("list node is null"); } + auto &layout_service = edit_context()->edit_layout(); + auto text_backend = edit_context()->rendering().text_rendering(); node->update_items(); node->clear_markers(); - const float saved_available_space = edit_context()->available_space(); + const float saved_available_space = layout_service.available_space(); const float list_available_width = max_width; for (std::size_t i = 0; i < node->children().size(); ++i) { auto text_child = std::dynamic_pointer_cast(node->children()[i]); if (!text_child) { - throw std::invalid_argument("List items must be Text nodes"); + throw docraft::exception::InvalidInputException("List items must be Text nodes"); } const float item_x = cursor.x(); @@ -74,12 +76,12 @@ namespace docraft::layout::handler { } else { generic::DocraftFontApplier font_applier(edit_context()); font_applier.apply_font(marker_text); - marker_width = edit_context()->text_backend()->measure_text_width(marker_text->text()); + marker_width = text_backend->measure_text_width(marker_text->text()); } const float marker_gap = marker_width > 0.0F ? 6.0F : 0.0F; const float content_width = std::max(0.0F, list_available_width - marker_width - marker_gap); - edit_context()->set_current_rect_width(content_width); + layout_service.set_current_rect_width(content_width); cursor.move_to(item_x + marker_width + marker_gap, item_y); const float original_padding = text_child->padding(); @@ -113,7 +115,7 @@ namespace docraft::layout::handler { cursor.move_to(item_x, cursor.y()); } - edit_context()->set_current_rect_width(saved_available_space); + layout_service.set_current_rect_width(saved_available_space); } bool DocraftLayoutListHandler::handle(const std::shared_ptr &request, diff --git a/docraft/src/docraft/layout/handler/docraft_layout_table_handler.cc b/docraft/src/docraft/layout/handler/docraft_layout_table_handler.cc index 31bd52b..4ab47b5 100644 --- a/docraft/src/docraft/layout/handler/docraft_layout_table_handler.cc +++ b/docraft/src/docraft/layout/handler/docraft_layout_table_handler.cc @@ -16,512 +16,31 @@ #include "docraft/layout/handler/docraft_layout_table_handler.h" -#include #include -#include -#include -#include "docraft/layout/docraft_layout_engine.h" -#include "docraft/model/docraft_layout.h" -#include "docraft/model/docraft_text.h" +#include "docraft/exception/docraft_exceptions.h" +#include "docraft/layout/handler/docraft_layout_horizontal_table_handler.h" +#include "docraft/layout/handler/docraft_layout_vertical_table_handler.h" #include "docraft/utils/docraft_logger.h" namespace docraft::layout::handler { - namespace { - /** - * @This function ensures that the title nodes of the table are created based on the titles provided. - * @param node - */ - void ensure_title_nodes(const std::shared_ptr &node) { - const auto &titles = node->titles(); - if (node->title_nodes().size() < titles.size()) { - for (std::size_t i = node->title_nodes().size(); i < titles.size(); ++i) { - auto title_node = std::make_shared(); - title_node->set_text(titles[i]); - node->add_title_node(title_node); - } - } - } - - /** - * @This function flattens the content nodes of the table into a single vector. - * @param node - * @return std::vector > - */ - std::vector > flatten_content_nodes( - const std::shared_ptr &node) { - std::vector > flat; - const auto grid = node->content_nodes(); - for (const auto &row: grid) { - for (const auto &cell: row) { - if (cell) { - flat.emplace_back(cell); - } - } - } - return flat; - } - - /** - * @This function calculates the available width for the table based on its properties and the context. - * @param node - * @param context - * @param fixed_x - * @param fallback - * @return float - */ - float available_width_for(const std::shared_ptr &node, - const std::shared_ptr &context, - const float fixed_x, - const float fallback) { - if (node->auto_fill_width()) { - return std::max(0.0F, context->available_space()); - } - float w = node->width(); - if (w > 0.0F) { - return std::max(0.0F, w); - } - return std::max(0.0F, fallback); - } - - /** - * @This function configures the cursor position for the table layout based on the table's position mode and properties. - * @param node - * @param docraft_cursor - */ - void configure_cursor_position(const std::shared_ptr & node, DocraftCursor & docraft_cursor) { - if (node->position_mode() == model::DocraftPositionType::kBlock) { - // For block position, the cursor is already in the correct position, so we do nothing. - } else { - // For absolute position, we move the cursor to the specified position of the table. - docraft_cursor.move_to(node->position().x, node->position().y); - } - } - - /** - * @brief Layout a table in horizontal orientation. - * @param node The table node to layout. - * @param box Optional transform to store the resulting layout box. - * @param context The PDF context for layout calculations. - */ - void layout_horizontal_table(const std::shared_ptr &node, - model::DocraftTransform *box, - const std::shared_ptr &context, - DocraftCursor& cursor) { - DocraftCursor table_cursor = cursor;//Use a custom cursor to not affect the main one - configure_cursor_position(node,table_cursor); - const float fixed_x = table_cursor.x(); - const float fixed_y = table_cursor.y()-node->padding();//Adjust for some top padding - - docraft::layout::DocraftLayoutEngine engine(context, false); - const float saved_available_space = context->available_space(); - constexpr float kCellPaddingY = 2.5F; - constexpr float kCellPaddingX = 2.5F; - const float baseline_offset = node->baseline_offset(); - auto center_text_in_row = [baseline_offset](const std::shared_ptr& text_node, - const float row_top_y, - const float row_height) { - if (!text_node) { - return; - } - const float current_center = (text_node->anchors().top_left.y + text_node->anchors().bottom_left.y) * 0.5F; - const float desired_center = row_top_y - (row_height * 0.5F) - + (baseline_offset * text_node->font_size()); - const float delta = current_center - desired_center; - if (delta != 0.0F) { - text_node->set_y_for_children(delta); - } - }; - - const auto &titles = node->titles(); - const std::size_t cols = titles.size(); - ensure_title_nodes(node); - - // --- Measure titles (natural sizes) --- - std::vector natural_widths(cols, 0.0F);// Natural widths of each column, natural means without constraints - float title_row_height = 0.0F; - - for (std::size_t i = 0; i < cols; ++i) { - const auto &title_node = node->title_nodes()[i]; - // Save cursor position - const float saved_x = table_cursor.x(); - const float saved_y = table_cursor.y(); - table_cursor.move_to(0.0F, context->page_height()); - (void) engine.compute_layout(title_node, table_cursor); - table_cursor.move_to(saved_x, saved_y); - - natural_widths[i] = title_node->width(); - title_row_height = std::max(title_row_height, title_node->height()); - } - - const float natural_sum = std::accumulate(natural_widths.begin(), natural_widths.end(), 0.0F); - const float available_width = available_width_for(node, context, fixed_x, natural_sum); - - // Optional per-column fixed widths coming from . - std::vector explicit_col_widths(cols, 0.0F); - const auto grid = node->content_nodes(); - for (const auto &row: grid) { - for (std::size_t c = 0; c < std::min(row.size(), cols); ++c) { - const auto &cell = row[c]; - if (!cell) { - continue; - } - if (cell->width() > 0.0F) { - explicit_col_widths[c] = std::max(explicit_col_widths[c], cell->width()); - } - } - } - const bool has_explicit_col_width = - std::any_of(explicit_col_widths.begin(), explicit_col_widths.end(), [](const float w) { return w > 0.0F; }); - - // --- Column widths from weights (respect natural minima) --- - std::vector weights = node->column_weights(); - if (weights.size() != cols) { - weights.assign(cols, 1.0F); - } - float total_weight = std::accumulate(weights.begin(), weights.end(), 0.0F); - if (total_weight <= 0.0F) { - weights.assign(cols, 1.0F); - total_weight = static_cast(cols); - } - - std::vector col_widths(cols, 0.0F); - float widths_sum = 0.0F; - for (std::size_t i = 0; i < cols; ++i) { - if (explicit_col_widths[i] > 0.0F) { - col_widths[i] = explicit_col_widths[i]; - } else { - const float target = available_width * (weights[i] / total_weight); - col_widths[i] = std::max(natural_widths[i], target); - } - widths_sum += col_widths[i]; - } - - if (!has_explicit_col_width && available_width > 0.0F && widths_sum > 0.0F && widths_sum != available_width) { - const float scale = available_width / widths_sum; - for (auto &w: col_widths) { - w *= scale; - } - widths_sum = available_width; - } - - // Column lefts, based on widths - std::vector col_lefts(cols, fixed_x); - { - float x = fixed_x; - for (std::size_t i = 0; i < cols; ++i) { - col_lefts[i] = x; - x += col_widths[i]; - } - } - - // --- Place titles (re-layout within column width to wrap correctly) --- - std::vector title_heights(cols, 0.0F); - title_row_height = 0.0F; - for (std::size_t i = 0; i < cols; ++i) { - const auto &title_node = node->title_nodes()[i]; - - const float saved_x = table_cursor.x(); - const float saved_y = table_cursor.y(); - const float inner_width = std::max(0.0F, col_widths[i] - (2.0F * kCellPaddingX)); - context->set_current_rect_width(inner_width); - table_cursor.move_to(col_lefts[i] + kCellPaddingX, fixed_y - kCellPaddingY); - (void) engine.compute_layout(title_node, table_cursor); - table_cursor.move_to(saved_x, saved_y); - - title_heights[i] = title_node->height(); - title_row_height = std::max(title_row_height, title_heights[i] + (2.0F * kCellPaddingY)); - } - - for (std::size_t i = 0; i < cols; ++i) { - const auto &title_node = node->title_nodes()[i]; - if (auto text_node = std::dynamic_pointer_cast(title_node)) { - center_text_in_row(text_node, fixed_y, title_row_height); - } - title_node->set_position({.x = col_lefts[i], .y = fixed_y}); - title_node->set_width(col_widths[i]); - title_node->set_height(title_row_height); - } - - // --- Place content rows --- - float y = fixed_y - title_row_height; - float total_content_height = 0.0F; - - for (const auto &row: grid) { - float row_height = 20.0F + (2.0F * kCellPaddingY); - - for (std::size_t c = 0; c < std::min(row.size(), cols); ++c) { - const auto &cell = row[c]; - if (!cell) { - continue; - } - - const float saved_x = table_cursor.x(); - const float saved_y = table_cursor.y(); - const float inner_width = std::max(0.0F, col_widths[c] - (2.0F * kCellPaddingX)); - context->set_current_rect_width(inner_width); - table_cursor.move_to(col_lefts[c] + kCellPaddingX, y - kCellPaddingY); - (void) engine.compute_layout(cell, table_cursor); - table_cursor.move_to(saved_x, saved_y); - - row_height = std::max(row_height, cell->height() + (2.0F * kCellPaddingY)); - } - - for (std::size_t c = 0; c < std::min(row.size(), cols); ++c) { - const auto &cell = row[c]; - if (!cell) { - continue; - } - if (auto text_cell = std::dynamic_pointer_cast(cell)) { - center_text_in_row(text_cell, y, row_height); - } - cell->set_position({.x = col_lefts[c], .y = y}); - cell->set_width(col_widths[c]); - cell->set_height(row_height); - } - - total_content_height += row_height; - y -= row_height; - } - - // --- Finalize table box --- - node->set_position({.x = fixed_x, .y = fixed_y}); - node->set_width(widths_sum); - node->set_height(title_row_height + total_content_height); - - if (box) { - box->set_position(node->position()); - box->set_width(node->width()); - box->set_height(node->height()); - } - context->set_current_rect_width(saved_available_space); - } - - void layout_vertical_table(const std::shared_ptr &node, - model::DocraftTransform *box, - const std::shared_ptr &context, - DocraftCursor& cursor) { - DocraftCursor table_cursor = cursor;//Use a custom cursor to not affect the main one - configure_cursor_position(node,table_cursor); - const float fixed_x = table_cursor.x(); - const float fixed_y = table_cursor.y(); - - docraft::layout::DocraftLayoutEngine engine(context, false); - const float saved_available_space = context->available_space(); - const float kCellPaddingY = 2.0F; - const float kCellPaddingX = 2.0F; - const float baseline_offset = node->baseline_offset(); - auto center_text_in_row = [baseline_offset](const std::shared_ptr& text_node, - const float row_top_y, - const float row_height) { - if (!text_node) { - return; - } - const float current_center = (text_node->anchors().top_left.y + text_node->anchors().bottom_left.y) * 0.5F; - const float desired_center = row_top_y - (row_height * 0.5F) - + (baseline_offset * text_node->font_size()); - const float delta = current_center - desired_center; - if (delta != 0.0F) { - text_node->set_y_for_children(delta); - } - }; - - const std::size_t rows = node->titles().empty() - ? node->title_nodes().size() - : node->titles().size(); - ensure_title_nodes(node); - - // In vertical mode: first column is titles (row headers), remaining columns are values. - const std::size_t total_cols = static_cast(std::max(2, node->cols())); - const std::size_t value_cols = static_cast(std::max(1, node->content_cols())); - - // Flatten content sequentially: each row consumes value_cols items. - const auto flat = flatten_content_nodes(node); - - // Measure title nodes (for natural title column width). - float title_col_natural_width = 0.0F; - - for (std::size_t r = 0; r < rows; ++r) { - const auto &title_node = node->title_nodes()[r]; - - const float saved_x = table_cursor.x(); - const float saved_y = table_cursor.y(); - context->set_current_rect_width(context->available_space()); - table_cursor.move_to(fixed_x, context->page_height()); - (void) engine.compute_layout(title_node, table_cursor); - table_cursor.move_to(saved_x, saved_y); - - title_col_natural_width = std::max(title_col_natural_width, title_node->width()); - } - - const float available_width = available_width_for(node, context, fixed_x, title_col_natural_width); - - // Use two weights: [title_col, values_block] - std::vector weights = node->column_weights(); - if (weights.size() < 2) { - weights.assign(2, 1.0F); - } - float total_weight = weights[0] + weights[1]; - if (total_weight <= 0.0F) { - weights = {.5F, .5F}; - total_weight = 1.0F; - } - - const float title_col_width = std::max(title_col_natural_width, - available_width * (weights[0] / total_weight)); - const float values_block_width = std::max(0.0F, available_width - title_col_width); - float value_col_width = (value_cols > 0) - ? (values_block_width / static_cast(value_cols)) - : values_block_width; - - // Respect explicit widths set on value cells () in vertical tables. - float explicit_value_col_width = 0.0F; - for (std::size_t idx = 0; idx < flat.size(); ++idx) { - if (flat[idx] && flat[idx]->width() > 0.0F) { - explicit_value_col_width = std::max(explicit_value_col_width, flat[idx]->width()); - } - } - if (explicit_value_col_width > 0.0F) { - value_col_width = explicit_value_col_width; - } - - float y = fixed_y; - float total_height = 0.0F; - - const float padding_x = 2.0F*kCellPaddingX; - const float padding_y = 2.0F*kCellPaddingY; - // Optional header row (vertical tables only). - float header_row_height = 0.0F; - if (!node->htitle_nodes().empty()) { - // Measure header row height by laying out htitle nodes within value column widths. - for (std::size_t c = 0; c < std::min(value_cols, node->htitle_nodes().size()); ++c) { - const auto &htitle_node = node->htitle_nodes()[c]; - const float saved_x = table_cursor.x(); - const float saved_y = table_cursor.y(); - const float inner_width = std::max(0.0F, value_col_width - padding_x); - context->set_current_rect_width(inner_width); - table_cursor.move_to( - fixed_x + title_col_width + (static_cast(c) * value_col_width) + kCellPaddingX, - y - kCellPaddingY); - (void) engine.compute_layout(htitle_node, table_cursor); - table_cursor.move_to(saved_x, saved_y); - header_row_height = std::max(header_row_height, htitle_node->height() + padding_y); - } - // Place header row htitle nodes. - for (std::size_t c = 0; c < std::min(value_cols, node->htitle_nodes().size()); ++c) { - const auto &htitle_node = node->htitle_nodes()[c]; - if (auto text_node = std::dynamic_pointer_cast(htitle_node)) { - center_text_in_row(text_node, y, header_row_height); - } - htitle_node->set_position({ - .x = fixed_x + title_col_width + (static_cast(c) * value_col_width), .y = y - }); - htitle_node->set_width(value_col_width); - htitle_node->set_height(header_row_height); - } - - total_height += header_row_height; - y -= header_row_height; - } - - for (std::size_t r = 0; r < rows; ++r) { - float row_height = 0.0F; - - // Layout title cell within the title column width. - { - const auto &title_node = node->title_nodes()[r]; - const float saved_x = table_cursor.x(); - const float saved_y = table_cursor.y(); - const float inner_width = std::max(0.0F, title_col_width -padding_x); - context->set_current_rect_width(inner_width); - table_cursor.move_to(fixed_x + kCellPaddingX, y - kCellPaddingY); - (void) engine.compute_layout(title_node, table_cursor); - table_cursor.move_to(saved_x, saved_y); - row_height = std::max(row_height, title_node->height() + padding_y); - } - - // Measure the row's value cells to find row height. - const std::size_t row_first = r * value_cols; - for (std::size_t vc = 0; vc < value_cols; ++vc) { - const std::size_t idx = row_first + vc; - if (idx >= flat.size()) { - break; - } - const auto &cell = flat[idx]; - - const float saved_x = table_cursor.x(); - const float saved_y = table_cursor.y(); - const float inner_width = std::max(0.0F, value_col_width - padding_x); - context->set_current_rect_width(inner_width); - table_cursor.move_to( - fixed_x + title_col_width + (static_cast(vc) * value_col_width) + kCellPaddingX, - y - kCellPaddingY); - (void) engine.compute_layout(cell, table_cursor); - table_cursor.move_to(saved_x, saved_y); - - row_height = std::max(row_height, cell->height() + padding_y); - } - - // Place title cell. - { - const auto &title_node = node->title_nodes()[r]; - if (auto text_node = std::dynamic_pointer_cast(title_node)) { - center_text_in_row(text_node, y, row_height); - } - title_node->set_position({.x = fixed_x, .y = y}); - title_node->set_width(title_col_width); - title_node->set_height(row_height); - } - - // Place value cells for this row. - for (std::size_t vc = 0; vc < value_cols; ++vc) { - const std::size_t idx = row_first + vc; - if (idx >= flat.size()) { - break; - } - const auto &cell = flat[idx]; - if (auto text_cell = std::dynamic_pointer_cast(cell)) { - center_text_in_row(text_cell, y, row_height); - } - cell->set_position({ - .x = fixed_x + title_col_width + (static_cast(vc) * value_col_width), .y = y - }); - cell->set_width(value_col_width); - cell->set_height(row_height); - } - - total_height += row_height; - y -= row_height; - } - - node->set_position({.x = fixed_x, .y = fixed_y}); - node->set_width(title_col_width + (value_col_width * static_cast(value_cols))); - node->set_height(total_height); - - if (box) { - box->set_position(node->position()); - box->set_width(node->width()); - box->set_height(node->height()); - } - context->set_current_rect_width(saved_available_space); - } - } void DocraftLayoutTableHandler::compute(const std::shared_ptr &node, model::DocraftTransform *box, - DocraftCursor& cursor) { + DocraftCursor &cursor) { if (!node) { - throw std::invalid_argument("table node is null"); + throw docraft::exception::InvalidInputException("table node is null"); } - if (node->titles().empty()) { - if (node->position_mode()==model::DocraftPositionType::kBlock) { + if (node->titles().empty() && node->title_nodes().empty()) { + if (node->position_mode() == model::DocraftPositionType::kBlock) { node->set_position({.x = cursor.x(), .y = cursor.y()}); - }else { + } else { node->set_position({.x = node->position().x, .y = node->position().y}); } node->set_width(0.0F); node->set_height(0.0F); + if (box) { box->set_position(node->position()); box->set_width(node->width()); @@ -531,28 +50,24 @@ namespace docraft::layout::handler { } switch (node->orientation()) { - case model::LayoutOrientation::kHorizontal: - layout_horizontal_table(node, box, edit_context(), cursor); + case model::LayoutOrientation::kHorizontal: { + DocraftLayoutHorizontalTableHandler h_handler(edit_context()); + h_handler.compute(node, box, cursor); break; - case model::LayoutOrientation::kVertical: - layout_vertical_table(node, box, edit_context(), cursor); + } + case model::LayoutOrientation::kVertical: { + DocraftLayoutVerticalTableHandler v_handler(edit_context()); + v_handler.compute(node, box, cursor); break; - default: - throw std::runtime_error("unsupported table orientation"); - } - //print cels positions for debug - for (const auto &row: node->content_nodes()) { - for (const auto &cell: row) { - if (cell) { - LOG_DEBUG(fmt::format("Cell at ({}, {}) with size ({}, {})", cell->position().x, cell->position().y, cell->width(), cell->height())); - } } + default: + throw docraft::exception::LayoutConfigurationException("unsupported table orientation"); } } bool DocraftLayoutTableHandler::handle(const std::shared_ptr &request, model::DocraftTransform *result, - DocraftCursor& cursor) { + DocraftCursor &cursor) { if (auto table_node = std::dynamic_pointer_cast(request)) { compute(table_node, result, cursor); return true; diff --git a/docraft/src/docraft/layout/handler/docraft_layout_table_handler_impl.h b/docraft/src/docraft/layout/handler/docraft_layout_table_handler_impl.h new file mode 100644 index 0000000..d7ee6af --- /dev/null +++ b/docraft/src/docraft/layout/handler/docraft_layout_table_handler_impl.h @@ -0,0 +1,177 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +/** + * @file docraft_layout_table_handler_impl.h + * @brief Internal shared helpers and input structs for table layout handlers. + * + * NOT part of the public API. Include only from table handler .cc files. + */ + +#pragma once + +#include +#include +#include +#include + +#include "docraft/docraft_cursor.h" +#include "docraft/docraft_document_context.h" +#include "docraft/layout/docraft_layout_engine.h" +#include "docraft/model/docraft_table.h" +#include "docraft/model/docraft_text.h" + +namespace docraft::layout::handler::table_impl { + // ── Shared padding constants ─────────────────────────────────────────────── + + constexpr float kHorizontalCellPaddingY = 2.5F; + constexpr float kHorizontalCellPaddingX = 2.5F; + constexpr float kVerticalCellPaddingY = 2.0F; + constexpr float kVerticalCellPaddingX = 2.0F; + + // ── Input structs ───────────────────────────────────────────────────────── + + /** + * @brief Input for computing the layout of a single table cell. + */ + struct CellLayoutInput { + const std::shared_ptr &node; + docraft::layout::DocraftLayoutEngine &engine; + const std::shared_ptr &context; + DocraftCursor &table_cursor; + float inner_width; + float x; + float y; + }; + + // ── Shared helper functions ─────────────────────────────────────────────── + + /** + * @brief Creates synthetic DocraftText title nodes for columns that have no node yet. + */ + inline void ensure_title_nodes(const std::shared_ptr &node) { + const auto &titles = node->titles(); + if (node->title_nodes().size() < titles.size()) { + for (std::size_t i = node->title_nodes().size(); i < titles.size(); ++i) { + auto title_node = std::make_shared(); + title_node->set_text(titles[i]); + node->add_title_node(title_node); + } + } + } + + /** + * @brief Flattens the 2-D content grid into a single sequential vector of nodes. + */ + inline std::vector > flatten_content_nodes( + const std::shared_ptr &node) { + std::vector > flat; + for (const auto &row: node->content_nodes()) { + for (const auto &cell: row) { + if (cell) { + flat.emplace_back(cell); + } + } + } + return flat; + } + + /** + * @brief Returns the usable width for the table, respecting auto_fill, explicit width, and fallback. + */ + inline float available_width_for(const std::shared_ptr &node, + const std::shared_ptr &context, + float fallback) { + if (node->auto_fill_width()) { + return std::max(0.0F, context->layout().available_space()); + } + const float w = node->width(); + if (w > 0.0F) return std::max(0.0F, w); + return std::max(0.0F, fallback); + } + + /** + * @brief Moves the cursor to the node's declared position when in absolute mode; + * does nothing for block-flow nodes (cursor is already correct). + */ + inline void configure_cursor_position(const std::shared_ptr &node, + DocraftCursor &cursor) { + if (node->position_mode() != model::DocraftPositionType::kBlock) { + cursor.move_to(node->position().x, node->position().y); + } + } + + /** + * @brief Vertically centers a text node within a row by adjusting its y-for-children offset. + * + * Has no effect on non-text nodes. + */ + inline void center_text_in_row(const std::shared_ptr &node, + const float row_top_y, + const float row_height, + const float baseline_offset) { + const auto text_node = std::dynamic_pointer_cast(node); + if (!text_node) return; + const float current_center = + (text_node->anchors().top_left.y + text_node->anchors().bottom_left.y) * 0.5F; + const float desired_center = + row_top_y - (row_height * 0.5F) + (baseline_offset * text_node->font_size()); + const float delta = current_center - desired_center; + if (delta != 0.0F) text_node->set_y_for_children(delta); + } + + /** + * @brief Runs the layout engine on a single cell, temporarily repositioning the cursor. + * + * Saves and restores the cursor position so surrounding cells are not affected. + */ + inline void compute_cell_layout(const CellLayoutInput &input) { + auto &layout_service = input.context->edit_layout(); + const float saved_x = input.table_cursor.x(); + const float saved_y = input.table_cursor.y(); + layout_service.set_current_rect_width(input.inner_width); + input.table_cursor.move_to(input.x, input.y); + (void) input.engine.compute_layout(input.node, input.table_cursor); + input.table_cursor.move_to(saved_x, saved_y); + } + + /** + * @brief Builds the left-edge x-coordinates for each column given a starting x and per-column widths. + */ + inline std::vector build_column_lefts(const float fixed_x, const std::vector &col_widths) { + std::vector col_lefts(col_widths.size(), fixed_x); + float x = fixed_x; + for (std::size_t i = 0; i < col_widths.size(); ++i) { + col_lefts[i] = x; + x += col_widths[i]; + } + return col_lefts; + } + + /** + * @brief Returns a weight vector of size @p cols, normalised so that no zero-sum occurs. + * + * If the source vector has a wrong size or sums to zero, equal weights are used. + */ + inline std::vector assign_weights(const std::vector &source, const std::size_t cols) { + std::vector weights = source; + if (weights.size() != cols) weights.assign(cols, 1.0F); + const float total = std::accumulate(weights.begin(), weights.end(), 0.0F); + if (total <= 0.0F) weights.assign(cols, 1.0F); + return weights; + } +} // namespace docraft::layout::handler::table_impl + diff --git a/docraft/src/docraft/layout/handler/docraft_layout_text_handler.cc b/docraft/src/docraft/layout/handler/docraft_layout_text_handler.cc index e1c4b7e..8bf7679 100644 --- a/docraft/src/docraft/layout/handler/docraft_layout_text_handler.cc +++ b/docraft/src/docraft/layout/handler/docraft_layout_text_handler.cc @@ -40,11 +40,11 @@ namespace docraft::layout::handler { float DocraftLayoutTextHandler::measure_text_width(const std::shared_ptr &node) const { generic::DocraftFontApplier font_applier(edit_context()); font_applier.apply_font(node); - return edit_context()->text_backend()->measure_text_width(node->text()); + return edit_context()->rendering().text_rendering()->measure_text_width(node->text()); } float DocraftLayoutTextHandler::measure_test_width(const std::string &text) const { - return edit_context()->text_backend()->measure_text_width(text); + return edit_context()->rendering().text_rendering()->measure_text_width(text); } void DocraftLayoutTextHandler::compute(const std::shared_ptr &node, @@ -54,7 +54,7 @@ namespace docraft::layout::handler { generic::DocraftFontApplier font_applier(edit_context()); font_applier.apply_font(node); auto global_cursor = cursor; - + float specified_height = node->height(); DocraftCursor text_cursor = cursor;//cursor for the text box, start from the current global cursor if (node->position_mode() == model::DocraftPositionType::kBlock) { node->set_position({.x=global_cursor.x(), .y=global_cursor.y()}); @@ -67,7 +67,7 @@ namespace docraft::layout::handler { node->clear_lines(); // Recompute wrapping from scratch to avoid duplicate lines. const float padding = std::max(0.0F, node->padding()); - const float available_width = std::max(0.0F, edit_context()->available_space() - (2.0F * padding)); + const float available_width = std::max(0.0F, edit_context()->layout().available_space() - (2.0F * padding)); auto add_wrapped_word = [this,available_width,node](const std::string &word) { if (word.empty()) { return; @@ -142,14 +142,14 @@ namespace docraft::layout::handler { } //Compute position for each line - float total_height = 0.0F; + float total_height = 0.0F; float total_width = 0.0F; const float line_height = node->font_size() * interline_space_; // Always move to the first baseline below the current cursor Y, // but clamp so the first line doesn't get clipped above the page. - const float kTopSafe = edit_context()->page_height() - line_height; + const float kTopSafe = edit_context()->layout().page_height() - line_height; float first_baseline_y = text_cursor.y() - line_height; if (first_baseline_y > kTopSafe) { first_baseline_y = kTopSafe; @@ -205,7 +205,11 @@ namespace docraft::layout::handler { } node->set_position({.x = global_cursor.x(), .y = global_cursor.y()}); - node->set_height(total_height); + if (specified_height < total_height) { + node->set_height(total_height); + } else { + node->set_height(specified_height); + } node->set_width(total_width); if (box) { diff --git a/docraft/src/docraft/layout/handler/docraft_layout_vertical_table_handler.cc b/docraft/src/docraft/layout/handler/docraft_layout_vertical_table_handler.cc new file mode 100644 index 0000000..2f8da00 --- /dev/null +++ b/docraft/src/docraft/layout/handler/docraft_layout_vertical_table_handler.cc @@ -0,0 +1,270 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 "docraft/layout/handler/docraft_layout_vertical_table_handler.h" + +#include +#include +#include + +#include "docraft_layout_table_handler_impl.h" +#include "docraft/layout/docraft_layout_engine.h" +#include "docraft/utils/docraft_logger.h" + +namespace docraft::layout::handler { + void DocraftLayoutVerticalTableHandler::setup_compute_state(const std::shared_ptr &node, + DocraftCursor &cursor) { + auto &layout_service = edit_context()->edit_layout(); + table_cursor_ = cursor; + table_impl::configure_cursor_position(node, table_cursor_); + + fixed_x_ = table_cursor_.x(); + fixed_y_ = table_cursor_.y(); + saved_available_space_ = layout_service.available_space(); + + context_ = edit_context(); + engine_.emplace(context_, false); + + constexpr float kCellPaddingY = table_impl::kVerticalCellPaddingY; + constexpr float kCellPaddingX = table_impl::kVerticalCellPaddingX; + baseline_offset_ = node->baseline_offset(); + cell_padding_x_ = kCellPaddingX; + cell_padding_y_ = kCellPaddingY; + padding_x_ = 2.0F * kCellPaddingX; + padding_y_ = 2.0F * kCellPaddingY; + } + + DocraftLayoutVerticalTableHandler::TableData DocraftLayoutVerticalTableHandler::collect_table_data( + const std::shared_ptr &node) { + table_impl::ensure_title_nodes(node); + + const std::size_t row_count = node->titles().empty() ? node->title_nodes().size() : node->titles().size(); + const std::size_t value_cols = static_cast(std::max(1, node->content_cols())); + + return TableData{ + .title_nodes = node->title_nodes(), + .flat_values = table_impl::flatten_content_nodes(node), + .row_count = row_count, + .value_cols = value_cols + }; + } + + float DocraftLayoutVerticalTableHandler::compute_title_natural_width(const TableData &data) { + auto &layout_service = edit_context()->edit_layout(); + float title_col_natural_width = 0.0F; + + for (std::size_t r = 0; r < data.row_count; ++r) { + const auto &title_node = data.title_nodes[r]; + const float saved_x = table_cursor_.x(); + const float saved_y = table_cursor_.y(); + layout_service.set_current_rect_width(layout_service.available_space()); + table_cursor_.move_to(fixed_x_, layout_service.page_height()); + (void) engine_->compute_layout(title_node, table_cursor_); + table_cursor_.move_to(saved_x, saved_y); + + title_col_natural_width = std::max(title_col_natural_width, title_node->width()); + } + + return title_col_natural_width; + } + + DocraftLayoutVerticalTableHandler::ColumnPlan DocraftLayoutVerticalTableHandler::resolve_column_plan( + const std::shared_ptr &node, + const TableData &data, + const float title_natural_width) const { + const float available_width = table_impl::available_width_for(node, context_, title_natural_width); + + const std::vector weights = table_impl::assign_weights(node->column_weights(), 2); + const float total_weight = weights[0] + weights[1]; + + const float title_col_width = std::max(title_natural_width, available_width * (weights[0] / total_weight)); + const float values_block_width = std::max(0.0F, available_width - title_col_width); + float value_col_width = (data.value_cols > 0) + ? (values_block_width / static_cast(data.value_cols)) + : values_block_width; + + float explicit_value_col_width = 0.0F; + for (const auto &cell: data.flat_values) { + if (cell && cell->width() > 0.0F) { + explicit_value_col_width = std::max(explicit_value_col_width, cell->width()); + } + } + if (explicit_value_col_width > 0.0F) { + value_col_width = explicit_value_col_width; + } + + return ColumnPlan{ + .title_col_width = title_col_width, + .value_col_width = value_col_width, + .value_cols = data.value_cols + }; + } + + float DocraftLayoutVerticalTableHandler::layout_header_row(const std::shared_ptr &node, + const ColumnPlan &plan, + const float row_top_y) { + if (node->htitle_nodes().empty()) { + return 0.0F; + } + + const std::size_t header_cols = std::min(plan.value_cols, node->htitle_nodes().size()); + std::vector > header_nodes; + std::vector header_lefts; + std::vector header_widths; + header_nodes.reserve(header_cols); + header_lefts.reserve(header_cols); + header_widths.reserve(header_cols); + + for (std::size_t c = 0; c < header_cols; ++c) { + header_nodes.emplace_back(node->htitle_nodes()[c]); + header_lefts.emplace_back(fixed_x_ + plan.title_col_width + (static_cast(c) * plan.value_col_width)); + header_widths.emplace_back(plan.value_col_width); + } + + const BandView header_band{.nodes = &header_nodes, .lefts = &header_lefts, .widths = &header_widths}; + const float header_row_height = compute_band_height(header_band, row_top_y); + place_band(header_band, row_top_y, header_row_height); + return header_row_height; + } + + float DocraftLayoutVerticalTableHandler::layout_body_rows(const TableData &data, + const ColumnPlan &plan, + const float row_top_y) { + float y = row_top_y; + float total_height = 0.0F; + + for (std::size_t r = 0; r < data.row_count; ++r) { + std::vector > row_nodes; + std::vector row_lefts; + std::vector row_widths; + row_nodes.reserve(plan.value_cols + 1); + row_lefts.reserve(plan.value_cols + 1); + row_widths.reserve(plan.value_cols + 1); + + row_nodes.emplace_back(data.title_nodes[r]); + row_lefts.emplace_back(fixed_x_); + row_widths.emplace_back(plan.title_col_width); + + const std::size_t row_first = r * plan.value_cols; + for (std::size_t vc = 0; vc < plan.value_cols; ++vc) { + const std::size_t idx = row_first + vc; + if (idx >= data.flat_values.size()) { + break; + } + row_nodes.emplace_back(data.flat_values[idx]); + row_lefts.emplace_back( + fixed_x_ + plan.title_col_width + (static_cast(vc) * plan.value_col_width)); + row_widths.emplace_back(plan.value_col_width); + } + + const BandView row_band{.nodes = &row_nodes, .lefts = &row_lefts, .widths = &row_widths}; + const float row_height = compute_band_height(row_band, y); + place_band(row_band, y, row_height); + + total_height += row_height; + y -= row_height; + } + + return total_height; + } + + void DocraftLayoutVerticalTableHandler::finalize_output(const std::shared_ptr &node, + model::DocraftTransform *box, + const float table_width, + const float table_height) { + auto &layout_service = edit_context()->edit_layout(); + node->set_position({.x = fixed_x_, .y = fixed_y_}); + node->set_width(table_width); + node->set_height(table_height); + + if (box) { + box->set_position(node->position()); + box->set_width(node->width()); + box->set_height(node->height()); + } + layout_service.set_current_rect_width(saved_available_space_); + } + + void DocraftLayoutVerticalTableHandler::log_cells(const std::shared_ptr &node) { + for (const auto &row: node->content_nodes()) { + for (const auto &cell: row) { + if (cell) { + LOG_DEBUG(fmt::format("Cell at ({}, {}) with size ({}, {})", cell->position().x, + cell->position().y, cell->width(), cell->height())); + } + } + } + } + + void DocraftLayoutVerticalTableHandler::clear_compute_state() { + engine_.reset(); + } + + float DocraftLayoutVerticalTableHandler::compute_band_height(const BandView &band, + const float row_top_y) { + float row_height = 0.0F; + for (std::size_t i = 0; i < band.nodes->size(); ++i) { + const auto &cell_node = (*band.nodes)[i]; + if (!cell_node) { + continue; + } + table_impl::compute_cell_layout({ + .node = cell_node, + .engine = *engine_, + .context = context_, + .table_cursor = table_cursor_, + .inner_width = std::max(0.0F, (*band.widths)[i] - padding_x_), + .x = (*band.lefts)[i] + cell_padding_x_, + .y = row_top_y - cell_padding_y_ + }); + row_height = std::max(row_height, cell_node->height() + padding_y_); + } + return row_height; + } + + void DocraftLayoutVerticalTableHandler::place_band(const BandView &band, + const float row_top_y, + const float row_height) const { + for (std::size_t i = 0; i < band.nodes->size(); ++i) { + const auto &cell_node = (*band.nodes)[i]; + if (!cell_node) { + continue; + } + table_impl::center_text_in_row(cell_node, row_top_y, row_height, baseline_offset_); + cell_node->set_position({.x = (*band.lefts)[i], .y = row_top_y}); + cell_node->set_width((*band.widths)[i]); + cell_node->set_height(row_height); + } + } + + void DocraftLayoutVerticalTableHandler::compute(const std::shared_ptr &node, + model::DocraftTransform *box, + DocraftCursor &cursor) { + setup_compute_state(node, cursor); + const TableData data = collect_table_data(node); + const float title_natural_width = compute_title_natural_width(data); + const ColumnPlan plan = resolve_column_plan(node, data, title_natural_width); + + const float header_height = layout_header_row(node, plan, fixed_y_); + const float body_top_y = fixed_y_ - header_height; + const float body_height = layout_body_rows(data, plan, body_top_y); + + const float table_width = plan.title_col_width + (plan.value_col_width * static_cast(plan.value_cols)); + finalize_output(node, box, table_width, header_height + body_height); + log_cells(node); + clear_compute_state(); + } +} // namespace docraft::layout::handler diff --git a/docraft/src/docraft/main.cpp b/docraft/src/docraft/main.cpp index e15d799..eb35480 100644 --- a/docraft/src/docraft/main.cpp +++ b/docraft/src/docraft/main.cpp @@ -19,7 +19,6 @@ #include #include #include -#include #include #include #include @@ -28,6 +27,7 @@ #include "docraft/craft/docraft_craft_language_parser.h" #include "docraft/docraft_document.h" +#include "docraft/exception/docraft_exceptions.h" #include "docraft/templating/docraft_template_engine.h" #include "nlohmann/json.hpp" #include "docraft/utils/docraft_logger.h" @@ -146,20 +146,21 @@ namespace { * @brief Parses a JSON file into an unordered map. * @param json_file Path to the JSON file. * @return An unordered map where keys are normalized (lowercase) and values are pairs of original key and value. - * @throws std::runtime_error if the file cannot be opened or if JSON is invalid. + * @throws docraft::exception::CannotOpenFileException if the file cannot be opened. + * @throws docraft::exception::InvalidJSONException if JSON is invalid. */ std::unordered_map > parse_json_file( const std::filesystem::path &json_file) { std::ifstream input(json_file); if (!input) { - throw std::runtime_error("Unable to open JSON file: " + json_file.string()); + throw docraft::exception::CannotOpenFileException("Unable to open JSON file: " + json_file.string()); } try { json json_obj = json::parse(input); return flatten_json(json_obj); } catch (const json::parse_error &ex) { - throw std::runtime_error( + throw docraft::exception::InvalidJSONException( "Invalid JSON in file '" + json_file.string() + "': " + std::string(ex.what())); } @@ -183,7 +184,8 @@ namespace { * @param argc Argument count. * @param argv Argument vector. * @return Parsed command-line options. - * @throws std::runtime_error if arguments are invalid or missing. + * @throws docraft::exception::ConfigurationException if arguments are invalid. + * @throws docraft::exception::MissingArgumentException if a required value is missing. */ CliOptions parse_args(int argc, char *argv[]) { CliOptions options; @@ -204,7 +206,7 @@ namespace { if (arg == "-d" || arg == "--data") { if (i + 1 >= argc) { - throw std::runtime_error("Missing file path after " + arg); + throw docraft::exception::MissingArgumentException("Missing file path after " + arg); } options.data_file = argv[++i]; //get the next argument as data file path options.has_data_file = true; @@ -212,14 +214,14 @@ namespace { } if (!arg.empty() && arg.front() == '-') { - throw std::runtime_error("Unknown option: " + arg); + throw docraft::exception::ConfigurationException("Unknown option: " + arg); } positional_args.push_back(arg); //collect positional arguments } if (positional_args.size() != 2) { - throw std::runtime_error("Expected required arguments: "); + throw docraft::exception::ConfigurationException("Expected required arguments: "); } options.craft_file = positional_args[0]; //first positional argument is the input .craft file, always @@ -227,10 +229,10 @@ namespace { // Validate input and output file paths if (!has_craft_extension(options.craft_file)) { - throw std::runtime_error("Input file must have .craft extension"); + throw docraft::exception::ConfigurationException("Input file must have .craft extension"); } if (options.output_file.filename().empty()) { - throw std::runtime_error("Output destination must include a filename"); + throw docraft::exception::ConfigurationException("Output destination must include a filename"); } // If output file has no extension, add .pdf. If it has an extension, ensure it's .pdf (case-insensitive). if (!options.output_file.has_extension()) { @@ -242,7 +244,7 @@ namespace { return static_cast(std::tolower(ch)); }); if (extension != ".pdf") { - throw std::runtime_error("Output file extension must be .pdf"); + throw docraft::exception::ConfigurationException("Output file extension must be .pdf"); } } @@ -253,13 +255,14 @@ namespace { * @brief Parses a mapping file with 'key: value' entries into an unordered map. * @param mapping_file Path to the mapping file. * @return An unordered map where keys are normalized (lowercase) and values are pairs of original key and value. - * @throws std::runtime_error if the file cannot be opened or if any line is invalid. + * @throws docraft::exception::CannotOpenFileException if the file cannot be opened. + * @throws docraft::exception::DataFormatException if a line is invalid. */ std::unordered_map > parse_data_mapping_file( const std::filesystem::path &mapping_file) { std::ifstream input(mapping_file); if (!input) { - throw std::runtime_error("Unable to open mapping file: " + mapping_file.string()); + throw docraft::exception::CannotOpenFileException("Unable to open mapping file: " + mapping_file.string()); } std::unordered_map > mappings; @@ -281,7 +284,7 @@ namespace { const std::size_t separator = trimmed.find(':'); if (separator == std::string::npos) { - throw std::runtime_error( + throw docraft::exception::DataFormatException( "Invalid mapping at line " + std::to_string(line_number) + " in file '" + mapping_file.string() + "': expected 'key: value'"); } @@ -289,7 +292,7 @@ namespace { std::string key = trim_copy(trimmed.substr(0, separator)); std::string value = trim_copy(trimmed.substr(separator + 1)); if (key.empty()) { - throw std::runtime_error( + throw docraft::exception::DataFormatException( "Invalid mapping at line " + std::to_string(line_number) + " in file '" + mapping_file.string() + "': empty key"); } @@ -319,7 +322,8 @@ int main(int argc, char *argv[]) { const CliOptions options = parse_args(argc, argv); if (!std::filesystem::exists(options.craft_file)) { - throw std::runtime_error("docraft/craft file not found: " + options.craft_file.string()); + throw docraft::exception::FileNotFoundException( + "docraft/craft file not found: " + options.craft_file.string()); } docraft::craft::DocraftCraftLanguageParser parser; @@ -327,16 +331,16 @@ int main(int argc, char *argv[]) { auto document = parser.edit_document(); if (!document) { - throw std::runtime_error("Unable to build document from .craft file"); + throw docraft::exception::RenderingFailedException("Unable to build document from .craft file"); } const std::filesystem::path output_parent = options.output_file.parent_path(); - document->set_document_path(output_parent.empty() ? "." : output_parent.string()); - document->set_document_title(options.output_file.stem().string()); + document->edit_config().set_document_path(output_parent.empty() ? "." : output_parent.string()); + document->edit_config().set_document_title(options.output_file.stem().string()); if (options.has_data_file) { if (!std::filesystem::exists(options.data_file)) { - throw std::runtime_error("Data file not found: " + options.data_file.string()); + throw docraft::exception::FileNotFoundException("Data file not found: " + options.data_file.string()); } std::unordered_map > mappings; @@ -354,10 +358,10 @@ int main(int argc, char *argv[]) { for (const auto &[_, mapping]: mappings) { template_engine->add_template_variable(mapping.first, mapping.second); } - document->set_document_template_engine(template_engine); + document->edit_config().set_document_template_engine(template_engine); } - std::filesystem::create_directories(document->document_path());//ensure output directory exists + std::filesystem::create_directories(document->config().document_path()); //ensure output directory exists LOG_INFO("Rendering document to PDF..."); auto timer = std::chrono::high_resolution_clock::now(); document->render();//render the document to PDF @@ -366,7 +370,7 @@ int main(int argc, char *argv[]) { LOG_INFO("Document rendered in " + std::to_string(duration) + " ms"); LOG_INFO("Generated: " + options.output_file.string()); return 0; - } catch (const std::exception &ex) { + } catch (const docraft::exception::DocraftException &ex) { LOG_ERROR("Error: " + std::string(ex.what())); LOG_INFO("Use -h or --help for usage information."); return 1; diff --git a/docraft/src/docraft/management/docraft_backend_cache.cc b/docraft/src/docraft/management/docraft_backend_cache.cc index 6b05aec..15789e7 100644 --- a/docraft/src/docraft/management/docraft_backend_cache.cc +++ b/docraft/src/docraft/management/docraft_backend_cache.cc @@ -15,12 +15,23 @@ */ #include "docraft/management/docraft_backend_cache.h" -#include "docraft/docraft_lib.h" namespace docraft::management { - void DocraftBackendCache::initialize_from_backend( - const std::shared_ptr &backend) { - refresh_caches(backend); + namespace { + template + std::shared_ptr alias_backend_interface( + const std::shared_ptr &owner, + InterfaceType *interface_pointer) { + if (!owner || !interface_pointer) { + return {}; + } + return std::shared_ptr(owner, interface_pointer); + } + } // namespace + + void DocraftBackendCache::initialize_from_provider( + const std::shared_ptr &rendering_provider) { + refresh_caches(rendering_provider); } std::shared_ptr DocraftBackendCache::line_backend() const { @@ -63,12 +74,27 @@ namespace docraft::management { return page_backend_; } - 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); + void DocraftBackendCache::refresh_caches( + const std::shared_ptr &rendering_provider) { + line_backend_ = alias_backend_interface(rendering_provider, + rendering_provider + ? rendering_provider->edit_line_rendering() + : nullptr); + shape_backend_ = alias_backend_interface(rendering_provider, + rendering_provider + ? rendering_provider->edit_shape_rendering() + : nullptr); + text_backend_ = alias_backend_interface(rendering_provider, + rendering_provider + ? rendering_provider->edit_text_rendering() + : nullptr); + image_backend_ = alias_backend_interface(rendering_provider, + rendering_provider + ? rendering_provider->edit_image_rendering() + : nullptr); + page_backend_ = alias_backend_interface(rendering_provider, + rendering_provider + ? rendering_provider->edit_page_rendering() + : nullptr); } } // docraft::management - diff --git a/docraft/src/docraft/model/docraft_children_container_node.cc b/docraft/src/docraft/model/docraft_children_container_node.cc index 137b61e..7fccd06 100644 --- a/docraft/src/docraft/model/docraft_children_container_node.cc +++ b/docraft/src/docraft/model/docraft_children_container_node.cc @@ -19,6 +19,8 @@ #include #include +#include "docraft/exception/docraft_exceptions.h" + namespace docraft::model { DocraftChildrenContainerNode::DocraftChildrenContainerNode(DocraftChildrenContainerNode *node) : DocraftNode(node) { @@ -29,7 +31,7 @@ namespace docraft::model { void DocraftChildrenContainerNode::add_child(const std::shared_ptr &child) { if (!child) { - throw std::invalid_argument("Child node cannot be null"); + throw docraft::exception::InvalidInputException("Child node cannot be null"); } children_.emplace_back(child); on_child_added(); @@ -37,7 +39,7 @@ namespace docraft::model { void DocraftChildrenContainerNode::insert_child(std::size_t index, const std::shared_ptr &child) { if (!child) { - throw std::invalid_argument("Child node cannot be null"); + throw docraft::exception::InvalidInputException("Child node cannot be null"); } if (index > children_.size()) { index = children_.size(); diff --git a/docraft/src/docraft/model/docraft_clone_utils.cc b/docraft/src/docraft/model/docraft_clone_utils.cc index 4829d83..f59e6d8 100644 --- a/docraft/src/docraft/model/docraft_clone_utils.cc +++ b/docraft/src/docraft/model/docraft_clone_utils.cc @@ -15,17 +15,18 @@ */ #include "docraft/model/docraft_clone_utils.h" - -#include +#include "docraft/exception/docraft_exceptions.h" namespace docraft::model { + using docraft::exception::ModelException; + std::shared_ptr clone_node(const std::shared_ptr &node) { if (!node) { return nullptr; } auto clonable = std::dynamic_pointer_cast(node); if (!clonable) { - throw std::runtime_error("Node does not implement IDocraftClonable"); + throw ModelException("Node does not implement IDocraftClonable"); } return clonable->clone(); } diff --git a/docraft/src/docraft/model/docraft_layout.cc b/docraft/src/docraft/model/docraft_layout.cc index 9d7db91..13ee0a0 100644 --- a/docraft/src/docraft/model/docraft_layout.cc +++ b/docraft/src/docraft/model/docraft_layout.cc @@ -21,6 +21,7 @@ #include #include +#include "docraft/exception/docraft_exceptions.h" #include "docraft/model/docraft_clone_utils.h" #include "docraft/utils/docraft_logger.h" @@ -72,7 +73,7 @@ namespace docraft::model { } void DocraftLayout::set_weight_for_child(const int index, float weight) const { if (index < 0 || std::cmp_greater_equal(index ,children().size())) { - throw std::out_of_range("Child index out of range"); + throw docraft::exception::InvalidInputException("Child index out of range"); } children()[index]->set_weight(weight); } diff --git a/docraft/src/docraft/model/docraft_list.cc b/docraft/src/docraft/model/docraft_list.cc index a541121..44741d3 100644 --- a/docraft/src/docraft/model/docraft_list.cc +++ b/docraft/src/docraft/model/docraft_list.cc @@ -17,13 +17,20 @@ #include "docraft/model/docraft_list.h" #include +#include +#include "docraft/exception/docraft_exceptions.h" #include "docraft/model/docraft_clone_utils.h" #include "docraft/renderer/docraft_renderer.h" +#include "docraft/utils/docraft_logger.h" namespace docraft::model { void DocraftList::draw(const std::shared_ptr &context) { const auto &items = children(); + if (!context) { + LOG_WARNING("Cannot draw list: context is null"); + return; + } for (size_t i = 0; i < items.size(); ++i) { auto text_node = std::dynamic_pointer_cast(items[i]); if (!text_node) { @@ -34,20 +41,21 @@ namespace docraft::model { } const auto &marker = markers_[i]; if (marker.kind == Marker::Kind::kBox) { - if (context && context->shape_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(); - shape->save_state(); - shape->set_stroke_color(rgb.r, rgb.g, rgb.b); - shape->set_line_width(1.0F); - shape->draw_rectangle(x, y, size, size); - shape->stroke(); - shape->restore_state(); + auto shape = context->rendering().shape_rendering(); + auto line = context->rendering().line_rendering(); + if (!shape || !line) { + continue; } - continue; + 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); + shape->save_state(); + 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(); } DocraftText marker_text; auto line = std::make_shared(marker.text); @@ -58,8 +66,8 @@ namespace docraft::model { line->set_alignment(TextAlignment::kLeft); line->set_underline(false); line->set_position(marker.position); - if (context && context->text_backend()) { - line->set_width(context->text_backend()->measure_text_width(marker.text)); + if (const auto text_backend = context->rendering().text_rendering()) { + line->set_width(text_backend->measure_text_width(marker.text)); } line->set_height(text_node->font_size() * 1.2F); marker_text.add_line(line); @@ -71,7 +79,7 @@ namespace docraft::model { std::shared_ptr DocraftList::clone() const { auto copy = std::make_shared(*this); copy->clear_children(); - for (const auto &child : children()) { + for (const auto &child: children()) { copy->add_child(clone_node(child)); } copy->update_items(); @@ -116,7 +124,7 @@ namespace docraft::model { } void DocraftList::on_child_removed(int index) { - if (index >= 0 && index < static_cast(raw_texts_.size())) { + if (index >= 0 && std::cmp_less(index, raw_texts_.size())) { raw_texts_.erase(raw_texts_.begin() + index); } update_items(); @@ -133,7 +141,7 @@ namespace docraft::model { } } - void DocraftList::apply_text_transform(const std::function &transform) { + void DocraftList::apply_text_transform(const std::function &transform) { if (!transform) { return; } @@ -151,7 +159,7 @@ namespace docraft::model { std::shared_ptr DocraftList::as_text_node(const std::shared_ptr &node) { auto text_node = std::dynamic_pointer_cast(node); if (!text_node) { - throw std::invalid_argument("List items must be Text nodes"); + throw docraft::exception::InvalidInputException("List items must be Text nodes"); } return text_node; } @@ -163,7 +171,7 @@ namespace docraft::model { } raw_texts_.clear(); raw_texts_.reserve(items.size()); - for (const auto &child : items) { + for (const auto &child: items) { auto text_node = as_text_node(child); raw_texts_.emplace_back(text_node->text()); } @@ -229,24 +237,24 @@ namespace docraft::model { const char *numeral; }; const Roman numerals[] = { - {1000, "M"}, - {900, "CM"}, - {500, "D"}, - {400, "CD"}, - {100, "C"}, - {90, "XC"}, - {50, "L"}, - {40, "XL"}, - {10, "X"}, - {9, "IX"}, - {5, "V"}, - {4, "IV"}, - {1, "I"}, + {.value = 1000, .numeral = "M"}, + {.value = 900, .numeral = "CM"}, + {.value = 500, .numeral = "D"}, + {.value = 400, .numeral = "CD"}, + {.value = 100, .numeral = "C"}, + {.value = 90, .numeral = "XC"}, + {.value = 50, .numeral = "L"}, + {.value = 40, .numeral = "XL"}, + {.value = 10, .numeral = "X"}, + {.value = 9, .numeral = "IX"}, + {.value = 5, .numeral = "V"}, + {.value = 4, .numeral = "IV"}, + {.value = 1, .numeral = "I"}, }; std::string result; int remaining = value; - for (const auto &item : numerals) { + for (const auto &item: numerals) { while (remaining >= item.value) { result += item.numeral; remaining -= item.value; diff --git a/docraft/src/docraft/model/docraft_node.cc b/docraft/src/docraft/model/docraft_node.cc index c5c06f1..1d1d18f 100644 --- a/docraft/src/docraft/model/docraft_node.cc +++ b/docraft/src/docraft/model/docraft_node.cc @@ -19,6 +19,8 @@ #include #include +#include "docraft/exception/docraft_exceptions.h" + namespace docraft::model { int DocraftNode::next_id_ = 0; @@ -90,7 +92,7 @@ namespace docraft::model { if (!context) { return true; } - const auto &page_backend = context->page_backend(); + const auto &page_backend = context->rendering().page_rendering(); if (!page_backend) { return true; } @@ -120,13 +122,13 @@ namespace docraft::model { void DocraftNode::set_weight(float weight) { if (weight < 0) { - throw std::invalid_argument("Weight must be positive"); + throw docraft::exception::InvalidInputException("Weight must be positive"); } if (weight == 0) { - throw std::invalid_argument("Weight cannot be zero"); + throw docraft::exception::InvalidInputException("Weight cannot be zero"); } if (weight > 1.0F) { - throw std::invalid_argument("Weight must be less than or equal to one"); + throw docraft::exception::InvalidInputException("Weight must be less than or equal to one"); } weight_ = weight; } diff --git a/docraft/src/docraft/model/docraft_page_number.cc b/docraft/src/docraft/model/docraft_page_number.cc index e41e248..06b556e 100644 --- a/docraft/src/docraft/model/docraft_page_number.cc +++ b/docraft/src/docraft/model/docraft_page_number.cc @@ -17,13 +17,15 @@ #include "docraft/model/docraft_page_number.h" #include -#include #include "docraft/backend/docraft_page_rendering_backend.h" +#include "docraft/exception/docraft_exceptions.h" #include "docraft/model/docraft_clone_utils.h" #include "docraft/renderer/docraft_renderer.h" namespace docraft::model { + using exception::ModelException; + DocraftPageNumber::DocraftPageNumber() { set_text("xxxxx"); // Placeholder text to reserve space during layout until the actual page number is resolved. } @@ -31,7 +33,7 @@ namespace docraft::model { void DocraftPageNumber::update_text_from_context(const std::shared_ptr& context) { std::size_t page_number = 1; if (context) { - if (const auto& page_backend = context->page_backend()) { + if (const auto &page_backend = context->rendering().page_rendering()) { page_number = page_backend->current_page_number(); } } @@ -52,7 +54,7 @@ namespace docraft::model { auto cloned_child = clone_node(child); auto text_child = std::dynamic_pointer_cast(cloned_child); if (!text_child) { - throw std::runtime_error("Page number line child does not clone to DocraftText"); + throw ModelException("Page number line child does not clone to DocraftText"); } copy->add_line(text_child); } diff --git a/docraft/src/docraft/model/docraft_table.cc b/docraft/src/docraft/model/docraft_table.cc index b65f9e7..b89ced5 100644 --- a/docraft/src/docraft/model/docraft_table.cc +++ b/docraft/src/docraft/model/docraft_table.cc @@ -18,8 +18,8 @@ #include #include -#include +#include "docraft/exception/docraft_exceptions.h" #include "docraft/model/docraft_clone_utils.h" #include "docraft/renderer/docraft_pdf_renderer.h" #include "docraft/utils/docraft_base64.h" @@ -32,35 +32,38 @@ namespace docraft::model { try { parsed = nlohmann::json::parse(json_str); } catch (const nlohmann::json::parse_error &e) { - throw std::invalid_argument( + throw docraft::exception::InvalidInputException( std::string("Table model must be a JSON matrix of strings: ") + e.what()); } if (!parsed.is_array() || parsed.empty()) { - throw std::invalid_argument("Table model must be a non-empty JSON matrix of strings"); + throw docraft::exception::InvalidInputException( + "Table model must be a non-empty JSON matrix of strings"); } std::vector> matrix; std::size_t expected_cols = 0; for (const auto &row : parsed) { if (!row.is_array() || row.empty()) { - throw std::invalid_argument("Table model must be a non-empty JSON matrix of strings"); + throw docraft::exception::InvalidInputException( + "Table model must be a non-empty JSON matrix of strings"); } std::vector row_values; row_values.reserve(row.size()); for (const auto &cell : row) { if (!cell.is_string()) { - throw std::invalid_argument("Table model must contain only strings"); + throw docraft::exception::InvalidInputException("Table model must contain only strings"); } row_values.emplace_back(cell.get()); } if (expected_cols == 0) { expected_cols = row_values.size(); } else if (row_values.size() != expected_cols) { - throw std::invalid_argument("Table model rows must have the same number of columns"); + throw docraft::exception::InvalidInputException( + "Table model rows must have the same number of columns"); } matrix.emplace_back(std::move(row_values)); } if (expected_cols == 0) { - throw std::invalid_argument("Table model must contain at least one column"); + throw docraft::exception::InvalidInputException("Table model must contain at least one column"); } return matrix; } @@ -70,17 +73,17 @@ namespace docraft::model { try { parsed = nlohmann::json::parse(json_str); } catch (const nlohmann::json::parse_error &e) { - throw std::invalid_argument( + throw docraft::exception::InvalidInputException( std::string("Table header must be a JSON array of strings: ") + e.what()); } if (!parsed.is_array() || parsed.empty()) { - throw std::invalid_argument("Table header must be a non-empty JSON array of strings"); + throw docraft::exception::InvalidInputException("Table header must be a non-empty JSON array of strings"); } std::vector header; header.reserve(parsed.size()); - for (const auto &cell : parsed) { + for (const auto &cell: parsed) { if (!cell.is_string()) { - throw std::invalid_argument("Table header must contain only strings"); + throw docraft::exception::InvalidInputException("Table header must contain only strings"); } header.emplace_back(cell.get()); } @@ -98,21 +101,22 @@ namespace docraft::model { std::vector> result; try { parsed = nlohmann::json::parse(json_str); - }catch (const nlohmann::json::parse_error &e) { - throw std::invalid_argument( + } catch (const nlohmann::json::parse_error &e) { + throw docraft::exception::InvalidInputException( std::string("Table model must be a JSON array of objects: ") + e.what()); } if (!parsed.is_array() || parsed.empty()) { - throw std::invalid_argument("Table model must be a non-empty JSON array of objects"); + throw docraft::exception::InvalidInputException("Table model must be a non-empty JSON array of objects"); } //build content items for each item in the array - for (const auto &item : parsed) { + for (const auto &item: parsed) { if (!item.is_object()) { - throw std::invalid_argument("Table model must be a JSON array of objects"); + throw docraft::exception::InvalidInputException("Table model must be a JSON array of objects"); } for (const auto &value : item.items()) { if (!value.value().is_string()) { - throw std::invalid_argument("Table model objects must contain only string values"); + throw docraft::exception::InvalidInputException( + "Table model objects must contain only string values"); } } @@ -140,6 +144,14 @@ namespace docraft::model { return result; } + /** + * @brief Splits a vector into two parts at the specified index, + * keeping the head in the original vector and returning the tail as a new vector. + * @tparam T + * @param source + * @param split_index + * @return + */ template std::vector split_tail(std::vector &source, std::size_t split_index) { split_index = std::min(split_index, source.size()); @@ -206,7 +218,7 @@ namespace docraft::model { auto cloned = clone_node(titles[i]); auto text = std::dynamic_pointer_cast(cloned); if (!text) { - throw std::runtime_error("Title node does not clone to DocraftText"); + throw docraft::exception::DocumentStateException("Title node does not clone to DocraftText"); } copy->set_title_node(static_cast(i), text); } @@ -216,7 +228,7 @@ namespace docraft::model { auto cloned = clone_node(htitles[i]); auto text = std::dynamic_pointer_cast(cloned); if (!text) { - throw std::runtime_error("Header title node does not clone to DocraftText"); + throw docraft::exception::DocumentStateException("Header title node does not clone to DocraftText"); } copy->set_htitle_node(static_cast(i), text); } @@ -254,14 +266,14 @@ namespace docraft::model { void DocraftTable::set_column_weight(int index, float weight) { if (index < 0 || std::cmp_greater_equal(index, column_weights_.size())) { - throw std::out_of_range("Column weight index out of range"); + throw docraft::exception::InvalidInputException("Column weight index out of range"); } column_weights_[index] = weight; } void DocraftTable::set_row_weight(int index, float weight) { if (index < 0 || std::cmp_greater_equal(index, row_weights_.size())) { - throw std::out_of_range("Row weight index out of range"); + throw docraft::exception::InvalidInputException("Row weight index out of range"); } row_weights_[index] = weight; } @@ -272,7 +284,7 @@ namespace docraft::model { void DocraftTable::set_title(int index, const std::string &title) { if (index < 0 || std::cmp_greater_equal(index, titles_.size())) { - throw std::out_of_range("Title index out of range"); + throw docraft::exception::InvalidInputException("Title index out of range"); } titles_[index] = title; } @@ -302,6 +314,9 @@ namespace docraft::model { std::optional background) { title_nodes_.emplace_back(node); title_backgrounds_.emplace_back(std::move(background)); + if (orientation_ == LayoutOrientation::kVertical) { + set_rows(static_cast(title_nodes_.size())); + } } void DocraftTable::add_htitle_node(const std::shared_ptr &node, @@ -310,8 +325,17 @@ namespace docraft::model { htitle_backgrounds_.emplace_back(std::move(background)); } + bool DocraftTable::is_content_allowed(const std::shared_ptr &node) { + return std::dynamic_pointer_cast(node) || std::dynamic_pointer_cast(node); + } + void DocraftTable::add_content_node(const std::shared_ptr &node, std::optional background) { + //table allow only text and image nodes as content, so we check the type of the node before adding it to the content nodes vector + if (!is_content_allowed(node)) { + throw docraft::exception::InvalidInputException( + "Content node must be either a DocraftText or a DocraftImage"); + } content_nodes_.emplace_back(node); content_backgrounds_.emplace_back(std::move(background)); if (orientation_ == LayoutOrientation::kHorizontal && content_cols() > 0) { @@ -323,14 +347,14 @@ namespace docraft::model { void DocraftTable::set_content_node(int index, const std::shared_ptr &node) { if (index < 0 || std::cmp_greater_equal(index, content_nodes_.size())) { - throw std::out_of_range("Content node index out of range"); + throw docraft::exception::InvalidInputException("Content node index out of range"); } content_nodes_[index] = node; } void DocraftTable::set_content_node_background(int index, const DocraftColor &color) { if (index < 0 || std::cmp_greater_equal(index, content_backgrounds_.size())) { - throw std::out_of_range("Content background index out of range"); + throw docraft::exception::InvalidInputException("Content background index out of range"); } content_backgrounds_[index] = color; } @@ -349,30 +373,33 @@ namespace docraft::model { void DocraftTable::set_title_node(int index, const std::shared_ptr &node) { if (index < 0 || std::cmp_greater_equal(index, title_nodes_.size())) { - throw std::out_of_range("Title node index out of range"); + throw docraft::exception::InvalidInputException("Title node index out of range"); } if (!node) { - throw std::invalid_argument("Title node cannot be null"); + throw docraft::exception::InvalidInputException("Title node cannot be null"); } title_nodes_[index] = node; } void DocraftTable::set_htitle_node(int index, const std::shared_ptr &node) { if (index < 0 || std::cmp_greater_equal(index, htitle_nodes_.size())) { - throw std::out_of_range("Header title node index out of range"); + throw docraft::exception::InvalidInputException("Header title node index out of range"); } if (!node) { - throw std::invalid_argument("Header title node cannot be null"); + throw docraft::exception::InvalidInputException("Header title node cannot be null"); } htitle_nodes_[index] = node; } - void DocraftTable::set_title_nodes(const std::vector> &nodes) { + void DocraftTable::set_title_nodes(const std::vector > &nodes) { if (nodes.size() != titles_.size()) { - throw std::invalid_argument("Number of title nodes must be equal to number of titles"); + throw docraft::exception::InvalidInputException("Number of title nodes must be equal to number of titles"); } title_nodes_ = nodes; title_backgrounds_.resize(nodes.size()); + if (orientation_ == LayoutOrientation::kVertical) { + set_rows(static_cast(title_nodes_.size())); + } } void DocraftTable::set_htitle_nodes(const std::vector> &nodes) { @@ -386,7 +413,7 @@ namespace docraft::model { void DocraftTable::set_row_background(int index, const DocraftColor &background) { if (index < 0 || std::cmp_greater_equal(index, row_backgrounds_.size())) { - throw std::out_of_range("Row background index out of range"); + throw docraft::exception::InvalidInputException("Row background index out of range"); } row_backgrounds_[index] = background; } @@ -413,14 +440,14 @@ namespace docraft::model { const std::string &DocraftTable::model_template() const { if (!model_template_.has_value()) { - throw std::runtime_error("docraft/model template not set"); + throw docraft::exception::TemplateException("docraft/model template not set"); } return *model_template_; } const std::string &DocraftTable::header_template() const { if (!header_template_.has_value()) { - throw std::runtime_error("Header template not set"); + throw docraft::exception::TemplateException("Header template not set"); } return *header_template_; } @@ -445,14 +472,14 @@ namespace docraft::model { const auto matrix = parse_json_matrix(json_str); if (matrix.empty()) { - throw std::invalid_argument("Table model must be a non-empty JSON matrix of strings"); + throw docraft::exception::InvalidInputException("Table model must be a non-empty JSON matrix of strings"); } const std::size_t cols = matrix.front().size(); const std::size_t rows = matrix.size(); if (!titles_.empty() && titles_.size() != cols) { - throw std::invalid_argument("Table model columns do not match header size"); + throw docraft::exception::InvalidInputException("Table model columns do not match header size"); } orientation_ = LayoutOrientation::kHorizontal; @@ -481,10 +508,10 @@ namespace docraft::model { void DocraftTable::apply_json_header(const std::string &json_str) { const auto header = parse_json_header(json_str); if (header.empty()) { - throw std::invalid_argument("Table header must be a non-empty JSON array of strings"); + throw docraft::exception::InvalidInputException("Table header must be a non-empty JSON array of strings"); } if (content_cols_ > 0 && static_cast(content_cols_) != header.size()) { - throw std::invalid_argument("Table header size does not match model columns"); + throw docraft::exception::InvalidInputException("Table header size does not match model columns"); } orientation_ = LayoutOrientation::kHorizontal; @@ -509,7 +536,7 @@ namespace docraft::model { } std::shared_ptr DocraftTable::split_after_row(std::size_t rows_to_keep, bool repeat_header) { - if (rows_to_keep >= static_cast(rows_)) { + if (std::cmp_greater_equal(rows_to_keep, rows_)) { return nullptr; } @@ -524,10 +551,10 @@ namespace docraft::model { htitle_nodes_, htitle_backgrounds_, remainder->htitle_nodes_, remainder->htitle_backgrounds_); } - const std::size_t total_rows = static_cast(rows_); + const auto total_rows = static_cast(rows_); const std::size_t keep_rows = std::min(rows_to_keep, total_rows); const std::size_t remain_rows = total_rows - keep_rows; - const std::size_t value_cols = static_cast(content_cols()); + const auto value_cols = static_cast(content_cols()); const std::size_t content_split_index = keep_rows * value_cols; remainder->content_nodes_ = split_tail(content_nodes_, content_split_index); @@ -603,7 +630,16 @@ namespace docraft::model { return static_cast(column_weights_.size()) - 1; } - const std::vector> &DocraftTable::title_nodes() const { + const std::vector > DocraftTable::title_nodes() const { + std::vector > result; + result.reserve(title_nodes_.size()); + for (const auto &title_node: title_nodes_) { + result.emplace_back(title_node); + } + return result; + } + + const std::vector > &DocraftTable::title_text_nodes() const { return title_nodes_; } @@ -677,7 +713,7 @@ namespace docraft::model { } } } catch (const nlohmann::json::parse_error &) { - throw std::invalid_argument("Table model must be a JSON array of arrays or a JSON array of objects"); + throw docraft::exception::InvalidInputException("Table model must be a JSON array of arrays or a JSON array of objects"); } return DocraftModelType::kNone; } diff --git a/docraft/src/docraft/model/docraft_text.cc b/docraft/src/docraft/model/docraft_text.cc index adca7c3..3179930 100644 --- a/docraft/src/docraft/model/docraft_text.cc +++ b/docraft/src/docraft/model/docraft_text.cc @@ -18,11 +18,13 @@ #include #include -#include +#include "docraft/exception/docraft_exceptions.h" #include "docraft/model/docraft_clone_utils.h" #include "docraft/renderer/docraft_renderer.h" namespace docraft::model { + using exception::ModelException; + DocraftText::DocraftText() { set_padding(kDefaultTextPadding); } @@ -43,7 +45,7 @@ namespace docraft::model { auto cloned_child = clone_node(child); auto text_child = std::dynamic_pointer_cast(cloned_child); if (!text_child) { - throw std::runtime_error("Text line child does not clone to DocraftText"); + throw ModelException("Text line child does not clone to DocraftText"); } copy->add_line(text_child); } diff --git a/docraft/src/docraft/model/docraft_triangle.cc b/docraft/src/docraft/model/docraft_triangle.cc index b75d8be..2872a5a 100644 --- a/docraft/src/docraft/model/docraft_triangle.cc +++ b/docraft/src/docraft/model/docraft_triangle.cc @@ -16,8 +16,7 @@ #include "docraft/model/docraft_triangle.h" -#include - +#include "docraft/exception/docraft_exceptions.h" #include "docraft/renderer/docraft_renderer.h" #include "docraft/utils/docraft_logger.h" @@ -29,7 +28,7 @@ namespace docraft::model { void DocraftTriangle::set_points(const std::vector &points) { if (points.size() != 3U) { - throw std::invalid_argument("Triangle requires exactly 3 points"); + throw docraft::exception::InvalidInputException("Triangle requires exactly 3 points"); } points_ = points; } diff --git a/docraft/src/docraft/renderer/docraft_pdf_renderer.cc b/docraft/src/docraft/renderer/docraft_pdf_renderer.cc index 9e16568..cc9537a 100644 --- a/docraft/src/docraft/renderer/docraft_pdf_renderer.cc +++ b/docraft/src/docraft/renderer/docraft_pdf_renderer.cc @@ -35,8 +35,10 @@ namespace docraft::renderer { painter.draw(context()); } void DocraftPDFRenderer::render_section(const model::DocraftSection §ion_node) { - auto backend = context()->shape_backend(); - if (!backend) return; + auto &rendering_service = context()->rendering(); + auto shape_backend = rendering_service.shape_rendering(); + auto line_backend = rendering_service.line_rendering(); + 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 +56,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..c832d8a 100644 --- a/docraft/src/docraft/renderer/painter/docraft_circle_painter.cc +++ b/docraft/src/docraft/renderer/painter/docraft_circle_painter.cc @@ -26,8 +26,10 @@ namespace docraft::renderer::painter { void DocraftCirclePainter::draw(const std::shared_ptr &context) { if (!context) return; - auto backend = context->shape_backend(); - if (!backend) return; + const auto &rendering_service = context->rendering(); + const auto shape_backend = rendering_service.shape_rendering(); + const auto line_backend = rendering_service.line_rendering(); + 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 +44,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_image_painter.cc b/docraft/src/docraft/renderer/painter/docraft_image_painter.cc index 8a63a2b..0c96e3e 100644 --- a/docraft/src/docraft/renderer/painter/docraft_image_painter.cc +++ b/docraft/src/docraft/renderer/painter/docraft_image_painter.cc @@ -16,9 +16,8 @@ #include "docraft/renderer/painter/docraft_image_painter.h" -#include - #include "docraft/backend/docraft_image_rendering_backend.h" +#include "docraft/exception/docraft_exceptions.h" namespace docraft::renderer::painter { DocraftImagePainter::DocraftImagePainter(const model::DocraftImage &image_node) : image_node_(image_node) { @@ -26,7 +25,7 @@ namespace docraft::renderer::painter { void DocraftImagePainter::draw(const std::shared_ptr &context) { if (!context) return; - auto backend = context->image_backend(); + auto backend = context->rendering().image_rendering(); if (!backend) return; if (!image_node_.visible()) { @@ -52,7 +51,7 @@ namespace docraft::renderer::painter { break; case model::ImageFormat::kRaw: if (image_node_.raw_data().empty()) { - throw std::runtime_error("Raw image data is empty"); + throw docraft::exception::InvalidInputException("Raw image data is empty"); } backend->draw_raw_rgb_image_from_memory( image_node_.raw_data().data(), @@ -64,7 +63,7 @@ namespace docraft::renderer::painter { image_node_.height()); break; default: - throw std::runtime_error("Unsupported image format"); + throw docraft::exception::RenderingFailedException("Unsupported image format"); } } } // docraft diff --git a/docraft/src/docraft/renderer/painter/docraft_line_painter.cc b/docraft/src/docraft/renderer/painter/docraft_line_painter.cc index d4f3393..999bb76 100644 --- a/docraft/src/docraft/renderer/painter/docraft_line_painter.cc +++ b/docraft/src/docraft/renderer/painter/docraft_line_painter.cc @@ -24,8 +24,10 @@ namespace docraft::renderer::painter { void DocraftLinePainter::draw(const std::shared_ptr &context) { if (!context) return; - auto backend = context->shape_backend(); - if (!backend) return; + auto &rendering_service = context->rendering(); + auto shape_backend = rendering_service.shape_rendering(); + auto line_backend = rendering_service.line_rendering(); + 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 +35,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 +52,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..8f42f1f 100644 --- a/docraft/src/docraft/renderer/painter/docraft_polygon_painter.cc +++ b/docraft/src/docraft/renderer/painter/docraft_polygon_painter.cc @@ -24,8 +24,10 @@ namespace docraft::renderer::painter { void DocraftPolygonPainter::draw(const std::shared_ptr &context) { if (!context) return; - auto backend = context->shape_backend(); - if (!backend) return; + auto &rendering_service = context->rendering(); + auto shape_backend = rendering_service.shape_rendering(); + auto line_backend = rendering_service.line_rendering(); + 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 +49,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..daeb4b1 100644 --- a/docraft/src/docraft/renderer/painter/docraft_rectangle_painter.cc +++ b/docraft/src/docraft/renderer/painter/docraft_rectangle_painter.cc @@ -26,8 +26,10 @@ 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 &rendering_service = context->rendering(); + auto shape_backend = rendering_service.shape_rendering(); + auto line_backend = rendering_service.line_rendering(); + if (!shape_backend || !line_backend) return; // const auto& box = rectangle_node_.transform_box(); const auto& bg_color = rectangle_node_.background_color().toRGB(); @@ -40,25 +42,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 +71,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_table_painter.cc b/docraft/src/docraft/renderer/painter/docraft_table_painter.cc index 4dd42f5..28e3e20 100644 --- a/docraft/src/docraft/renderer/painter/docraft_table_painter.cc +++ b/docraft/src/docraft/renderer/painter/docraft_table_painter.cc @@ -30,8 +30,9 @@ namespace docraft::renderer::painter { void DocraftTablePainter::draw(const std::shared_ptr &context) { if (!context) return; - auto line_backend = context->line_backend(); - auto shape_backend = context->shape_backend(); + auto &rendering_service = context->rendering(); + auto line_backend = rendering_service.line_rendering(); + auto shape_backend = rendering_service.shape_rendering(); if (!line_backend) return; line_backend->set_stroke_color(0.0F, 0.0F, 0.0F); float start_x = table_node_.position().x; @@ -57,12 +58,15 @@ namespace docraft::renderer::painter { shape_backend->restore_state(); }; + const auto title_nodes = table_node_.title_nodes(); + const auto htitle_nodes = table_node_.htitle_nodes(); + // draw backgrounds first (rows, titles, cells) if (table_node_.orientation() == model::LayoutOrientation::kHorizontal) { // Title backgrounds const auto &title_bgs = table_node_.title_backgrounds(); - for (std::size_t i = 0; i < table_node_.title_nodes().size(); ++i) { - const auto &title = table_node_.title_nodes()[i]; + for (std::size_t i = 0; i < title_nodes.size(); ++i) { + const auto &title = title_nodes[i]; if (!title) continue; const auto &anchors = title->anchors(); const std::optional bg = @@ -72,8 +76,8 @@ namespace docraft::renderer::painter { } else { // Header title backgrounds (vertical) const auto &htitle_bgs = table_node_.htitle_backgrounds(); - for (std::size_t i = 0; i < table_node_.htitle_nodes().size(); ++i) { - const auto &title = table_node_.htitle_nodes()[i]; + for (std::size_t i = 0; i < htitle_nodes.size(); ++i) { + const auto &title = htitle_nodes[i]; if (!title) continue; const auto &anchors = title->anchors(); const std::optional bg = @@ -82,8 +86,8 @@ namespace docraft::renderer::painter { } // Row title backgrounds (vertical) const auto &title_bgs = table_node_.title_backgrounds(); - for (std::size_t i = 0; i < table_node_.title_nodes().size(); ++i) { - const auto &title = table_node_.title_nodes()[i]; + for (std::size_t i = 0; i < title_nodes.size(); ++i) { + const auto &title = title_nodes[i]; if (!title) continue; const auto &anchors = title->anchors(); const std::optional bg = @@ -98,6 +102,7 @@ namespace docraft::renderer::painter { const auto &default_cell_bg = table_node_.default_cell_background(); const auto content_rows = table_node_.content_nodes(); int content_cols = table_node_.content_cols(); + const std::size_t safe_content_cols = content_cols > 0 ? static_cast(content_cols) : 0U; for (std::size_t r = 0; r < content_rows.size(); ++r) { const auto &row = content_rows[r]; @@ -106,8 +111,8 @@ namespace docraft::renderer::painter { const auto &row_bg = row_bgs[r]; const model::DocraftNode *row_ref = nullptr; if (table_node_.orientation() == model::LayoutOrientation::kVertical) { - if (r < table_node_.title_nodes().size()) { - row_ref = table_node_.title_nodes()[r].get(); + if (r < title_nodes.size() && title_nodes[r]) { + row_ref = title_nodes[r].get(); } } else { for (const auto &cell : row) { @@ -126,7 +131,9 @@ namespace docraft::renderer::painter { // Cell backgrounds for (std::size_t c = 0; c < row.size(); ++c) { - const std::size_t flat_index = (r * static_cast(content_cols)) + c; + const std::size_t flat_index = safe_content_cols > 0 + ? ((r * safe_content_cols) + c) + : c; if (!row[c]) continue; std::optional bg = std::nullopt; if (flat_index < cell_bgs.size() && cell_bgs[flat_index].has_value()) { @@ -173,8 +180,9 @@ namespace docraft::renderer::painter { } if (table_node_.orientation() == model::LayoutOrientation::kHorizontal && - !table_node_.title_nodes().empty()) { - for (const auto &title: table_node_.title_nodes()) { + !title_nodes.empty()) { + for (const auto &title: title_nodes) { + if (!title) continue; // Line of columns line_backend->draw_line( title->anchors().top_left.x, @@ -182,27 +190,32 @@ namespace docraft::renderer::painter { title->anchors().top_left.x, table_node_.anchors().bottom_left.y); } - float line_y = table_node_.title_nodes().front()->anchors().bottom_left.y; - line_backend->draw_line( - start_x, - line_y, - start_x + table_width, - line_y); // Line below titles + for (const auto &title: title_nodes) { + if (!title) continue; + const float line_y = title->anchors().bottom_left.y; + line_backend->draw_line(start_x, line_y, start_x + table_width, line_y); // Line below titles + break; + } } if (table_node_.orientation() == model::LayoutOrientation::kVertical) { // Vertical column separators (use header titles or first row cells) - if (!table_node_.htitle_nodes().empty()) { - for (const auto &title : table_node_.htitle_nodes()) { - line_backend->draw_line( + if (!htitle_nodes.empty()) { + for (const auto &title: htitle_nodes) { + if (!title) continue; + line_backend->draw_line( title->anchors().top_left.x, table_node_.anchors().top_left.y, title->anchors().top_left.x, table_node_.anchors().bottom_left.y); } - const float header_line_y = table_node_.htitle_nodes().front()->anchors().bottom_left.y; - line_backend->draw_line(start_x, header_line_y, start_x + table_width, header_line_y); - } else if (!content_rows.empty() && !content_rows.front().empty() && content_rows.front().front()) { + for (const auto &title: htitle_nodes) { + if (!title) continue; + const float header_line_y = title->anchors().bottom_left.y; + line_backend->draw_line(start_x, header_line_y, start_x + table_width, header_line_y); + break; + } + } else if (!content_rows.empty() && !content_rows.front().empty() && content_rows.front().front()) { for (const auto &cell : content_rows.front()) { if (!cell) continue; line_backend->draw_line( @@ -218,19 +231,28 @@ namespace docraft::renderer::painter { // compute boundaries for horizontal tables (column titles define columns) std::vector col_lefts; if (table_node_.orientation() == model::LayoutOrientation::kHorizontal) { - for (const auto &title : table_node_.title_nodes()) { - const auto &tb = title->anchors(); + for (const auto &title: title_nodes) { + if (!title) continue; + const auto &tb = title->anchors(); col_lefts.push_back(tb.top_left.x); } } float content_top = start_y; if (table_node_.orientation() == model::LayoutOrientation::kHorizontal && - !table_node_.title_nodes().empty()) { - content_top = table_node_.title_nodes().front()->anchors().bottom_left.y; + !title_nodes.empty()) { + for (const auto &title: title_nodes) { + if (!title) continue; + content_top = title->anchors().bottom_left.y; + break; + } } else if (table_node_.orientation() == model::LayoutOrientation::kVertical && - !table_node_.htitle_nodes().empty()) { - content_top = table_node_.htitle_nodes().front()->anchors().bottom_left.y; + !htitle_nodes.empty()) { + for (const auto &title: htitle_nodes) { + if (!title) continue; + content_top = title->anchors().bottom_left.y; + break; + } } for (std::size_t row_index = 0; row_index < content_rows.size(); ++row_index) { @@ -265,18 +287,21 @@ namespace docraft::renderer::painter { // Draw titles last for clarity if (table_node_.orientation() == model::LayoutOrientation::kHorizontal && - !table_node_.title_nodes().empty()) { - for (const auto &title: table_node_.title_nodes()) { + !title_nodes.empty()) { + for (const auto &title: title_nodes) { + if (!title) continue; title->draw(context); } } if (table_node_.orientation() == model::LayoutOrientation::kVertical) { - for (const auto &title: table_node_.title_nodes()) { - title->draw(context); - } - for (const auto &title: table_node_.htitle_nodes()) { - title->draw(context); - } + for (const auto &title: title_nodes) { + if (!title) continue; + title->draw(context); + } + for (const auto &title: htitle_nodes) { + if (!title) continue; + title->draw(context); + } } } diff --git a/docraft/src/docraft/renderer/painter/docraft_text_painter.cc b/docraft/src/docraft/renderer/painter/docraft_text_painter.cc index 6b760c9..f0d5752 100644 --- a/docraft/src/docraft/renderer/painter/docraft_text_painter.cc +++ b/docraft/src/docraft/renderer/painter/docraft_text_painter.cc @@ -30,7 +30,7 @@ namespace docraft::renderer::painter { void DocraftTextPainter::render_justified(const std::shared_ptr &context, const std::string &text) { - auto backend = context->text_backend(); + auto backend = context->rendering().text_rendering(); // Use the computed line width as the target for justification. float max_width = current_line_->width(); @@ -65,7 +65,7 @@ namespace docraft::renderer::painter { std::pair, std::pair > DocraftTextPainter::render_text( const std::shared_ptr &context, const std::string &text) { - auto backend = context->text_backend(); + auto backend = context->rendering().text_rendering(); //begin drawing backend->begin_text(); @@ -95,7 +95,14 @@ namespace docraft::renderer::painter { void DocraftTextPainter:: draw_underline(const std::shared_ptr &context, const std::string &text) { - auto backend = context->text_backend(); + const auto text_backend = context->rendering().text_rendering(); + if (!text_backend) { + return; + } + auto line_backend = context->rendering().line_rendering(); + if (!line_backend) { + return; + } // 1. Draw the text normally first auto result = draw_text(context, text); @@ -106,17 +113,19 @@ 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) { + auto font_applier = context->typography().font_applier(); + auto text_backend = context->rendering().text_rendering(); for (const auto &line: text_node_.lines()) { current_line_ = line; - context->font_applier()->apply_font(current_line_); + font_applier->apply_font(current_line_); auto rgb = current_line_->color().toRGB(); - context->text_backend()->set_text_color(rgb.r, rgb.g, rgb.b); + text_backend->set_text_color(rgb.r, rgb.g, rgb.b); if (line->underline()) { draw_underline(context, line->text()); } else { diff --git a/docraft/src/docraft/renderer/painter/docraft_triangle_painter.cc b/docraft/src/docraft/renderer/painter/docraft_triangle_painter.cc index f2e0766..b0392dd 100644 --- a/docraft/src/docraft/renderer/painter/docraft_triangle_painter.cc +++ b/docraft/src/docraft/renderer/painter/docraft_triangle_painter.cc @@ -24,8 +24,10 @@ namespace docraft::renderer::painter { void DocraftTrianglePainter::draw(const std::shared_ptr &context) { if (!context) return; - auto backend = context->shape_backend(); - if (!backend) return; + auto &rendering_service = context->rendering(); + auto shape_backend = rendering_service.shape_rendering(); + auto line_backend = rendering_service.line_rendering(); + 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 +49,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/services/docraft_layout_service.cc b/docraft/src/docraft/services/docraft_layout_service.cc new file mode 100644 index 0000000..add90de --- /dev/null +++ b/docraft/src/docraft/services/docraft_layout_service.cc @@ -0,0 +1,73 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 "docraft/services/docraft_layout_service.h" + +namespace docraft::services { + LayoutService::LayoutService() { + cursor_ = std::make_unique(); + } + + LayoutService::~LayoutService() = default; + + DocraftCursor &LayoutService::cursor() { + return *cursor_; + } + + const DocraftCursor &LayoutService::cursor() const { + return *cursor_; + } + + float LayoutService::page_width() const { + return page_width_; + } + + float LayoutService::page_height() const { + return page_height_; + } + + float LayoutService::available_space() const { + // Legacy layout code uses available_space() as horizontal available width. + return current_rect_width_ > 0.0F ? current_rect_width_ : page_width_; + } + + void LayoutService::set_current_rect_width(float width) { + current_rect_width_ = width; + } + + float LayoutService::current_rect_width() const { + return current_rect_width_; + } + + void LayoutService::set_page_format(model::DocraftPageOrientation orientation) { + // Placeholder: actual conversion from size/orientation enums to points + // For now, use A4 defaults + if (orientation == model::DocraftPageOrientation::kPortrait) { + page_width_ = 595.0F; + page_height_ = 842.0F; + } else { + page_width_ = 842.0F; + page_height_ = 595.0F; + } + current_rect_width_ = page_width_; + } + + void LayoutService::set_page_dimensions(float width, float height) { + page_width_ = width > 0.0F ? width : page_width_; + page_height_ = height > 0.0F ? height : page_height_; + current_rect_width_ = page_width_; + } +} // namespace docraft::services diff --git a/docraft/src/docraft/services/docraft_navigation_service.cc b/docraft/src/docraft/services/docraft_navigation_service.cc new file mode 100644 index 0000000..a4ea464 --- /dev/null +++ b/docraft/src/docraft/services/docraft_navigation_service.cc @@ -0,0 +1,91 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 "docraft/services/docraft_navigation_service.h" +#include "docraft/model/docraft_header.h" +#include "docraft/model/docraft_body.h" +#include "docraft/model/docraft_footer.h" + +namespace docraft::services { + NavigationService::NavigationService() + : section_manager_(std::make_unique()) { + } + + NavigationService::~NavigationService() = default; + + management::DocraftDocumentSectionManager &NavigationService::section_manager() { + return *section_manager_; + } + + const management::DocraftDocumentSectionManager &NavigationService::section_manager() const { + return *section_manager_; + } + + void NavigationService::set_header(const std::shared_ptr &header) { + section_manager_->set_header(header); + } + + std::shared_ptr NavigationService::header() const { + return section_manager_->header(); + } + + std::shared_ptr NavigationService::edit_header() { + return section_manager_->edit_header(); + } + + void NavigationService::set_body(const std::shared_ptr &body) { + section_manager_->set_body(body); + } + + std::shared_ptr NavigationService::body() const { + return section_manager_->body(); + } + + std::shared_ptr NavigationService::edit_body() { + return section_manager_->edit_body(); + } + + void NavigationService::set_footer(const std::shared_ptr &footer) { + section_manager_->set_footer(footer); + } + + std::shared_ptr NavigationService::footer() const { + return section_manager_->footer(); + } + + std::shared_ptr NavigationService::edit_footer() { + return section_manager_->edit_footer(); + } + + void NavigationService::set_section_ratios(float header_ratio, float body_ratio, float footer_ratio) { + section_manager_->set_section_ratios(header_ratio, body_ratio, footer_ratio); + } + + float NavigationService::header_ratio() const { + return section_manager_->header_ratio(); + } + + float NavigationService::body_ratio() const { + return section_manager_->body_ratio(); + } + + float NavigationService::footer_ratio() const { + return section_manager_->footer_ratio(); + } +} // namespace docraft::services + + + diff --git a/docraft/src/docraft/services/docraft_rendering_service.cc b/docraft/src/docraft/services/docraft_rendering_service.cc new file mode 100644 index 0000000..608d940 --- /dev/null +++ b/docraft/src/docraft/services/docraft_rendering_service.cc @@ -0,0 +1,220 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 "docraft/services/docraft_rendering_service.h" +#include "docraft/backend/pdf/docraft_haru_backend_providers_factory.h" +#include "docraft/utils/docraft_logger.h" + +#include + +#include "docraft/exception/docraft_backend_exceptions.h" + +namespace docraft::services { + using docraft::exception::CapabilityUnavailableException; + + namespace { + std::shared_ptr default_capability_factory() { + return std::make_shared(); + } + + void handle_missing_capability(const std::string &capability_name, MissingCapabilityPolicy policy) { + const std::string message = "Required capability not available: " + capability_name; + switch (policy) { + case MissingCapabilityPolicy::kFail: + throw CapabilityUnavailableException(message); + case MissingCapabilityPolicy::kWarn: + LOG_WARNING(message); + return; + case MissingCapabilityPolicy::kIgnore: + return; + } + } + + template + std::shared_ptr from_const_provider( + const std::shared_ptr &provider, + const CapabilityInterface *(ProviderInterface::*getter)() const) { + if (!provider) { + return {}; + } + if (const auto *capability = (provider.get()->*getter)()) { + return std::shared_ptr(provider, capability); + } + return {}; + } + + template + std::shared_ptr from_mutable_provider( + const std::shared_ptr &provider, + CapabilityInterface *(ProviderInterface::*getter)()) { + if (!provider) { + return {}; + } + if (auto *capability = (provider.get()->*getter)()) { + return std::shared_ptr(provider, capability); + } + return {}; + } + } // namespace + + RenderingService::RenderingService( + const std::shared_ptr &capability_providers_factory) + : capability_providers_factory_(capability_providers_factory + ? capability_providers_factory + : default_capability_factory()) { + capability_providers_ = capability_providers_factory_->create_capability_providers(); + } + + RenderingService::RenderingService() + : RenderingService(default_capability_factory()) { + } + + RenderingService::~RenderingService() = default; + + void RenderingService::set_capability_providers_factory( + const std::shared_ptr &capability_providers_factory) { + capability_providers_factory_ = capability_providers_factory + ? capability_providers_factory + : default_capability_factory(); + capability_providers_ = capability_providers_factory_->create_capability_providers(); + } + + void RenderingService::set_backend_providers_factory( + const std::shared_ptr &backend_providers_factory) { + set_capability_providers_factory(backend_providers_factory); + } + + void RenderingService::set_missing_capability_policy(MissingCapabilityPolicy policy) { + missing_capability_policy_ = policy; + } + + MissingCapabilityPolicy RenderingService::missing_capability_policy() const { + return missing_capability_policy_; + } + + void RenderingService::validate_capabilities(const CapabilityRequirements &requirements) const { + if (requirements.line_rendering && !line_rendering()) { + handle_missing_capability("line_rendering", missing_capability_policy_); + } + if (requirements.text_rendering && !text_rendering()) { + handle_missing_capability("text_rendering", missing_capability_policy_); + } + if (requirements.shape_rendering && !shape_rendering()) { + handle_missing_capability("shape_rendering", missing_capability_policy_); + } + if (requirements.image_rendering && !image_rendering()) { + handle_missing_capability("image_rendering", missing_capability_policy_); + } + if (requirements.page_rendering && !page_rendering()) { + handle_missing_capability("page_rendering", missing_capability_policy_); + } + if (requirements.font_backend && !font_backend()) { + handle_missing_capability("font_backend", missing_capability_policy_); + } + if (requirements.output_backend && !output_backend()) { + handle_missing_capability("output_backend", missing_capability_policy_); + } + if (requirements.metadata_backend && !metadata_backend()) { + handle_missing_capability("metadata_backend", missing_capability_policy_); + } + } + + std::shared_ptr RenderingService::line_rendering() const { + return from_const_provider( + capability_providers_.rendering_provider, + &backend::IDocraftRenderingCapabilityProvider::line_rendering); + } + + std::shared_ptr RenderingService::edit_line_rendering() { + return from_mutable_provider( + capability_providers_.rendering_provider, + &backend::IDocraftRenderingCapabilityProvider::edit_line_rendering); + } + + std::shared_ptr RenderingService::text_rendering() const { + return from_const_provider( + capability_providers_.rendering_provider, + &backend::IDocraftRenderingCapabilityProvider::text_rendering); + } + + std::shared_ptr RenderingService::edit_text_rendering() { + return from_mutable_provider( + capability_providers_.rendering_provider, + &backend::IDocraftRenderingCapabilityProvider::edit_text_rendering); + } + + std::shared_ptr RenderingService::shape_rendering() const { + return from_const_provider( + capability_providers_.rendering_provider, + &backend::IDocraftRenderingCapabilityProvider::shape_rendering); + } + + std::shared_ptr RenderingService::edit_shape_rendering() { + return from_mutable_provider( + capability_providers_.rendering_provider, + &backend::IDocraftRenderingCapabilityProvider::edit_shape_rendering); + } + + std::shared_ptr RenderingService::image_rendering() const { + return from_const_provider( + capability_providers_.rendering_provider, + &backend::IDocraftRenderingCapabilityProvider::image_rendering); + } + + std::shared_ptr RenderingService::edit_image_rendering() { + return from_mutable_provider( + capability_providers_.rendering_provider, + &backend::IDocraftRenderingCapabilityProvider::edit_image_rendering); + } + + std::shared_ptr RenderingService::page_rendering() const { + return from_const_provider( + capability_providers_.rendering_provider, + &backend::IDocraftRenderingCapabilityProvider::page_rendering); + } + + std::shared_ptr RenderingService::edit_page_rendering() { + return from_mutable_provider( + capability_providers_.rendering_provider, + &backend::IDocraftRenderingCapabilityProvider::edit_page_rendering); + } + + std::shared_ptr RenderingService::font_backend() const { + return from_const_provider( + capability_providers_.resource_provider, + &backend::IDocraftResourceCapabilityProvider::font_backend); + } + + std::shared_ptr RenderingService::output_backend() const { + return from_const_provider( + capability_providers_.lifecycle_provider, + &backend::IDocraftLifecycleCapabilityProvider::output_backend); + } + + std::shared_ptr RenderingService::metadata_backend() const { + return from_const_provider( + capability_providers_.lifecycle_provider, + &backend::IDocraftLifecycleCapabilityProvider::metadata_backend); + } + + std::shared_ptr RenderingService::edit_metadata_backend() { + return from_mutable_provider( + capability_providers_.lifecycle_provider, + &backend::IDocraftLifecycleCapabilityProvider::edit_metadata_backend); + } +} // namespace docraft::services + + diff --git a/docraft/src/docraft/services/docraft_typography_service.cc b/docraft/src/docraft/services/docraft_typography_service.cc new file mode 100644 index 0000000..237fd0f --- /dev/null +++ b/docraft/src/docraft/services/docraft_typography_service.cc @@ -0,0 +1,36 @@ +/* + * Copyright 2026 Matteo Cadoni (https://github.com/cadons) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 "docraft/services/docraft_typography_service.h" +#include "docraft/generic/docraft_font_applier.h" + +namespace docraft::services { + TypographyService::TypographyService() = default; + + TypographyService::~TypographyService() = default; + + std::shared_ptr TypographyService::font_applier() const { + return font_applier_; + } + + std::shared_ptr TypographyService::edit_font_applier() { + return font_applier_; + } + + void TypographyService::set_font_applier(const std::shared_ptr &applier) { + font_applier_ = applier; + } +} // namespace docraft::services diff --git a/docraft/src/docraft/templating/docraft_template_engine.cc b/docraft/src/docraft/templating/docraft_template_engine.cc index 45b35c7..0a0ff71 100644 --- a/docraft/src/docraft/templating/docraft_template_engine.cc +++ b/docraft/src/docraft/templating/docraft_template_engine.cc @@ -39,6 +39,10 @@ #include "docraft/utils/docraft_parser_utilis.h" namespace docraft::templating { + using exception::TemplateImageDataException; + using exception::TemplateVariableExistsException; + using exception::TemplateVariableNotFoundException; + void DocraftTemplateEngine::template_nodes(const std::vector > &nodes) { for (const auto &node: nodes) { template_node(node); @@ -48,7 +52,7 @@ namespace docraft::templating { void DocraftTemplateEngine::add_template_variable(const std::string &name, const std::string &value) { auto normalized_name = normalize_name(name); if (has_template_variable(normalized_name)) { - throw std::runtime_error(fmt::format("Template variable '{}' already exists.", name)); + throw TemplateVariableExistsException(fmt::format("Template variable '{}' already exists.", name)); } template_variables_.insert({normalized_name, value}); } @@ -57,7 +61,7 @@ namespace docraft::templating { auto normalized_name = normalize_name(name); auto it = template_variables_.find(normalized_name); if (it == template_variables_.end()) { - throw std::runtime_error(fmt::format("Template variable '{}' not found.", name)); + throw TemplateVariableNotFoundException(fmt::format("Template variable '{}' not found.", name)); } return it->second; } @@ -70,7 +74,7 @@ namespace docraft::templating { auto normalized_name = normalize_name(name); auto it = template_variables_.find(normalized_name); if (it == template_variables_.end()) { - throw std::runtime_error(fmt::format("Template variable '{}' not found.", name)); + throw TemplateVariableNotFoundException(fmt::format("Template variable '{}' not found.", name)); } template_variables_.erase(it); } @@ -127,8 +131,8 @@ namespace docraft::templating { try { std::string resolved_color = render_template_string(text_node->color().template_expression(), foreach_item); text_node->set_color(DocraftColor(resolved_color)); - } catch (const std::exception &e) { - LOG_ERROR("Failed to resolve color template expression: " + std::string(e.what())); + } catch (...) { + LOG_ERROR("Failed to resolve color template expression"); } } } @@ -138,8 +142,8 @@ namespace docraft::templating { try { std::string resolved_color = render_template_string(rectangle->background_color_template_expression(), foreach_item); rectangle->set_background_color(DocraftColor(resolved_color)); - } catch (const std::exception &e) { - LOG_ERROR("Failed to resolve background color template expression: " + std::string(e.what())); + } catch (...) { + LOG_ERROR("Failed to resolve background color template expression"); } } // Handle border color template expression @@ -147,8 +151,8 @@ namespace docraft::templating { try { std::string resolved_color = render_template_string(rectangle->border_color_template_expression(), foreach_item); rectangle->set_border_color(DocraftColor(resolved_color)); - } catch (const std::exception &e) { - LOG_ERROR("Failed to resolve border color template expression: " + std::string(e.what())); + } catch (...) { + LOG_ERROR("Failed to resolve border color template expression"); } } } @@ -164,8 +168,8 @@ namespace docraft::templating { rendered_data.data, rendered_data.width, rendered_data.height); - } catch (const std::exception &e) { - LOG_ERROR("Failed to render raw data for image '" + image->path() + "': " + std::string(e.what())); + } catch (...) { + LOG_ERROR("Failed to render raw data for image '" + image->path() + "'"); } } else if (!image->path().empty()) { std::string rendered_path = render_template_string(image->path(), foreach_item); @@ -209,7 +213,7 @@ namespace docraft::templating { foreach_node->clear_template_nodes(); } else { LOG_WARNING("docraft/model for foreach node is not an array: " + model); - throw std::runtime_error("docraft/model for foreach node is not an array or a valid json"); + throw exception::DataFormatException("docraft/model for foreach node is not an array or a valid json"); } return; } @@ -245,8 +249,8 @@ namespace docraft::templating { LOG_WARNING("Template variable '" + variable_name + "' not found in template engine."); variable_value = text; } - } catch (const std::exception &e) { - LOG_ERROR("Template variable '" + variable_name + "' not found: " + std::string(e.what())); + } catch (...) { + LOG_ERROR("Template variable '" + variable_name + "' not found"); variable_value = ""; } result.replace(pos, end_pos - pos + 1, variable_value); @@ -275,8 +279,8 @@ namespace docraft::templating { LOG_WARNING("Template variable '" + variable_name + "' not found in template engine."); variable_value = text; } - } catch (const std::exception &e) { - LOG_ERROR("Template variable '" + variable_name + "' not found: " + std::string(e.what())); + } catch (...) { + LOG_ERROR("Template variable '" + variable_name + "' not found"); variable_value = ""; } result.replace(pos, end_pos - pos + 1, variable_value); @@ -291,10 +295,10 @@ namespace docraft::templating { int height) { auto name = normalize_name(image_id); if (image_data_.contains(name)) { - throw std::runtime_error(fmt::format("Image data for '{}' already exists.", image_id)); + throw TemplateImageDataException(fmt::format("Image data for '{}' already exists.", image_id)); } if (width <= 0 || height <= 0) { - throw std::runtime_error(fmt::format("Image data for '{}' has invalid dimensions.", image_id)); + throw TemplateImageDataException(fmt::format("Image data for '{}' has invalid dimensions.", image_id)); } //emplace the key also in the standard unordered_map with the same normalized name to ensure consistency template_variables_.insert({name, name}); @@ -306,7 +310,7 @@ namespace docraft::templating { auto name = normalize_name(image_id); auto it = image_data_.find(name); if (it == image_data_.end()) { - throw std::runtime_error(fmt::format("Image data for '{}' not found.", image_id)); + throw TemplateImageDataException(fmt::format("Image data for '{}' not found.", image_id)); } return it->second; } @@ -319,7 +323,8 @@ namespace docraft::templating { const auto expected_size = static_cast(width) * static_cast(height) * 3U; //3 bytes per pixel for RGB if (decoded.size() != expected_size) { - throw std::runtime_error(fmt::format("Base64 image '{}' size does not match dimensions (RGB expected).", image_id)); + throw TemplateImageDataException( + fmt::format("Base64 image '{}' size does not match dimensions (RGB expected).", image_id)); } add_image_data(image_id, decoded, width, height); } diff --git a/docraft/src/docraft/utils/docraft_base64.cc b/docraft/src/docraft/utils/docraft_base64.cc index 32c978c..bd88574 100644 --- a/docraft/src/docraft/utils/docraft_base64.cc +++ b/docraft/src/docraft/utils/docraft_base64.cc @@ -19,6 +19,8 @@ #include #include +#include "docraft/exception/docraft_exceptions.h" + namespace docraft::utils { namespace { constexpr unsigned char kInvalid = 0xFF; @@ -64,7 +66,7 @@ namespace docraft::utils { } unsigned char value = kDecodeTable[static_cast(ch)];//lookup the value of the base64 character using the decode table if (value == kInvalid) {//if the character is not a valid base64 character, throw an error - throw std::invalid_argument("Invalid base64 character."); + throw docraft::exception::InvalidInputException("Invalid base64 character."); } quartet[quartet_len++] = value;//add the decoded value to the quartet if (quartet_len == 4) {//once we have 4 valid base64 characters (or padding), we can decode them into bytes @@ -82,7 +84,7 @@ namespace docraft::utils { // Any trailing, incomplete quartet indicates invalid base64 length. if (quartet_len != 0) { - throw std::invalid_argument("Invalid base64 length."); + throw docraft::exception::InvalidInputException("Invalid base64 length."); } return out; diff --git a/docraft/src/docraft/utils/docraft_font_registry.cc b/docraft/src/docraft/utils/docraft_font_registry.cc index 1e8bd6b..bd71356 100644 --- a/docraft/src/docraft/utils/docraft_font_registry.cc +++ b/docraft/src/docraft/utils/docraft_font_registry.cc @@ -47,8 +47,8 @@ namespace docraft::utils { } registry_.insert({name, {data.release(), static_cast(std::filesystem::file_size(file_path))}}); return true; - } catch (const std::exception &e) { - std::cerr << "Error registering font from file: " << e.what() << std::endl; + } catch (...) { + std::cerr << "Error registering font from file." << std::endl; return false; } } diff --git a/docraft/src/docraft/utils/docraft_parser_utilis.cc b/docraft/src/docraft/utils/docraft_parser_utilis.cc index 2a4f412..8dc50dc 100644 --- a/docraft/src/docraft/utils/docraft_parser_utilis.cc +++ b/docraft/src/docraft/utils/docraft_parser_utilis.cc @@ -55,8 +55,8 @@ namespace docraft::utils { } return ""; - } catch (const std::exception &e) { - LOG_ERROR("Error extracting data attribute '" + field + "': " + e.what()); + } catch (...) { + LOG_ERROR("Error extracting data attribute '" + field + "'"); return ""; } } diff --git a/docraft/test/CMakeLists.txt b/docraft/test/CMakeLists.txt index 05ec3ac..f36ae3c 100644 --- a/docraft/test/CMakeLists.txt +++ b/docraft/test/CMakeLists.txt @@ -1,8 +1,6 @@ - - - #gtest setup find_package(GTest REQUIRED) +include(GoogleTest) #source files set(TEST_SOURCES docraft/main.cpp @@ -19,6 +17,7 @@ set(TEST_SOURCES docraft/model/docraft_settings_test.cc docraft/model/docraft_node_test.cc docraft/model/docraft_z_index_test.cc + docraft/exception/docraft_exception_test.cc docraft/craft/docraft_table_parser_test.cc docraft/craft/docraft_craft_language_parser_test.cc docraft/craft/docraft_shape_parser_test.cc @@ -34,11 +33,29 @@ set(TEST_SOURCES docraft/utils/docraft_parser_utils.cc ) +set(STRESS_TEST_SOURCES + docraft/main.cpp + docraft/layout/docraft_pagination_stress_test.cc +) + add_executable(docraft_test ${TEST_SOURCES} ) target_link_libraries(docraft_test PRIVATE docraft GTest::GTest) target_include_directories(docraft_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) -gtest_discover_tests(docraft_test - DISCOVERY_MODE PRE_TEST +gtest_add_tests(TARGET docraft_test + TEST_LIST DOCRAFT_TESTS +) + +add_executable(docraft_stress_test + ${STRESS_TEST_SOURCES} +) +target_link_libraries(docraft_stress_test PRIVATE docraft GTest::GTest) +target_include_directories(docraft_stress_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) +gtest_add_tests(TARGET docraft_stress_test + TEST_PREFIX "Stress." + TEST_LIST DOCRAFT_STRESS_TESTS ) +if (DOCRAFT_STRESS_TESTS) + set_tests_properties(${DOCRAFT_STRESS_TESTS} PROPERTIES LABELS "stress") +endif () diff --git a/docraft/test/docraft/backend/docraft_haru_backend_test.cc b/docraft/test/docraft/backend/docraft_haru_backend_test.cc index 2202b24..0454f0e 100644 --- a/docraft/test/docraft/backend/docraft_haru_backend_test.cc +++ b/docraft/test/docraft/backend/docraft_haru_backend_test.cc @@ -4,8 +4,10 @@ #include #include "docraft/backend/pdf/docraft_haru_backend.h" +#include "docraft/backend/pdf/docraft_haru_page_backend.h" #include "docraft/docraft_document_metadata.h" +#include "docraft/exception/docraft_exceptions.h" #include "docraft/model/docraft_page_format.h" namespace docraft::test::backend { @@ -20,104 +22,147 @@ 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); + ASSERT_NE(const_backend.output_backend(), nullptr); + ASSERT_NE(const_backend.font_backend(), nullptr); + ASSERT_NE(const_backend.metadata_backend(), 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()); + EXPECT_EQ(const_backend.output_backend(), backend().edit_output_backend()); + EXPECT_EQ(const_backend.font_backend(), backend().edit_font_backend()); + EXPECT_EQ(const_backend.metadata_backend(), backend().edit_metadata_backend()); +} + 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(), docraft::exception::PageStateException); } 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(), docraft::exception::PageStateException); } 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), docraft::exception::PageStateException); + EXPECT_THROW(edit_page_backend().go_to_page(2U), docraft::exception::PageStateException); } TEST_F(DocraftHaruBackendTest, SupportsBuiltInFontAndTextMeasure) { - EXPECT_TRUE(backend().can_use_font("Helvetica", nullptr)); - EXPECT_NO_THROW(backend().set_font("Helvetica", 12.0F, nullptr)); + ASSERT_NE(backend().font_backend(), nullptr); + EXPECT_TRUE(backend().font_backend()->can_use_font("Helvetica", nullptr)); + EXPECT_NO_THROW(backend().font_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) { - EXPECT_EQ(backend().file_extension(), ".pdf"); + ASSERT_NE(backend().output_backend(), nullptr); + EXPECT_EQ(backend().output_backend()->file_extension(), ".pdf"); } TEST_F(DocraftHaruBackendTest, ThrowsWhenSettingUnknownFont) { - EXPECT_THROW(backend().set_font("__missing_font__", 12.0F, nullptr), std::runtime_error); - EXPECT_FALSE(backend().can_use_font("__missing_font__", nullptr)); + ASSERT_NE(backend().font_backend(), nullptr); + EXPECT_THROW(backend().font_backend()->set_font("__missing_font__", 12.0F, nullptr), + docraft::exception::BackendStateException); + EXPECT_FALSE(backend().font_backend()->can_use_font("__missing_font__", nullptr)); } TEST_F(DocraftHaruBackendTest, SavesPdfToFile) { const auto output_path = std::filesystem::temp_directory_path() / "docraft_haru_backend_test_output.pdf"; - backend().save_to_file(output_path.string()); + ASSERT_NE(backend().output_backend(), nullptr); + backend().output_backend()->save_to_file(output_path.string()); ASSERT_TRUE(std::filesystem::exists(output_path)); EXPECT_GT(std::filesystem::file_size(output_path), 0U); @@ -138,10 +183,12 @@ TEST_F(DocraftHaruBackendTest, SavesPdfWithMetadataInfo) { metadata.set_creation_date({2026, 2, 20, 8, 30, 15, '+', 0, 0}); metadata.set_modification_date({2026, 2, 20, 9, 45, 10, '+', 0, 0}); - EXPECT_NO_THROW(backend().set_document_metadata(metadata)); + ASSERT_NE(backend().edit_metadata_backend(), nullptr); + EXPECT_NO_THROW(backend().edit_metadata_backend()->set_document_metadata(metadata)); const auto output_path = std::filesystem::temp_directory_path() / "docraft_haru_backend_test_metadata_output.pdf"; - backend().save_to_file(output_path.string()); + ASSERT_NE(backend().output_backend(), nullptr); + backend().output_backend()->save_to_file(output_path.string()); ASSERT_TRUE(std::filesystem::exists(output_path)); EXPECT_GT(std::filesystem::file_size(output_path), 0U); @@ -149,4 +196,15 @@ TEST_F(DocraftHaruBackendTest, SavesPdfWithMetadataInfo) { std::filesystem::remove(output_path); } +TEST(DocraftHaruPageBackendLifetimeTest, ClearsProviderRegistrationOnDestruction) { + auto state = std::make_shared(); + + { + auto page_backend = std::make_unique(state); + ASSERT_EQ(state->page_operations_provider, page_backend.get()); + } + + EXPECT_EQ(state->page_operations_provider, nullptr); + EXPECT_THROW(state->ensure_page_provider(), docraft::exception::BackendStateException); +} } // namespace docraft::test::backend diff --git a/docraft/test/docraft/craft/docraft_craft_language_parser_test.cc b/docraft/test/docraft/craft/docraft_craft_language_parser_test.cc index d32133a..e66fbc7 100644 --- a/docraft/test/docraft/craft/docraft_craft_language_parser_test.cc +++ b/docraft/test/docraft/craft/docraft_craft_language_parser_test.cc @@ -1,6 +1,7 @@ #include #include "docraft/craft/docraft_craft_language_parser.h" +#include "docraft/exception/docraft_exceptions.h" #include "docraft/model/docraft_text.h" TEST(DocraftCraftLanguageParserTest, ParsesTitleSubtitleAndTextWithPredefinedDefaults) { @@ -96,8 +97,8 @@ TEST(DocraftCraftLanguageParserTest, ParsesMetadataSubtags) { auto document = parser.get_document(); ASSERT_TRUE(document); - EXPECT_EQ(document->document_title(), "Metadata Driven Title"); - const auto &metadata = document->document_metadata(); + EXPECT_EQ(document->config().document_title(), "Metadata Driven Title"); + const auto &metadata = document->config().document_metadata(); ASSERT_TRUE(metadata.author().has_value()); EXPECT_EQ(metadata.author().value(), "Mario Rossi"); @@ -156,7 +157,7 @@ TEST(DocraftCraftLanguageParserTest, AutoKeywordsAreExtractedAndMergedWhenEnable auto document = parser.get_document(); ASSERT_TRUE(document); - const auto &metadata = document->document_metadata(); + const auto &metadata = document->config().document_metadata(); ASSERT_TRUE(metadata.keywords().has_value()); EXPECT_EQ(metadata.keywords().value(), "manuale, parser, documento, metadata, tecnica"); } @@ -178,7 +179,7 @@ TEST(DocraftCraftLanguageParserTest, AutoKeywordsUsesConfiguredStopwordLanguages auto document = parser.get_document(); ASSERT_TRUE(document); - const auto &metadata = document->document_metadata(); + const auto &metadata = document->config().document_metadata(); ASSERT_TRUE(metadata.keywords().has_value()); EXPECT_EQ(metadata.keywords().value(), "analyse, modelo, system"); } @@ -197,7 +198,7 @@ TEST(DocraftCraftLanguageParserTest, ParsesDocumentPathAttribute) { auto document = parser.get_document(); ASSERT_TRUE(document); - EXPECT_EQ(document->document_path(), "exports/reports"); + EXPECT_EQ(document->config().document_path(), "exports/reports"); } TEST(DocraftCraftLanguageParserTest, RejectsNestedTextInText) { @@ -214,7 +215,7 @@ TEST(DocraftCraftLanguageParserTest, RejectsNestedTextInText) { docraft::craft::DocraftCraftLanguageParser parser; EXPECT_THROW({ parser.parse(xml); - }, std::invalid_argument); + }, docraft::exception::InvalidInputException); } TEST(DocraftCraftLanguageParserTest, RejectsTitleInText) { @@ -231,7 +232,7 @@ TEST(DocraftCraftLanguageParserTest, RejectsTitleInText) { docraft::craft::DocraftCraftLanguageParser parser; EXPECT_THROW({ parser.parse(xml); - }, std::invalid_argument); + }, docraft::exception::InvalidInputException); } TEST(DocraftCraftLanguageParserTest, RejectsSubtitleInText) { @@ -248,7 +249,7 @@ TEST(DocraftCraftLanguageParserTest, RejectsSubtitleInText) { docraft::craft::DocraftCraftLanguageParser parser; EXPECT_THROW({ parser.parse(xml); - }, std::invalid_argument); + }, docraft::exception::InvalidInputException); } TEST(DocraftCraftLanguageParserTest, RejectsPageNumberInText) { @@ -265,7 +266,7 @@ TEST(DocraftCraftLanguageParserTest, RejectsPageNumberInText) { docraft::craft::DocraftCraftLanguageParser parser; EXPECT_THROW({ parser.parse(xml); - }, std::invalid_argument); + }, docraft::exception::InvalidInputException); } TEST(DocraftCraftLanguageParserTest, AllowsLayoutInBodyWithMultipleText) { @@ -308,11 +309,11 @@ TEST(DocraftCraftLanguageParserTest, EditDocumentReturnsMutableDocument) { const auto readonly_document = parser.get_document(); ASSERT_TRUE(readonly_document); - EXPECT_EQ(readonly_document->document_title(), "Untitled Document"); + EXPECT_EQ(readonly_document->config().document_title(), "Untitled Document"); auto editable_document = parser.edit_document(); ASSERT_TRUE(editable_document); - editable_document->set_document_title("Edited"); + editable_document->edit_config().set_document_title("Edited"); - EXPECT_EQ(readonly_document->document_title(), "Edited"); + EXPECT_EQ(readonly_document->config().document_title(), "Edited"); } diff --git a/docraft/test/docraft/craft/docraft_image_parser_test.cc b/docraft/test/docraft/craft/docraft_image_parser_test.cc index 9cf7788..5d53618 100644 --- a/docraft/test/docraft/craft/docraft_image_parser_test.cc +++ b/docraft/test/docraft/craft/docraft_image_parser_test.cc @@ -2,6 +2,7 @@ #include #include "docraft/craft/parser/docraft_parser.h" +#include "docraft/exception/docraft_exceptions.h" #include "docraft/model/docraft_image.h" TEST(DocraftImageParserTest, ParsesBase64RawImageData) { @@ -35,5 +36,5 @@ TEST(DocraftImageParserTest, RejectsBase64MissingDimensions) { ASSERT_TRUE(doc.load_string(xml)); docraft::craft::parser::DocraftImageParser parser; - EXPECT_THROW(parser.parse(doc.child("Image")), std::invalid_argument); + EXPECT_THROW(parser.parse(doc.child("Image")), docraft::exception::InvalidInputException); } diff --git a/docraft/test/docraft/craft/docraft_settings_parser_test.cc b/docraft/test/docraft/craft/docraft_settings_parser_test.cc index 56473d2..393dcf2 100644 --- a/docraft/test/docraft/craft/docraft_settings_parser_test.cc +++ b/docraft/test/docraft/craft/docraft_settings_parser_test.cc @@ -1,6 +1,7 @@ #include #include +#include "docraft/exception/docraft_exceptions.h" #include "docraft/craft/parser/docraft_parser.h" #include "docraft/model/docraft_settings.h" @@ -58,7 +59,7 @@ namespace docraft::test::craft { pugi::xml_document doc; ASSERT_TRUE(doc.load_string(xml)); - EXPECT_THROW(parser.parse(doc.child("Settings")), std::invalid_argument); + EXPECT_THROW(parser.parse(doc.child("Settings")), docraft::exception::InvalidInputException); } TEST_F(DocraftSettingsParserTest, RejectsInvalidRatios) { @@ -69,7 +70,7 @@ namespace docraft::test::craft { pugi::xml_document doc; ASSERT_TRUE(doc.load_string(xml)); - EXPECT_THROW(parser.parse(doc.child("Settings")), std::invalid_argument); + EXPECT_THROW(parser.parse(doc.child("Settings")), docraft::exception::InvalidInputException); } TEST_F(DocraftSettingsParserTest, RejectsInvalidPageSize) { @@ -80,7 +81,7 @@ namespace docraft::test::craft { pugi::xml_document doc; ASSERT_TRUE(doc.load_string(xml)); - EXPECT_THROW(parser.parse(doc.child("Settings")), std::invalid_argument); + EXPECT_THROW(parser.parse(doc.child("Settings")), docraft::exception::InvalidInputException); } TEST_F(DocraftSettingsParserTest, RejectsInvalidPageOrientation) { @@ -91,6 +92,6 @@ namespace docraft::test::craft { pugi::xml_document doc; ASSERT_TRUE(doc.load_string(xml)); - EXPECT_THROW(parser.parse(doc.child("Settings")), std::invalid_argument); + EXPECT_THROW(parser.parse(doc.child("Settings")), docraft::exception::InvalidInputException); } } diff --git a/docraft/test/docraft/craft/docraft_shape_parser_test.cc b/docraft/test/docraft/craft/docraft_shape_parser_test.cc index 2c2d0c4..67af3ec 100644 --- a/docraft/test/docraft/craft/docraft_shape_parser_test.cc +++ b/docraft/test/docraft/craft/docraft_shape_parser_test.cc @@ -7,6 +7,7 @@ #include "docraft/craft/parser/docraft_triangle_parser.h" #include "docraft/craft/parser/docraft_parser.h" #include "docraft/docraft_color.h" +#include "docraft/exception/docraft_exceptions.h" #include "docraft/model/docraft_circle.h" #include "docraft/model/docraft_line.h" #include "docraft/model/docraft_polygon.h" @@ -97,7 +98,7 @@ TEST(DocraftTriangleParserTest, RejectsInvalidPointCount) { ASSERT_TRUE(doc.load_string(xml)); docraft::craft::parser::DocraftTriangleParser parser; - EXPECT_THROW(parser.parse(doc.child("Triangle")), std::invalid_argument); + EXPECT_THROW(parser.parse(doc.child("Triangle")), docraft::exception::InvalidInputException); } TEST(DocraftPolygonParserTest, ParsesPolygonPoints) { diff --git a/docraft/test/docraft/craft/docraft_table_parser_test.cc b/docraft/test/docraft/craft/docraft_table_parser_test.cc index aec78c4..539b259 100644 --- a/docraft/test/docraft/craft/docraft_table_parser_test.cc +++ b/docraft/test/docraft/craft/docraft_table_parser_test.cc @@ -2,6 +2,7 @@ #include #include "docraft/craft/parser/docraft_parser.h" +#include "docraft/exception/docraft_exceptions.h" #include "docraft/model/docraft_table.h" namespace { @@ -86,7 +87,7 @@ TEST(DocraftTableParserTest, RejectsLegacyTitleTagInTableHeader) { ASSERT_TRUE(doc.load_string(xml)); docraft::craft::parser::DocraftTableParser parser; - EXPECT_THROW(parser.parse(doc.child("Table")), std::invalid_argument); + EXPECT_THROW(parser.parse(doc.child("Table")), docraft::exception::InvalidInputException); } TEST(DocraftTableParserTest, ParsesVerticalTableWithHeaderRow) { @@ -185,7 +186,7 @@ TEST(DocraftTableParserTest, RejectsInvalidJsonModelAttribute) { ASSERT_TRUE(doc.load_string(xml)); docraft::craft::parser::DocraftTableParser parser; - EXPECT_THROW(parser.parse(doc.child("Table")), std::invalid_argument); + EXPECT_THROW(parser.parse(doc.child("Table")), docraft::exception::InvalidInputException); } TEST(DocraftTableParserTest, AcceptsTemplateModelAttribute) { @@ -216,9 +217,9 @@ TEST(DocraftTableParserTest, AcceptsJsonHeaderAttribute) { auto node = parser.parse(doc.child("Table")); auto table = std::dynamic_pointer_cast(node); ASSERT_TRUE(table); - ASSERT_EQ(table->title_nodes().size(), 2U); - EXPECT_EQ(table->title_nodes()[0]->text(), "H1"); - EXPECT_EQ(table->title_nodes()[1]->text(), "H2"); + ASSERT_EQ(table->title_text_nodes().size(), 2U); + EXPECT_EQ(table->title_text_nodes()[0]->text(), "H1"); + EXPECT_EQ(table->title_text_nodes()[1]->text(), "H2"); } TEST(DocraftTableParserTest, ParsesCellWidthAttribute) { diff --git a/docraft/test/docraft/docraft_document_test.cc b/docraft/test/docraft/docraft_document_test.cc index 3149983..f1cf45a 100644 --- a/docraft/test/docraft/docraft_document_test.cc +++ b/docraft/test/docraft/docraft_document_test.cc @@ -6,6 +6,7 @@ #include #include "docraft/docraft_document.h" +#include "docraft/exception/docraft_exceptions.h" #include "docraft/model/docraft_body.h" #include "docraft/model/docraft_list.h" #include "docraft/model/docraft_rectangle.h" @@ -178,18 +179,18 @@ namespace docraft::test { TEST(DocraftDocumentTest, SettingDocumentTitleUpdatesMetadataTitle) { DocraftDocument document("Initial Title"); - document.set_document_title("Updated Title"); + document.edit_config().set_document_title("Updated Title"); - ASSERT_TRUE(document.document_metadata().title().has_value()); - EXPECT_EQ(document.document_metadata().title().value(), "Updated Title"); + ASSERT_TRUE(document.config().document_metadata().title().has_value()); + EXPECT_EQ(document.config().document_metadata().title().value(), "Updated Title"); } TEST(DocraftDocumentTest, SettingDocumentPathUpdatesOutputDirectory) { DocraftDocument document("Initial Title"); - document.set_document_path("exports/reports"); + document.edit_config().set_document_path("exports/reports"); - EXPECT_EQ(document.document_path(), "exports/reports"); + EXPECT_EQ(document.config().document_path(), "exports/reports"); } TEST(DocraftDocumentTest, SettingMetadataWithTitleUpdatesDocumentTitle) { @@ -198,11 +199,11 @@ namespace docraft::test { metadata.set_title("Metadata Title"); metadata.set_author("Metadata Author"); - document.set_document_metadata(metadata); + document.edit_config().set_document_metadata(metadata); - EXPECT_EQ(document.document_title(), "Metadata Title"); - ASSERT_TRUE(document.document_metadata().author().has_value()); - EXPECT_EQ(document.document_metadata().author().value(), "Metadata Author"); + EXPECT_EQ(document.config().document_title(), "Metadata Title"); + ASSERT_TRUE(document.config().document_metadata().author().has_value()); + EXPECT_EQ(document.config().document_metadata().author().value(), "Metadata Author"); } TEST(DocraftDocumentTest, AutoKeywordsCanBeEnabledFromCpp) { @@ -212,19 +213,19 @@ namespace docraft::test { DocraftDocumentMetadata metadata; metadata.set_keywords("manuale"); - document.set_document_metadata(metadata); + document.edit_config().set_document_metadata(metadata); - document.set_auto_keywords_config({ + document.edit_config().set_auto_keywords_config({ .max_keywords = 3, .min_length = 4, .stop_word_languages = {"it", "en"} }); - document.enable_auto_keywords(); + document.edit_config().enable_auto_keywords(); document.refresh_auto_keywords(); - EXPECT_TRUE(document.auto_keywords_enabled()); - ASSERT_TRUE(document.document_metadata().keywords().has_value()); - EXPECT_EQ(document.document_metadata().keywords().value(), "manuale, engine, parser, metadata"); + EXPECT_TRUE(document.config().auto_keywords_enabled()); + ASSERT_TRUE(document.config().document_metadata().keywords().has_value()); + EXPECT_EQ(document.config().document_metadata().keywords().value(), "manuale, engine, parser, metadata"); } TEST(DocraftDocumentTest, RenderWritesMetadataSetFromCode) { @@ -243,10 +244,10 @@ namespace docraft::test { metadata.set_author("Docraft Author From Code"); metadata.set_subject("Docraft Subject From Code"); metadata.set_keywords("alpha,beta"); - document.set_document_metadata(metadata); + document.edit_config().set_document_metadata(metadata); - const std::filesystem::path output_path = document.document_title() + ".pdf"; - EXPECT_EQ(document.document_title(), "Docraft Title From Code"); + const std::filesystem::path output_path = document.config().document_title() + ".pdf"; + EXPECT_EQ(document.config().document_title(), "Docraft Title From Code"); document.render(); ASSERT_TRUE(std::filesystem::exists(output_path)); @@ -273,12 +274,31 @@ namespace docraft::test { .extension = ".mock" }); - document.set_backend(mock_backend); + document.set_backend_providers_factory( + std::make_shared(mock_backend)); document.render(); EXPECT_EQ(mock_backend->last_saved_path(), "custom_backend_test.mock"); } + TEST(DocraftDocumentTest, RenderUsesBackendProvidersFactorySetOnDocument) { + DocraftDocument document("factory_backend_test"); + auto body = std::make_shared(); + body->add_child(std::make_shared("Simple text")); + document.add_node(body); + + auto mock_backend = std::make_shared( + docraft::test::utils::MockRenderingBackend::Config{ + .extension = ".factory" + }); + + document.set_backend_providers_factory( + std::make_shared(mock_backend)); + document.render(); + + EXPECT_EQ(mock_backend->last_saved_path(), "factory_backend_test.factory"); + } + TEST(DocraftDocumentTest, RenderUsesDocumentPathWhenSet) { DocraftDocument document("custom_backend_test"); auto body = std::make_shared(); @@ -290,9 +310,9 @@ namespace docraft::test { .extension = ".mock" }); - document.set_backend(mock_backend); - document.set_document_path("exports/reports"); - document.set_document_title("monthly_summary"); + document.set_backend_providers_factory(std::make_shared(mock_backend)); + document.edit_config().set_document_path("exports/reports"); + document.edit_config().set_document_title("monthly_summary"); document.render(); EXPECT_EQ( @@ -315,8 +335,9 @@ namespace docraft::test { .extension = ".mock" }); - document.set_backend(mock_backend); - document.set_backend(nullptr); + document.set_backend_providers_factory( + std::make_shared(mock_backend)); + document.set_backend_providers_factory(nullptr); document.render(); EXPECT_TRUE(mock_backend->last_saved_path().empty()); @@ -324,4 +345,36 @@ namespace docraft::test { std::filesystem::remove(output_path, ec); } + + TEST(DocraftDocumentTest, MockBackendCapabilityMatrixReturnsNullWhenDisabled) { + auto mock_backend = std::make_shared( + docraft::test::utils::MockRenderingBackend::Config{ + .supports_line_backend = false, + .supports_text_backend = false, + .supports_shape_backend = true, + .supports_image_backend = false, + .supports_page_backend = false, + .supports_output_backend = true, + .supports_font_backend = true, + .supports_metadata_backend = true + }); + + EXPECT_EQ(mock_backend->line_rendering(), nullptr); + EXPECT_EQ(mock_backend->text_rendering(), nullptr); + EXPECT_EQ(mock_backend->image_rendering(), nullptr); + EXPECT_EQ(mock_backend->page_rendering(), nullptr); + ASSERT_NE(mock_backend->shape_rendering(), nullptr); + } + + TEST(DocraftDocumentTest, MockBackendEnforcesTextScopeWhenRequired) { + auto mock_backend = std::make_shared( + docraft::test::utils::MockRenderingBackend::Config{ + .require_text_scope = true + }); + + EXPECT_THROW(mock_backend->draw_text("hello", 1.0F, 1.0F), docraft::exception::BackendStateException); + EXPECT_NO_THROW(mock_backend->begin_text()); + EXPECT_NO_THROW(mock_backend->draw_text("hello", 1.0F, 1.0F)); + EXPECT_NO_THROW(mock_backend->end_text()); + } } // namespace docraft::test diff --git a/docraft/test/docraft/exception/docraft_exception_test.cc b/docraft/test/docraft/exception/docraft_exception_test.cc new file mode 100644 index 0000000..15ee6c9 --- /dev/null +++ b/docraft/test/docraft/exception/docraft_exception_test.cc @@ -0,0 +1,39 @@ +#include + +#include + +#include "docraft/exception/docraft_exceptions.h" +#include "docraft/utils/docraft_mock_rendering_backend.h" + +// DocraftException is a custom polymorphic base type. +static_assert(std::is_polymorphic_v); +static_assert(std::is_base_of_v); +static_assert(std::is_base_of_v); +static_assert(std::is_base_of_v); +static_assert(std::is_base_of_v); + +TEST(DocraftExceptionTest, MockBackendThrowsCustomBackendStateException) { + auto config = docraft::test::utils::MockBackendSharedState::Config{}; + config.supports_page_backend = false; + auto state = std::make_shared(config); + + EXPECT_THROW(state->ensure_page_available(), docraft::exception::BackendStateException); +} + +TEST(DocraftExceptionTest, MockPageBackendThrowsCustomBackendStateException) { + auto config = docraft::test::utils::MockBackendSharedState::Config{}; + config.initial_pages = 1; + config.supports_page_backend = true; + auto state = std::make_shared(config); + docraft::test::utils::MockPageBackend page_backend{state}; + + EXPECT_THROW(page_backend.move_to_next_page(), docraft::exception::BackendStateException); +} + + + + diff --git a/docraft/test/docraft/generic/docraft_font_applier_test.cc b/docraft/test/docraft/generic/docraft_font_applier_test.cc index a3d3d22..c746465 100644 --- a/docraft/test/docraft/generic/docraft_font_applier_test.cc +++ b/docraft/test/docraft/generic/docraft_font_applier_test.cc @@ -13,7 +13,7 @@ using namespace docraft; TEST(DocraftFontApplier, ResolvesBuiltInBold) { auto context = std::make_shared(); auto applier = std::make_shared(context); - context->set_font_applier(applier); + context->edit_typography().set_font_applier(applier); auto text = std::make_shared("Hello"); text->set_font_name("Helvetica"); @@ -26,7 +26,7 @@ TEST(DocraftFontApplier, ResolvesBuiltInBold) { TEST(DocraftFontApplier, ResolvesBuiltInItalic) { auto context = std::make_shared(); auto applier = std::make_shared(context); - context->set_font_applier(applier); + context->edit_typography().set_font_applier(applier); auto text = std::make_shared("Hello"); text->set_font_name("Helvetica"); @@ -39,7 +39,7 @@ TEST(DocraftFontApplier, ResolvesBuiltInItalic) { TEST(DocraftFontApplier, ResolvesBuiltInBoldItalic) { auto context = std::make_shared(); auto applier = std::make_shared(context); - context->set_font_applier(applier); + context->edit_typography().set_font_applier(applier); auto text = std::make_shared("Hello"); text->set_font_name("Helvetica"); @@ -52,7 +52,7 @@ TEST(DocraftFontApplier, ResolvesBuiltInBoldItalic) { TEST(DocraftFontApplier, FallsBackToHelveticaWhenUnknown) { auto context = std::make_shared(); auto applier = std::make_shared(context); - context->set_font_applier(applier); + context->edit_typography().set_font_applier(applier); auto text = std::make_shared("Hello"); text->set_font_name("UnknownFont"); diff --git a/docraft/test/docraft/layout/docraft_layout_engine_test.cc b/docraft/test/docraft/layout/docraft_layout_engine_test.cc index 461906b..e3b028d 100644 --- a/docraft/test/docraft/layout/docraft_layout_engine_test.cc +++ b/docraft/test/docraft/layout/docraft_layout_engine_test.cc @@ -3,6 +3,7 @@ #include #include +#include "docraft/exception/docraft_exceptions.h" #include "docraft/model/docraft_blank_line.h" #include "docraft/model/docraft_body.h" #include "docraft/model/docraft_footer.h" @@ -88,7 +89,7 @@ namespace docraft::test::layout { EXPECT_EQ(layout_node->children().size(), 2); auto layout = engine->compute_layout(layout_node); - const float available_width_for_children = context->page_width() - kHorizontalSpacing_; + const float available_width_for_children = context->layout().page_width() - kHorizontalSpacing_; const float allocated_width = available_width_for_children * child1->weight(); EXPECT_NEAR(child2->position().x, allocated_width + kHorizontalSpacing_, 0.001); EXPECT_GE(layout.width(), child2->anchors().top_right.x - layout_node->position().x); @@ -129,7 +130,7 @@ namespace docraft::test::layout { EXPECT_EQ(child1->position().x, 0.0F); EXPECT_EQ(child1->position().y, inner_layout->anchors().top_left.y); //Child 2 position advances by allocated width, not by child1's rendered width - const float inner_available_width = context->page_width() - kHorizontalSpacing_; + const float inner_available_width = context->layout().page_width() - kHorizontalSpacing_; const float inner_allocated_width = inner_available_width * child1->weight(); EXPECT_NEAR(child2->position().x, inner_allocated_width + kHorizontalSpacing_, 0.001); EXPECT_EQ(child2->position().y, child1->anchors().top_right.y); @@ -148,7 +149,7 @@ namespace docraft::test::layout { auto layout = engine->compute_layout(blank_line); EXPECT_EQ(layout.height(), 1.0F); EXPECT_NE(layout.width(), 0.0F); - EXPECT_EQ(layout.width(),context->page_width()); + EXPECT_EQ(layout.width(), context->layout().page_width()); } TEST_F(DocraftLayoutEngineTest, ComputeLayoutEmptyLayoutNode) { auto& engine = this->engine(); @@ -169,9 +170,9 @@ namespace docraft::test::layout { table->set_auto_fill_width(true); // 2x2 grid content (rectangles are deterministic for height) - auto c00 = std::make_shared(); + auto c00 = std::make_shared(); c00->set_height(10.0F); - auto c01 = std::make_shared(); + auto c01 = std::make_shared(); c01->set_height(20.0F); auto c10 = std::make_shared(); c10->set_text("Cell 10"); @@ -188,11 +189,11 @@ namespace docraft::test::layout { // Table should start at the initial cursor (engine ctor places cursor at top-left of page). // Note: The cursor may be offset by default body padding EXPECT_FLOAT_EQ(table->position().x, 0.0F); - EXPECT_NEAR(table->position().y, context->page_height(), 15.0F); + EXPECT_NEAR(table->position().y, context->layout().page_height(), 15.0F); // Auto-fill width should consume the remaining page width. - EXPECT_NEAR(table->width(), context->page_width(), 0.001F); - EXPECT_NEAR(box.width(), context->page_width(), 0.001F); + EXPECT_NEAR(table->width(), context->layout().page_width(), 0.001F); + EXPECT_NEAR(box.width(), context->layout().page_width(), 0.001F); // Titles should be generated and have a non-zero row height. ASSERT_EQ(table->title_nodes().size(), 2U); @@ -228,9 +229,9 @@ namespace docraft::test::layout { table->set_cols(2); // key/value table->set_auto_fill_width(true); - auto v0 = std::make_shared(); + auto v0 = std::make_shared(); v0->set_height(10.0F); - auto v1 = std::make_shared(); + auto v1 = std::make_shared(); v1->set_height(20.0F); table->add_content_node(v0); @@ -240,11 +241,11 @@ namespace docraft::test::layout { // Starts at initial cursor. EXPECT_FLOAT_EQ(table->position().x, 0.0F); - EXPECT_FLOAT_EQ(table->position().y, context->page_height()); + EXPECT_FLOAT_EQ(table->position().y, context->layout().page_height()); // Auto-fill width. - EXPECT_NEAR(table->width(), context->page_width(), 0.001F); - EXPECT_NEAR(box.width(), context->page_width(), 0.001F); + EXPECT_NEAR(table->width(), context->layout().page_width(), 0.001F); + EXPECT_NEAR(box.width(), context->layout().page_width(), 0.001F); // One title per row. ASSERT_EQ(table->title_nodes().size(), 2U); @@ -295,19 +296,19 @@ namespace docraft::test::layout { engine->compute_document_layout(document_nodes); //Verify header layout EXPECT_EQ(header->position().x, 0); - EXPECT_EQ(header->position().y, context->page_height()); - EXPECT_EQ(header->width(), context->page_width()); - EXPECT_EQ(header->height(), context->page_height()*kHeaderHeightRatio_); + EXPECT_EQ(header->position().y, context->layout().page_height()); + EXPECT_EQ(header->width(), context->layout().page_width()); + EXPECT_EQ(header->height(), context->layout().page_height()*kHeaderHeightRatio_); // Verify header item position EXPECT_EQ(header_text->position().x, 0.0F); // section padding is vertical only EXPECT_NEAR(header_text->position().y, header->anchors().top_left.y - 2.0F, 0.01F); //Verify body layout EXPECT_EQ(body->position().x, 0); EXPECT_EQ(body->position().y, header->anchors().bottom_left.y); - EXPECT_EQ(body->width(), context->page_width()); - const float expected_body_height = context->page_height() - - context->page_height() * kHeaderHeightRatio_ - - context->page_height() * kFooterHeightRatio_; + EXPECT_EQ(body->width(), context->layout().page_width()); + const float expected_body_height = context->layout().page_height() - + context->layout().page_height() * kHeaderHeightRatio_ - + context->layout().page_height() * kFooterHeightRatio_; EXPECT_NEAR(body->height(), expected_body_height, 0.01F); // Verify body item position EXPECT_EQ(body_text->position().x, 0.0F); // section padding is vertical only @@ -315,8 +316,8 @@ namespace docraft::test::layout { //Verify footer layout EXPECT_EQ(footer->position().x, 0); EXPECT_EQ(footer->position().y, body->anchors().bottom_left.y); - EXPECT_EQ(footer->width(), context->page_width()); - EXPECT_EQ(footer->height(), context->page_height()*kFooterHeightRatio_); + EXPECT_EQ(footer->width(), context->layout().page_width()); + EXPECT_EQ(footer->height(), context->layout().page_height()*kFooterHeightRatio_); // Verify footer item position EXPECT_EQ(footer_text->position().x, 0.0F); // section padding is vertical only EXPECT_NEAR(footer_text->position().y, footer->anchors().top_left.y - 2.0F, 0.01F); @@ -329,7 +330,7 @@ namespace docraft::test::layout { document_nodes.push_back(header); auto footer = std::make_shared(); document_nodes.push_back(footer); - EXPECT_THROW(engine->compute_document_layout(document_nodes), std::runtime_error); + EXPECT_THROW(engine->compute_document_layout(document_nodes), docraft::exception::DocumentStateException); } TEST_F(DocraftLayoutEngineTest, ComputeDocumentWithOnlyBody) { auto& engine =this->engine(); @@ -343,9 +344,9 @@ namespace docraft::test::layout { engine->compute_document_layout(document_nodes); //Verify body layout EXPECT_EQ(body->position().x, 10); - EXPECT_EQ(body->position().y, context->page_height()); - EXPECT_EQ(body->width(), context->page_width()-20); - EXPECT_EQ(body->height(), context->page_height()); + EXPECT_EQ(body->position().y, context->layout().page_height()); + EXPECT_EQ(body->width(), context->layout().page_width()-20); + EXPECT_EQ(body->height(), context->layout().page_height()); // Verify body item position EXPECT_EQ(body_text->position().x, 10.0F); EXPECT_EQ(body_text->position().y, body->anchors().top_left.y); @@ -367,18 +368,18 @@ namespace docraft::test::layout { engine->compute_document_layout(document_nodes); //Verify header layout EXPECT_EQ(header->position().x, 10); - EXPECT_EQ(header->position().y, context->page_height()); - EXPECT_EQ(header->width(), context->page_width()-20); - EXPECT_EQ(header->height(), context->page_height()*kHeaderHeightRatio_); + EXPECT_EQ(header->position().y, context->layout().page_height()); + EXPECT_EQ(header->width(), context->layout().page_width()-20); + EXPECT_EQ(header->height(), context->layout().page_height()*kHeaderHeightRatio_); // Verify header item position EXPECT_EQ(header_text->position().x, 10.0F); EXPECT_NEAR(header_text->position().y, header->anchors().top_left.y - 2.0F, 0.01F); //Verify body layout EXPECT_EQ(body->position().x, 10); EXPECT_EQ(body->position().y, header->anchors().bottom_left.y); - EXPECT_EQ(body->width(), context->page_width()-20); - const float expected_body_height = context->page_height() - - context->page_height() * kHeaderHeightRatio_; + EXPECT_EQ(body->width(), context->layout().page_width()-20); + const float expected_body_height = context->layout().page_height() - + context->layout().page_height() * kHeaderHeightRatio_; EXPECT_EQ(body->height(), expected_body_height); // Verify body item position EXPECT_EQ(body_text->position().x, 10.0F); @@ -401,10 +402,10 @@ namespace docraft::test::layout { engine->compute_document_layout(document_nodes); //Verify body layout EXPECT_EQ(body->position().x, 10); - EXPECT_EQ(body->position().y, context->page_height()); - EXPECT_EQ(body->width(), context->page_width()-20); - const float expected_body_height = context->page_height() - - context->page_height() * kFooterHeightRatio_; + EXPECT_EQ(body->position().y, context->layout().page_height()); + EXPECT_EQ(body->width(), context->layout().page_width()-20); + const float expected_body_height = context->layout().page_height() - + context->layout().page_height() * kFooterHeightRatio_; EXPECT_EQ(body->height(), expected_body_height); // Verify body item position EXPECT_EQ(body_text->position().x, 10.0F); @@ -412,8 +413,8 @@ namespace docraft::test::layout { //Verify footer layout EXPECT_EQ(footer->position().x, 10); EXPECT_EQ(footer->position().y, body->anchors().bottom_left.y); - EXPECT_EQ(footer->width(), context->page_width()-20); - EXPECT_EQ(footer->height(), context->page_height()*kFooterHeightRatio_); + EXPECT_EQ(footer->width(), context->layout().page_width()-20); + EXPECT_EQ(footer->height(), context->layout().page_height()*kFooterHeightRatio_); // Verify footer item position EXPECT_EQ(footer_text->position().x, 10.0F); EXPECT_NEAR(footer_text->position().y, footer->anchors().top_left.y - 2.0F, 0.01F); @@ -597,7 +598,7 @@ namespace docraft::test::layout { // === COMPUTE LAYOUT === auto result = engine->compute_layout(body); - const float page_w = context->page_width() - (body->margin_left() + body->margin_right()); + const float page_w = context->layout().page_width() - (body->margin_left() + body->margin_right()); // --- Structural assertions --- @@ -677,10 +678,10 @@ namespace docraft::test::layout { table->set_column_weights({.5F, .5F}); table->set_width(300.0F); - auto c00 = std::make_shared(); + auto c00 = std::make_shared(); c00->set_width(90.0F); // Simulates ... c00->set_height(10.0F); - auto c01 = std::make_shared(); + auto c01 = std::make_shared(); c01->set_height(10.0F); table->add_content_node(c00); diff --git a/docraft/test/docraft/layout/docraft_layout_text_handler_test.cc b/docraft/test/docraft/layout/docraft_layout_text_handler_test.cc index bc40161..37a823d 100644 --- a/docraft/test/docraft/layout/docraft_layout_text_handler_test.cc +++ b/docraft/test/docraft/layout/docraft_layout_text_handler_test.cc @@ -28,7 +28,7 @@ namespace docraft::test::layout { TEST_F(DocraftLayoutTextHandlerTest, TextNewlinesArePreservedAsSeparateLines) { auto text = std::make_shared(); text->set_text("Alpha\nBeta\nGamma"); - context()->set_current_rect_width(context()->page_width()); + context()->edit_layout().set_current_rect_width(context()->layout().page_width()); engine()->compute_layout(text); @@ -42,7 +42,7 @@ namespace docraft::test::layout { TEST_F(DocraftLayoutTextHandlerTest, TextLinePositionsAreMonotonicNonIncreasing) { auto text = std::make_shared(); text->set_text("Line1\nLine2\nLine3\nLine4"); - context()->set_current_rect_width(context()->page_width()); + context()->edit_layout().set_current_rect_width(context()->layout().page_width()); engine()->compute_layout(text); @@ -58,7 +58,7 @@ namespace docraft::test::layout { text->set_text("Uno due tre quattro cinque sei sette otto nove dieci"); text->set_alignment(docraft::model::TextAlignment::kJustified); text->set_font_size(12.0F); - context()->set_current_rect_width(80.0F); + context()->edit_layout().set_current_rect_width(80.0F); engine()->compute_layout(text); @@ -66,6 +66,6 @@ namespace docraft::test::layout { ASSERT_GT(lines.size(), 1U); const auto &last_line = lines.back(); EXPECT_EQ(last_line->alignment(), docraft::model::TextAlignment::kLeft); - EXPECT_LT(last_line->width(), context()->available_space()); + EXPECT_LT(last_line->width(), context()->layout().available_space()); } } // namespace docraft::test::layout diff --git a/docraft/test/docraft/layout/docraft_pagination_stress_test.cc b/docraft/test/docraft/layout/docraft_pagination_stress_test.cc new file mode 100644 index 0000000..25b0c72 --- /dev/null +++ b/docraft/test/docraft/layout/docraft_pagination_stress_test.cc @@ -0,0 +1,682 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "docraft/backend/pdf/docraft_haru_backend_providers_factory.h" +#include "docraft/docraft_document_context.h" +#include "docraft/craft/docraft_craft_language_tokens.h" +#include "docraft/generic/docraft_font_applier.h" +#include "docraft/layout/docraft_layout_engine.h" +#include "docraft/renderer/docraft_pdf_renderer.h" + +#include "docraft/model/docraft_blank_line.h" +#include "docraft/model/docraft_body.h" +#include "docraft/model/docraft_circle.h" +#include "docraft/model/docraft_footer.h" +#include "docraft/model/docraft_foreach.h" +#include "docraft/model/docraft_header.h" +#include "docraft/model/docraft_image.h" +#include "docraft/model/docraft_layout.h" +#include "docraft/model/docraft_line.h" +#include "docraft/model/docraft_list.h" +#include "docraft/model/docraft_new_page.h" +#include "docraft/model/docraft_page_number.h" +#include "docraft/model/docraft_polygon.h" +#include "docraft/model/docraft_rectangle.h" +#include "docraft/model/docraft_table.h" +#include "docraft/model/docraft_text.h" +#include "docraft/model/docraft_triangle.h" + +#include "docraft/utils/docraft_mock_rendering_backend.h" + +namespace docraft::test::layout { + class DocraftPaginationStressTest : public ::testing::Test { + protected: + void SetUp() override { + const std::string backend_mode = requested_backend_mode(); + if (backend_mode == "mock") { + setup_mock_backend(); + } else if (backend_mode == "haru") { + setup_haru_backend(); + } else if (!try_setup_haru_backend()) { + setup_mock_backend(); + } + + context_->set_renderer(std::make_shared(context_)); + context_->edit_typography().set_font_applier(std::make_shared(context_)); + engine_ = std::make_unique(context_); + } + + void TearDown() override { + if (!use_haru_ || !context_) { + return; + } + + const auto *test_info = ::testing::UnitTest::GetInstance()->current_test_info(); + if (!test_info) { + return; + } + + const auto output = context_->rendering().output_backend(); + if (!output) { + return; + } + + std::string file_name = std::string(test_info->test_suite_name()) + "_" + test_info->name() + ".pdf"; + for (char &ch: file_name) { + const auto value = static_cast(ch); + if (!(std::isalnum(value) || ch == '_' || ch == '-' || ch == '.')) { + ch = '_'; + } + } + + std::filesystem::create_directories("stress_artifacts"); + output->save_to_file("stress_artifacts/" + file_name); + } + + [[nodiscard]] std::size_t total_page_count() const { + if (!context_) { + return 0; + } + const auto page_backend = context_->rendering().page_rendering(); + if (!page_backend) { + return 0; + } + return page_backend->total_page_count(); + } + + [[nodiscard]] std::size_t page_threshold(std::size_t mock_pages, std::size_t haru_pages) const { + return use_haru_ ? haru_pages : mock_pages; + } + + [[nodiscard]] std::size_t fragment_threshold(std::size_t mock_fragments, std::size_t haru_fragments) const { + return use_haru_ ? haru_fragments : mock_fragments; + } + + void layout_and_render(const std::vector > &nodes) { + engine_->compute_document_layout(nodes); + auto &rendering_service = context_->edit_rendering(); + const auto page_backend = rendering_service.edit_page_rendering(); + if (page_backend) { + page_backend->go_to_first_page(); + } + + for (const auto &node: nodes) { + if (!node || !node->visible()) { + continue; + } + + if (node->page_owner() == -1 && page_backend) { + const auto page_count = page_backend->total_page_count(); + for (std::size_t i = 0; i < page_count; ++i) { + page_backend->go_to_page(i); + node->draw(context_); + } + continue; + } + + if (page_backend && node->page_owner() > 0) { + page_backend->go_to_page(static_cast(node->page_owner() - 1)); + } + if (node->should_render(context_)) { + node->draw(context_); + } + } + } + + [[nodiscard]] static std::string requested_backend_mode() { + const char *value = std::getenv("DOCRAFT_STRESS_BACKEND"); + if (!value || std::string(value).empty()) { + return "auto"; + } + std::string mode(value); + std::ranges::transform(mode, mode.begin(), + [](const unsigned char ch) { return static_cast(std::tolower(ch)); }); + if (mode == "mock" || mode == "haru") { + return mode; + } + return "auto"; + } + + void setup_mock_backend() { + backend_ = std::make_shared( + docraft::test::utils::MockRenderingBackend::Config{ + .page_width = 100.0F, + .page_height = 100.0F, + .text_width_factor = 5.0F, + .initial_pages = 1, + .extension = ".pdf", + .can_use_font = true + }); + context_ = std::make_shared( + std::make_shared(backend_)); + context_->edit_layout().set_page_dimensions(100.0F, 100.0F); + use_haru_ = false; + } + + void setup_haru_backend() { + context_ = std::make_shared( + std::make_shared()); + + const auto page_backend = context_->edit_rendering().edit_page_rendering(); + if (!page_backend) { + throw std::runtime_error("Haru page backend is not available"); + } + + page_backend->set_page_format(model::DocraftPageSize::kA4, model::DocraftPageOrientation::kPortrait); + context_->edit_layout().set_page_dimensions(page_backend->page_width(), page_backend->page_height()); + use_haru_ = true; + } + + [[nodiscard]] bool try_setup_haru_backend() { + try { + setup_haru_backend(); + return true; + } catch (...) { + return false; + } + } + + std::shared_ptr backend_; + std::shared_ptr context_; + std::unique_ptr engine_; + bool use_haru_ = false; + }; + + // Stress sequential block flow: ensures repeated overflow keeps deterministic owner ordering. + TEST_F(DocraftPaginationStressTest, ManyBlocksPaginateAcrossMultiplePagesKeepingOrder) { + auto body = std::make_shared(); + body->set_margin_left(0.0F); + body->set_margin_right(0.0F); + + std::vector > blocks; + blocks.reserve(24); + for (int i = 0; i < 24; ++i) { + auto rect = std::make_shared(); + rect->set_height(40.0F); + body->add_child(rect); + blocks.push_back(rect); + } + + std::vector > nodes{body}; + layout_and_render(nodes); + + EXPECT_GE(total_page_count(), page_threshold(6U, 2U)); + + int previous_owner = 1; + for (const auto &block: blocks) { + EXPECT_GE(block->page_owner(), 1); + EXPECT_GE(block->page_owner(), previous_owner); + previous_owner = block->page_owner(); + } + } + + // Stress row-fit logic with asymmetric cell heights and validate no row is lost after splitting. + TEST_F(DocraftPaginationStressTest, MixedHeightTableRowsSplitWithoutRowLoss) { + auto body = std::make_shared(); + body->set_margin_left(0.0F); + body->set_margin_right(0.0F); + + auto table = std::make_shared(); + table->set_titles({"A", "B"}); + table->set_column_weights({0.5F, 0.5F}); + table->set_auto_fill_width(true); + + constexpr std::size_t expected_rows = 8; + for (std::size_t i = 0; i < expected_rows; ++i) { + auto c_left = std::make_shared(); + c_left->set_height(6.0F); + auto c_right = std::make_shared(); + c_right->set_height((i % 2 == 0) ? 34.0F : 44.0F); + table->add_content_node(c_left); + table->add_content_node(c_right); + } + + body->add_child(table); + + std::vector > nodes{body}; + layout_and_render(nodes); + + EXPECT_GE(total_page_count(), page_threshold(2U, 1U)); + + std::size_t accumulated_rows = 0; + std::size_t table_fragments = 0; + for (const auto &node: body->children()) { + auto table_fragment = std::dynamic_pointer_cast(node); + if (!table_fragment) { + continue; + } + ++table_fragments; + accumulated_rows += static_cast(table_fragment->rows()); + EXPECT_GT(table_fragment->rows(), 0); + } + + EXPECT_GE(table_fragments, fragment_threshold(2U, 1U)); + EXPECT_EQ(accumulated_rows, expected_rows); + } + + // Stress recursive layout traversal depth and verify geometry remains finite and non-degenerate. + TEST_F(DocraftPaginationStressTest, DeepNestedLayoutsWithManyNodesRemainStable) { + auto body = std::make_shared(); + body->set_margin_left(0.0F); + body->set_margin_right(0.0F); + + auto root = std::make_shared(); + root->set_orientation(model::LayoutOrientation::kVertical); + body->add_child(root); + + auto current = root; + for (int depth = 0; depth < 30; ++depth) { + auto nested = std::make_shared(); + nested->set_orientation(depth % 2 == 0 + ? model::LayoutOrientation::kVertical + : model::LayoutOrientation::kHorizontal); + + for (int j = 0; j < 8; ++j) { + auto rect = std::make_shared(); + rect->set_height(6.0F + static_cast((depth + j) % 5)); + rect->set_weight(1.0F / 8.0F); + nested->add_child(rect); + } + + current->add_child(nested); + current = nested; + } + + std::vector > nodes{body}; + layout_and_render(nodes); + + EXPECT_TRUE(std::isfinite(root->width())); + EXPECT_TRUE(std::isfinite(root->height())); + EXPECT_GT(root->width(), 0.0F); + EXPECT_GT(root->height(), 0.0F); + EXPECT_GE(total_page_count(), 1U); + } + + // Stress explicit page-break churn and verify page ownership remains monotonic. + TEST_F(DocraftPaginationStressTest, ManyExplicitNewPagesDoNotCorruptOwnership) { + auto body = std::make_shared(); + body->set_margin_left(0.0F); + body->set_margin_right(0.0F); + + std::vector > payload_nodes; + payload_nodes.reserve(12); + + for (int i = 0; i < 12; ++i) { + auto rect = std::make_shared(); + rect->set_height(8.0F); + body->add_child(rect); + payload_nodes.push_back(rect); + + auto page_break = std::make_shared(); + body->add_child(page_break); + } + + std::vector > nodes{body}; + layout_and_render(nodes); + + EXPECT_GE(total_page_count(), 12U); + + int previous_owner = 1; + for (const auto &node: payload_nodes) { + EXPECT_GE(node->page_owner(), previous_owner); + previous_owner = node->page_owner(); + } + } + + // Stress large table fragmentation over many pages and assert split integrity by row count. + TEST_F(DocraftPaginationStressTest, VeryLargeTableSplitPreservesAllRows) { + auto body = std::make_shared(); + body->set_margin_left(0.0F); + body->set_margin_right(0.0F); + + auto table = std::make_shared(); + table->set_titles({"C1", "C2", "C3"}); + table->set_column_weights({0.34F, 0.33F, 0.33F}); + table->set_auto_fill_width(true); + + constexpr std::size_t kExpectedRows = 120; + for (std::size_t r = 0; r < kExpectedRows; ++r) { + for (int c = 0; c < 3; ++c) { + auto cell = std::make_shared(); + cell->set_height(6.0F + static_cast((r + static_cast(c)) % 7)); + table->add_content_node(cell); + } + } + + body->add_child(table); + + std::vector > nodes{body}; + layout_and_render(nodes); + + std::size_t fragments = 0; + std::size_t accumulated_rows = 0; + int max_owner = 1; + + for (const auto &node: body->children()) { + auto fragment = std::dynamic_pointer_cast(node); + if (!fragment) { + continue; + } + ++fragments; + accumulated_rows += static_cast(fragment->rows()); + EXPECT_GT(fragment->rows(), 0); + EXPECT_GE(fragment->page_owner(), 1); + max_owner = std::max(max_owner, fragment->page_owner()); + } + + EXPECT_GE(fragments, fragment_threshold(5U, 2U)); + EXPECT_EQ(accumulated_rows, kExpectedRows); + EXPECT_GE(static_cast(max_owner), 2U); + } + + // Brutal mixed scenario: deep nesting, forced page breaks, and huge table in one run. + TEST_F(DocraftPaginationStressTest, BrutalCompositePaginationStressKeepsIntegrity) { + auto body = std::make_shared(); + body->set_margin_left(0.0F); + body->set_margin_right(0.0F); + + // Deep nested layout chain with many children per level. + auto root = std::make_shared(); + root->set_orientation(model::LayoutOrientation::kVertical); + body->add_child(root); + + auto current = root; + for (int depth = 0; depth < 40; ++depth) { + auto nested = std::make_shared(); + nested->set_orientation(depth % 2 == 0 + ? model::LayoutOrientation::kHorizontal + : model::LayoutOrientation::kVertical); + for (int j = 0; j < 10; ++j) { + auto rect = std::make_shared(); + rect->set_height(5.0F + static_cast((depth + j) % 9)); + rect->set_weight(0.1F); + nested->add_child(rect); + } + current->add_child(nested); + current = nested; + } + + // Add explicit hard page breaks with payload nodes. + std::vector > payload_nodes; + payload_nodes.reserve(30); + for (int i = 0; i < 30; ++i) { + auto payload = std::make_shared(); + payload->set_height(9.0F + static_cast(i % 4)); + body->add_child(payload); + payload_nodes.push_back(payload); + body->add_child(std::make_shared()); + } + + // Massive table that must split many times. + auto huge_table = std::make_shared(); + huge_table->set_titles({"A", "B", "C", "D"}); + huge_table->set_column_weights({0.25F, 0.25F, 0.25F, 0.25F}); + huge_table->set_auto_fill_width(true); + + constexpr std::size_t kExpectedRows = 300; + for (std::size_t r = 0; r < kExpectedRows; ++r) { + for (int c = 0; c < 4; ++c) { + auto cell = std::make_shared(); + cell->set_height(4.0F + static_cast((r + static_cast(c)) % 11)); + huge_table->add_content_node(cell); + } + } + body->add_child(huge_table); + + std::vector > nodes{body}; + layout_and_render(nodes); + + EXPECT_GE(total_page_count(), page_threshold(20U, 20U)); + EXPECT_TRUE(std::isfinite(root->width())); + EXPECT_TRUE(std::isfinite(root->height())); + EXPECT_GT(root->width(), 0.0F); + EXPECT_GT(root->height(), 0.0F); + + int previous_owner = 1; + for (const auto &node: payload_nodes) { + EXPECT_GE(node->page_owner(), previous_owner); + previous_owner = node->page_owner(); + } + + std::size_t fragments = 0; + std::size_t accumulated_rows = 0; + for (const auto &node: body->children()) { + auto fragment = std::dynamic_pointer_cast(node); + if (!fragment) { + continue; + } + ++fragments; + accumulated_rows += static_cast(fragment->rows()); + EXPECT_GT(fragment->rows(), 0); + EXPECT_GE(fragment->page_owner(), 1); + } + + EXPECT_GE(fragments, fragment_threshold(8U, 2U)); + EXPECT_EQ(accumulated_rows, kExpectedRows); + } + + // All-elements scenario: combines section nodes plus heterogeneous content and pagination pressure. + TEST_F(DocraftPaginationStressTest, AllElementsCompositeStressScenarioRemainsConsistent) { + auto header = std::make_shared(); + header->set_margin_left(0.0F); + header->set_margin_right(0.0F); + auto header_title = std::make_shared("Stress Header"); + header->add_child(header_title); + + auto body = std::make_shared(); + body->set_margin_left(0.0F); + body->set_margin_right(0.0F); + + auto footer = std::make_shared(); + footer->set_margin_left(0.0F); + footer->set_margin_right(0.0F); + auto page_number = std::make_shared(); + footer->add_child(page_number); + + // Main flow layout containing heterogeneous node types. + auto root = std::make_shared(); + root->set_orientation(model::LayoutOrientation::kVertical); + + auto text = std::make_shared("Long stress paragraph for layout engine"); + text->set_font_size(9.0F); + root->add_child(text); + + auto blank = std::make_shared(); + root->add_child(blank); + + auto list = std::make_shared(); + list->set_kind(model::ListKind::kOrdered); + list->set_ordered_style(model::OrderedListStyle::kRoman); + for (int i = 0; i < 6; ++i) { + auto item = std::make_shared("Item " + std::to_string(i)); + list->add_child(item); + } + root->add_child(list); + + auto image = std::make_shared(); + image->set_raw_data({255, 0, 0}, 1, 1); + image->set_width(100.0F); + image->set_height(100.0F); + root->add_child(image); + + auto circle = std::make_shared(); + circle->set_width(40.0F); + circle->set_height(40.0F); + circle->set_position_mode(model::DocraftPositionType::kAbsolute); + circle->set_background_color(DocraftColor::fromColorName(ColorName::kBlue)); + circle->set_position({.x = 500.0F, .y = 500.0F}); + root->add_child(circle); + + auto line = std::make_shared(); + line->set_start({.x = 100.0F, .y = 0.0F}); + line->set_end({.x = 500.0F, .y = 500.0F}); + line->set_border_color(DocraftColor::fromColorName(ColorName::kRed)); + line->set_border_width(2.0F); + + root->add_child(line); + + auto triangle = std::make_shared(); + triangle->set_points({ + {.x = 0.0F, .y = 0.0F}, + {.x = 14.0F, .y = 0.0F}, + {.x = 7.0F, .y = 12.0F} + }); + triangle->set_width(14.0F); + triangle->set_height(12.0F); + triangle->set_background_color(DocraftColor::fromColorName(ColorName::kGreen)); + root->add_child(triangle); + + auto polygon = std::make_shared(); + polygon->set_points({ + {.x = 0.0F, .y = 0.0F}, + {.x = 8.0F, .y = 0.0F}, + {.x = 12.0F, .y = 6.0F}, + {.x = 6.0F, .y = 12.0F}, + {.x = 0.0F, .y = 6.0F} + }); + polygon->set_width(12.0F); + polygon->set_height(12.0F); + polygon->set_background_color(DocraftColor::fromColorName(ColorName::kYellow)); + root->add_child(polygon); + + // Foreach with rendered children stress-path. + auto foreach_node = std::make_shared(); + foreach_node->set_model("${items}"); + for (int i = 0; i < 8; ++i) { + auto foreach_text = std::make_shared("Foreach child " + std::to_string(i)); + foreach_node->add_child(foreach_text); + } + root->add_child(foreach_node); + + // Vertical table with header titles and variable cells/colors. + auto vertical_table = std::make_shared(); + vertical_table->set_orientation(model::LayoutOrientation::kVertical); + vertical_table->set_content_cols(2); + vertical_table->set_column_weights({0.30F, 0.35F, 0.35F}); + vertical_table->set_auto_fill_width(true); + vertical_table->add_htitle_node( + std::make_shared("Metric"), + std::optional(DocraftColor::fromColorName(ColorName::kYellow))); + vertical_table->add_htitle_node( + std::make_shared("Value"), + std::optional(DocraftColor::fromColorName(ColorName::kGreen))); + + constexpr std::size_t kVerticalRows = 100; + for (std::size_t r = 0; r < kVerticalRows; ++r) { + auto vtitle = std::make_shared(std::format("VTitle {}", r)); + vtitle->set_height(7.0F + static_cast(r % 4)); + auto title_color = std::optional( + r % 2 == 0 + ? DocraftColor::fromColorName(ColorName::kPurple) + : DocraftColor::fromColorName(ColorName::kBlue)); + vertical_table->add_title_node(vtitle, title_color); + + for (int c = 0; c < 2; ++c) { + auto cell = std::make_shared(std::format("VR{}C{}", r, c)); + const auto h = 6.0F + static_cast((r + static_cast(c)) % 8); + cell->set_height(h); + auto bg = std::optional( + ((r + static_cast(c)) % 3 == 0) + ? DocraftColor::fromColorName(ColorName::kCyan) + : DocraftColor::fromColorName(ColorName::kRed)); + vertical_table->add_content_node(cell, bg); + } + } + + // Heavy horizontal table to trigger splits. + auto table = std::make_shared(); + table->set_titles({"C1", "C2", "C3"}); + table->set_column_weights({0.34F, 0.33F, 0.33F}); + table->set_auto_fill_width(true); + constexpr std::size_t kRows = 140; + for (std::size_t r = 0; r < kRows; ++r) { + for (int c = 0; c < 3; ++c) { + auto cell = std::make_shared(); + cell->set_weight(1.0F / 3.0F); + auto height = 6.0F + static_cast((r + static_cast(c)) % 9); + //height variation to increase fragmentation pressure + cell->set_text(std::format("Row {} Col {}: {}", r, c, height)); + cell->set_height(height); + //alternate colors + auto color = std::optional( + r % 2 == 0 + ? DocraftColor::fromColorName(ColorName::kPurple) + : DocraftColor::fromColorName(ColorName::kBlue)); + if (c % 2 == 0) { + color = std::optional(r % 2 == 0 + ? DocraftColor::fromColorName(ColorName::kRed) + : DocraftColor::fromColorName(ColorName::kCyan)); + } + table->add_content_node(cell, color); + } + } + body->add_child(root); + body->add_child(std::make_shared()); + body->add_child(vertical_table); + body->add_child(std::make_shared()); + body->add_child(table); + + // Add explicit page breaks intermixed with payload to stress owner propagation. + std::vector > payload_nodes; + for (int i = 0; i < 12; ++i) { + auto payload = std::make_shared(); + payload->set_height(10.0F + static_cast(i % 3)); + body->add_child(payload); + payload_nodes.push_back(payload); + payload->set_background_color(DocraftColor::fromColorName(ColorName::kCyan)); + body->add_child(std::make_shared()); + } + + std::vector > nodes{header, body, footer}; + layout_and_render(nodes); + + EXPECT_GE(total_page_count(), 8U); + EXPECT_EQ(header->page_owner(), -1); + EXPECT_EQ(footer->page_owner(), -1); + EXPECT_TRUE(std::isfinite(root->width())); + EXPECT_TRUE(std::isfinite(root->height())); + EXPECT_GT(root->width(), 0.0F); + EXPECT_GT(root->height(), 0.0F); + + int previous_owner = 1; + for (const auto &payload: payload_nodes) { + EXPECT_GE(payload->page_owner(), previous_owner); + previous_owner = payload->page_owner(); + } + + std::size_t horizontal_table_fragments = 0; + std::size_t horizontal_accumulated_rows = 0; + std::size_t vertical_table_fragments = 0; + std::size_t vertical_accumulated_rows = 0; + for (const auto &node: body->children()) { + auto table_fragment = std::dynamic_pointer_cast(node); + if (!table_fragment) { + continue; + } + if (table_fragment->orientation() == model::LayoutOrientation::kVertical) { + ++vertical_table_fragments; + const auto vertical_rows = table_fragment->title_nodes().size(); + vertical_accumulated_rows += vertical_rows; + EXPECT_GT(vertical_rows, 0U); + const std::size_t value_cols = static_cast(std::max(1, table_fragment->content_cols())); + EXPECT_EQ(table_fragment->content_backgrounds().size(), vertical_rows * value_cols); + } else { + ++horizontal_table_fragments; + horizontal_accumulated_rows += static_cast(table_fragment->rows()); + EXPECT_GT(table_fragment->rows(), 0); + } + } + + EXPECT_GE(horizontal_table_fragments, 2U); + EXPECT_EQ(horizontal_accumulated_rows, kRows); + EXPECT_GE(vertical_table_fragments, 1U); + EXPECT_EQ(vertical_accumulated_rows, kVerticalRows); + } +} // namespace docraft::test::layout diff --git a/docraft/test/docraft/layout/docraft_pagination_test.cc b/docraft/test/docraft/layout/docraft_pagination_test.cc index 0440629..3ac7537 100644 --- a/docraft/test/docraft/layout/docraft_pagination_test.cc +++ b/docraft/test/docraft/layout/docraft_pagination_test.cc @@ -25,7 +25,8 @@ namespace docraft::test::layout { .extension = ".pdf", .can_use_font = true }); - context_ = std::make_shared(backend_); + context_ = std::make_shared( + std::make_shared(backend_)); engine_ = std::make_unique(context_); } @@ -112,6 +113,22 @@ namespace docraft::test::layout { // Remainder table must be re-laid out at the top of the new page. EXPECT_FLOAT_EQ(second_table->position().y+10, body->position().y); // 10 is the padding of the body section (defaultì) - EXPECT_GE(second_table->anchors().bottom_left.y, body->anchors().bottom_left.y); + } + + TEST_F(DocraftPaginationTest, OversizedNodeAtPageTopDoesNotCreateExtraPage) { + auto body = std::make_shared(); + body->set_margin_left(0.0F); + body->set_margin_right(0.0F); + + auto oversized_rect = std::make_shared(); + oversized_rect->set_height(300.0F); // Intentionally larger than body content area. + body->add_child(oversized_rect); + + std::vector > nodes{body}; + engine_->compute_document_layout(nodes); + + // Even if the node overflows, it already started from a fresh page top: no blank-page churn. + EXPECT_EQ(backend_->total_page_count(), 1U); + EXPECT_EQ(oversized_rect->page_owner(), 1); } } // namespace docraft::test::layout diff --git a/docraft/test/docraft/model/docraft_clone_test.cc b/docraft/test/docraft/model/docraft_clone_test.cc index 9643520..4855746 100644 --- a/docraft/test/docraft/model/docraft_clone_test.cc +++ b/docraft/test/docraft/model/docraft_clone_test.cc @@ -139,8 +139,8 @@ namespace docraft::test::model { EXPECT_EQ(cloned->row_weights(), table->row_weights()); EXPECT_FLOAT_EQ(cloned->baseline_offset(), table->baseline_offset()); - const auto &original_titles = table->title_nodes(); - const auto &cloned_titles = cloned->title_nodes(); + const auto &original_titles = table->title_text_nodes(); + const auto &cloned_titles = cloned->title_text_nodes(); ASSERT_EQ(cloned_titles.size(), original_titles.size()); EXPECT_NE(cloned_titles[0].get(), original_titles[0].get()); EXPECT_EQ(cloned_titles[0]->text(), original_titles[0]->text()); diff --git a/docraft/test/docraft/model/docraft_node_test.cc b/docraft/test/docraft/model/docraft_node_test.cc index 81b11e7..c916c91 100644 --- a/docraft/test/docraft/model/docraft_node_test.cc +++ b/docraft/test/docraft/model/docraft_node_test.cc @@ -17,7 +17,8 @@ namespace docraft::test::model { .extension = ".pdf", .can_use_font = true }); - context_ = std::make_shared(backend_); + context_ = std::make_shared( + std::make_shared(backend_)); } std::shared_ptr backend_; diff --git a/docraft/test/docraft/renderer/docraft_image_painter_test.cc b/docraft/test/docraft/renderer/docraft_image_painter_test.cc index 2917839..c27922e 100644 --- a/docraft/test/docraft/renderer/docraft_image_painter_test.cc +++ b/docraft/test/docraft/renderer/docraft_image_painter_test.cc @@ -3,6 +3,7 @@ #include #include "docraft/docraft_document_context.h" +#include "docraft/exception/docraft_exceptions.h" #include "docraft/model/docraft_image.h" #include "docraft/renderer/painter/docraft_image_painter.h" @@ -32,5 +33,5 @@ TEST(DocraftImagePainter, ThrowsOnEmptyRawData) { image.set_raw_data({}, 1, 1); renderer::painter::DocraftImagePainter painter(image); - EXPECT_THROW(painter.draw(context), std::runtime_error); + EXPECT_THROW(painter.draw(context), docraft::exception::InvalidInputException); } diff --git a/docraft/test/docraft/renderer/docraft_painter_smoke_test.cc b/docraft/test/docraft/renderer/docraft_painter_smoke_test.cc index bf9f2dc..a21445c 100644 --- a/docraft/test/docraft/renderer/docraft_painter_smoke_test.cc +++ b/docraft/test/docraft/renderer/docraft_painter_smoke_test.cc @@ -95,7 +95,7 @@ TEST(DocraftPolygonPainter, DrawsBasicPolygon) { TEST(DocraftTextPainter, DrawsSingleLine) { auto context = std::make_shared(); - context->set_font_applier(std::make_shared(context)); + context->edit_typography().set_font_applier(std::make_shared(context)); model::DocraftText text("Hello"); text.set_font_name("Helvetica"); diff --git a/docraft/test/docraft/renderer/docraft_section_margin_test.cc b/docraft/test/docraft/renderer/docraft_section_margin_test.cc index b203adb..f286581 100644 --- a/docraft/test/docraft/renderer/docraft_section_margin_test.cc +++ b/docraft/test/docraft/renderer/docraft_section_margin_test.cc @@ -18,7 +18,8 @@ namespace docraft::test::renderer { .extension = ".pdf", .can_use_font = true }); - auto context = std::make_shared(backend); + auto context = std::make_shared( + std::make_shared(backend)); context->set_renderer(std::make_shared(context)); model::DocraftHeader header; @@ -34,6 +35,6 @@ namespace docraft::test::renderer { header.draw(context); - EXPECT_EQ(backend->line_count, 4); + EXPECT_EQ(backend->line_count(), 4); } } // namespace docraft::test::renderer diff --git a/docraft/test/docraft/templating/docraft_template_engine_test.cc b/docraft/test/docraft/templating/docraft_template_engine_test.cc index f1b2657..e0702ff 100644 --- a/docraft/test/docraft/templating/docraft_template_engine_test.cc +++ b/docraft/test/docraft/templating/docraft_template_engine_test.cc @@ -1,6 +1,7 @@ #include #include "docraft/docraft_document.h" +#include "docraft/exception/docraft_exceptions.h" #include "docraft/model/docraft_foreach.h" #include "docraft/model/docraft_list.h" #include "docraft/model/docraft_table.h" @@ -34,13 +35,14 @@ namespace docraft::test::templating { TEST_F(DocraftTemplateEngineTest, AddDuplicateVariableThrows) { engine_.add_template_variable("title", "Docraft"); - EXPECT_THROW(engine_.add_template_variable("title", "Other"), std::runtime_error); + EXPECT_THROW(engine_.add_template_variable("title", "Other"), + docraft::exception::TemplateVariableExistsException); EXPECT_EQ(engine_.items(), 1); EXPECT_EQ(engine_.find_template_variable("title"), "Docraft"); } TEST_F(DocraftTemplateEngineTest, GetMissingVariableThrows) { - EXPECT_THROW(engine_.find_template_variable("missing"), std::runtime_error); + EXPECT_THROW(engine_.find_template_variable("missing"), docraft::exception::TemplateVariableNotFoundException); } TEST_F(DocraftTemplateEngineTest, RemoveVariable) { @@ -52,7 +54,8 @@ namespace docraft::test::templating { } TEST_F(DocraftTemplateEngineTest, RemoveMissingVariableThrows) { - EXPECT_THROW(engine_.remove_template_variable("missing"), std::runtime_error); + EXPECT_THROW(engine_.remove_template_variable("missing"), + docraft::exception::TemplateVariableNotFoundException); } TEST_F(DocraftTemplateEngineTest, ClearVariables) { @@ -70,7 +73,7 @@ namespace docraft::test::templating { auto template_engine = std::make_shared(); template_engine->add_template_variable("title", "Docraft"); template_engine->add_template_variable("value", "42"); - document.set_document_template_engine(template_engine); + document.edit_config().set_document_template_engine(template_engine); auto table = std::make_shared(); table->set_cols(1); @@ -82,7 +85,7 @@ namespace docraft::test::templating { document.template_document(); - EXPECT_EQ(table->title_nodes()[0]->text(), "Docraft"); + EXPECT_EQ(table->title_text_nodes()[0]->text(), "Docraft"); auto rows = table->content_nodes(); auto cell_text = std::dynamic_pointer_cast(rows[0][0]); EXPECT_EQ(cell_text->text(), "42"); @@ -92,7 +95,7 @@ namespace docraft::test::templating { docraft::DocraftDocument document("Test Document"); auto template_engine = std::make_shared(); template_engine->add_template_variable("item", "Alpha"); - document.set_document_template_engine(template_engine); + document.edit_config().set_document_template_engine(template_engine); auto list = std::make_shared(); list->add_child(std::make_shared("${item}")); @@ -111,8 +114,8 @@ namespace docraft::test::templating { docraft::DocraftDocument document("Test Document"); auto template_engine = std::make_shared(); template_engine->add_template_variable("title", "Docraft"); - document.set_document_template_engine(template_engine); - EXPECT_EQ(document.document_template_engine()->find_template_variable("title"), "Docraft"); + document.edit_config().set_document_template_engine(template_engine); + EXPECT_EQ(document.config().document_template_engine()->find_template_variable("title"), "Docraft"); //text std::shared_ptr text_node1 = std::make_shared("${title} is a great library!"); document.add_node(text_node1); @@ -132,7 +135,7 @@ namespace docraft::test::templating { template_engine->add_template_variable( "employees", R"([{"name":"Alice","tasks":[{"task":"Plan"},{"task":"Review"}]},{"name":"Bob","tasks":[{"task":"Ship"}]}])"); - document.set_document_template_engine(template_engine); + document.edit_config().set_document_template_engine(template_engine); auto outer = std::make_shared(); outer->set_model("${employees}"); diff --git a/docraft/test/docraft/utils/docraft_mock_rendering_backend.h b/docraft/test/docraft/utils/docraft_mock_rendering_backend.h index 53d7ca9..731776b 100644 --- a/docraft/test/docraft/utils/docraft_mock_rendering_backend.h +++ b/docraft/test/docraft/utils/docraft_mock_rendering_backend.h @@ -1,14 +1,17 @@ #pragma once -#include +#include #include +#include +#include #include +#include "docraft/backend/docraft_backend_providers_factory.h" #include "docraft/backend/docraft_rendering_backend.h" +#include "docraft/exception/docraft_exceptions.h" namespace docraft::test::utils { - class MockRenderingBackend : public backend::IDocraftRenderingBackend { - public: + struct MockBackendSharedState { struct Config { float page_width = 100.0F; float page_height = 100.0F; @@ -16,116 +19,484 @@ namespace docraft::test::utils { std::size_t initial_pages = 1; std::string extension = ".pdf"; bool can_use_font = true; + bool supports_line_backend = true; + bool supports_text_backend = true; + bool supports_shape_backend = true; + bool supports_image_backend = true; + bool supports_page_backend = true; + bool supports_output_backend = true; + bool supports_font_backend = true; + bool supports_metadata_backend = true; + bool strict_page_lifecycle = true; + bool require_text_scope = false; }; - MockRenderingBackend() : MockRenderingBackend(Config{}) {} + explicit MockBackendSharedState(Config cfg) + : config(std::move(cfg)) { + pages = config.initial_pages > 0 ? config.initial_pages : 1; + } + + static void ensure_supported(bool supported, const char *message) { + if (!supported) { + throw docraft::exception::BackendStateException(message); + } + } + + void ensure_page_available() const { + if (!config.strict_page_lifecycle) { + return; + } + ensure_supported(config.supports_page_backend, "Page backend capability not supported"); + if (pages == 0 || current_page >= pages) { + throw docraft::exception::BackendStateException("No valid current page"); + } + } + + void ensure_text_scope_if_required() const { + if (config.require_text_scope && !text_scope_active) { + throw docraft::exception::BackendStateException("Text scope required before drawing text"); + } + } + + Config config; + std::size_t pages = 1; + std::size_t current_page = 0; + mutable bool text_scope_active = false; + mutable int line_count = 0; + mutable std::string last_saved_path; + + // Simple hash set to track registered fonts by their internal names. + struct StringHash { + using is_transparent = void; // Enables heterogeneous operations. + std::size_t operator()(std::string_view sv) const { + std::hash hasher; + return hasher(sv); + } + }; + + std::unordered_set registered_fonts; + }; + + class MockLineBackend final : public backend::IDocraftLineRenderingBackend { + public: + explicit MockLineBackend(std::shared_ptr state) : state_(std::move(state)) { + } + + void set_stroke_color(float, float, float) const override { + set_line_width(1.0F); // Reuse checks from set_line_width. + } + + void set_line_width(float) const override { + MockBackendSharedState::ensure_supported(state_->config.supports_line_backend, + "Line backend capability not supported"); + state_->ensure_page_available(); + } + + void draw_line(float, float, float, float) const override { + MockBackendSharedState::ensure_supported(state_->config.supports_line_backend, + "Line backend capability not supported"); + state_->ensure_page_available(); + ++state_->line_count; + } + + private: + std::shared_ptr state_; + }; + + class MockTextBackend final : public backend::IDocraftTextRenderingBackend { + public: + explicit MockTextBackend(std::shared_ptr state) : state_(std::move(state)) { + } - explicit MockRenderingBackend(Config config) : config_(std::move(config)) { - pages_ = config_.initial_pages > 0 ? config_.initial_pages : 1; - current_page_ = 0; + void begin_text() const override { + MockBackendSharedState::ensure_supported(state_->config.supports_text_backend, "Text backend capability not supported"); + state_->ensure_page_available(); + if (state_->text_scope_active) { + throw docraft::exception::BackendStateException("Text scope already active"); + } + state_->text_scope_active = true; + } + + void end_text() const override { + MockBackendSharedState::ensure_supported(state_->config.supports_text_backend, "Text backend capability not supported"); + if (!state_->text_scope_active) { + throw docraft::exception::BackendStateException("Text scope not active"); + } + state_->text_scope_active = false; + } + + void draw_text(const std::string &, float, float) const override { + MockBackendSharedState::ensure_supported(state_->config.supports_text_backend, "Text backend capability not supported"); + state_->ensure_page_available(); + state_->ensure_text_scope_if_required(); + } + + void set_text_color(float, float, float) const override { + MockBackendSharedState::ensure_supported(state_->config.supports_text_backend, "Text backend capability not supported"); + state_->ensure_page_available(); + } + + void draw_text_matrix(const std::string &, float, float, float, float, float, float) const override { + draw_text("", 0.0F, 0.0F); // Reuse checks from draw_text. } - void begin_text() const override {} - void end_text() const override {} - void draw_text(const std::string &, float, float) const override {} - void set_text_color(float, float, float) const override {} - void draw_text_matrix(const std::string &, float, float, float, float, float, float) const override {} float measure_text_width(const std::string &text) const override { - return static_cast(text.size()) * config_.text_width_factor; - } - - void set_stroke_color(float, float, float) const override {} - void set_line_width(float) const override {} - void draw_line(float, float, float, float) const override { ++line_count; } - - void save_state() const override {} - void restore_state() const override {} - void set_fill_color(float, float, float) const override {} - void set_fill_alpha(float) const override {} - void set_stroke_alpha(float) const override {} - void draw_rectangle(float, float, float, float) const override {} - void draw_circle(float, float, float) const override {} - void draw_polygon(const std::vector &) const override {} - void fill() const override {} - void stroke() const override {} - void fill_stroke() const override {} - - void draw_png_image(const std::string &, float, float, float, float) const override {} - void draw_png_image_from_memory(const unsigned char *, std::size_t, float, float, float, float) const override {} - void draw_jpeg_image(const std::string &, float, float, float, float) const override {} - void draw_jpeg_image_from_memory(const unsigned char *, std::size_t, float, float, float, float) const override {} - void draw_raw_rgb_image(const std::string &, int, int, float, float, float, float) const override {} - void draw_raw_rgb_image_from_memory(const unsigned char *, int, int, float, float, float, float) const override {} - - void save_to_file(const std::string &path) const override { last_saved_path_ = path; } - [[nodiscard]] std::string file_extension() const override { return config_.extension; } - const char *register_ttf_font_from_file(const std::string &, bool) const override { return "Helvetica"; } - bool can_use_font(const std::string &, const char *) const override { return config_.can_use_font; } - void set_font(const std::string &, float, const char *) const override {} - void set_document_metadata(const DocraftDocumentMetadata &) override {} - - float page_width() const override { return config_.page_width; } - float page_height() const override { return config_.page_height; } + MockBackendSharedState::ensure_supported(state_->config.supports_text_backend, "Text backend capability not supported"); + return static_cast(text.size()) * state_->config.text_width_factor; + } + + private: + std::shared_ptr state_; + }; + + class MockShapeBackend final : public backend::IDocraftShapeRenderingBackend { + public: + explicit MockShapeBackend(std::shared_ptr state) : state_(std::move(state)) { + } + + void save_state() const override { require(); } + void restore_state() const override { require(); } + void set_fill_color(float, float, float) const override { require(); } + void set_fill_alpha(float) const override { require(); } + void set_stroke_alpha(float) const override { require(); } + void draw_rectangle(float, float, float, float) const override { require(); } + void draw_circle(float, float, float) const override { require(); } + void draw_polygon(const std::vector &) const override { require(); } + void fill() const override { require(); } + void stroke() const override { require(); } + void fill_stroke() const override { require(); } + + private: + void require() const { + MockBackendSharedState::ensure_supported(state_->config.supports_shape_backend, "Shape backend capability not supported"); + state_->ensure_page_available(); + } + + std::shared_ptr state_; + }; + + class MockImageBackend final : public backend::IDocraftImageRenderingBackend { + public: + explicit MockImageBackend(std::shared_ptr state) : state_(std::move(state)) { + } + + void draw_png_image(const std::string &, float, float, float, float) const override { require(); } + + void draw_png_image_from_memory(const unsigned char *, std::size_t, float, float, float, float) const override { + require(); + } + + void draw_jpeg_image(const std::string &, float, float, float, float) const override { require(); } + + void draw_jpeg_image_from_memory(const unsigned char *, std::size_t, float, float, float, + float) const override { + require(); + } + + void draw_raw_rgb_image(const std::string &, int, int, float, float, float, float) const override { require(); } + + void draw_raw_rgb_image_from_memory(const unsigned char *, int, int, float, float, float, + float) const override { + require(); + } + + private: + void require() const { + MockBackendSharedState::ensure_supported(state_->config.supports_image_backend, "Image backend capability not supported"); + state_->ensure_page_available(); + } + + std::shared_ptr state_; + }; + + class MockPageBackend final : public backend::IDocraftPageRenderingBackend { + public: + explicit MockPageBackend(std::shared_ptr state) : state_(std::move(state)) { + } + + float page_width() const override { + MockBackendSharedState::ensure_supported(state_->config.supports_page_backend, "Page backend capability not supported"); + return state_->config.page_width; + } + + float page_height() const override { + MockBackendSharedState::ensure_supported(state_->config.supports_page_backend, "Page backend capability not supported"); + return state_->config.page_height; + } void add_new_page() override { - ++pages_; - current_page_ = pages_ - 1; + MockBackendSharedState::ensure_supported(state_->config.supports_page_backend, "Page backend capability not supported"); + ++state_->pages; + state_->current_page = state_->pages - 1; } void move_to_next_page() override { - if (current_page_ + 1 >= pages_) { - throw std::runtime_error("Already at the last page"); + MockBackendSharedState::ensure_supported(state_->config.supports_page_backend, "Page backend capability not supported"); + if (state_->current_page + 1 >= state_->pages) { + throw docraft::exception::BackendStateException("Already at the last page"); } - ++current_page_; + ++state_->current_page; } void go_to_page(std::size_t page_number) override { - if (page_number >= pages_) { - throw std::runtime_error("Invalid page number"); + MockBackendSharedState::ensure_supported(state_->config.supports_page_backend, "Page backend capability not supported"); + if (page_number >= state_->pages) { + throw docraft::exception::BackendStateException("Invalid page number"); } - current_page_ = page_number; + state_->current_page = page_number; } void go_to_first_page() override { - if (pages_ == 0) { - throw std::runtime_error("No pages"); + MockBackendSharedState::ensure_supported(state_->config.supports_page_backend, "Page backend capability not supported"); + if (state_->pages == 0) { + throw docraft::exception::BackendStateException("No pages"); } - current_page_ = 0; + state_->current_page = 0; } void go_to_previous_page() override { - if (current_page_ == 0) { - throw std::runtime_error("Already at first page"); + MockBackendSharedState::ensure_supported(state_->config.supports_page_backend, "Page backend capability not supported"); + if (state_->current_page == 0) { + throw docraft::exception::BackendStateException("Already at first page"); } - --current_page_; + --state_->current_page; } void go_to_last_page() override { - if (pages_ == 0) { - throw std::runtime_error("No pages"); + MockBackendSharedState::ensure_supported(state_->config.supports_page_backend, "Page backend capability not supported"); + if (state_->pages == 0) { + throw docraft::exception::BackendStateException("No pages"); } - current_page_ = pages_ - 1; + state_->current_page = state_->pages - 1; + } + + void set_page_format(model::DocraftPageSize, model::DocraftPageOrientation) override { + MockBackendSharedState::ensure_supported(state_->config.supports_page_backend, "Page backend capability not supported"); } - void set_page_format(model::DocraftPageSize, model::DocraftPageOrientation) override {} + std::size_t current_page_number() const override { + MockBackendSharedState::ensure_supported(state_->config.supports_page_backend, "Page backend capability not supported"); + return state_->current_page + 1; + } + + std::size_t total_page_count() const override { + MockBackendSharedState::ensure_supported(state_->config.supports_page_backend, "Page backend capability not supported"); + return state_->pages; + } - std::size_t current_page_number() const override { return current_page_ + 1; } - std::size_t total_page_count() const override { return pages_; } + private: + std::shared_ptr state_; + }; + + class MockOutputBackend final : public backend::IDocraftOutputBackend { + public: + explicit MockOutputBackend(std::shared_ptr state) : state_(std::move(state)) { + } + + void save_to_file(const std::string &path) const override { + MockBackendSharedState::ensure_supported(state_->config.supports_output_backend, "Output backend capability not supported"); + state_->last_saved_path = path; + } + + [[nodiscard]] std::string file_extension() const override { + MockBackendSharedState::ensure_supported(state_->config.supports_output_backend, "Output backend capability not supported"); + return state_->config.extension; + } + + private: + std::shared_ptr state_; + }; + + class MockFontBackend final : public backend::IDocraftFontBackend { + public: + explicit MockFontBackend(std::shared_ptr state) : state_(std::move(state)) { + } + + const char *register_ttf_font_from_file(const std::string &path, bool) const override { + if (!state_->config.supports_font_backend || path.empty()) { + return nullptr; + } + const auto [it, inserted] = state_->registered_fonts.insert(path); + (void) inserted; + return it->c_str(); + } + + bool can_use_font(const std::string &internal_name, const char *) const override { + if (!state_->config.supports_font_backend || !state_->config.can_use_font) { + return false; + } + if (internal_name == "Helvetica") { + return true; + } + return state_->registered_fonts.contains(internal_name); + } + + void set_font(const std::string &, float, const char *) const override { + MockBackendSharedState::ensure_supported(state_->config.supports_font_backend, "Font backend capability not supported"); + } + + private: + std::shared_ptr state_; + }; + + class MockMetadataBackend final : public backend::IDocraftMetadataBackend { + public: + explicit MockMetadataBackend(std::shared_ptr state) : state_(std::move(state)) { + } + + void set_document_metadata(const DocraftDocumentMetadata &) override { + MockBackendSharedState::ensure_supported(state_->config.supports_metadata_backend, + "Metadata backend capability not supported"); + } + + private: + std::shared_ptr state_; + }; + + class MockRenderingBackend : public backend::IDocraftRenderingCapabilityProvider, + public backend::IDocraftResourceCapabilityProvider, + public backend::IDocraftLifecycleCapabilityProvider { + public: + using Config = MockBackendSharedState::Config; + + MockRenderingBackend() : MockRenderingBackend(Config{}) { + } + + explicit MockRenderingBackend(Config config) + : state_(std::make_shared(std::move(config))), + line_backend_(std::make_unique(state_)), + text_backend_(std::make_unique(state_)), + shape_backend_(std::make_unique(state_)), + image_backend_(std::make_unique(state_)), + page_backend_(std::make_unique(state_)), + output_backend_(std::make_unique(state_)), + font_backend_(std::make_unique(state_)), + metadata_backend_(std::make_unique(state_)) { + } + + [[nodiscard]] const backend::IDocraftLineRenderingBackend *line_rendering() const override { + return state_->config.supports_line_backend ? line_backend_.get() : nullptr; + } + + [[nodiscard]] backend::IDocraftLineRenderingBackend *edit_line_rendering() override { + return state_->config.supports_line_backend ? line_backend_.get() : nullptr; + } + + [[nodiscard]] const backend::IDocraftTextRenderingBackend *text_rendering() const override { + return state_->config.supports_text_backend ? text_backend_.get() : nullptr; + } + + [[nodiscard]] backend::IDocraftTextRenderingBackend *edit_text_rendering() override { + return state_->config.supports_text_backend ? text_backend_.get() : nullptr; + } + + [[nodiscard]] const backend::IDocraftShapeRenderingBackend *shape_rendering() const override { + return state_->config.supports_shape_backend ? shape_backend_.get() : nullptr; + } + + [[nodiscard]] backend::IDocraftShapeRenderingBackend *edit_shape_rendering() override { + return state_->config.supports_shape_backend ? shape_backend_.get() : nullptr; + } + + [[nodiscard]] const backend::IDocraftImageRenderingBackend *image_rendering() const override { + return state_->config.supports_image_backend ? image_backend_.get() : nullptr; + } + + [[nodiscard]] backend::IDocraftImageRenderingBackend *edit_image_rendering() override { + return state_->config.supports_image_backend ? image_backend_.get() : nullptr; + } + + [[nodiscard]] const backend::IDocraftPageRenderingBackend *page_rendering() const override { + return state_->config.supports_page_backend ? page_backend_.get() : nullptr; + } + + [[nodiscard]] backend::IDocraftPageRenderingBackend *edit_page_rendering() override { + return state_->config.supports_page_backend ? page_backend_.get() : nullptr; + } + + [[nodiscard]] const backend::IDocraftOutputBackend *output_backend() const override { + return state_->config.supports_output_backend ? output_backend_.get() : nullptr; + } + + [[nodiscard]] backend::IDocraftOutputBackend *edit_output_backend() override { + return state_->config.supports_output_backend ? output_backend_.get() : nullptr; + } + + [[nodiscard]] const backend::IDocraftFontBackend *font_backend() const override { + return state_->config.supports_font_backend ? font_backend_.get() : nullptr; + } + + [[nodiscard]] backend::IDocraftFontBackend *edit_font_backend() override { + return state_->config.supports_font_backend ? font_backend_.get() : nullptr; + } + + [[nodiscard]] const backend::IDocraftMetadataBackend *metadata_backend() const override { + return state_->config.supports_metadata_backend ? metadata_backend_.get() : nullptr; + } + + [[nodiscard]] backend::IDocraftMetadataBackend *edit_metadata_backend() override { + return state_->config.supports_metadata_backend ? metadata_backend_.get() : nullptr; + } void set_current_page(std::size_t one_based_page_number) { - current_page_ = one_based_page_number > 0 ? one_based_page_number - 1 : 0; - if (current_page_ >= pages_) { - current_page_ = pages_ - 1; + MockBackendSharedState::ensure_supported(state_->config.supports_page_backend, "Page backend capability not supported"); + state_->current_page = one_based_page_number > 0 ? one_based_page_number - 1 : 0; + if (state_->current_page >= state_->pages) { + state_->current_page = state_->pages - 1; } } - mutable int line_count = 0; - [[nodiscard]] const std::string &last_saved_path() const { return last_saved_path_; } + std::size_t total_page_count() const { + return page_backend_->total_page_count(); + } + + std::size_t current_page_number() const { + return page_backend_->current_page_number(); + } + + void begin_text() const { + text_backend_->begin_text(); + } + + void draw_text(const std::string &text, float x, float y) const { + text_backend_->draw_text(text, x, y); + } + + void end_text() const { + text_backend_->end_text(); + } + + [[nodiscard]] int line_count() const { + return state_->line_count; + } + + [[nodiscard]] const std::string &last_saved_path() const { + return state_->last_saved_path; + } + + private: + std::shared_ptr state_; + std::unique_ptr line_backend_; + std::unique_ptr text_backend_; + std::unique_ptr shape_backend_; + std::unique_ptr image_backend_; + std::unique_ptr page_backend_; + std::unique_ptr output_backend_; + std::unique_ptr font_backend_; + std::unique_ptr metadata_backend_; + }; + + class MockBackendProvidersFactory final : public backend::IDocraftBackendProvidersFactory { + public: + explicit MockBackendProvidersFactory(std::shared_ptr backend) + : backend_(std::move(backend)) { + } + + [[nodiscard]] backend::DocraftCapabilityProviders create_capability_providers() const override { + return {backend_, backend_, backend_}; + } private: - Config config_; - std::size_t pages_ = 1; - std::size_t current_page_ = 0; - mutable std::string last_saved_path_; + std::shared_ptr backend_; }; } // namespace docraft::test::utils