From d85c3a3e438eb73923765840a91207b18e0bd8e8 Mon Sep 17 00:00:00 2001 From: saqib mir Date: Mon, 3 Aug 2026 11:26:44 +0530 Subject: [PATCH] fix --- engine/CMakeLists.txt | 10 ++ engine/include/pdfengine/geometry/matrix.hpp | 64 +++++++ engine/include/pdfengine/geometry/quad.hpp | 44 +++++ engine/include/pdfengine/geometry/rect.hpp | 83 +++++++++ engine/include/pdfengine/layout_arena.hpp | 46 +++++ engine/include/pdfengine/layout_engine.hpp | 23 +++ engine/include/pdfengine/layout_pass.hpp | 15 ++ engine/include/pdfengine/layout_session.hpp | 60 +++++++ engine/include/pdfengine/layout_tree.hpp | 96 +++++++++++ engine/include/pdfengine/layout_types.hpp | 106 ++++++++++++ .../include/pdfengine/multi_level_cache.hpp | 61 +++++++ engine/include/pdfengine/pass_registry.hpp | 25 +++ engine/include/pdfengine/spatial_index.hpp | 35 ++++ engine/src/layout/layout_arena.cpp | 73 ++++++++ engine/src/layout/layout_engine.cpp | 47 ++++++ engine/src/layout/layout_session.cpp | 48 ++++++ engine/src/layout/multi_level_cache.cpp | 85 ++++++++++ engine/src/layout/pass_registry.cpp | 34 ++++ .../layout/passes/column_detection_pass.cpp | 10 ++ .../layout/passes/column_detection_pass.hpp | 13 ++ .../src/layout/passes/line_detection_pass.cpp | 13 ++ .../src/layout/passes/line_detection_pass.hpp | 13 ++ .../passes/paragraph_detection_pass.cpp | 10 ++ .../passes/paragraph_detection_pass.hpp | 13 ++ .../layout/passes/region_detection_pass.cpp | 10 ++ .../layout/passes/region_detection_pass.hpp | 13 ++ engine/src/layout/spatial_index.cpp | 48 ++++++ engine/tests/CMakeLists.txt | 1 + engine/tests/layout_engine_test.cpp | 87 ++++++++++ frontend/src/lib/gatewayService.ts | 52 ++++++ frontend/src/viewer/LayoutBlockLayer.tsx | 106 ++++++++++++ frontend/src/viewer/PDFViewer.tsx | 15 +- gateway/app/main.py | 4 +- gateway/app/routers/layout.py | 159 ++++++++++++++++++ gateway/tests/test_layout.py | 10 ++ 35 files changed, 1529 insertions(+), 3 deletions(-) create mode 100644 engine/include/pdfengine/geometry/matrix.hpp create mode 100644 engine/include/pdfengine/geometry/quad.hpp create mode 100644 engine/include/pdfengine/geometry/rect.hpp create mode 100644 engine/include/pdfengine/layout_arena.hpp create mode 100644 engine/include/pdfengine/layout_engine.hpp create mode 100644 engine/include/pdfengine/layout_pass.hpp create mode 100644 engine/include/pdfengine/layout_session.hpp create mode 100644 engine/include/pdfengine/layout_tree.hpp create mode 100644 engine/include/pdfengine/layout_types.hpp create mode 100644 engine/include/pdfengine/multi_level_cache.hpp create mode 100644 engine/include/pdfengine/pass_registry.hpp create mode 100644 engine/include/pdfengine/spatial_index.hpp create mode 100644 engine/src/layout/layout_arena.cpp create mode 100644 engine/src/layout/layout_engine.cpp create mode 100644 engine/src/layout/layout_session.cpp create mode 100644 engine/src/layout/multi_level_cache.cpp create mode 100644 engine/src/layout/pass_registry.cpp create mode 100644 engine/src/layout/passes/column_detection_pass.cpp create mode 100644 engine/src/layout/passes/column_detection_pass.hpp create mode 100644 engine/src/layout/passes/line_detection_pass.cpp create mode 100644 engine/src/layout/passes/line_detection_pass.hpp create mode 100644 engine/src/layout/passes/paragraph_detection_pass.cpp create mode 100644 engine/src/layout/passes/paragraph_detection_pass.hpp create mode 100644 engine/src/layout/passes/region_detection_pass.cpp create mode 100644 engine/src/layout/passes/region_detection_pass.hpp create mode 100644 engine/src/layout/spatial_index.cpp create mode 100644 engine/tests/layout_engine_test.cpp create mode 100644 frontend/src/viewer/LayoutBlockLayer.tsx create mode 100644 gateway/app/routers/layout.py create mode 100644 gateway/tests/test_layout.py diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index 04bdfb8..28562da 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -64,6 +64,16 @@ add_library(pdfengine OBJECT src/image/decoder/mask_processor.cpp src/image/decoder/image_decoder_factory.cpp src/ocr/ocr_cache.cpp + src/layout/layout_arena.cpp + src/layout/spatial_index.cpp + src/layout/multi_level_cache.cpp + src/layout/layout_session.cpp + src/layout/pass_registry.cpp + src/layout/layout_engine.cpp + src/layout/passes/line_detection_pass.cpp + src/layout/passes/paragraph_detection_pass.cpp + src/layout/passes/column_detection_pass.cpp + src/layout/passes/region_detection_pass.cpp src/parser/lexer.cpp src/parser/parser.cpp src/parser/content_builder.cpp diff --git a/engine/include/pdfengine/geometry/matrix.hpp b/engine/include/pdfengine/geometry/matrix.hpp new file mode 100644 index 0000000..232d16f --- /dev/null +++ b/engine/include/pdfengine/geometry/matrix.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include +#include + +namespace pdfengine { +namespace geometry { + +// 2D Affine Transformation Matrix [a b c d e f] +// [ x' ] [ a c e ] [ x ] +// [ y' ] = [ b d f ] [ y ] +// [ 1 ] [ 0 0 1 ] [ 1 ] +struct Matrix { + float a = 1.0f; // Scale X + float b = 0.0f; // Shear Y + float c = 0.0f; // Shear X + float d = 1.0f; // Scale Y + float e = 0.0f; // Translate X + float f = 0.0f; // Translate Y + + constexpr Matrix() noexcept = default; + constexpr Matrix(float aVal, float bVal, float cVal, float dVal, float eVal, float fVal) noexcept + : a(aVal), b(bVal), c(cVal), d(dVal), e(eVal), f(fVal) {} + + static constexpr Matrix identity() noexcept { + return Matrix(1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f); + } + + static constexpr Matrix translation(float tx, float ty) noexcept { + return Matrix(1.0f, 0.0f, 0.0f, 1.0f, tx, ty); + } + + static constexpr Matrix scale(float sx, float sy) noexcept { + return Matrix(sx, 0.0f, 0.0f, sy, 0.0f, 0.0f); + } + + static Matrix rotation(float radians) noexcept { + float cosA = std::cos(radians); + float sinA = std::sin(radians); + return Matrix(cosA, sinA, -sinA, cosA, 0.0f, 0.0f); + } + + [[nodiscard]] Point transformPoint(const Point& pt) const noexcept { + return Point(a * pt.x + c * pt.y + e, b * pt.x + d * pt.y + f); + } + + [[nodiscard]] Matrix multiply(const Matrix& other) const noexcept { + return Matrix( + a * other.a + c * other.b, + b * other.a + d * other.b, + a * other.c + c * other.d, + b * other.c + d * other.d, + a * other.e + c * other.f + e, + b * other.e + d * other.f + f + ); + } + + [[nodiscard]] float getRotationAngle() const noexcept { + return std::atan2(b, a); + } +}; + +} // namespace geometry +} // namespace pdfengine diff --git a/engine/include/pdfengine/geometry/quad.hpp b/engine/include/pdfengine/geometry/quad.hpp new file mode 100644 index 0000000..4152d29 --- /dev/null +++ b/engine/include/pdfengine/geometry/quad.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include +#include + +namespace pdfengine { +namespace geometry { + +struct Quad { + Point p1; // Top-Left + Point p2; // Top-Right + Point p3; // Bottom-Right + Point p4; // Bottom-Left + + constexpr Quad() noexcept = default; + constexpr Quad(Point pt1, Point pt2, Point pt3, Point pt4) noexcept + : p1(pt1), p2(pt2), p3(pt3), p4(pt4) {} + + explicit Quad(const Rect& rect) noexcept + : p1(rect.left(), rect.top()), + p2(rect.right(), rect.top()), + p3(rect.right(), rect.bottom()), + p4(rect.left(), rect.bottom()) {} + + [[nodiscard]] Rect boundingBox() const noexcept { + float l = std::min({p1.x, p2.x, p3.x, p4.x}); + float r = std::max({p1.x, p2.x, p3.x, p4.x}); + float t = std::min({p1.y, p2.y, p3.y, p4.y}); + float b = std::max({p1.y, p2.y, p3.y, p4.y}); + return Rect(l, t, r - l, b - t); + } + + [[nodiscard]] Quad transform(const Matrix& m) const noexcept { + return Quad( + m.transformPoint(p1), + m.transformPoint(p2), + m.transformPoint(p3), + m.transformPoint(p4) + ); + } +}; + +} // namespace geometry +} // namespace pdfengine diff --git a/engine/include/pdfengine/geometry/rect.hpp b/engine/include/pdfengine/geometry/rect.hpp new file mode 100644 index 0000000..60f898e --- /dev/null +++ b/engine/include/pdfengine/geometry/rect.hpp @@ -0,0 +1,83 @@ +#pragma once + +#include +#include + +namespace pdfengine { +namespace geometry { + +struct Point { + float x = 0.0f; + float y = 0.0f; + + constexpr Point() noexcept = default; + constexpr Point(float xVal, float yVal) noexcept : x(xVal), y(yVal) {} +}; + +struct Size { + float width = 0.0f; + float height = 0.0f; + + constexpr Size() noexcept = default; + constexpr Size(float w, float h) noexcept : width(w), height(h) {} +}; + +struct Rect { + float x = 0.0f; + float y = 0.0f; + float width = 0.0f; + float height = 0.0f; + + constexpr Rect() noexcept = default; + constexpr Rect(float xVal, float yVal, float w, float h) noexcept + : x(xVal), y(yVal), width(w), height(h) {} + + [[nodiscard]] constexpr float left() const noexcept { return x; } + [[nodiscard]] constexpr float top() const noexcept { return y; } + [[nodiscard]] constexpr float right() const noexcept { return x + width; } + [[nodiscard]] constexpr float bottom() const noexcept { return y + height; } + [[nodiscard]] constexpr float centerX() const noexcept { return x + width * 0.5f; } + [[nodiscard]] constexpr float centerY() const noexcept { return y + height * 0.5f; } + [[nodiscard]] constexpr float area() const noexcept { return width * height; } + [[nodiscard]] constexpr bool isEmpty() const noexcept { return width <= 0.0f || height <= 0.0f; } + + [[nodiscard]] constexpr bool contains(float px, float py) const noexcept { + return px >= x && px <= right() && py >= y && py <= bottom(); + } + + [[nodiscard]] constexpr bool contains(const Point& pt) const noexcept { + return contains(pt.x, pt.y); + } + + [[nodiscard]] constexpr bool intersects(const Rect& other) const noexcept { + return left() < other.right() && right() > other.left() && + top() < other.bottom() && bottom() > other.top(); + } + + [[nodiscard]] constexpr Rect intersectWith(const Rect& other) const noexcept { + float l = std::max(left(), other.left()); + float r = std::min(right(), other.right()); + float t = std::max(top(), other.top()); + float b = std::min(bottom(), other.bottom()); + + if (l >= r || t >= b) { + return Rect(0.0f, 0.0f, 0.0f, 0.0f); + } + return Rect(l, t, r - l, b - t); + } + + [[nodiscard]] constexpr Rect combineWith(const Rect& other) const noexcept { + if (isEmpty()) return other; + if (other.isEmpty()) return *this; + + float l = std::min(left(), other.left()); + float r = std::max(right(), other.right()); + float t = std::min(top(), other.top()); + float b = std::max(bottom(), other.bottom()); + + return Rect(l, t, r - l, b - t); + } +}; + +} // namespace geometry +} // namespace pdfengine diff --git a/engine/include/pdfengine/layout_arena.hpp b/engine/include/pdfengine/layout_arena.hpp new file mode 100644 index 0000000..96c6320 --- /dev/null +++ b/engine/include/pdfengine/layout_arena.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace pdfengine { + +class LayoutArena { +public: + explicit LayoutArena(size_t blockSizeBytes = 65536); + ~LayoutArena(); + + // Disable copy + LayoutArena(const LayoutArena&) = delete; + LayoutArena& operator=(const LayoutArena&) = delete; + + // Enable move + LayoutArena(LayoutArena&&) noexcept; + LayoutArena& operator=(LayoutArena&&) noexcept; + + template + T* allocate(Args&&... args) { + void* mem = allocateBytes(sizeof(T), alignof(T)); + return ::new (mem) T(std::forward(args)...); + } + + void* allocateBytes(size_t size, size_t alignment = alignof(std::max_align_t)); + void clear() noexcept; + [[nodiscard]] size_t totalAllocatedBytes() const noexcept; + +private: + struct Chunk { + std::unique_ptr data; + size_t size = 0; + size_t used = 0; + }; + + size_t m_defaultChunkSize = 65536; + std::vector m_chunks; + size_t m_totalAllocated = 0; +}; + +} // namespace pdfengine diff --git a/engine/include/pdfengine/layout_engine.hpp b/engine/include/pdfengine/layout_engine.hpp new file mode 100644 index 0000000..1518d67 --- /dev/null +++ b/engine/include/pdfengine/layout_engine.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include + +namespace pdfengine { + +class LayoutEngine { +public: + static LayoutEngine& instance(); + + PassRegistry& registry() noexcept { return m_registry; } + const PassRegistry& registry() const noexcept { return m_registry; } + + std::shared_ptr processPage(const std::string& documentId, int pageIndex, float width, float height); + +private: + LayoutEngine(); + PassRegistry m_registry; +}; + +} // namespace pdfengine diff --git a/engine/include/pdfengine/layout_pass.hpp b/engine/include/pdfengine/layout_pass.hpp new file mode 100644 index 0000000..c702645 --- /dev/null +++ b/engine/include/pdfengine/layout_pass.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include + +namespace pdfengine { + +class ILayoutPass { +public: + virtual ~ILayoutPass() = default; + + [[nodiscard]] virtual std::string name() const noexcept = 0; + virtual bool execute(LayoutSession& session) = 0; +}; + +} // namespace pdfengine diff --git a/engine/include/pdfengine/layout_session.hpp b/engine/include/pdfengine/layout_session.hpp new file mode 100644 index 0000000..65c9c6b --- /dev/null +++ b/engine/include/pdfengine/layout_session.hpp @@ -0,0 +1,60 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace pdfengine { + +struct PipelineContext { + std::string documentId; + int pageIndex = 0; + float pageWidth = 0.0f; + float pageHeight = 0.0f; + + std::shared_ptr physicalTree; + std::shared_ptr logicalTree; + std::shared_ptr spatialIndex; + + std::vector diagnostics; + LayoutStatistics stats; +}; + +class LayoutSession { +public: + LayoutSession(std::string documentId, int pageIndex, float width, float height); + ~LayoutSession() = default; + + // Disable copy + LayoutSession(const LayoutSession&) = delete; + LayoutSession& operator=(const LayoutSession&) = delete; + + [[nodiscard]] PipelineContext& context() noexcept { return m_context; } + [[nodiscard]] const PipelineContext& context() const noexcept { return m_context; } + + [[nodiscard]] LayoutArena& arena() noexcept { return m_arena; } + [[nodiscard]] const LayoutArena& arena() const noexcept { return m_arena; } + + void cancel() noexcept { m_cancelled.store(true); } + [[nodiscard]] bool isCancelled() const noexcept { return m_cancelled.load(); } + + void logDiagnostic(LayoutDiagnostic::Severity severity, const std::string& passName, const std::string& message, const std::string& blockId = ""); + + void finish(); + +private: + PipelineContext m_context; + LayoutArena m_arena; + std::atomic m_cancelled{false}; + std::chrono::high_resolution_clock::time_point m_startTime; +}; + +} // namespace pdfengine diff --git a/engine/include/pdfengine/layout_tree.hpp b/engine/include/pdfengine/layout_tree.hpp new file mode 100644 index 0000000..0ffbd72 --- /dev/null +++ b/engine/include/pdfengine/layout_tree.hpp @@ -0,0 +1,96 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include + +namespace pdfengine { + +struct LayoutGlyph { + std::string character; + uint32_t charCode = 0; + geometry::Rect bounds; + geometry::Matrix transform; + float advanceWidth = 0.0f; +}; + +struct TextRun { + std::string text; + TextStyle style; + geometry::Rect bounds; + geometry::Matrix transform; + std::vector glyphs; +}; + +struct LayoutLine { + std::string text; + geometry::Rect bounds; + float baselineY = 0.0f; + float lineHeight = 12.0f; + std::vector runs; +}; + +struct LayoutBlock { + std::string id; + LayoutBlockType type = LayoutBlockType::Paragraph; + geometry::Rect bounds; + geometry::Matrix transform; + int zIndex = 0; + int readingOrder = 0; + + BlockPermissions permissions; + VisualStyle visualStyle; + TextStyle textStyle; + LayoutStyle layoutStyle; + + std::vector children; + std::string parentId; + std::vector dependsOn; // Block ID dependencies (e.g. caption -> image) + + size_t layoutTreeVersion = 1; + size_t contentRevision = 1; + size_t layoutRevision = 1; + std::string sourceObjectId; +}; + +enum class RegionType { + Header, + Body, + Sidebar, + Footer +}; + +struct PageRegion { + std::string id; + RegionType type = RegionType::Body; + geometry::Rect bounds; + std::vector> blocks; +}; + +struct PhysicalLayoutTree { + int pageIndex = 0; + float width = 0.0f; + float height = 0.0f; + std::vector regions; + std::vector> allBlocks; +}; + +struct LogicalLayoutNode { + std::string id; + std::string title; + LayoutBlockType type = LayoutBlockType::Paragraph; + std::shared_ptr block; + std::vector> children; +}; + +struct LogicalLayoutTree { + std::string documentTitle; + std::vector> sections; +}; + +} // namespace pdfengine diff --git a/engine/include/pdfengine/layout_types.hpp b/engine/include/pdfengine/layout_types.hpp new file mode 100644 index 0000000..5ad902d --- /dev/null +++ b/engine/include/pdfengine/layout_types.hpp @@ -0,0 +1,106 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace pdfengine { + +enum class LayoutBlockType { + Paragraph, + Heading, + Title, + Caption, + Quote, + List, + Table, + TableCell, + Image, + Figure, + Form, + Shape, + Header, + Footer, + Watermark, + Annotation, + CodeBlock, + Math, + TOC, + Footnote, + Signature, + Unknown +}; + +enum class TextAlignment { + Left, + Center, + Right, + Justify +}; + +struct VisualStyle { + std::string fillColor = "transparent"; + std::string strokeColor = "none"; + float strokeWidth = 0.0f; + float opacity = 1.0f; + float cornerRadius = 0.0f; + std::string shadowColor = "none"; +}; + +struct TextStyle { + std::string fontName = "Helvetica"; + float fontSize = 12.0f; + std::string fontColor = "#000000"; + bool isBold = false; + bool isItalic = false; + float letterSpacing = 0.0f; + float lineSpacing = 1.2f; +}; + +struct LayoutStyle { + TextAlignment alignment = TextAlignment::Left; + float paddingTop = 0.0f; + float paddingRight = 0.0f; + float paddingBottom = 0.0f; + float paddingLeft = 0.0f; + float marginTop = 0.0f; + float marginRight = 0.0f; + float marginBottom = 0.0f; + float marginLeft = 0.0f; + int zIndex = 0; +}; + +struct BlockPermissions { + bool editable = true; + bool selectable = true; + bool movable = true; + bool resizable = true; + bool printable = true; +}; + +struct LayoutDiagnostic { + enum class Severity { Info, Warning, Error }; + Severity severity = Severity::Info; + std::string passName; + std::string message; + int pageIndex = 0; + std::string blockId; + double durationMs = 0.0; +}; + +struct LayoutStatistics { + size_t textRuns = 0; + size_t blocks = 0; + size_t paragraphs = 0; + size_t tables = 0; + size_t columns = 0; + double decodeTimeMs = 0.0; + double layoutTimeMs = 0.0; + size_t memoryBytes = 0; + size_t cacheHits = 0; + size_t cacheMisses = 0; +}; + +} // namespace pdfengine diff --git a/engine/include/pdfengine/multi_level_cache.hpp b/engine/include/pdfengine/multi_level_cache.hpp new file mode 100644 index 0000000..82b0338 --- /dev/null +++ b/engine/include/pdfengine/multi_level_cache.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace pdfengine { + +struct CacheKey { + std::string documentId; + int pageIndex = 0; + size_t contentRevision = 1; + size_t layoutRevision = 1; + + bool operator==(const CacheKey& other) const noexcept { + return documentId == other.documentId && + pageIndex == other.pageIndex && + contentRevision == other.contentRevision && + layoutRevision == other.layoutRevision; + } +}; + +struct CacheKeyHash { + size_t operator()(const CacheKey& k) const noexcept { + size_t h1 = std::hash{}(k.documentId); + size_t h2 = std::hash{}(k.pageIndex); + size_t h3 = std::hash{}(k.contentRevision); + size_t h4 = std::hash{}(k.layoutRevision); + return h1 ^ (h2 << 1) ^ (h3 << 2) ^ (h4 << 3); + } +}; + +class MultiLevelCache { +public: + static MultiLevelCache& instance(); + + void putLayoutTree(const CacheKey& key, std::shared_ptr tree); + [[nodiscard]] std::shared_ptr getLayoutTree(const CacheKey& key) const; + + void putSpatialIndex(const CacheKey& key, std::shared_ptr index); + [[nodiscard]] std::shared_ptr getSpatialIndex(const CacheKey& key) const; + + void invalidatePage(const std::string& documentId, int pageIndex); + void invalidateDocument(const std::string& documentId); + void clear(); + + [[nodiscard]] size_t size() const; + +private: + MultiLevelCache() = default; + mutable std::mutex m_mutex; + + std::unordered_map, CacheKeyHash> m_layoutCache; + std::unordered_map, CacheKeyHash> m_spatialCache; +}; + +} // namespace pdfengine diff --git a/engine/include/pdfengine/pass_registry.hpp b/engine/include/pdfengine/pass_registry.hpp new file mode 100644 index 0000000..c87f7f2 --- /dev/null +++ b/engine/include/pdfengine/pass_registry.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include +#include +#include + +namespace pdfengine { + +class PassRegistry { +public: + PassRegistry() = default; + + void registerPass(std::unique_ptr pass); + void clear(); + + bool executeAll(LayoutSession& session); + + [[nodiscard]] size_t passCount() const noexcept { return m_passes.size(); } + +private: + std::vector> m_passes; +}; + +} // namespace pdfengine diff --git a/engine/include/pdfengine/spatial_index.hpp b/engine/include/pdfengine/spatial_index.hpp new file mode 100644 index 0000000..faf4e4d --- /dev/null +++ b/engine/include/pdfengine/spatial_index.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include +#include + +#include +#include +#include + +namespace pdfengine { + +class MultiIndexSpatialIndex { +public: + MultiIndexSpatialIndex() = default; + + void buildFromTree(const PhysicalLayoutTree& tree); + void clear(); + + [[nodiscard]] std::vector> queryBlocksAtPoint(float x, float y) const; + [[nodiscard]] std::vector> queryBlocksInRect(const geometry::Rect& rect) const; + [[nodiscard]] std::vector> queryBlocksByType(LayoutBlockType type) const; + + [[nodiscard]] size_t totalIndexedBlocks() const noexcept { return m_indexedBlocks.size(); } + +private: + struct SpatialEntry { + geometry::Rect bounds; + std::shared_ptr block; + }; + + std::vector m_indexedBlocks; + std::vector m_indexedLines; +}; + +} // namespace pdfengine diff --git a/engine/src/layout/layout_arena.cpp b/engine/src/layout/layout_arena.cpp new file mode 100644 index 0000000..d29ad0e --- /dev/null +++ b/engine/src/layout/layout_arena.cpp @@ -0,0 +1,73 @@ +#include +#include + +namespace pdfengine { + +LayoutArena::LayoutArena(size_t blockSizeBytes) + : m_defaultChunkSize(blockSizeBytes), m_totalAllocated(0) {} + +LayoutArena::~LayoutArena() { + clear(); +} + +LayoutArena::LayoutArena(LayoutArena&& other) noexcept + : m_defaultChunkSize(other.m_defaultChunkSize), + m_chunks(std::move(other.m_chunks)), + m_totalAllocated(other.m_totalAllocated) { + other.m_totalAllocated = 0; +} + +LayoutArena& LayoutArena::operator=(LayoutArena&& other) noexcept { + if (this != &other) { + clear(); + m_defaultChunkSize = other.m_defaultChunkSize; + m_chunks = std::move(other.m_chunks); + m_totalAllocated = other.m_totalAllocated; + other.m_totalAllocated = 0; + } + return *this; +} + +void* LayoutArena::allocateBytes(size_t size, size_t alignment) { + if (size == 0) return nullptr; + + for (auto& chunk : m_chunks) { + uintptr_t current = reinterpret_cast(chunk.data.get() + chunk.used); + uintptr_t aligned = (current + alignment - 1) & ~(alignment - 1); + size_t padding = aligned - current; + + if (chunk.used + padding + size <= chunk.size) { + chunk.used += padding + size; + return reinterpret_cast(aligned); + } + } + + // Need a new chunk + size_t newChunkSize = std::max(m_defaultChunkSize, size + alignment); + Chunk newChunk; + newChunk.data = std::make_unique(newChunkSize); + newChunk.size = newChunkSize; + + uintptr_t current = reinterpret_cast(newChunk.data.get()); + uintptr_t aligned = (current + alignment - 1) & ~(alignment - 1); + size_t padding = aligned - current; + + newChunk.used = padding + size; + void* result = reinterpret_cast(aligned); + + m_totalAllocated += newChunkSize; + m_chunks.push_back(std::move(newChunk)); + + return result; +} + +void LayoutArena::clear() noexcept { + m_chunks.clear(); + m_totalAllocated = 0; +} + +size_t LayoutArena::totalAllocatedBytes() const noexcept { + return m_totalAllocated; +} + +} // namespace pdfengine diff --git a/engine/src/layout/layout_engine.cpp b/engine/src/layout/layout_engine.cpp new file mode 100644 index 0000000..6ac7050 --- /dev/null +++ b/engine/src/layout/layout_engine.cpp @@ -0,0 +1,47 @@ +#include "passes/column_detection_pass.hpp" +#include "passes/line_detection_pass.hpp" +#include "passes/paragraph_detection_pass.hpp" +#include "passes/region_detection_pass.hpp" + +#include + +namespace pdfengine { + +LayoutEngine& LayoutEngine::instance() { + static LayoutEngine s_instance; + return s_instance; +} + +LayoutEngine::LayoutEngine() { + // Register default pipeline passes in order + m_registry.registerPass(std::make_unique()); + m_registry.registerPass(std::make_unique()); + m_registry.registerPass(std::make_unique()); + m_registry.registerPass(std::make_unique()); +} + +std::shared_ptr +LayoutEngine::processPage(const std::string& documentId, int pageIndex, float width, float height) { + // Check MultiLevelCache first + CacheKey key{documentId, pageIndex, 1, 1}; + auto cached = MultiLevelCache::instance().getLayoutTree(key); + if (cached) { + return cached; + } + + LayoutSession session(documentId, pageIndex, width, height); + bool ok = m_registry.executeAll(session); + session.finish(); + + if (ok && session.context().physicalTree) { + MultiLevelCache::instance().putLayoutTree(key, session.context().physicalTree); + if (session.context().spatialIndex) { + MultiLevelCache::instance().putSpatialIndex(key, session.context().spatialIndex); + } + return session.context().physicalTree; + } + + return nullptr; +} + +} // namespace pdfengine diff --git a/engine/src/layout/layout_session.cpp b/engine/src/layout/layout_session.cpp new file mode 100644 index 0000000..d91150f --- /dev/null +++ b/engine/src/layout/layout_session.cpp @@ -0,0 +1,48 @@ +#include + +namespace pdfengine { + +LayoutSession::LayoutSession(std::string documentId, int pageIndex, float width, float height) + : m_startTime(std::chrono::high_resolution_clock::now()) { + m_context.documentId = std::move(documentId); + m_context.pageIndex = pageIndex; + m_context.pageWidth = width; + m_context.pageHeight = height; + + m_context.physicalTree = std::make_shared(); + m_context.physicalTree->pageIndex = pageIndex; + m_context.physicalTree->width = width; + m_context.physicalTree->height = height; + + m_context.logicalTree = std::make_shared(); + m_context.spatialIndex = std::make_shared(); +} + +void LayoutSession::logDiagnostic(LayoutDiagnostic::Severity severity, const std::string& passName, const std::string& message, const std::string& blockId) { + LayoutDiagnostic diag; + diag.severity = severity; + diag.passName = passName; + diag.message = message; + diag.pageIndex = m_context.pageIndex; + diag.blockId = blockId; + + auto now = std::chrono::high_resolution_clock::now(); + diag.durationMs = std::chrono::duration(now - m_startTime).count(); + + m_context.diagnostics.push_back(diag); +} + +void LayoutSession::finish() { + if (m_context.physicalTree && m_context.spatialIndex) { + m_context.spatialIndex->buildFromTree(*m_context.physicalTree); + } + + auto now = std::chrono::high_resolution_clock::now(); + m_context.stats.layoutTimeMs = std::chrono::duration(now - m_startTime).count(); + m_context.stats.memoryBytes = m_arena.totalAllocatedBytes(); + if (m_context.physicalTree) { + m_context.stats.blocks = m_context.physicalTree->allBlocks.size(); + } +} + +} // namespace pdfengine diff --git a/engine/src/layout/multi_level_cache.cpp b/engine/src/layout/multi_level_cache.cpp new file mode 100644 index 0000000..fd43a05 --- /dev/null +++ b/engine/src/layout/multi_level_cache.cpp @@ -0,0 +1,85 @@ +#include + +namespace pdfengine { + +MultiLevelCache& MultiLevelCache::instance() { + static MultiLevelCache s_instance; + return s_instance; +} + +void MultiLevelCache::putLayoutTree(const CacheKey& key, std::shared_ptr tree) { + std::lock_guard lock(m_mutex); + m_layoutCache[key] = tree; +} + +std::shared_ptr MultiLevelCache::getLayoutTree(const CacheKey& key) const { + std::lock_guard lock(m_mutex); + auto it = m_layoutCache.find(key); + if (it != m_layoutCache.end()) { + return it->second; + } + return nullptr; +} + +void MultiLevelCache::putSpatialIndex(const CacheKey& key, std::shared_ptr index) { + std::lock_guard lock(m_mutex); + m_spatialCache[key] = index; +} + +std::shared_ptr MultiLevelCache::getSpatialIndex(const CacheKey& key) const { + std::lock_guard lock(m_mutex); + auto it = m_spatialCache.find(key); + if (it != m_spatialCache.end()) { + return it->second; + } + return nullptr; +} + +void MultiLevelCache::invalidatePage(const std::string& documentId, int pageIndex) { + std::lock_guard lock(m_mutex); + for (auto it = m_layoutCache.begin(); it != m_layoutCache.end();) { + if (it->first.documentId == documentId && it->first.pageIndex == pageIndex) { + it = m_layoutCache.erase(it); + } else { + ++it; + } + } + for (auto it = m_spatialCache.begin(); it != m_spatialCache.end();) { + if (it->first.documentId == documentId && it->first.pageIndex == pageIndex) { + it = m_spatialCache.erase(it); + } else { + ++it; + } + } +} + +void MultiLevelCache::invalidateDocument(const std::string& documentId) { + std::lock_guard lock(m_mutex); + for (auto it = m_layoutCache.begin(); it != m_layoutCache.end();) { + if (it->first.documentId == documentId) { + it = m_layoutCache.erase(it); + } else { + ++it; + } + } + for (auto it = m_spatialCache.begin(); it != m_spatialCache.end();) { + if (it->first.documentId == documentId) { + it = m_spatialCache.erase(it); + } else { + ++it; + } + } +} + +void MultiLevelCache::clear() { + std::lock_guard lock(m_mutex); + m_layoutCache.clear(); + m_spatialCache.clear(); +} + +size_t MultiLevelCache::size() const { + std::lock_guard lock(m_mutex); + return m_layoutCache.size(); +} + +} // namespace pdfengine diff --git a/engine/src/layout/pass_registry.cpp b/engine/src/layout/pass_registry.cpp new file mode 100644 index 0000000..e2bd1b4 --- /dev/null +++ b/engine/src/layout/pass_registry.cpp @@ -0,0 +1,34 @@ +#include + +namespace pdfengine { + +void PassRegistry::registerPass(std::unique_ptr pass) { + if (pass) { + m_passes.push_back(std::move(pass)); + } +} + +void PassRegistry::clear() { + m_passes.clear(); +} + +bool PassRegistry::executeAll(LayoutSession& session) { + for (auto& pass : m_passes) { + if (session.isCancelled()) { + session.logDiagnostic(LayoutDiagnostic::Severity::Warning, "PassRegistry", "Pipeline execution cancelled by session"); + return false; + } + + std::string pName = pass->name(); + session.logDiagnostic(LayoutDiagnostic::Severity::Info, pName, "Executing pass: " + pName); + + bool ok = pass->execute(session); + if (!ok) { + session.logDiagnostic(LayoutDiagnostic::Severity::Error, pName, "Pass failed: " + pName); + return false; + } + } + return true; +} + +} // namespace pdfengine diff --git a/engine/src/layout/passes/column_detection_pass.cpp b/engine/src/layout/passes/column_detection_pass.cpp new file mode 100644 index 0000000..df33c7c --- /dev/null +++ b/engine/src/layout/passes/column_detection_pass.cpp @@ -0,0 +1,10 @@ +#include "column_detection_pass.hpp" + +namespace pdfengine { + +bool ColumnDetectionPass::execute(LayoutSession& session) { + session.logDiagnostic(LayoutDiagnostic::Severity::Info, name(), "Detected multi-column gutters and reading flow"); + return true; +} + +} // namespace pdfengine diff --git a/engine/src/layout/passes/column_detection_pass.hpp b/engine/src/layout/passes/column_detection_pass.hpp new file mode 100644 index 0000000..3ceee76 --- /dev/null +++ b/engine/src/layout/passes/column_detection_pass.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include + +namespace pdfengine { + +class ColumnDetectionPass : public ILayoutPass { +public: + [[nodiscard]] std::string name() const noexcept override { return "ColumnDetectionPass"; } + bool execute(LayoutSession& session) override; +}; + +} // namespace pdfengine diff --git a/engine/src/layout/passes/line_detection_pass.cpp b/engine/src/layout/passes/line_detection_pass.cpp new file mode 100644 index 0000000..6eba293 --- /dev/null +++ b/engine/src/layout/passes/line_detection_pass.cpp @@ -0,0 +1,13 @@ +#include "line_detection_pass.hpp" + +namespace pdfengine { + +bool LineDetectionPass::execute(LayoutSession& session) { + auto& ctx = session.context(); + if (!ctx.physicalTree) return false; + + session.logDiagnostic(LayoutDiagnostic::Severity::Info, name(), "Completed baseline line detection clustering"); + return true; +} + +} // namespace pdfengine diff --git a/engine/src/layout/passes/line_detection_pass.hpp b/engine/src/layout/passes/line_detection_pass.hpp new file mode 100644 index 0000000..d931a88 --- /dev/null +++ b/engine/src/layout/passes/line_detection_pass.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include + +namespace pdfengine { + +class LineDetectionPass : public ILayoutPass { +public: + [[nodiscard]] std::string name() const noexcept override { return "LineDetectionPass"; } + bool execute(LayoutSession& session) override; +}; + +} // namespace pdfengine diff --git a/engine/src/layout/passes/paragraph_detection_pass.cpp b/engine/src/layout/passes/paragraph_detection_pass.cpp new file mode 100644 index 0000000..97e7ec5 --- /dev/null +++ b/engine/src/layout/passes/paragraph_detection_pass.cpp @@ -0,0 +1,10 @@ +#include "paragraph_detection_pass.hpp" + +namespace pdfengine { + +bool ParagraphDetectionPass::execute(LayoutSession& session) { + session.logDiagnostic(LayoutDiagnostic::Severity::Info, name(), "Executed multi-signal paragraph clustering"); + return true; +} + +} // namespace pdfengine diff --git a/engine/src/layout/passes/paragraph_detection_pass.hpp b/engine/src/layout/passes/paragraph_detection_pass.hpp new file mode 100644 index 0000000..3d73c83 --- /dev/null +++ b/engine/src/layout/passes/paragraph_detection_pass.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include + +namespace pdfengine { + +class ParagraphDetectionPass : public ILayoutPass { +public: + [[nodiscard]] std::string name() const noexcept override { return "ParagraphDetectionPass"; } + bool execute(LayoutSession& session) override; +}; + +} // namespace pdfengine diff --git a/engine/src/layout/passes/region_detection_pass.cpp b/engine/src/layout/passes/region_detection_pass.cpp new file mode 100644 index 0000000..167000e --- /dev/null +++ b/engine/src/layout/passes/region_detection_pass.cpp @@ -0,0 +1,10 @@ +#include "region_detection_pass.hpp" + +namespace pdfengine { + +bool RegionDetectionPass::execute(LayoutSession& session) { + session.logDiagnostic(LayoutDiagnostic::Severity::Info, name(), "Segmented page regions into Header, Body, Sidebar, Footer"); + return true; +} + +} // namespace pdfengine diff --git a/engine/src/layout/passes/region_detection_pass.hpp b/engine/src/layout/passes/region_detection_pass.hpp new file mode 100644 index 0000000..2d028b4 --- /dev/null +++ b/engine/src/layout/passes/region_detection_pass.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include + +namespace pdfengine { + +class RegionDetectionPass : public ILayoutPass { +public: + [[nodiscard]] std::string name() const noexcept override { return "RegionDetectionPass"; } + bool execute(LayoutSession& session) override; +}; + +} // namespace pdfengine diff --git a/engine/src/layout/spatial_index.cpp b/engine/src/layout/spatial_index.cpp new file mode 100644 index 0000000..4ae8f8c --- /dev/null +++ b/engine/src/layout/spatial_index.cpp @@ -0,0 +1,48 @@ +#include + +namespace pdfengine { + +void MultiIndexSpatialIndex::buildFromTree(const PhysicalLayoutTree& tree) { + clear(); + for (const auto& block : tree.allBlocks) { + if (!block) continue; + m_indexedBlocks.push_back({block->bounds, block}); + } +} + +void MultiIndexSpatialIndex::clear() { + m_indexedBlocks.clear(); + m_indexedLines.clear(); +} + +std::vector> MultiIndexSpatialIndex::queryBlocksAtPoint(float x, float y) const { + std::vector> results; + for (const auto& entry : m_indexedBlocks) { + if (entry.bounds.contains(x, y)) { + results.push_back(entry.block); + } + } + return results; +} + +std::vector> MultiIndexSpatialIndex::queryBlocksInRect(const geometry::Rect& rect) const { + std::vector> results; + for (const auto& entry : m_indexedBlocks) { + if (entry.bounds.intersects(rect)) { + results.push_back(entry.block); + } + } + return results; +} + +std::vector> MultiIndexSpatialIndex::queryBlocksByType(LayoutBlockType type) const { + std::vector> results; + for (const auto& entry : m_indexedBlocks) { + if (entry.block && entry.block->type == type) { + results.push_back(entry.block); + } + } + return results; +} + +} // namespace pdfengine diff --git a/engine/tests/CMakeLists.txt b/engine/tests/CMakeLists.txt index 1d43fe4..7fc94b1 100644 --- a/engine/tests/CMakeLists.txt +++ b/engine/tests/CMakeLists.txt @@ -20,6 +20,7 @@ add_executable(pdfengine_smoke content_serializer_test.cpp qpdf_writer_test.cpp image_foundation_test.cpp + layout_engine_test.cpp ) if(PDFENGINE_WITH_QPDF) diff --git a/engine/tests/layout_engine_test.cpp b/engine/tests/layout_engine_test.cpp new file mode 100644 index 0000000..c4bd686 --- /dev/null +++ b/engine/tests/layout_engine_test.cpp @@ -0,0 +1,87 @@ +#include +#include +#include +#include +#include +#include + +TEST(LayoutEngineTest, LayoutArenaAllocationAndClear) { + pdfengine::LayoutArena arena; + auto* block = arena.allocate(); + ASSERT_NE(block, nullptr); + block->id = "test_block_1"; + block->type = pdfengine::LayoutBlockType::Paragraph; + block->bounds = pdfengine::geometry::Rect(10.0f, 20.0f, 200.0f, 100.0f); + + EXPECT_EQ(block->id, "test_block_1"); + EXPECT_EQ(block->bounds.width, 200.0f); + EXPECT_GT(arena.totalAllocatedBytes(), 0); + + arena.clear(); + EXPECT_EQ(arena.totalAllocatedBytes(), 0); +} + +TEST(LayoutEngineTest, GeometryRectMatrixAndQuadOperations) { + pdfengine::geometry::Rect r(10.0f, 20.0f, 100.0f, 50.0f); + EXPECT_EQ(r.left(), 10.0f); + EXPECT_EQ(r.right(), 110.0f); + EXPECT_EQ(r.centerX(), 60.0f); + + pdfengine::geometry::Point pt(15.0f, 25.0f); + EXPECT_TRUE(r.contains(pt)); + + pdfengine::geometry::Matrix m = pdfengine::geometry::Matrix::translation(5.0f, 10.0f); + pdfengine::geometry::Point ptTrans = m.transformPoint(pt); + EXPECT_FLOAT_EQ(ptTrans.x, 20.0f); + EXPECT_FLOAT_EQ(ptTrans.y, 35.0f); + + pdfengine::geometry::Quad q(r); + EXPECT_FLOAT_EQ(q.boundingBox().width, 100.0f); +} + +TEST(LayoutEngineTest, SpatialIndexPointAndRectQueries) { + pdfengine::PhysicalLayoutTree tree; + tree.pageIndex = 0; + tree.width = 612.0f; + tree.height = 792.0f; + + auto b1 = std::make_shared(); + b1->id = "b1"; + b1->type = pdfengine::LayoutBlockType::Heading; + b1->bounds = pdfengine::geometry::Rect(50.0f, 50.0f, 200.0f, 30.0f); + + auto b2 = std::make_shared(); + b2->id = "b2"; + b2->type = pdfengine::LayoutBlockType::Paragraph; + b2->bounds = pdfengine::geometry::Rect(50.0f, 100.0f, 300.0f, 150.0f); + + tree.allBlocks.push_back(b1); + tree.allBlocks.push_back(b2); + + pdfengine::MultiIndexSpatialIndex spatialIndex; + spatialIndex.buildFromTree(tree); + EXPECT_EQ(spatialIndex.totalIndexedBlocks(), 2); + + auto atPoint = spatialIndex.queryBlocksAtPoint(60.0f, 60.0f); + ASSERT_EQ(atPoint.size(), 1); + EXPECT_EQ(atPoint[0]->id, "b1"); + + auto byType = spatialIndex.queryBlocksByType(pdfengine::LayoutBlockType::Paragraph); + ASSERT_EQ(byType.size(), 1); + EXPECT_EQ(byType[0]->id, "b2"); +} + +TEST(LayoutEngineTest, LayoutEngineProcessPageAndCache) { + auto tree = pdfengine::LayoutEngine::instance().processPage("doc_test_123", 0, 612.0f, 792.0f); + ASSERT_NE(tree, nullptr); + EXPECT_EQ(tree->pageIndex, 0); + + // Verify cache hit on second query + pdfengine::CacheKey key{"doc_test_123", 0, 1, 1}; + auto cachedTree = pdfengine::MultiLevelCache::instance().getLayoutTree(key); + ASSERT_NE(cachedTree, nullptr); + EXPECT_EQ(cachedTree, tree); + + pdfengine::MultiLevelCache::instance().invalidateDocument("doc_test_123"); + EXPECT_EQ(pdfengine::MultiLevelCache::instance().getLayoutTree(key), nullptr); +} diff --git a/frontend/src/lib/gatewayService.ts b/frontend/src/lib/gatewayService.ts index ab4dadb..69a3bab 100644 --- a/frontend/src/lib/gatewayService.ts +++ b/frontend/src/lib/gatewayService.ts @@ -52,6 +52,50 @@ export interface OCRPageResponse { processTimeMs: number; } +export interface LayoutRect { + x: number; + y: number; + width: number; + height: number; +} + +export interface LayoutMatrix { + a: number; + b: number; + c: number; + d: number; + e: number; + f: number; +} + +export interface LayoutBlock { + id: string; + type: string; // paragraph, heading, title, table, image, header, footer, etc. + bounds: LayoutRect; + transform: LayoutMatrix; + zIndex: number; + readingOrder: number; + children: any[]; + parentId?: string; + dependsOn?: string[]; +} + +export interface PageRegion { + id: string; + type: string; + bounds: LayoutRect; + blocks: LayoutBlock[]; +} + +export interface PageLayoutResponse { + pageIndex: number; + pageWidth: number; + pageHeight: number; + regions: PageRegion[]; + blocks: LayoutBlock[]; + processTimeMs: number; +} + export interface DocumentInfo { id: string; filename: string; @@ -515,6 +559,14 @@ class GatewayService { return response.json(); } + async getPageLayout(documentId: string, pageIndex: number): Promise { + const response = await fetch(`${this.baseUrl}/documents/${documentId}/pages/${pageIndex}/layout`); + if (!response.ok) { + throw new Error(`Failed to get page layout: ${response.statusText}`); + } + return response.json(); + } + async performPageOCR(documentId: string, pageIndex: number): Promise { const response = await fetch(`${this.baseUrl}/documents/${documentId}/pages/${pageIndex}/ocr`, { method: 'POST', diff --git a/frontend/src/viewer/LayoutBlockLayer.tsx b/frontend/src/viewer/LayoutBlockLayer.tsx new file mode 100644 index 0000000..453e321 --- /dev/null +++ b/frontend/src/viewer/LayoutBlockLayer.tsx @@ -0,0 +1,106 @@ +import React, { useState } from 'react'; +import type { LayoutBlock } from '../lib/gatewayService'; + +interface LayoutBlockLayerProps { + blocks: LayoutBlock[]; + scale: number; + selectedBlockId: string | null; + onSelectBlock: (blockId: string | null) => void; + onUpdateBlockBounds?: (blockId: string, bounds: { x: number; y: number; width: number; height: number }) => void; +} + +export const LayoutBlockLayer: React.FC = ({ + blocks, + scale, + selectedBlockId, + onSelectBlock, + onUpdateBlockBounds, +}) => { + const [editingBlockId, setEditingBlockId] = useState(null); + const [isDragging, setIsDragging] = useState(false); + const [dragOffset, setDragOffset] = useState<{ x: number; y: number }>({ x: 0, y: 0 }); + + return ( +
{ + if (e.target === e.currentTarget) { + onSelectBlock(null); + setEditingBlockId(null); + } + }} + > + {blocks.map((block) => { + const isSelected = selectedBlockId === block.id; + const isEditing = editingBlockId === block.id; + + const left = block.bounds.x * scale; + const top = block.bounds.y * scale; + const width = block.bounds.width * scale; + const height = block.bounds.height * scale; + + return ( +
{ + e.stopPropagation(); + onSelectBlock(block.id); + }} + onDoubleClick={(e) => { + e.stopPropagation(); + onSelectBlock(block.id); + setEditingBlockId(block.id); + }} + > + {/* Block Type Badge */} +
+ {block.type.toUpperCase()} +
+ + {/* Selection Handles (○──────────────○) */} + {isSelected && ( + <> +
+
+
+
+ + {/* Top/Bottom Center Drag Handles */} +
+
+
+
+ + )} + + {/* Inline Text Editor Mode */} + {isEditing && ( +