From e8620947a55f235575114cb68b6aafc7bdf514cc Mon Sep 17 00:00:00 2001 From: Furqan-14 Date: Mon, 22 Jun 2026 15:18:47 +0530 Subject: [PATCH] fix: code cleanup --- engine/fuzz/fuzz_load.cpp | 9 +- engine/include/pdfengine/ast.hpp | 4 +- engine/include/pdfengine/content_object.hpp | 12 +- engine/include/pdfengine/content_stream.hpp | 14 +- engine/include/pdfengine/display_list.hpp | 12 +- engine/include/pdfengine/graphics_state.hpp | 20 +- engine/include/pdfengine/hardened_limits.h | 30 +- engine/include/pdfengine/image.hpp | 8 +- engine/include/pdfengine/path.hpp | 7 +- engine/include/pdfengine/pdf_engine.hpp | 12 +- engine/include/pdfengine/skia_renderer.hpp | 6 +- engine/include/pdfengine/token.hpp | 33 +- engine/src/core/display_list.cpp | 2 +- engine/src/core/engine_info.cpp | 2 +- engine/src/core/graphics_state.cpp | 9 +- engine/src/core/image_decoder.cpp | 6 +- engine/src/core/image_decoder.hpp | 7 +- engine/src/core/skia_renderer.cpp | 16 +- engine/src/fonts/cache/glyph_bitmap.hpp | 14 +- engine/src/fonts/cache/glyph_cache.cpp | 9 +- engine/src/fonts/cache/glyph_cache.hpp | 12 +- engine/src/fonts/face/font_face.cpp | 14 +- engine/src/fonts/face/font_face.hpp | 10 +- engine/src/fonts/face/free_type_manager.cpp | 2 +- engine/src/fonts/face/free_type_manager.hpp | 3 +- engine/src/fonts/loader/font_resolver.cpp | 6 +- engine/src/fonts/loader/font_resolver.hpp | 5 +- .../pdf_fonts/encoding/cjk_collection_db.cpp | 366 +++++++++--------- .../pdf_fonts/encoding/cjk_collection_db.hpp | 4 +- .../src/fonts/pdf_fonts/encoding/encoding.cpp | 105 +++-- .../src/fonts/pdf_fonts/encoding/encoding.hpp | 11 +- .../pdf_fonts/encoding/tounicode_parser.cpp | 16 +- .../pdf_fonts/encoding/tounicode_parser.hpp | 4 +- engine/src/fonts/pdf_fonts/font.hpp | 16 +- .../src/fonts/pdf_fonts/font_descriptor.cpp | 29 +- .../src/fonts/pdf_fonts/font_descriptor.hpp | 5 +- engine/src/fonts/pdf_fonts/font_fallback.cpp | 7 +- engine/src/fonts/pdf_fonts/font_fallback.hpp | 2 +- engine/src/fonts/pdf_fonts/font_loader.cpp | 6 +- engine/src/fonts/pdf_fonts/font_loader.hpp | 9 +- engine/src/fonts/pdf_fonts/font_subset.cpp | 20 +- engine/src/fonts/pdf_fonts/font_subset.hpp | 6 +- engine/src/fonts/pdf_fonts/types/cid_font.cpp | 6 +- engine/src/fonts/pdf_fonts/types/cid_font.hpp | 7 +- .../fonts/pdf_fonts/types/truetype_font.cpp | 4 +- .../fonts/pdf_fonts/types/truetype_font.hpp | 3 +- .../src/fonts/pdf_fonts/types/type1_font.cpp | 4 +- .../src/fonts/pdf_fonts/types/type1_font.hpp | 4 +- engine/src/fonts/shaping/hb_shaper.cpp | 18 +- engine/src/fonts/shaping/hb_shaper.hpp | 4 +- engine/src/parser/content_builder.cpp | 6 +- engine/src/parser/content_builder.hpp | 6 +- engine/src/parser/content_stream_parser.cpp | 16 +- engine/src/parser/content_stream_parser.hpp | 8 +- engine/src/parser/decoration_builder.cpp | 8 +- engine/src/parser/decoration_builder.hpp | 18 +- engine/src/parser/lexer.cpp | 34 +- engine/src/parser/lexer.hpp | 4 +- engine/src/parser/parser.cpp | 22 +- engine/src/parser/parser.hpp | 2 +- engine/src/parser/pdfium_loader.cpp | 4 +- engine/src/parser/pdfium_loader.hpp | 12 +- engine/src/parser/resource_resolver.hpp | 2 +- engine/src/qpdf/qpdf_extractor.cpp | 8 +- engine/src/qpdf/qpdf_extractor.hpp | 2 +- engine/src/qpdf/qpdf_resource_resolver.cpp | 2 +- engine/src/qpdf/qpdf_resource_resolver.hpp | 2 +- engine/src/qpdf/qpdf_writer.cpp | 8 +- engine/src/qpdf/qpdf_writer.hpp | 3 +- engine/src/serializer/ast_serializer.cpp | 6 +- engine/src/serializer/ast_serializer.hpp | 2 +- engine/src/serializer/content_serializer.cpp | 7 +- engine/src/serializer/content_serializer.hpp | 2 +- engine/src/text/selection.cpp | 56 +-- engine/tests/content_builder_test.cpp | 4 +- engine/tests/display_list_test.cpp | 3 - engine/tests/document_test.cpp | 145 +------ engine/tests/fonts_test.cpp | 229 +++-------- engine/tests/graphics_state_test.cpp | 11 +- engine/tests/image_xobject_test.cpp | 2 +- engine/tests/lexer_test.cpp | 8 +- engine/tests/parser_test.cpp | 6 - engine/tests/qpdf_extractor_test.cpp | 4 +- engine/tests/qpdf_writer_test.cpp | 8 - engine/tests/skia_renderer_test.cpp | 5 +- engine/tests/smoke_test.cpp | 4 - engine/tests/wasm_hello.cpp | 2 - frontend/src/App.tsx | 37 +- frontend/src/components/InspectorPanel.tsx | 5 - frontend/src/components/PasswordModal.tsx | 3 +- frontend/src/components/SignatureModal.tsx | 2 +- frontend/src/components/Thumbnail.tsx | 9 +- frontend/src/components/Toolbar.tsx | 2 - frontend/src/components/TopBar.tsx | 4 - frontend/src/components/WasmInspector.tsx | 13 - .../custom/CustomConfirmationModal.tsx | 2 - frontend/src/components/icons.tsx | 3 - frontend/src/lib/coordinateMapping.ts | 6 +- frontend/src/lib/fontFaceLoader.ts | 16 +- frontend/src/lib/textSelection.ts | 46 +-- frontend/src/lib/tools.ts | 2 +- frontend/src/lib/wasmLoader.ts | 24 +- frontend/src/viewer/AnnotationLayer.tsx | 6 +- frontend/src/viewer/CanvasLayer.tsx | 3 - frontend/src/viewer/OverlayLayer.tsx | 9 - frontend/src/viewer/SearchOverlayLayer.tsx | 1 - frontend/src/viewer/SelectionLayer.tsx | 11 - frontend/src/viewer/StreamEditLayer.tsx | 21 +- frontend/tsconfig.app.json | 2 +- frontend/vite.config.ts | 1 - gateway/app/routers/documents.py | 26 +- gateway/app/routers/render.py | 4 - gateway/app/services/store.py | 3 - gateway/create_form.py | 4 - gateway/tests/api_validate.py | 16 +- gateway/tests/font_api_validation.py | 24 +- gateway/tests/test_final_extraction.py | 40 +- gateway/tests/test_font_regression.py | 37 +- gateway/tests/test_glyph_metrics.py | 8 - gateway/tests/test_health.py | 1 - gateway/tests/test_inplace_editing.py | 5 - gateway/tests/test_replace_text.py | 7 - gateway/tests/test_routes.py | 40 -- scripts/fetch_corpus.py | 6 +- scripts/update_timeline.py | 19 +- test_bindings.py | 4 - tests/edits/test_decorations.py | 5 +- tests/edits/test_reflow_paragraph.py | 7 +- tests/edits/test_replace_text.py | 4 +- tests/regression/run.py | 10 +- tests/security/test_permissions.py | 18 +- tools/generate_corpus.py | 26 +- tools/validate_concurrency.py | 5 - tools/validate_memory.py | 2 - tools/verify_utf8.py | 2 - wasm/bindings/pdf_engine_facade.cpp | 47 +-- wasm/bindings/wasm_rasterizer.hpp | 11 +- wasm/hello.cpp | 16 +- wasm/hello.test.mjs | 13 - wasm/pdfengine.test.mjs | 38 +- wasm/pdfium-engine.smoke.mjs | 2 +- 141 files changed, 554 insertions(+), 1735 deletions(-) diff --git a/engine/fuzz/fuzz_load.cpp b/engine/fuzz/fuzz_load.cpp index a94e885..1ca2d1f 100644 --- a/engine/fuzz/fuzz_load.cpp +++ b/engine/fuzz/fuzz_load.cpp @@ -1,10 +1,3 @@ -// libFuzzer entry point: drive the full load → inspect → render → select path -// with arbitrary bytes, so the fuzzer can find crashes, OOMs, and UB in the -// PDF parsing and rendering code. -// -// Build with a Clang toolchain via the `fuzz-linux` preset (see engine/fuzz/ -// README.md). Every operation is wrapped so a clean error never aborts the run — -// only a real crash (caught by the sanitizer) should stop the fuzzer. #include "pdfengine/hardened_limits.h" #include "pdfengine/pdf_document.hpp" @@ -29,7 +22,7 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { (void)d.metadata(); (void)d.extractOutline(); - const int limit = pages < 3 ? pages : 3; // bound work per input + const int limit = pages < 3 ? pages : 3; for (int i = 0; i < limit; ++i) { auto page = d.getPage(i); if (!page) continue; diff --git a/engine/include/pdfengine/ast.hpp b/engine/include/pdfengine/ast.hpp index 00076ed..04adfb0 100644 --- a/engine/include/pdfengine/ast.hpp +++ b/engine/include/pdfengine/ast.hpp @@ -27,11 +27,9 @@ public: double numberValue = 0.0; bool boolValue = false; - // We use a vector of shared_ptr for recursive data structures so the node is easily copyable/movable std::vector> arrayItems; std::unordered_map> dictItems; - // Constructors for convenience AstNode() = default; explicit AstNode(AstNodeType t) : type(t) {} }; @@ -41,4 +39,4 @@ struct Operation { std::vector> operands; }; -} // namespace pdfengine +} diff --git a/engine/include/pdfengine/content_object.hpp b/engine/include/pdfengine/content_object.hpp index 3e58600..fea0b48 100644 --- a/engine/include/pdfengine/content_object.hpp +++ b/engine/include/pdfengine/content_object.hpp @@ -32,12 +32,10 @@ class TextObject : public ContentObject { public: ContentObjectType getType() const override { return ContentObjectType::Text; } - std::string text; // The decoded text string - std::string fontName; // Font resource name (e.g. "F1") - double fontSize = 0.0; // Font size + std::string text; + std::string fontName; + double fontSize = 0.0; - // Text Transformation Matrix (a, b, c, d, e, f) - // Default is identity matrix: [1 0 0 1 0 0] double tm[6] = {1.0, 0.0, 0.0, 1.0, 0.0, 0.0}; }; @@ -53,10 +51,8 @@ public: int bitsPerComponent = 8; bool hasSoftMask = false; - // Decoded raw pixels (RGBA format for Skia) std::vector pixelData; - // The Current Transformation Matrix (CTM) at the time the 'Do' operator was invoked Matrix transform; }; @@ -75,4 +71,4 @@ public: Matrix transform; }; -} // namespace pdfengine +} diff --git a/engine/include/pdfengine/content_stream.hpp b/engine/include/pdfengine/content_stream.hpp index 949baa7..889326c 100644 --- a/engine/include/pdfengine/content_stream.hpp +++ b/engine/include/pdfengine/content_stream.hpp @@ -5,12 +5,6 @@ namespace pdfengine { -/// Result of extracting a raw PDF content stream from a page. -/// rawContent — bytes as-found in the PDF (may be compressed) -/// decodedContent — after applying all /Filter chains (FlateDecode, etc.) -/// pageIndex — 0-based page index -/// filters — list of filter names applied, e.g. {"FlateDecode"} -/// compressed — true if at least one filter was applied struct ExtractedStream { std::string rawContent; std::string decodedContent; @@ -20,16 +14,14 @@ struct ExtractedStream { bool multiStream = false; }; -/// Verifies structural integrity of a decoded content stream. -/// Returns true if all of: BT, ET, Tf, Tj/TJ are present. struct StreamVerification { bool hasBT = false; bool hasET = false; bool hasTf = false; - bool hasTj = false; // Tj or TJ - bool multiStream = false; // page had multiple /Contents streams + bool hasTj = false; + bool multiStream = false; }; StreamVerification verifyContentStream(const ExtractedStream& stream); -} // namespace pdfengine +} diff --git a/engine/include/pdfengine/display_list.hpp b/engine/include/pdfengine/display_list.hpp index 91cdac2..0eb611c 100644 --- a/engine/include/pdfengine/display_list.hpp +++ b/engine/include/pdfengine/display_list.hpp @@ -10,13 +10,11 @@ namespace pdfengine { class CommandVisitor; -// Base class for all drawing commands struct Command { virtual ~Command() = default; virtual void accept(CommandVisitor& visitor) const = 0; }; -// --- Specific Command Types --- struct SaveStateCommand : public Command { void accept(CommandVisitor& visitor) const override; @@ -41,7 +39,6 @@ struct FillRectCommand : public Command { struct DrawTextCommand : public Command { std::string text; float x, y; - // We would eventually have a font reference here too DrawTextCommand(std::string text, float x, float y) : text(std::move(text)), x(x), y(y) {} void accept(CommandVisitor& visitor) const override; }; @@ -69,9 +66,7 @@ struct DrawImageCommand : public Command { }; -// --- Visitor Interface --- -// The visitor interface that the renderer (or replay engine) implements class CommandVisitor { public: virtual ~CommandVisitor() = default; @@ -85,20 +80,15 @@ public: virtual void visit(const DrawImageCommand& cmd) = 0; }; -// --- Display List Container --- -// A container that stores a sequence of drawing commands. class DisplayList { public: DisplayList() = default; - // Add commands directly void addCommand(std::unique_ptr cmd); - // Replay the commands to a visitor (renderer) void replay(CommandVisitor& visitor) const; - // Helper methods to easily append common commands void saveState(); void restoreState(); void setTransform(const Matrix& m); @@ -115,4 +105,4 @@ private: std::vector> m_commands; }; -} // namespace pdfengine +} diff --git a/engine/include/pdfengine/graphics_state.hpp b/engine/include/pdfengine/graphics_state.hpp index 1e2012d..36c59ed 100644 --- a/engine/include/pdfengine/graphics_state.hpp +++ b/engine/include/pdfengine/graphics_state.hpp @@ -5,10 +5,6 @@ namespace pdfengine { -// 2D Affine Transformation Matrix (3x3 matrix optimized for 2D) -// [ a b 0 ] -// [ c d 0 ] -// [ e f 1 ] struct Matrix { float a = 1.0f, b = 0.0f; float c = 0.0f, d = 1.0f; @@ -18,10 +14,8 @@ struct Matrix { Matrix(float a, float b, float c, float d, float e, float f) : a(a), b(b), c(c), d(d), e(e), f(f) {} - // Multiply this matrix by another matrix [[nodiscard]] Matrix multiply(const Matrix& other) const noexcept; - // Transform a 2D point using this matrix void transform(float& x, float& y) const noexcept; }; @@ -31,32 +25,22 @@ struct Color { float b = 0.0f; }; -// Represents the current graphics state in a PDF document struct GraphicsState { - Matrix ctm; // Current Transformation Matrix + Matrix ctm; Color fillColor; Color strokeColor; float lineWidth = 1.0f; - // In the future, this will also hold: - // - Clipping paths - // - Font state (current font, font size) - // - Dash patterns - // - Line cap/join styles }; -// Manages the q/Q stack of graphics states class GraphicsStateStack { public: GraphicsStateStack(); - // Corresponds to the 'q' operator (save graphics state) void push(); - // Corresponds to the 'Q' operator (restore graphics state) void pop(); - // Access the current active graphics state [[nodiscard]] GraphicsState& current(); [[nodiscard]] const GraphicsState& current() const; @@ -64,4 +48,4 @@ private: std::vector m_stack; }; -} // namespace pdfengine +} diff --git a/engine/include/pdfengine/hardened_limits.h b/engine/include/pdfengine/hardened_limits.h index 96f85c2..955d2a0 100644 --- a/engine/include/pdfengine/hardened_limits.h +++ b/engine/include/pdfengine/hardened_limits.h @@ -1,15 +1,3 @@ -// Resource limits for hardening against malicious / malformed PDFs. -// -// These guard the allocation-sizing arithmetic in the load and render paths -// against integer overflow and pathological out-of-memory inputs (a 2-billion-pt -// page, a million-page document, a multi-gigabyte raster). The ceilings are set -// far above anything a legitimate document needs, so enforcing them never -// rejects real files — they exist purely to turn "crash / OOM" into a clean, -// recoverable error, which is exactly what a fuzzer needs to make progress. -// -// Header-only and dependency-free so the fuzz harness and the engine share one -// source of truth. - #ifndef PDFENGINE_HARDENED_LIMITS_H #define PDFENGINE_HARDENED_LIMITS_H @@ -17,33 +5,21 @@ namespace pdfengine::limits { -// Largest input document we will even attempt to parse (1 GiB). inline constexpr std::uint64_t kMaxDocumentBytes = 1ull << 30; -// PDF hard-caps a page at 14,400 user units (200 in) per side; allow a very -// generous multiple of that to tolerate odd-but-real documents. -inline constexpr double kMaxPageDimensionPt = 200'000.0; // ~2,777 inches +inline constexpr double kMaxPageDimensionPt = 200'000.0; -// No legitimate document has this many pages; stops runaway iteration. inline constexpr int kMaxPageCount = 100'000; -// MAX_OBJECTS: ceiling on the number of content objects on a single page. Guards -// against content-stream "object bombs" that would explode parsing/rendering -// time and memory. No real page comes near this. inline constexpr int kMaxObjects = 5'000'000; -// Cap a single rasterised page at ~256 megapixels (≈1 GiB at 4 bytes/px). At -// 96 dpi that is roughly a 16k × 16k page — well beyond any real render. inline constexpr std::int64_t kMaxRasterPixels = 256ll * 1024 * 1024; -// True if a raw page size (in points) is sane to render. inline constexpr bool pageDimensionsOk(double widthPt, double heightPt) noexcept { return widthPt > 0.0 && heightPt > 0.0 && widthPt <= kMaxPageDimensionPt && heightPt <= kMaxPageDimensionPt; } -// True if a target raster (in pixels) fits the pixel budget without overflowing -// the width*height*4 byte computation. inline constexpr bool rasterSizeOk(std::int64_t widthPx, std::int64_t heightPx) noexcept { if (widthPx <= 0 || heightPx <= 0) return false; if (widthPx > kMaxRasterPixels || heightPx > kMaxRasterPixels) return false; @@ -62,6 +38,6 @@ inline constexpr bool objectCountOk(int objects) noexcept { return objects >= 0 && objects <= kMaxObjects; } -} // namespace pdfengine::limits +} -#endif // PDFENGINE_HARDENED_LIMITS_H +#endif diff --git a/engine/include/pdfengine/image.hpp b/engine/include/pdfengine/image.hpp index a9960d6..e5b6d05 100644 --- a/engine/include/pdfengine/image.hpp +++ b/engine/include/pdfengine/image.hpp @@ -5,7 +5,6 @@ namespace pdfengine { -// Basic color spaces we might encounter enum class ColorSpace { DeviceGray, DeviceRGB, @@ -13,12 +12,11 @@ enum class ColorSpace { Indexed }; -// Holds decoded image data ready for the display list (typically RGBA) struct ImageInfo { int width = 0; int height = 0; - int channels = 4; // 4 = RGBA - std::vector pixelData; // Decoded raw pixels + int channels = 4; + std::vector pixelData; }; -} // namespace pdfengine +} diff --git a/engine/include/pdfengine/path.hpp b/engine/include/pdfengine/path.hpp index 7b12865..0c39ffc 100644 --- a/engine/include/pdfengine/path.hpp +++ b/engine/include/pdfengine/path.hpp @@ -4,13 +4,11 @@ namespace pdfengine { -// Basic point structure struct Point { float x = 0.0f; float y = 0.0f; }; -// Represents a 2D vector path constructed from basic drawing commands. class Path { public: enum class Verb { @@ -22,7 +20,7 @@ public: struct Segment { Verb verb; - Point points[3]; // Up to 3 points depending on verb (e.g., Cubic bezier) + Point points[3]; }; Path() = default; @@ -43,7 +41,6 @@ public: m_segments.push_back({Verb::Close, {{}, {}, {}}}); } - // Helper for 're' (rectangle) operator void addRect(float x, float y, float w, float h) { moveTo(x, y); lineTo(x + w, y); @@ -63,4 +60,4 @@ private: std::vector m_segments; }; -} // namespace pdfengine +} diff --git a/engine/include/pdfengine/pdf_engine.hpp b/engine/include/pdfengine/pdf_engine.hpp index 079d578..bc4cc4d 100644 --- a/engine/include/pdfengine/pdf_engine.hpp +++ b/engine/include/pdfengine/pdf_engine.hpp @@ -1,8 +1,3 @@ -// pdfengine — public umbrella header for the PDF SDK core. -// -// Phase 0 surface only: version + build introspection. The real document API -// (PdfDocument / PdfPage) is frozen at Gate G0b and added in Phase 1 — see -// pdf_document.hpp. #pragma once #include @@ -10,19 +5,14 @@ namespace pdfengine { -// Human-readable engine version, e.g. "0.1.0". [[nodiscard]] std::string_view engineVersion() noexcept; -// One-line build descriptor, e.g. "pdfengine 0.1.0 (pdfium=off)". [[nodiscard]] std::string_view engineBuildInfo() noexcept; -// True if this build was compiled and linked against the PDFium parser core. [[nodiscard]] bool engineHasPdfium() noexcept; -// True if this build was compiled and linked against the Skia graphics core. [[nodiscard]] bool engineHasSkia() noexcept; -// Emits engineBuildInfo() through spdlog at info level. void engineLogBuildInfo(); -} // namespace pdfengine +} diff --git a/engine/include/pdfengine/skia_renderer.hpp b/engine/include/pdfengine/skia_renderer.hpp index 0545d10..5d12907 100644 --- a/engine/include/pdfengine/skia_renderer.hpp +++ b/engine/include/pdfengine/skia_renderer.hpp @@ -7,11 +7,8 @@ class SkCanvas; namespace pdfengine { -// A visitor that replays a DisplayList onto a Skia canvas. class SkiaRenderer : public CommandVisitor { public: - // Takes a pointer to an external SkCanvas. - // The caller is responsible for the canvas's lifecycle. explicit SkiaRenderer(SkCanvas* canvas); void visit(const SaveStateCommand& cmd) override; @@ -22,7 +19,6 @@ public: void visit(const FillPathCommand& cmd) override; void visit(const StrokePathCommand& cmd) override; void visit(const DrawImageCommand& cmd) override; - // Renders the entire display list to the canvas void render(const DisplayList& displayList); private: @@ -30,4 +26,4 @@ private: GraphicsStateStack m_stateStack; }; -} // namespace pdfengine +} diff --git a/engine/include/pdfengine/token.hpp b/engine/include/pdfengine/token.hpp index 88437cb..ea1e08e 100644 --- a/engine/include/pdfengine/token.hpp +++ b/engine/include/pdfengine/token.hpp @@ -7,29 +7,28 @@ namespace pdfengine { enum class TokenType { - Operator, // e.g., "Tj", "BT", "ET", "Tf", "re", "f" - String, // e.g., "(Hello World)", fully unescaped - HexString, // e.g., "<48656C6C6F>", fully decoded to bytes - Name, // e.g., "/F1" (without the slash, unescaped) - Number, // e.g., "12.3", "-4", stored as string/double - ArrayStart, // "[" - ArrayEnd, // "]" - DictStart, // "<<" - DictEnd, // ">>" - Boolean, // "true", "false" - Null, // "null" - EndOfStream // EOF marker + Operator, + String, + HexString, + Name, + Number, + ArrayStart, + ArrayEnd, + DictStart, + DictEnd, + Boolean, + Null, + EndOfStream }; struct Token { TokenType type; - std::string stringValue; // Used for Operator, String, Name - std::vector bytesValue; // Used for HexString - double numberValue = 0.0; // Used for Number + std::string stringValue; + std::vector bytesValue; + double numberValue = 0.0; - // Position tracking for error reporting (optional but helpful) size_t startOffset = 0; size_t endOffset = 0; }; -} // namespace pdfengine +} diff --git a/engine/src/core/display_list.cpp b/engine/src/core/display_list.cpp index 3bd19b7..8bb792d 100644 --- a/engine/src/core/display_list.cpp +++ b/engine/src/core/display_list.cpp @@ -55,4 +55,4 @@ void DisplayList::drawImage(const ImageInfo& image, const Matrix& m, float opaci addCommand(std::make_unique(image, m, opacity)); } -} // namespace pdfengine +} diff --git a/engine/src/core/engine_info.cpp b/engine/src/core/engine_info.cpp index aa0c31b..94719a4 100644 --- a/engine/src/core/engine_info.cpp +++ b/engine/src/core/engine_info.cpp @@ -37,4 +37,4 @@ void engineLogBuildInfo() { spdlog::info("{}", engineBuildInfo()); } -} // namespace pdfengine +} diff --git a/engine/src/core/graphics_state.cpp b/engine/src/core/graphics_state.cpp index a6a4004..aaf2c9f 100644 --- a/engine/src/core/graphics_state.cpp +++ b/engine/src/core/graphics_state.cpp @@ -21,12 +21,10 @@ void Matrix::transform(float& x, float& y) const noexcept { } GraphicsStateStack::GraphicsStateStack() { - // A PDF always starts with one default graphics state on the stack. m_stack.emplace_back(); } void GraphicsStateStack::push() { - // Duplicate the current state and push it onto the stack. if (!m_stack.empty()) { m_stack.push_back(m_stack.back()); } else { @@ -35,14 +33,9 @@ void GraphicsStateStack::push() { } void GraphicsStateStack::pop() { - // The stack should never be empty, but we must protect against popping the initial state - // if the PDF is malformed (e.g. more 'Q' operators than 'q' operators). if (m_stack.size() > 1) { m_stack.pop_back(); } else { - // We could throw an exception or just ignore the invalid pop. - // For a resilient engine, ignoring is often better, but we could log a warning. - // For now, we do nothing to prevent crashing on the root state. } } @@ -54,4 +47,4 @@ const GraphicsState& GraphicsStateStack::current() const { return m_stack.back(); } -} // namespace pdfengine +} diff --git a/engine/src/core/image_decoder.cpp b/engine/src/core/image_decoder.cpp index f6641db..f5e8280 100644 --- a/engine/src/core/image_decoder.cpp +++ b/engine/src/core/image_decoder.cpp @@ -49,7 +49,7 @@ void jpegErrorExit(j_common_ptr cinfo) { longjmp(manager->jump, 1); } -} // namespace +} namespace pdfengine { @@ -291,8 +291,6 @@ std::vector ImageDecoder::convertCmykToRgba(const std::vector ImageDecoder::convertCmykToRgba(const std::vector decode(QPDFObjectHandle imageStream, const std::string& colorSpace, int width, int height, @@ -21,14 +19,11 @@ private: static std::vector decodeJpeg(const std::vector& jpegBytes); static void applySoftMask(QPDFObjectHandle imageStream, std::vector& rgba, int width, int height); - // Converts raw DeviceGray (1 byte per pixel) to RGBA (4 bytes per pixel) static std::vector convertGrayToRgba(const std::vector& rawBytes, int width, int height); - // Converts raw DeviceRGB (3 bytes per pixel) to RGBA (4 bytes per pixel) static std::vector convertRgbToRgba(const std::vector& rawBytes, int width, int height); - // Converts raw DeviceCMYK (4 bytes per pixel) to RGBA (4 bytes per pixel) static std::vector convertCmykToRgba(const std::vector& rawBytes, int width, int height); }; -} // namespace pdfengine +} diff --git a/engine/src/core/skia_renderer.cpp b/engine/src/core/skia_renderer.cpp index 0046ba2..3165902 100644 --- a/engine/src/core/skia_renderer.cpp +++ b/engine/src/core/skia_renderer.cpp @@ -19,7 +19,6 @@ SkiaRenderer::SkiaRenderer(SkCanvas* canvas) : m_canvas(canvas) {} void SkiaRenderer::render(const DisplayList& displayList) { if (!m_canvas) return; - // Iterate and accept commands displayList.replay(*this); } @@ -46,10 +45,6 @@ void SkiaRenderer::visit(const SetTransformCommand& cmd) { #ifdef PDFENGINE_WITH_SKIA if (m_canvas) { SkMatrix skMatrix; - // Map 2D affine matrix to Skia's 3x3 matrix - // [ a c e ] (Skia) vs [ a b 0 ] - // [ b d f ] [ c d 0 ] - // [ 0 0 1 ] [ e f 1 ] skMatrix.setAll( cmd.matrix.a, cmd.matrix.c, cmd.matrix.e, cmd.matrix.b, cmd.matrix.d, cmd.matrix.f, @@ -58,7 +53,6 @@ void SkiaRenderer::visit(const SetTransformCommand& cmd) { m_canvas->concat(skMatrix); } #endif - // Also update internal state if we need it later m_stateStack.current().ctm = m_stateStack.current().ctm.multiply(cmd.matrix); } @@ -95,7 +89,6 @@ void SkiaRenderer::visit(const DrawTextCommand& cmd) { static_cast(color.g * 255), static_cast(color.b * 255))); - // For phase 0/1 testing, we use a default typeface SkFont font(nullptr, 12.0f); m_canvas->drawString(cmd.text.c_str(), cmd.x, cmd.y, font, paint); @@ -174,13 +167,12 @@ void SkiaRenderer::visit(const StrokePathCommand& cmd) { paint.setAntiAlias(true); paint.setStyle(SkPaint::kStroke_Style); - // In a real implementation we would use strokeColor and strokeWidth from graphics state const auto& color = m_stateStack.current().fillColor; paint.setColor(SkColorSetARGB(255, static_cast(color.r * 255), static_cast(color.g * 255), static_cast(color.b * 255))); - paint.setStrokeWidth(1.0f); // Default for now + paint.setStrokeWidth(1.0f); m_canvas->drawPath(skPath, paint); #endif @@ -191,7 +183,6 @@ void SkiaRenderer::visit(const DrawImageCommand& cmd) { #ifdef PDFENGINE_WITH_SKIA if (!m_canvas || cmd.image.pixelData.empty()) return; - // Create an SkImage from the raw RGBA pixels SkImageInfo info = SkImageInfo::Make( cmd.image.width, cmd.image.height, @@ -207,7 +198,7 @@ void SkiaRenderer::visit(const DrawImageCommand& cmd) { sk_sp skImage = SkImages::RasterFromData( info, std::move(data), - cmd.image.width * 4 // rowBytes + cmd.image.width * 4 ); if (skImage) { @@ -221,7 +212,6 @@ void SkiaRenderer::visit(const DrawImageCommand& cmd) { ); m_canvas->concat(skMatrix); - // In PDF, images are drawn into a 1x1 rect at the origin in the current coordinate system SkRect destRect = SkRect::MakeXYWH(0, 0, 1.0f, 1.0f); SkPaint paint; @@ -239,4 +229,4 @@ void SkiaRenderer::visit(const DrawImageCommand& cmd) { #endif } -} // namespace pdfengine +} diff --git a/engine/src/fonts/cache/glyph_bitmap.hpp b/engine/src/fonts/cache/glyph_bitmap.hpp index fd252c9..14b5cc0 100644 --- a/engine/src/fonts/cache/glyph_bitmap.hpp +++ b/engine/src/fonts/cache/glyph_bitmap.hpp @@ -5,12 +5,12 @@ namespace pdfengine::fonts { struct GlyphBitmap { - std::vector pixels; // 8-bit grayscale pixels (0 = transparent, 255 = fully opaque) - int width = 0; // Width of the glyph bitmap in pixels - int height = 0; // Height of the glyph bitmap in pixels - int bearingX = 0; // Horizontal bearing X (bitmap_left) in pixels - int bearingY = 0; // Horizontal bearing Y (bitmap_top) in pixels - double advance = 0.0; // Horizontal advance in pixels + std::vector pixels; + int width = 0; + int height = 0; + int bearingX = 0; + int bearingY = 0; + double advance = 0.0; }; -} // namespace pdfengine::fonts +} diff --git a/engine/src/fonts/cache/glyph_cache.cpp b/engine/src/fonts/cache/glyph_cache.cpp index 5856e1f..b602feb 100644 --- a/engine/src/fonts/cache/glyph_cache.cpp +++ b/engine/src/fonts/cache/glyph_cache.cpp @@ -26,11 +26,10 @@ std::optional GlyphCache::get(const FontFace& fontFace, unsigned in auto it = shard.cache_map_.find(key); if (it == shard.cache_map_.end()) { shard.misses_++; - return std::nullopt; // Cache miss + return std::nullopt; } shard.hits_++; - // Cache hit: move the referenced key to the front of the LRU list shard.lru_list_.splice(shard.lru_list_.begin(), shard.lru_list_, it->second.second); return it->second.first; @@ -49,23 +48,19 @@ void GlyphCache::insert(const FontFace& fontFace, unsigned int glyphIndex, unsig std::lock_guard lock(shard.mutex_); auto it = shard.cache_map_.find(key); if (it != shard.cache_map_.end()) { - // Element already exists: update bitmap and move it to the front it->second.first = bitmap; shard.lru_list_.splice(shard.lru_list_.begin(), shard.lru_list_, it->second.second); return; } - // Capacity per shard std::size_t shard_capacity = (capacity_ + shards_.size() - 1) / shards_.size(); - // Evict oldest element if at capacity if (shard.cache_map_.size() >= shard_capacity && shard_capacity > 0) { GlyphCacheKey oldest = shard.lru_list_.back(); shard.cache_map_.erase(oldest); shard.lru_list_.pop_back(); } - // Insert new element if (shard_capacity > 0) { shard.lru_list_.push_front(key); shard.cache_map_[key] = std::make_pair(bitmap, shard.lru_list_.begin()); @@ -120,4 +115,4 @@ void GlyphCache::resetStats() { } } -} // namespace pdfengine::fonts +} diff --git a/engine/src/fonts/cache/glyph_cache.hpp b/engine/src/fonts/cache/glyph_cache.hpp index 3c8b619..775c10c 100644 --- a/engine/src/fonts/cache/glyph_cache.hpp +++ b/engine/src/fonts/cache/glyph_cache.hpp @@ -28,7 +28,6 @@ struct GlyphCacheKeyHash { std::size_t h1 = std::hash{}(key.fontId); std::size_t h2 = std::hash{}(key.glyphIndex); std::size_t h3 = std::hash{}(key.fontSize); - // Combine hashes using standard boost hash_combine algorithm return h1 ^ (h2 + 0x9e3779b9 + (h1 << 6) + (h1 >> 2)) ^ (h3 + 0x9e3779b9 + (h2 << 6) + (h2 >> 2)); } }; @@ -38,31 +37,23 @@ public: explicit GlyphCache(std::size_t capacity); ~GlyphCache(); - // Cache is move-only to prevent copying internal list iterators GlyphCache(const GlyphCache&) = delete; GlyphCache& operator=(const GlyphCache&) = delete; GlyphCache(GlyphCache&&) noexcept = default; GlyphCache& operator=(GlyphCache&&) noexcept = default; - // Retrieves a glyph from the cache (and marks it as most recently used on hit) std::optional get(const FontFace& fontFace, unsigned int glyphIndex, unsigned int fontSize); - // Inserts a glyph into the cache. Evicts the least recently used glyph if full. void insert(const FontFace& fontFace, unsigned int glyphIndex, unsigned int fontSize, const GlyphBitmap& bitmap); - // Returns the current number of cached glyphs std::size_t size() const; - // Returns the maximum capacity of the cache std::size_t capacity() const; - // Clears all elements from the cache void clear(); - // Returns the cache hit rate (hits / (hits + misses)). Returns 0.0 if no lookups have occurred. double hitRate() const; - // Resets hit and miss counters void resetStats(); private: @@ -85,10 +76,9 @@ private: static constexpr std::size_t NUM_SHARDS = 16; std::vector> shards_; - // Helper to get shard index based on key hash std::size_t getShardIndex(const GlyphCacheKey& key) const { return GlyphCacheKeyHash{}(key) % shards_.size(); } }; -} // namespace pdfengine::fonts +} diff --git a/engine/src/fonts/face/font_face.cpp b/engine/src/fonts/face/font_face.cpp index 3f7eda3..d3325b5 100644 --- a/engine/src/fonts/face/font_face.cpp +++ b/engine/src/fonts/face/font_face.cpp @@ -65,8 +65,6 @@ bool FontFace::loadFromFile(const std::string& path) { return false; } - // Set a default pixel size of 16px so that font coordinates and shaping - // advances are non-zero by default. FT_Set_Pixel_Sizes(face_, 0, 16); return true; @@ -83,7 +81,6 @@ bool FontFace::loadFromMemory(const std::vector& data) { face_ = nullptr; } - // Copy to internal buffer to guarantee its lifetime aligns with face_ font_data_ = data; if (FT_New_Memory_Face( @@ -98,8 +95,6 @@ bool FontFace::loadFromMemory(const std::vector& data) { return false; } - // Set a default pixel size of 16px so that font coordinates and shaping - // advances are non-zero by default. FT_Set_Pixel_Sizes(face_, 0, 16); return true; @@ -113,10 +108,9 @@ bool FontFace::coversUnicode(uint32_t codepoint) const { if (!face_) return false; std::lock_guard lock(*mutex_); FT_CharMap prev = face_->charmap; - // Ignore failure: if there's no Unicode cmap, keep whatever charmap is current. FT_Select_Charmap(face_, FT_ENCODING_UNICODE); FT_UInt gid = FT_Get_Char_Index(face_, codepoint); - if (prev) FT_Set_Charmap(face_, prev); // restore so shaping/measurement is unaffected + if (prev) FT_Set_Charmap(face_, prev); return gid != 0; } @@ -135,12 +129,10 @@ std::optional FontFace::renderGlyph(unsigned int glyphIndex, unsign std::lock_guard lock(*mutex_); - // Set font size in pixels. if (FT_Set_Pixel_Sizes(face_, 0, fontSize)) { return std::nullopt; } - // Load and render the glyph bitmap into the face->glyph slot. if (FT_Load_Glyph(face_, glyphIndex, FT_LOAD_RENDER)) { return std::nullopt; } @@ -154,10 +146,8 @@ std::optional FontFace::renderGlyph(unsigned int glyphIndex, unsign glyph_bitmap.bearingX = slot->bitmap_left; glyph_bitmap.bearingY = slot->bitmap_top; - // Advance is in 26.6 fractional pixels. Convert to double. glyph_bitmap.advance = static_cast(slot->advance.x) / 64.0; - // Extract the pixels. Pitch specifies bytes per row. if (glyph_bitmap.width > 0 && glyph_bitmap.height > 0) { glyph_bitmap.pixels.resize(glyph_bitmap.width * glyph_bitmap.height); for (int r = 0; r < glyph_bitmap.height; ++r) { @@ -172,4 +162,4 @@ std::optional FontFace::renderGlyph(unsigned int glyphIndex, unsign return glyph_bitmap; } -} // namespace pdfengine::fonts +} diff --git a/engine/src/fonts/face/font_face.hpp b/engine/src/fonts/face/font_face.hpp index 3f524eb..8794ca7 100644 --- a/engine/src/fonts/face/font_face.hpp +++ b/engine/src/fonts/face/font_face.hpp @@ -18,7 +18,6 @@ public: FontFace(); ~FontFace(); - // FontFace is move-only to avoid double freeing FreeType resources. FontFace(const FontFace&) = delete; FontFace& operator=(const FontFace&) = delete; FontFace(FontFace&& other) noexcept; @@ -31,20 +30,15 @@ public: uint64_t getId() const; std::mutex& getMutex() const; - // True if this font can map `codepoint` to a real glyph via its Unicode cmap. Selects the - // Unicode charmap first (so codepoints resolve through the (3,1)/(0,x) subtable rather than a - // symbol cmap that would false-negate), then restores the prior charmap. Used to decide - // whether to keep smart punctuation (en-dash, curly quotes) or ASCII-degrade it on reflow. bool coversUnicode(uint32_t codepoint) const; - // Renders a glyph by index and size, returning a GlyphBitmap on success. std::optional renderGlyph(unsigned int glyphIndex, unsigned int fontSize); private: uint64_t font_id_; FT_Face face_; std::unique_ptr mutex_; - std::vector font_data_; // Keeps the loaded memory buffer alive for FT_Face + std::vector font_data_; }; -} // namespace pdfengine::fonts +} diff --git a/engine/src/fonts/face/free_type_manager.cpp b/engine/src/fonts/face/free_type_manager.cpp index 59a70d4..148399c 100644 --- a/engine/src/fonts/face/free_type_manager.cpp +++ b/engine/src/fonts/face/free_type_manager.cpp @@ -25,4 +25,4 @@ FT_Library FreeTypeManager::getLibrary() const { return library_; } -} // namespace pdfengine::fonts +} diff --git a/engine/src/fonts/face/free_type_manager.hpp b/engine/src/fonts/face/free_type_manager.hpp index 1b04377..bed47cd 100644 --- a/engine/src/fonts/face/free_type_manager.hpp +++ b/engine/src/fonts/face/free_type_manager.hpp @@ -11,7 +11,6 @@ public: FT_Library getLibrary() const; - // Delete copy/move constructors and assignment operators for singleton FreeTypeManager(const FreeTypeManager&) = delete; FreeTypeManager& operator=(const FreeTypeManager&) = delete; FreeTypeManager(FreeTypeManager&&) = delete; @@ -24,4 +23,4 @@ private: FT_Library library_; }; -} // namespace pdfengine::fonts +} diff --git a/engine/src/fonts/loader/font_resolver.cpp b/engine/src/fonts/loader/font_resolver.cpp index a81e683..79f9e74 100644 --- a/engine/src/fonts/loader/font_resolver.cpp +++ b/engine/src/fonts/loader/font_resolver.cpp @@ -21,7 +21,6 @@ std::expected, std::string> FontResolver::resol const auto& bytes = dataRes.value(); - // Build FontDescriptor from FontInfo auto descriptor = std::make_unique(); descriptor->setFontName(fontInfo.fontName); descriptor->setFlags(fontInfo.flags); @@ -29,7 +28,6 @@ std::expected, std::string> FontResolver::resol descriptor->setDescent(fontInfo.descent); descriptor->setCapHeight(fontInfo.capHeight); - // Route to the appropriate FontLoader method based on FontInfo type if (fontInfo.type == "TrueType") { auto font = pdf_fonts::FontLoader::loadTrueTypeFromMemory(fontInfo.normalizedFamily, bytes, std::move(descriptor)); if (font) return font; @@ -47,10 +45,8 @@ std::expected, std::string> FontResolver::resol return std::unexpected("Failed to parse extracted font data"); } else { - // Handle System Fallback spdlog::info("Resolving system fallback font for: {}", fontInfo.fontName); - // Build FontDescriptor from FontInfo auto descriptor = std::make_unique(); descriptor->setFontName(fontInfo.fontName); descriptor->setFlags(fontInfo.flags); @@ -73,4 +69,4 @@ std::expected, std::string> FontResolver::resol } } -} // namespace pdfengine::fonts::loader +} diff --git a/engine/src/fonts/loader/font_resolver.hpp b/engine/src/fonts/loader/font_resolver.hpp index 30f9e21..e95f2d6 100644 --- a/engine/src/fonts/loader/font_resolver.hpp +++ b/engine/src/fonts/loader/font_resolver.hpp @@ -12,13 +12,10 @@ class FontResolver { public: explicit FontResolver(std::shared_ptr document); - // Resolves a FontInfo object into a fully loaded Font ready for shaping. - // If the font is embedded, it extracts the raw bytes from the PdfDocument. - // If it's a system fallback, it uses FontLoader to load it from the OS. std::expected, std::string> resolveFont(const FontInfo& fontInfo); private: std::shared_ptr document_; }; -} // namespace pdfengine::fonts::loader +} diff --git a/engine/src/fonts/pdf_fonts/encoding/cjk_collection_db.cpp b/engine/src/fonts/pdf_fonts/encoding/cjk_collection_db.cpp index 2b93589..58016e8 100644 --- a/engine/src/fonts/pdf_fonts/encoding/cjk_collection_db.cpp +++ b/engine/src/fonts/pdf_fonts/encoding/cjk_collection_db.cpp @@ -5,230 +5,218 @@ namespace pdfengine::fonts::pdf_fonts { uint32_t CjkCollectionDB::resolveCID(const std::string& collection, uint32_t cid) { if (collection == "Adobe-Japan1" || collection.find("Japan") != std::string::npos) { - // Hiragana mapping (basic Hiragana starts from U+3041 to U+3093) - // CIDs 1010 to 1092 inside standard Adobe-Japan1 map to Hiragana if (cid >= 1010 && cid <= 1092) { return 0x3041 + (cid - 1010); } - // Katakana mapping (basic Katakana starts from U+30A1 to U+30F6) - // CIDs 1125 to 1205 inside standard Adobe-Japan1 map to Katakana if (cid >= 1125 && cid <= 1205) { if (cid == 1205) return 0x30F6; return 0x30A1 + (cid - 1125); } - // Expanded Kanji mappings (CIDs 1206-1255) - // Maps common Japanese Kanji in the JIS X 0208 standard block to Unicode if (cid >= 1206 && cid <= 1255) { switch (cid) { - case 1206: return 0x4E00; // 一 - case 1207: return 0x4E01; // 丁 - case 1208: return 0x4E03; // 七 - case 1209: return 0x4E07; // 万 - case 1210: return 0x4E08; // 丈 - case 1211: return 0x4E09; // 三 - case 1212: return 0x4E0A; // 上 - case 1213: return 0x4E0B; // 下 - case 1214: return 0x4E0D; // 不 - case 1215: return 0x4E0E; // 与 - case 1216: return 0x4E10; // 丐 - case 1217: return 0x4E11; // 丑 - case 1218: return 0x4E14; // 且 - case 1219: return 0x4E15; // 丕 - case 1220: return 0x4E16; // 世 - case 1221: return 0x4E17; // 丘 - case 1222: return 0x4E18; // 丙 - case 1223: return 0x4E19; // 両 - case 1224: return 0x4E1D; // 丞 - case 1225: return 0x4E2D; // 中 - case 1226: return 0x4E32; // 串 - case 1227: return 0x4E38; // 丸 - case 1228: return 0x4E39; // 丹 - case 1229: return 0x4E3B; // 主 - case 1230: return 0x4E3C; // 丼 - case 1231: return 0x4E3F; // 丿 - case 1232: return 0x4E42; // 乂 - case 1233: return 0x4E43; // 乃 - case 1234: return 0x4E45; // 久 - case 1235: return 0x4E4B; // 之 - case 1236: return 0x4E4D; // 乍 - case 1237: return 0x4E4E; // 乎 - case 1238: return 0x4E4F; // 乏 - case 1239: return 0x4E56; // 乖 - case 1240: return 0x4E57; // 乗 - case 1241: return 0x4E58; // 乘 - case 1242: return 0x4E59; // 乙 - case 1243: return 0x4E5D; // 九 - case 1244: return 0x4E5E; // 乞 - case 1245: return 0x4E5F; // 也 - case 1246: return 0x4E62; // 乱 - case 1247: return 0x4E73; // 乳 - case 1248: return 0x4E7E; // 乾 - case 1249: return 0x4E82; // 亂 - case 1250: return 0x4E86; // 了 - case 1251: return 0x4E88; // 予 - case 1252: return 0x4E89; // 争 - case 1253: return 0x4E8B; // 事 - case 1254: return 0x4E8C; // 二 - case 1255: return 0x4E8E; // 于 + case 1206: return 0x4E00; + case 1207: return 0x4E01; + case 1208: return 0x4E03; + case 1209: return 0x4E07; + case 1210: return 0x4E08; + case 1211: return 0x4E09; + case 1212: return 0x4E0A; + case 1213: return 0x4E0B; + case 1214: return 0x4E0D; + case 1215: return 0x4E0E; + case 1216: return 0x4E10; + case 1217: return 0x4E11; + case 1218: return 0x4E14; + case 1219: return 0x4E15; + case 1220: return 0x4E16; + case 1221: return 0x4E17; + case 1222: return 0x4E18; + case 1223: return 0x4E19; + case 1224: return 0x4E1D; + case 1225: return 0x4E2D; + case 1226: return 0x4E32; + case 1227: return 0x4E38; + case 1228: return 0x4E39; + case 1229: return 0x4E3B; + case 1230: return 0x4E3C; + case 1231: return 0x4E3F; + case 1232: return 0x4E42; + case 1233: return 0x4E43; + case 1234: return 0x4E45; + case 1235: return 0x4E4B; + case 1236: return 0x4E4D; + case 1237: return 0x4E4E; + case 1238: return 0x4E4F; + case 1239: return 0x4E56; + case 1240: return 0x4E57; + case 1241: return 0x4E58; + case 1242: return 0x4E59; + case 1243: return 0x4E5D; + case 1244: return 0x4E5E; + case 1245: return 0x4E5F; + case 1246: return 0x4E62; + case 1247: return 0x4E73; + case 1248: return 0x4E7E; + case 1249: return 0x4E82; + case 1250: return 0x4E86; + case 1251: return 0x4E88; + case 1252: return 0x4E89; + case 1253: return 0x4E8B; + case 1254: return 0x4E8C; + case 1255: return 0x4E8E; default: break; } } } else if (collection == "Adobe-Korea1" || collection.find("Korea") != std::string::npos) { - // Standard Korean Hangul Syllables mapping (U+AC00 block) - // CIDs 101 to 150 map to the first segment of KS X 1001 Hangul syllables if (cid >= 101 && cid <= 160) { switch (cid) { - case 101: return 0xAC00; // 가 - case 102: return 0xAC01; // 각 - case 103: return 0xAC04; // 간 - case 104: return 0xAC07; // 갇 - case 105: return 0xAC08; // 갈 - case 106: return 0xAC09; // 갉 - case 107: return 0xAC0A; // 갊 - case 108: return 0xAC10; // 감 - case 109: return 0xAC11; // 갑 - case 110: return 0xAC12; // 값 - case 111: return 0xAC13; // 갓 - case 112: return 0xAC14; // 갔 - case 113: return 0xAC15; // 강 - case 114: return 0xAC16; // 갖 - case 115: return 0xAC17; // 갗 - case 116: return 0xAC19; // 같 - case 117: return 0xAC1A; // 갚 - case 118: return 0xAC1B; // 갛 - case 119: return 0xAC1C; // 개 - case 120: return 0xAC1D; // 객 - case 121: return 0xAC20; // 갠 - case 122: return 0xAC24; // 갤 - case 123: return 0xAC2C; // 갬 - case 124: return 0xAC2D; // 갭 - case 125: return 0xAC2F; // 갯 - case 126: return 0xAC30; // 갰 - case 127: return 0xAC31; // 갱 - case 128: return 0xAC38; // 갸 - case 129: return 0xAC39; // 갹 - case 130: return 0xAC3C; // 갼 - case 131: return 0xAC40; // 걀 - case 132: return 0xAC48; // 걈 - case 133: return 0xAC49; // 걉 - case 134: return 0xAC4B; // 걋 - case 135: return 0xAC4C; // 걍 - case 136: return 0xAC54; // 개의 - case 137: return 0xAC70; // 거 - case 138: return 0xAC71; // 걱 - case 139: return 0xAC74; // 건 - case 140: return 0xAC77; // 걷 - case 141: return 0xAC78; // 걸 - case 142: return 0xAC7A; // 걺 - case 143: return 0xAC80; // 검 - case 144: return 0xAC81; // 겁 - case 145: return 0xAC83; // 것 - case 146: return 0xAC84; // 겄 - case 147: return 0xAC85; // 겡 - case 148: return 0xAC8C; // 게 - case 149: return 0xAC8D; // 겐 - case 150: return 0xAC90; // 겔 - case 151: return 0xAC94; // 겝 - case 152: return 0xAC9F; // 겟 - case 153: return 0xACA0; // 겠 - case 154: return 0xACA1; // 겡 - case 155: return 0xACA8; // 겯 - case 156: return 0xACA9; // 결 - case 157: return 0xACB8; // 겸 - case 158: return 0xACB9; // 겹 - case 159: return 0xACBC; // 겻 - case 160: return 0xACBD; // 겼 + case 101: return 0xAC00; + case 102: return 0xAC01; + case 103: return 0xAC04; + case 104: return 0xAC07; + case 105: return 0xAC08; + case 106: return 0xAC09; + case 107: return 0xAC0A; + case 108: return 0xAC10; + case 109: return 0xAC11; + case 110: return 0xAC12; + case 111: return 0xAC13; + case 112: return 0xAC14; + case 113: return 0xAC15; + case 114: return 0xAC16; + case 115: return 0xAC17; + case 116: return 0xAC19; + case 117: return 0xAC1A; + case 118: return 0xAC1B; + case 119: return 0xAC1C; + case 120: return 0xAC1D; + case 121: return 0xAC20; + case 122: return 0xAC24; + case 123: return 0xAC2C; + case 124: return 0xAC2D; + case 125: return 0xAC2F; + case 126: return 0xAC30; + case 127: return 0xAC31; + case 128: return 0xAC38; + case 129: return 0xAC39; + case 130: return 0xAC3C; + case 131: return 0xAC40; + case 132: return 0xAC48; + case 133: return 0xAC49; + case 134: return 0xAC4B; + case 135: return 0xAC4C; + case 136: return 0xAC54; + case 137: return 0xAC70; + case 138: return 0xAC71; + case 139: return 0xAC74; + case 140: return 0xAC77; + case 141: return 0xAC78; + case 142: return 0xAC7A; + case 143: return 0xAC80; + case 144: return 0xAC81; + case 145: return 0xAC83; + case 146: return 0xAC84; + case 147: return 0xAC85; + case 148: return 0xAC8C; + case 149: return 0xAC8D; + case 150: return 0xAC90; + case 151: return 0xAC94; + case 152: return 0xAC9F; + case 153: return 0xACA0; + case 154: return 0xACA1; + case 155: return 0xACA8; + case 156: return 0xACA9; + case 157: return 0xACB8; + case 158: return 0xACB9; + case 159: return 0xACBC; + case 160: return 0xACBD; default: break; } } } else if (collection == "Adobe-CNS1" || collection.find("CNS1") != std::string::npos) { - // Standard Traditional Chinese mappings (U+4E00 block) - // CIDs 100 onwards maps core Traditional Chinese characters if (cid >= 100 && cid <= 140) { switch (cid) { - case 100: return 0x4E00; // 一 - case 101: return 0x4E03; // 七 - case 102: return 0x4E07; // 万 - case 103: return 0x4E09; // 三 - case 104: return 0x4E0A; // 上 - case 105: return 0x4E0B; // 下 - case 106: return 0x4E10; // 丐 - case 107: return 0x4E11; // 丑 - case 108: return 0x4E14; // 且 - case 109: return 0x4E15; // 丕 - case 110: return 0x4E16; // 世 - case 111: return 0x4E18; // 丙 - case 112: return 0x4E2D; // 中 - case 113: return 0x4E86; // 了 - case 114: return 0x4E92; // 互 - case 115: return 0x4E95; // 井 - case 116: return 0x4E99; // 亥 - case 117: return 0x4EBA; // 人 - case 118: return 0x4EC0; // 什 - case 119: return 0x4EC1; // 仁 - case 120: return 0x4EC4; // 仃 - case 121: return 0x4EC6; // 仄 - case 122: return 0x4EC7; // 仇 - case 123: return 0x4ECA; // 今 - case 124: return 0x4ECB; // 介 - case 125: return 0x4ECD; // 仍 - case 126: return 0x4ECE; // 从 - case 127: return 0x4ED4; // 仔 - case 128: return 0x4ED5; // 仕 - case 129: return 0x4ED6; // 他 - case 130: return 0x4ED7; // 仗 - case 131: return 0x4ED8; // 付 - case 132: return 0x4ED9; // 仙 - case 133: return 0x4EDD; // 仝 - case 134: return 0x4EDE; // 仞 - case 135: return 0x4EDF; // 仟 - case 136: return 0x4EE1; // 仡 - case 137: return 0x4EE3; // 代 - case 138: return 0x4EE4; // 令 - case 139: return 0x4EE5; // 以 - case 140: return 0x4F01; // 企 + case 100: return 0x4E00; + case 101: return 0x4E03; + case 102: return 0x4E07; + case 103: return 0x4E09; + case 104: return 0x4E0A; + case 105: return 0x4E0B; + case 106: return 0x4E10; + case 107: return 0x4E11; + case 108: return 0x4E14; + case 109: return 0x4E15; + case 110: return 0x4E16; + case 111: return 0x4E18; + case 112: return 0x4E2D; + case 113: return 0x4E86; + case 114: return 0x4E92; + case 115: return 0x4E95; + case 116: return 0x4E99; + case 117: return 0x4EBA; + case 118: return 0x4EC0; + case 119: return 0x4EC1; + case 120: return 0x4EC4; + case 121: return 0x4EC6; + case 122: return 0x4EC7; + case 123: return 0x4ECA; + case 124: return 0x4ECB; + case 125: return 0x4ECD; + case 126: return 0x4ECE; + case 127: return 0x4ED4; + case 128: return 0x4ED5; + case 129: return 0x4ED6; + case 130: return 0x4ED7; + case 131: return 0x4ED8; + case 132: return 0x4ED9; + case 133: return 0x4EDD; + case 134: return 0x4EDE; + case 135: return 0x4EDF; + case 136: return 0x4EE1; + case 137: return 0x4EE3; + case 138: return 0x4EE4; + case 139: return 0x4EE5; + case 140: return 0x4F01; default: break; } } } else if (collection == "Adobe-GB1" || collection.find("GB1") != std::string::npos) { - // Standard GB simplified Chinese maps - if (cid == 1) return 0x3000; // Ideographic space - if (cid == 2) return 0x3001; // Ideographic comma - if (cid == 3) return 0x3002; // Ideographic full stop + if (cid == 1) return 0x3000; + if (cid == 2) return 0x3001; + if (cid == 3) return 0x3002; - // Additional common GB1 characters if (cid >= 100 && cid <= 120) { switch (cid) { - case 100: return 0x4E00; // 一 - case 101: return 0x4E03; // 七 - case 102: return 0x4E07; // 万 - case 103: return 0x4E09; // 三 - case 104: return 0x4E0A; // 上 - case 105: return 0x4E0B; // 下 - case 106: return 0x4E10; // 丐 - case 107: return 0x4E11; // 丑 - case 108: return 0x4E14; // 且 - case 109: return 0x4E15; // 丕 - case 110: return 0x4E16; // 世 - case 111: return 0x4E18; // 丙 - case 112: return 0x4E2D; // 中 - case 113: return 0x4E86; // 了 - case 114: return 0x4E92; // 互 - case 115: return 0x4E95; // 井 - case 116: return 0x4E99; // 亥 - case 117: return 0x4EBA; // 人 - case 118: return 0x4EC0; // 什 - case 119: return 0x4EC1; // 仁 - case 120: return 0x4EC4; // 仃 + case 100: return 0x4E00; + case 101: return 0x4E03; + case 102: return 0x4E07; + case 103: return 0x4E09; + case 104: return 0x4E0A; + case 105: return 0x4E0B; + case 106: return 0x4E10; + case 107: return 0x4E11; + case 108: return 0x4E14; + case 109: return 0x4E15; + case 110: return 0x4E16; + case 111: return 0x4E18; + case 112: return 0x4E2D; + case 113: return 0x4E86; + case 114: return 0x4E92; + case 115: return 0x4E95; + case 116: return 0x4E99; + case 117: return 0x4EBA; + case 118: return 0x4EC0; + case 119: return 0x4EC1; + case 120: return 0x4EC4; default: break; } } } - return 0; // fallback + return 0; } -} // namespace pdfengine::fonts::pdf_fonts +} diff --git a/engine/src/fonts/pdf_fonts/encoding/cjk_collection_db.hpp b/engine/src/fonts/pdf_fonts/encoding/cjk_collection_db.hpp index 99247cb..49cca37 100644 --- a/engine/src/fonts/pdf_fonts/encoding/cjk_collection_db.hpp +++ b/engine/src/fonts/pdf_fonts/encoding/cjk_collection_db.hpp @@ -7,9 +7,7 @@ namespace pdfengine::fonts::pdf_fonts { class CjkCollectionDB { public: - // Resolves a CID inside a standard collection (e.g. "Adobe-Japan1") to a Unicode codepoint. - // Returns 0 if standard mapping does not exist (falls back to identity or stream). static uint32_t resolveCID(const std::string& collection, uint32_t cid); }; -} // namespace pdfengine::fonts::pdf_fonts +} diff --git a/engine/src/fonts/pdf_fonts/encoding/encoding.cpp b/engine/src/fonts/pdf_fonts/encoding/encoding.cpp index 2d1981e..45a01a9 100644 --- a/engine/src/fonts/pdf_fonts/encoding/encoding.cpp +++ b/engine/src/fonts/pdf_fonts/encoding/encoding.cpp @@ -10,61 +10,58 @@ namespace pdfengine::fonts::pdf_fonts { namespace { -// Maps Windows CP-1252 exceptions (0x80 - 0x9F) for WinAnsiEncoding uint32_t getWinAnsiException(uint32_t code) { switch (code) { - case 128: return 0x20AC; // Euro - case 130: return 0x201A; // Single low-9 quote - case 131: return 0x0192; // Florin - case 132: return 0x201E; // Double low-9 quote - case 133: return 0x2026; // Ellipsis - case 134: return 0x2020; // Dagger - case 135: return 0x2021; // Double Dagger - case 136: return 0x02C6; // Circumflex - case 137: return 0x2030; // Per mille - case 138: return 0x0160; // S Caron - case 139: return 0x2039; // Single guillemet left - case 140: return 0x0152; // OE - case 142: return 0x017D; // Z Caron - case 145: return 0x2018; // Single quote left - case 146: return 0x2019; // Single quote right - case 147: return 0x201C; // Double quote left - case 148: return 0x201D; // Double quote right - case 149: return 0x2022; // Bullet - case 150: return 0x2013; // En dash - case 151: return 0x2014; // Em dash - case 152: return 0x02DC; // Tilde - case 153: return 0x2122; // Trademark - case 154: return 0x0161; // s Caron - case 155: return 0x203A; // Single guillemet right - case 156: return 0x0153; // oe - case 158: return 0x017E; // z Caron - case 159: return 0x0178; // Y Dieresis + case 128: return 0x20AC; + case 130: return 0x201A; + case 131: return 0x0192; + case 132: return 0x201E; + case 133: return 0x2026; + case 134: return 0x2020; + case 135: return 0x2021; + case 136: return 0x02C6; + case 137: return 0x2030; + case 138: return 0x0160; + case 139: return 0x2039; + case 140: return 0x0152; + case 142: return 0x017D; + case 145: return 0x2018; + case 146: return 0x2019; + case 147: return 0x201C; + case 148: return 0x201D; + case 149: return 0x2022; + case 150: return 0x2013; + case 151: return 0x2014; + case 152: return 0x02DC; + case 153: return 0x2122; + case 154: return 0x0161; + case 155: return 0x203A; + case 156: return 0x0153; + case 158: return 0x017E; + case 159: return 0x0178; default: return 0; } } -// Maps Standard MacRoman characters from 128 to 255 const uint32_t kMacRomanHighPage[128] = { - 0x00C4, 0x00C5, 0x00C7, 0x00C9, 0x00D1, 0x00D6, 0x00DC, 0x00E1, // 128-135 - 0x00E0, 0x00E2, 0x00E4, 0x00E3, 0x00E5, 0x00E7, 0x00E9, 0x00E8, // 136-143 - 0x00EA, 0x00EB, 0x00ED, 0x00EC, 0x00EE, 0x00EF, 0x00F1, 0x00F3, // 144-151 - 0x00F2, 0x00F4, 0x00F6, 0x00F5, 0x00FA, 0x00F9, 0x00FB, 0x00FC, // 152-159 - 0x2020, 0x00B0, 0x00A2, 0x00A3, 0x00A7, 0x2022, 0x00B6, 0x00DF, // 160-167 - 0x00AE, 0x00A9, 0x2122, 0x00B4, 0x00A8, 0x2260, 0x00C6, 0x00D8, // 168-175 - 0x221E, 0x00B1, 0x2264, 0x2265, 0x00A5, 0x00B5, 0x2202, 0x2211, // 176-183 - 0x220F, 0x03C0, 0x222B, 0x00AA, 0x00BA, 0x2126, 0x00E6, 0x00F8, // 184-191 - 0x00BF, 0x00A1, 0x00AC, 0x221A, 0x0192, 0x2248, 0x2206, 0x00AB, // 192-199 - 0x00BB, 0x2026, 0x00A0, 0x00C0, 0x00C3, 0x00D5, 0x0152, 0x0153, // 200-207 - 0x2013, 0x2014, 0x201C, 0x201D, 0x2018, 0x2019, 0x00F7, 0x25CA, // 208-215 - 0x00FF, 0x0178, 0x2044, 0x00A4, 0x2039, 0x203A, 0xFB01, 0xFB02, // 216-223 - 0x2021, 0x00B7, 0x201A, 0x201E, 0x2030, 0x00C2, 0x00CA, 0x00C1, // 224-231 - 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF, 0x00CC, 0x00D3, 0x00D4, // 232-239 - 0xF8FF, 0x00D2, 0x00DA, 0x00DB, 0x00D9, 0x0131, 0x02C6, 0x02DC, // 240-247 - 0x00AF, 0x02D8, 0x02D9, 0x02DA, 0x00B8, 0x02DD, 0x02DB, 0x02C7 // 248-255 + 0x00C4, 0x00C5, 0x00C7, 0x00C9, 0x00D1, 0x00D6, 0x00DC, 0x00E1, + 0x00E0, 0x00E2, 0x00E4, 0x00E3, 0x00E5, 0x00E7, 0x00E9, 0x00E8, + 0x00EA, 0x00EB, 0x00ED, 0x00EC, 0x00EE, 0x00EF, 0x00F1, 0x00F3, + 0x00F2, 0x00F4, 0x00F6, 0x00F5, 0x00FA, 0x00F9, 0x00FB, 0x00FC, + 0x2020, 0x00B0, 0x00A2, 0x00A3, 0x00A7, 0x2022, 0x00B6, 0x00DF, + 0x00AE, 0x00A9, 0x2122, 0x00B4, 0x00A8, 0x2260, 0x00C6, 0x00D8, + 0x221E, 0x00B1, 0x2264, 0x2265, 0x00A5, 0x00B5, 0x2202, 0x2211, + 0x220F, 0x03C0, 0x222B, 0x00AA, 0x00BA, 0x2126, 0x00E6, 0x00F8, + 0x00BF, 0x00A1, 0x00AC, 0x221A, 0x0192, 0x2248, 0x2206, 0x00AB, + 0x00BB, 0x2026, 0x00A0, 0x00C0, 0x00C3, 0x00D5, 0x0152, 0x0153, + 0x2013, 0x2014, 0x201C, 0x201D, 0x2018, 0x2019, 0x00F7, 0x25CA, + 0x00FF, 0x0178, 0x2044, 0x00A4, 0x2039, 0x203A, 0xFB01, 0xFB02, + 0x2021, 0x00B7, 0x201A, 0x201E, 0x2030, 0x00C2, 0x00CA, 0x00C1, + 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF, 0x00CC, 0x00D3, 0x00D4, + 0xF8FF, 0x00D2, 0x00DA, 0x00DB, 0x00D9, 0x0131, 0x02C6, 0x02DC, + 0x00AF, 0x02D8, 0x02D9, 0x02DA, 0x00B8, 0x02DD, 0x02DB, 0x02C7 }; -// Safe hexadecimal digit conversions bool parseHexValue(const std::string& hexStr, uint32_t& value) { if (hexStr.empty()) return false; std::string cleanHex; @@ -83,11 +80,8 @@ bool parseHexValue(const std::string& hexStr, uint32_t& value) { } } -} // namespace +} -// ========================================== -// PredefinedEncoding Implementation -// ========================================== PredefinedEncoding::PredefinedEncoding(SimpleEncodingType type) : type_(type) {} uint32_t PredefinedEncoding::decode(uint32_t charCode) const { @@ -115,9 +109,6 @@ uint32_t PredefinedEncoding::decode(uint32_t charCode) const { return charCode; } -// ========================================== -// CustomEncoding Implementation -// ========================================== CustomEncoding::CustomEncoding(std::unique_ptr baseEncoding) : base_(std::move(baseEncoding)) {} @@ -136,9 +127,6 @@ void CustomEncoding::addDifference(uint32_t code, const std::string& glyphName) } } -// ========================================== -// ToUnicodeCMap Implementation -// ========================================== uint32_t ToUnicodeCMap::decode(uint32_t charCode) const { auto it = cmap_.find(charCode); if (it != cmap_.end()) { @@ -151,9 +139,6 @@ bool ToUnicodeCMap::parseCMapStream(const std::string& streamStr) { return ToUnicodeParser::parse(streamStr, cmap_); } -// ========================================== -// Adobe Glyph List (AGL) Implementation -// ========================================== uint32_t resolveGlyphNameToUnicode(const std::string& name) { if (name.empty()) return 0; @@ -209,4 +194,4 @@ uint32_t resolveGlyphNameToUnicode(const std::string& name) { return 0; } -} // namespace pdfengine::fonts::pdf_fonts +} diff --git a/engine/src/fonts/pdf_fonts/encoding/encoding.hpp b/engine/src/fonts/pdf_fonts/encoding/encoding.hpp index 9868c15..5be200d 100644 --- a/engine/src/fonts/pdf_fonts/encoding/encoding.hpp +++ b/engine/src/fonts/pdf_fonts/encoding/encoding.hpp @@ -17,16 +17,13 @@ enum class SimpleEncodingType { Identity_V }; -// Base interface for PDF character code to Unicode codepoint translation class Encoding { public: virtual ~Encoding() = default; - // Translates a PDF character code (typically 1-4 bytes) to a Unicode codepoint virtual uint32_t decode(uint32_t charCode) const = 0; }; -// Standard predefined simple encodings class PredefinedEncoding : public Encoding { public: explicit PredefinedEncoding(SimpleEncodingType type); @@ -39,7 +36,6 @@ private: SimpleEncodingType type_; }; -// Custom encodings constructed with a base predefined encoding and a set of `/Differences` class CustomEncoding : public Encoding { public: explicit CustomEncoding(std::unique_ptr baseEncoding); @@ -47,7 +43,6 @@ public: uint32_t decode(uint32_t charCode) const override; - // Maps a character code to a postscript glyph name using Adobe Glyph List (AGL) void addDifference(uint32_t code, const std::string& glyphName); private: @@ -55,7 +50,6 @@ private: std::unordered_map custom_map_; }; -// Advanced CMaps for CJK/CID /ToUnicode translation streams class ToUnicodeCMap : public Encoding { public: ToUnicodeCMap() = default; @@ -63,10 +57,8 @@ public: uint32_t decode(uint32_t charCode) const override; - // Parses a `/ToUnicode` CMap definition from a PDF stream string bool parseCMapStream(const std::string& streamStr); - // Explicitly add mapping for testing void addMapping(uint32_t code, uint32_t unicode) { cmap_[code] = unicode; } @@ -75,7 +67,6 @@ private: std::unordered_map cmap_; }; -// Adobe Glyph List resolver: translates a standard PostScript glyph name to a Unicode codepoint. uint32_t resolveGlyphNameToUnicode(const std::string& name); -} // namespace pdfengine::fonts::pdf_fonts +} diff --git a/engine/src/fonts/pdf_fonts/encoding/tounicode_parser.cpp b/engine/src/fonts/pdf_fonts/encoding/tounicode_parser.cpp index 25ba819..1a208cc 100644 --- a/engine/src/fonts/pdf_fonts/encoding/tounicode_parser.cpp +++ b/engine/src/fonts/pdf_fonts/encoding/tounicode_parser.cpp @@ -9,7 +9,6 @@ namespace pdfengine::fonts::pdf_fonts { namespace { -// Helper to convert hex strings (e.g. "<001E>") to uint32_t bool parseHexValue(const std::string& hexStr, uint32_t& value) { if (hexStr.empty()) return false; std::string cleanHex; @@ -28,20 +27,17 @@ bool parseHexValue(const std::string& hexStr, uint32_t& value) { } } -// Tokenizes CMap stream input, ignoring comments and robustly handling delimiters to prevent hangs std::vector tokenizeCMap(const std::string& input) { std::vector tokens; size_t i = 0; size_t len = input.length(); while (i < len) { - // Skip whitespaces if (std::isspace(static_cast(input[i])) || input[i] == '\0') { i++; continue; } - // Bypassing comments starting with % if (input[i] == '%') { while (i < len && input[i] != '\n' && input[i] != '\r') { i++; @@ -49,7 +45,6 @@ std::vector tokenizeCMap(const std::string& input) { continue; } - // Check for double angle brackets << and >> (delimiters) if (i + 1 < len && input[i] == '<' && input[i + 1] == '<') { tokens.push_back("<<"); i += 2; @@ -61,7 +56,6 @@ std::vector tokenizeCMap(const std::string& input) { continue; } - // Parse hex strings <001E> if (input[i] == '<') { std::string hexToken; hexToken += input[i++]; @@ -75,7 +69,6 @@ std::vector tokenizeCMap(const std::string& input) { continue; } - // Check for other standalone delimiters if (input[i] == '[' || input[i] == ']' || input[i] == '>') { std::string delimToken(1, input[i]); tokens.push_back(delimToken); @@ -83,7 +76,6 @@ std::vector tokenizeCMap(const std::string& input) { continue; } - // Standard token parsing std::string token; while (i < len && !std::isspace(static_cast(input[i])) && input[i] != '\0' && input[i] != '%' && input[i] != '<' && input[i] != '>' && @@ -97,7 +89,7 @@ std::vector tokenizeCMap(const std::string& input) { return tokens; } -} // namespace +} bool ToUnicodeParser::parse(const std::string& streamStr, std::unordered_map& outMap) { std::vector tokens = tokenizeCMap(streamStr); @@ -141,8 +133,7 @@ bool ToUnicodeParser::parse(const std::string& streamStr, std::unordered_map <0003> [<0041> <0042> <0043>] - idx += 3; // skip start, end, "[" + idx += 3; uint32_t currentCode = startCode; while (idx < size && tokens[idx] != "]" && currentCode <= endCode) { uint32_t destVal = 0; @@ -159,7 +150,6 @@ bool ToUnicodeParser::parse(const std::string& streamStr, std::unordered_map <0003> <0041> uint32_t destStart = 0; if (parseHexValue(destToken, destStart)) { for (uint32_t code = startCode; code <= endCode; ++code) { @@ -182,4 +172,4 @@ bool ToUnicodeParser::parse(const std::string& streamStr, std::unordered_map& outMap); }; -} // namespace pdfengine::fonts::pdf_fonts +} diff --git a/engine/src/fonts/pdf_fonts/font.hpp b/engine/src/fonts/pdf_fonts/font.hpp index 9fdcc2d..6730730 100644 --- a/engine/src/fonts/pdf_fonts/font.hpp +++ b/engine/src/fonts/pdf_fonts/font.hpp @@ -40,19 +40,15 @@ public: virtual FontType getType() const = 0; virtual bool isEmbedded() const = 0; - // Gets reference to underlying FontFace rendering object virtual pdfengine::fonts::FontFace& getFontFace() = 0; virtual const pdfengine::fonts::FontFace& getFontFace() const = 0; - // Gets the font descriptor (returns nullptr if none exists) virtual const FontDescriptor* getDescriptor() const = 0; - // Gets the font encoding (returns nullptr if none exists) virtual const Encoding* getEncoding() const = 0; virtual const FontSubset* getSubsetInfo() const = 0; - // Advanced Layout APIs virtual FontSource getSourceType() const { return source_type_; } virtual void setSourceType(FontSource source) { source_type_ = source; } @@ -87,17 +83,15 @@ public: FT_Face rawFace = getFontFace().getFace(); FT_Set_Pixel_Sizes(rawFace, 0, static_cast(fontSize)); - // Convert from 26.6 to double metrics_cache_.ascent = static_cast(rawFace->size->metrics.ascender) / 64.0; metrics_cache_.descent = static_cast(rawFace->size->metrics.descender) / 64.0; metrics_cache_.lineGap = static_cast(rawFace->size->metrics.height - rawFace->size->metrics.ascender + rawFace->size->metrics.descender) / 64.0; - // Heuristic for capHeight: height of 'H' FT_UInt hIndex = FT_Get_Char_Index(rawFace, 'H'); if (hIndex > 0 && FT_Load_Glyph(rawFace, hIndex, FT_LOAD_DEFAULT) == 0) { metrics_cache_.capHeight = static_cast(rawFace->glyph->metrics.horiBearingY) / 64.0; } else { - metrics_cache_.capHeight = metrics_cache_.ascent * 0.7; // Fallback + metrics_cache_.capHeight = metrics_cache_.ascent * 0.7; } metrics_cached_ = true; @@ -106,13 +100,10 @@ public: return metrics_cache_; } - // Translates a raw character code to a Unicode codepoint virtual uint32_t decodeToUnicode(uint32_t charCode) const = 0; - // Translates a sequence of raw character codes to a UTF-8 string virtual std::string decodeStringToUnicode(const std::vector& charCodes) const = 0; - // Widths methods virtual void setWidths(uint32_t firstChar, uint32_t lastChar, const std::vector& widths) { first_char_ = firstChar; last_char_ = lastChar; @@ -131,12 +122,9 @@ public: if (charCode >= first_char_ && charCode <= last_char_) { size_t index = charCode - first_char_; if (index < widths_.size()) { - // PDF widths are in 1/1000 units of text space. - // Scale to requested fontSize. return (widths_[index] / 1000.0) * fontSize; } } - // Fallback to missing width if descriptor is available const auto* desc = getDescriptor(); if (desc && desc->getMissingWidth() > 0.0) { return (desc->getMissingWidth() / 1000.0) * fontSize; @@ -198,4 +186,4 @@ protected: bool has_vertical_metrics_ = false; }; -} // namespace pdfengine::fonts::pdf_fonts +} diff --git a/engine/src/fonts/pdf_fonts/font_descriptor.cpp b/engine/src/fonts/pdf_fonts/font_descriptor.cpp index af21460..3d9e7ec 100644 --- a/engine/src/fonts/pdf_fonts/font_descriptor.cpp +++ b/engine/src/fonts/pdf_fonts/font_descriptor.cpp @@ -13,7 +13,6 @@ void skipWhitespace(const std::string& str, size_t& pos) { if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\0') { pos++; } else if (c == '%') { - // comment, skip to end of line while (pos < str.size() && str[pos] != '\n' && str[pos] != '\r') { pos++; } @@ -23,32 +22,32 @@ void skipWhitespace(const std::string& str, size_t& pos) { } } -} // namespace +} bool FontDescriptor::parseFromDictionaryString(const std::string& dictStr) { size_t pos = 0; skipWhitespace(dictStr, pos); if (pos + 2 > dictStr.size() || dictStr[pos] != '<' || dictStr[pos+1] != '<') { - return false; // must start with << + return false; } pos += 2; while (true) { skipWhitespace(dictStr, pos); if (pos >= dictStr.size()) { - return false; // missing closing >> + return false; } if (pos + 2 <= dictStr.size() && dictStr[pos] == '>' && dictStr[pos+1] == '>') { pos += 2; - break; // successfully reached closing >> + break; } if (dictStr[pos] != '/') { - return false; // key must start with / + return false; } - pos++; // skip '/' + pos++; size_t keyStart = pos; while (pos < dictStr.size()) { @@ -61,19 +60,19 @@ bool FontDescriptor::parseFromDictionaryString(const std::string& dictStr) { pos++; } if (pos == keyStart) { - return false; // empty key name + return false; } std::string key = dictStr.substr(keyStart, pos - keyStart); skipWhitespace(dictStr, pos); if (pos >= dictStr.size()) { - return false; // key with no value + return false; } std::string valueStr; if (dictStr[pos] == '[') { size_t arrStart = pos; - pos++; // skip '[' + pos++; int depth = 1; while (pos < dictStr.size() && depth > 0) { if (dictStr[pos] == '[') depth++; @@ -81,11 +80,11 @@ bool FontDescriptor::parseFromDictionaryString(const std::string& dictStr) { pos++; } if (depth > 0) { - return false; // unmatched brackets + return false; } valueStr = dictStr.substr(arrStart, pos - arrStart); } else if (dictStr[pos] == '/') { - pos++; // skip '/' + pos++; size_t valStart = pos; while (pos < dictStr.size()) { char c = dictStr[pos]; @@ -157,7 +156,7 @@ bool FontDescriptor::parseFromDictionaryString(const std::string& dictStr) { idx += processed; } if (nums.size() != 4) { - return false; // FontBBox must have 4 coordinates + return false; } font_bbox_.llx = static_cast(nums[0]); font_bbox_.lly = static_cast(nums[1]); @@ -165,11 +164,11 @@ bool FontDescriptor::parseFromDictionaryString(const std::string& dictStr) { font_bbox_.ury = static_cast(nums[3]); } } catch (const std::exception&) { - return false; // parsing or range exception + return false; } } return true; } -} // namespace pdfengine::fonts::pdf_fonts +} diff --git a/engine/src/fonts/pdf_fonts/font_descriptor.hpp b/engine/src/fonts/pdf_fonts/font_descriptor.hpp index eaad43c..3ccd629 100644 --- a/engine/src/fonts/pdf_fonts/font_descriptor.hpp +++ b/engine/src/fonts/pdf_fonts/font_descriptor.hpp @@ -21,7 +21,6 @@ public: FontDescriptor() = default; ~FontDescriptor() = default; - // Getters and Setters std::string getFontName() const { return font_name_; } void setFontName(const std::string& name) { font_name_ = name; } @@ -64,7 +63,6 @@ public: double getMissingWidth() const { return missing_width_; } void setMissingWidth(double missing_width) { missing_width_ = missing_width; } - // Flags helper functions (PDF Spec Section 5.7.1) bool isFixedPitch() const { return (flags_ & 1) != 0; } bool isSerif() const { return (flags_ & 2) != 0; } bool isSymbolic() const { return (flags_ & 4) != 0; } @@ -75,7 +73,6 @@ public: bool isSmallCap() const { return (flags_ & 131072) != 0; } bool isForceBold() const { return (flags_ & 262144) != 0; } - // Parses a PDF dictionary string representing a FontDescriptor. bool parseFromDictionaryString(const std::string& dictStr); private: @@ -95,4 +92,4 @@ private: double missing_width_ = 0.0; }; -} // namespace pdfengine::fonts::pdf_fonts +} diff --git a/engine/src/fonts/pdf_fonts/font_fallback.cpp b/engine/src/fonts/pdf_fonts/font_fallback.cpp index d7e0ef2..b28f74c 100644 --- a/engine/src/fonts/pdf_fonts/font_fallback.cpp +++ b/engine/src/fonts/pdf_fonts/font_fallback.cpp @@ -13,7 +13,7 @@ std::string toLower(const std::string& str) { }); return lower; } -} // namespace +} FontFallback& FontFallback::getInstance() { static FontFallback instance; @@ -157,11 +157,6 @@ std::string FontFallback::getFallbackFontPath(const std::string& fontName, bool } } - // Generic bundled catch-all: any font that matched NO specific rule above (an exotic/embedded - // family we don't recognise) still gets a real, present-on-both-engines fallback instead of OS - // Arial (native only) or nothing (WASM -> garble). Pick serif (Tinos) vs sans (Carlito) from the - // name; default sans. Recognised families (helvetica/arial/times/calibri/courier) already - // returned above, so this never overrides them. Keeps WASM == native for every font. #ifdef PDFENGINE_FONT_DIR { auto has = [&](const char* s) { return lowerName.find(s) != std::string::npos; }; diff --git a/engine/src/fonts/pdf_fonts/font_fallback.hpp b/engine/src/fonts/pdf_fonts/font_fallback.hpp index 1692ea4..5811db5 100644 --- a/engine/src/fonts/pdf_fonts/font_fallback.hpp +++ b/engine/src/fonts/pdf_fonts/font_fallback.hpp @@ -36,4 +36,4 @@ private: mutable std::mutex rules_mutex_; }; -} // namespace pdfengine::fonts::pdf_fonts +} diff --git a/engine/src/fonts/pdf_fonts/font_loader.cpp b/engine/src/fonts/pdf_fonts/font_loader.cpp index bbaa380..17daf20 100644 --- a/engine/src/fonts/pdf_fonts/font_loader.cpp +++ b/engine/src/fonts/pdf_fonts/font_loader.cpp @@ -41,7 +41,6 @@ std::unique_ptr FontLoader::loadType1SystemFallback( std::unique_ptr descriptor, std::unique_ptr encoding ) { - // Determine bold/italic style modifiers from name std::string lowerName = baseFont; std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); @@ -50,7 +49,6 @@ std::unique_ptr FontLoader::loadType1SystemFallback( bool bold = (lowerName.find("bold") != std::string::npos); bool italic = (lowerName.find("italic") != std::string::npos || lowerName.find("oblique") != std::string::npos); - // Resolve system path dynamically via fallback manager std::string fontPath = FontFallback::getInstance().getFallbackFontPath(baseFont, bold, italic); auto font = std::make_unique(baseFont, false, std::move(descriptor), std::move(encoding)); @@ -80,7 +78,6 @@ std::unique_ptr FontLoader::loadCIDFontSystemFallback( std::unique_ptr descriptor, std::unique_ptr encoding ) { - // Determine CJK style modifiers if any std::string lowerName = baseFont; std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); @@ -88,7 +85,6 @@ std::unique_ptr FontLoader::loadCIDFontSystemFallback( bool bold = (lowerName.find("bold") != std::string::npos); bool italic = (lowerName.find("italic") != std::string::npos || lowerName.find("oblique") != std::string::npos); - // Resolve system path dynamically via fallback manager std::string fontPath = FontFallback::getInstance().getFallbackFontPath(baseFont, bold, italic); auto font = std::make_unique(baseFont, subtype, false, std::move(descriptor), std::move(encoding)); @@ -98,4 +94,4 @@ std::unique_ptr FontLoader::loadCIDFontSystemFallback( return font; } -} // namespace pdfengine::fonts::pdf_fonts +} diff --git a/engine/src/fonts/pdf_fonts/font_loader.hpp b/engine/src/fonts/pdf_fonts/font_loader.hpp index 9e6cf20..f066861 100644 --- a/engine/src/fonts/pdf_fonts/font_loader.hpp +++ b/engine/src/fonts/pdf_fonts/font_loader.hpp @@ -11,7 +11,6 @@ namespace pdfengine::fonts::pdf_fonts { class FontLoader { public: - // Factory method to load an embedded TrueType font from its raw stream bytes static std::unique_ptr loadTrueTypeFromMemory( const std::string& baseFont, const std::vector& streamData, @@ -19,7 +18,6 @@ public: std::unique_ptr encoding = nullptr ); - // Factory method to load an embedded Type1 font from raw memory bytes static std::unique_ptr loadType1FromMemory( const std::string& baseFont, const std::vector& streamData, @@ -27,26 +25,23 @@ public: std::unique_ptr encoding = nullptr ); - // Factory method to load a non-embedded standard Type1 font (resolves to standard system fallback) static std::unique_ptr loadType1SystemFallback( const std::string& baseFont, std::unique_ptr descriptor = nullptr, std::unique_ptr encoding = nullptr ); - // Factory method to load an embedded CIDFont from raw stream bytes static std::unique_ptr loadCIDFontFromMemory( const std::string& baseFont, - FontType subtype, // CIDFontType0 or CIDFontType2 + FontType subtype, const std::vector& streamData, std::unique_ptr descriptor = nullptr, std::unique_ptr encoding = nullptr ); - // Factory method to load a non-embedded CJK CIDFont (resolves to CJK system fallbacks) static std::unique_ptr loadCIDFontSystemFallback( const std::string& baseFont, - FontType subtype, // CIDFontType0 or CIDFontType2 + FontType subtype, std::unique_ptr descriptor = nullptr, std::unique_ptr encoding = nullptr ); diff --git a/engine/src/fonts/pdf_fonts/font_subset.cpp b/engine/src/fonts/pdf_fonts/font_subset.cpp index 40a2e24..8cc8a99 100644 --- a/engine/src/fonts/pdf_fonts/font_subset.cpp +++ b/engine/src/fonts/pdf_fonts/font_subset.cpp @@ -16,8 +16,6 @@ void FontSubset::populateFromFace(FontSubset& subset, void* ftFace) { FT_UInt gindex; FT_ULong charcode = FT_Get_First_Char(face, &gindex); while (gindex != 0) { - // In embedded subsets, the TrueType cmap typically maps the - // Original GID or CID (charcode) to the new Subset GID (gindex). subset.addGlyphMapping(gindex, static_cast(charcode)); charcode = FT_Get_Next_Char(face, charcode, &gindex); } @@ -55,10 +53,8 @@ std::vector FontSubset::buildSubset(const std::vector& origina hb_set_add(glyph_set, gid); } - // Always keep GID 0 (the .notdef glyph) hb_set_add(glyph_set, 0); - // Retain layout tables for complex text shaping hb_subset_input_set_flags(input, HB_SUBSET_FLAGS_RETAIN_GIDS); hb_face_t* subset_face = hb_subset_or_fail(face, input); @@ -81,12 +77,6 @@ std::vector FontSubset::buildSubset(const std::vector& origina return result; } -// Overwrite a few bytes of the sfnt 'name' table strings (family/full/PostScript/typographic -// names) with a deterministic seed so the subset has a UNIQUE name. Two reflow subsets of the -// same base font (e.g. Calibri) otherwise share the name "Calibri", and PDFium merges them — -// making the second subset's text map to the first's glyphs (scrambled output). Identical inputs -// hash to the same seed → same name → safe to share. The name is only used for de-dup; rendering -// uses the embedded glyphs + cmap, so a "garbled" name is harmless. static void makeFontNameUnique(std::vector& font, uint64_t seed) { auto rd16 = [&](size_t o) -> uint32_t { return (o + 1 < font.size()) ? (uint32_t(font[o]) << 8) | font[o + 1] : 0; }; auto rd32 = [&](size_t o) -> uint32_t { return (o + 3 < font.size()) ? (uint32_t(font[o]) << 24) | (uint32_t(font[o + 1]) << 16) | (uint32_t(font[o + 2]) << 8) | font[o + 3] : 0; }; @@ -109,7 +99,7 @@ static void makeFontNameUnique(std::vector& font, uint64_t seed) { uint32_t len = rd16(rec + 8); size_t s = storageOff + rd16(rec + 10); for (uint32_t b = 0; b < len && b < 8; ++b) { - if (s + b < font.size()) font[s + b] = static_cast('A' + ((seed >> ((b % 8) * 4)) & 0x0F)); // 'A'..'P' + if (s + b < font.size()) font[s + b] = static_cast('A' + ((seed >> ((b % 8) * 4)) & 0x0F)); } } } @@ -117,11 +107,6 @@ static void makeFontNameUnique(std::vector& font, uint64_t seed) { std::vector FontSubset::buildSubsetByUnicode(const std::vector& originalStream, const std::vector& codepoints) { - // Subset a font down to only the glyphs needed for `codepoints`, for embedding - // a substitute font compactly (Acrobat-style). Unlike buildSubset (GID-based, - // RETAIN_GIDS for existing subset streams), this drives the subset by UNICODE - // and lets HarfBuzz compact glyph ids + rebuild the cmap, so PDFium's - // FPDFText_SetText (unicode → glyph via cmap) maps correctly into the result. std::vector result; if (originalStream.empty() || codepoints.empty()) { return result; @@ -152,7 +137,6 @@ std::vector FontSubset::buildSubsetByUnicode(const std::vector hb_set_add(unicodes, cp); } } - // No RETAIN_GIDS flag: compact the glyph ids and rebuild the cmap. hb_face_t* subset_face = hb_subset_or_fail(face, input); hb_subset_input_destroy(input); @@ -223,4 +207,4 @@ bool FontSubset::hasGlyphMapping(uint32_t subsetGid) const { return subset_to_original_map_.find(subsetGid) != subset_to_original_map_.end(); } -} // namespace pdfengine::fonts::pdf_fonts +} diff --git a/engine/src/fonts/pdf_fonts/font_subset.hpp b/engine/src/fonts/pdf_fonts/font_subset.hpp index 8670054..46e61f0 100644 --- a/engine/src/fonts/pdf_fonts/font_subset.hpp +++ b/engine/src/fonts/pdf_fonts/font_subset.hpp @@ -16,14 +16,10 @@ public: static std::string getSubsetPrefix(const std::string& fontName); - // Iterates the FT_Face cmap to populate the mapping dictionary static void populateFromFace(FontSubset& subset, void* ftFace); - // Rebuilds the TTF/CID stream keeping only the specified GIDs using HarfBuzz static std::vector buildSubset(const std::vector& originalStream, const std::vector& glyphIdsToKeep); - // Subset a font by the UNICODE codepoints used (compacts GIDs + rebuilds cmap). - // Use when embedding a substitute font for newly-typed text. static std::vector buildSubsetByUnicode(const std::vector& originalStream, const std::vector& codepoints); explicit FontSubset(const std::string& fontName); @@ -45,4 +41,4 @@ private: std::unordered_map subset_to_original_map_; }; -} // namespace pdfengine::fonts::pdf_fonts +} diff --git a/engine/src/fonts/pdf_fonts/types/cid_font.cpp b/engine/src/fonts/pdf_fonts/types/cid_font.cpp index f9faebe..c8d5322 100644 --- a/engine/src/fonts/pdf_fonts/types/cid_font.cpp +++ b/engine/src/fonts/pdf_fonts/types/cid_font.cpp @@ -33,7 +33,7 @@ std::string unicodeToUtf8(uint32_t codepoint) { return utf8; } -} // namespace +} CIDFont::CIDFont( const std::string& baseFont, @@ -128,7 +128,6 @@ uint32_t CIDFont::decodeToUnicode(uint32_t charCode) const { } } - // Check standard collection DB (e.g. Adobe-Japan1) if (descriptor_) { std::string fontName = descriptor_->getFontName(); std::string lowerName = fontName; @@ -171,7 +170,6 @@ uint32_t CIDFont::decodeToUnicode(uint32_t charCode) const { } if (registry != "None") { - // Resolve CJK collections (e.g. matching standard Japanese font mappings) uint32_t cjkResolved = CjkCollectionDB::resolveCID(registry, charCode); if (cjkResolved != 0) { return cjkResolved; @@ -226,4 +224,4 @@ void CIDFont::buildGidToUnicodeMap() const { is_gid_to_unicode_map_built_ = true; } -} // namespace pdfengine::fonts::pdf_fonts +} diff --git a/engine/src/fonts/pdf_fonts/types/cid_font.hpp b/engine/src/fonts/pdf_fonts/types/cid_font.hpp index fe3a93a..d01a92a 100644 --- a/engine/src/fonts/pdf_fonts/types/cid_font.hpp +++ b/engine/src/fonts/pdf_fonts/types/cid_font.hpp @@ -17,17 +17,15 @@ class CIDFont : public Font { public: CIDFont( const std::string& baseFont, - FontType subtype, // CIDFontType0 or CIDFontType2 + FontType subtype, bool isEmbedded, std::unique_ptr descriptor = nullptr, std::unique_ptr encoding = nullptr ); ~CIDFont() override; - // Load the font from raw embedded stream bytes bool loadFromStream(const std::vector& streamData); - // Load the font from a system or CJK fallback file path bool loadFromFile(const std::string& filePath); std::string getBaseFont() const override; @@ -44,7 +42,6 @@ public: uint32_t decodeToUnicode(uint32_t charCode) const override; std::string decodeStringToUnicode(const std::vector& charCodes) const override; - // CID to GID translation methods uint32_t mapCIDToGID(uint32_t cid) const; void setCIDToGIDMap(std::unordered_map cidToGid); void setIdentityCIDToGIDMap(); @@ -68,4 +65,4 @@ private: void buildGidToUnicodeMap() const; }; -} // namespace pdfengine::fonts::pdf_fonts +} diff --git a/engine/src/fonts/pdf_fonts/types/truetype_font.cpp b/engine/src/fonts/pdf_fonts/types/truetype_font.cpp index 9a2d4d1..8cacefd 100644 --- a/engine/src/fonts/pdf_fonts/types/truetype_font.cpp +++ b/engine/src/fonts/pdf_fonts/types/truetype_font.cpp @@ -31,7 +31,7 @@ std::string unicodeToUtf8(uint32_t codepoint) { return utf8; } -} // namespace +} TrueTypeFont::TrueTypeFont( const std::string& baseFont, @@ -130,4 +130,4 @@ std::string TrueTypeFont::decodeStringToUnicode(const std::vector& cha return result; } -} // namespace pdfengine::fonts::pdf_fonts +} diff --git a/engine/src/fonts/pdf_fonts/types/truetype_font.hpp b/engine/src/fonts/pdf_fonts/types/truetype_font.hpp index c2e9ebd..8edd92b 100644 --- a/engine/src/fonts/pdf_fonts/types/truetype_font.hpp +++ b/engine/src/fonts/pdf_fonts/types/truetype_font.hpp @@ -20,7 +20,6 @@ public: ); ~TrueTypeFont() override; - // Load the font from raw embedded stream bytes bool loadFromStream(const std::vector& streamData); std::string getBaseFont() const override; @@ -46,4 +45,4 @@ private: std::unique_ptr subset_info_; }; -} // namespace pdfengine::fonts::pdf_fonts +} diff --git a/engine/src/fonts/pdf_fonts/types/type1_font.cpp b/engine/src/fonts/pdf_fonts/types/type1_font.cpp index 7e93c10..2037621 100644 --- a/engine/src/fonts/pdf_fonts/types/type1_font.cpp +++ b/engine/src/fonts/pdf_fonts/types/type1_font.cpp @@ -31,7 +31,7 @@ std::string unicodeToUtf8(uint32_t codepoint) { return utf8; } -} // namespace +} Type1Font::Type1Font( const std::string& baseFont, @@ -134,4 +134,4 @@ std::string Type1Font::decodeStringToUnicode(const std::vector& charCo return result; } -} // namespace pdfengine::fonts::pdf_fonts +} diff --git a/engine/src/fonts/pdf_fonts/types/type1_font.hpp b/engine/src/fonts/pdf_fonts/types/type1_font.hpp index 1117c3e..127f87e 100644 --- a/engine/src/fonts/pdf_fonts/types/type1_font.hpp +++ b/engine/src/fonts/pdf_fonts/types/type1_font.hpp @@ -21,10 +21,8 @@ public: ); ~Type1Font() override; - // Load the font from raw embedded stream bytes bool loadFromStream(const std::vector& streamData); - // Load the font from a system or fallback file path bool loadFromFile(const std::string& filePath); std::string getBaseFont() const override; @@ -50,4 +48,4 @@ private: std::unique_ptr subset_info_; }; -} // namespace pdfengine::fonts::pdf_fonts +} diff --git a/engine/src/fonts/shaping/hb_shaper.cpp b/engine/src/fonts/shaping/hb_shaper.cpp index d1da0c7..839b290 100644 --- a/engine/src/fonts/shaping/hb_shaper.cpp +++ b/engine/src/fonts/shaping/hb_shaper.cpp @@ -22,60 +22,47 @@ std::vector HbShaper::shapeRun( return result; } - // Acquire lock to prevent concurrent mutation of FT_Face's active pixel size std::lock_guard lock(font.getMutex()); - // Set the pixel size on the FreeType face before shaping. - // This ensures HarfBuzz measures everything using the requested font size context. if (FT_Set_Pixel_Sizes(ftFace, 0, fontSize)) { return result; } - // Create a HarfBuzz font wrapper around the FreeType face. hb_font_t* hbFont = hb_ft_font_create_referenced(ftFace); if (!hbFont) { return result; } - // Sync HarfBuzz font with FreeType face changes. hb_ft_font_changed(hbFont); - // Create a text buffer. hb_buffer_t* hbBuffer = hb_buffer_create(); if (!hbBuffer) { hb_font_destroy(hbFont); return result; } - // Add text to buffer as UTF-8. hb_buffer_add_utf8(hbBuffer, text.c_str(), static_cast(text.length()), 0, -1); - // Let HarfBuzz guess direction, script, and language properties. hb_buffer_guess_segment_properties(hbBuffer); if (writingMode == WritingMode::Vertical) { hb_buffer_set_direction(hbBuffer, HB_DIRECTION_TTB); } - // Enable professional typography features (Kerning and Ligatures) hb_feature_t features[2]; - // Enable kerning features[0].tag = HB_TAG('k', 'e', 'r', 'n'); features[0].value = 1; features[0].start = 0; features[0].end = static_cast(-1); - // Enable standard ligatures features[1].tag = HB_TAG('l', 'i', 'g', 'a'); features[1].value = 1; features[1].start = 0; features[1].end = static_cast(-1); - // Shape the text inside the buffer using the font. hb_shape(hbFont, hbBuffer, features, 2); - // Retrieve the results. unsigned int glyphCount = 0; hb_glyph_info_t* glyphInfos = hb_buffer_get_glyph_infos(hbBuffer, &glyphCount); hb_glyph_position_t* glyphPositions = hb_buffer_get_glyph_positions(hbBuffer, &glyphCount); @@ -85,8 +72,6 @@ std::vector HbShaper::shapeRun( for (unsigned int i = 0; i < glyphCount; ++i) { ShapedGlyph sg; sg.glyphIndex = glyphInfos[i].codepoint; - // HarfBuzz coordinates are fractional 26.6 pixels (1/64 of a pixel). - // Convert to standard double-precision float values. sg.advanceX = static_cast(glyphPositions[i].x_advance) / 64.0; sg.advanceY = static_cast(glyphPositions[i].y_advance) / 64.0; sg.offsetX = static_cast(glyphPositions[i].x_offset) / 64.0; @@ -96,11 +81,10 @@ std::vector HbShaper::shapeRun( } } - // Clean up HarfBuzz resources. hb_buffer_destroy(hbBuffer); hb_font_destroy(hbFont); return result; } -} // namespace pdfengine::fonts +} diff --git a/engine/src/fonts/shaping/hb_shaper.hpp b/engine/src/fonts/shaping/hb_shaper.hpp index af39f0b..0b18d78 100644 --- a/engine/src/fonts/shaping/hb_shaper.hpp +++ b/engine/src/fonts/shaping/hb_shaper.hpp @@ -31,8 +31,6 @@ public: Vertical }; - // Shapes the input UTF-8 text run using the given FontFace and fontSize. - // Returns a vector of shaped glyphs. std::vector shapeRun( const std::string& text, FontFace& font, @@ -41,4 +39,4 @@ public: ); }; -} // namespace pdfengine::fonts +} diff --git a/engine/src/parser/content_builder.cpp b/engine/src/parser/content_builder.cpp index 76d830d..3267cac 100644 --- a/engine/src/parser/content_builder.cpp +++ b/engine/src/parser/content_builder.cpp @@ -33,7 +33,7 @@ void ContentBuilder::processOperation(const Operation& op, std::vector= 6) { - // Tm takes 6 operands: a b c d e f auto it = op.operands.end(); auto fNode = *(--it); auto eNode = *(--it); @@ -87,7 +86,6 @@ void ContentBuilder::handleTd(const Operation& op) { if (eNode->type == AstNodeType::Number) state_.tm[4] = eNode->numberValue; if (fNode->type == AstNodeType::Number) state_.tm[5] = fNode->numberValue; } else if ((op.op == "Td" || op.op == "TD") && op.operands.size() >= 2) { - // Td simply offsets e and f in the matrix auto it = op.operands.end(); auto yNode = *(--it); auto xNode = *(--it); @@ -306,4 +304,4 @@ void ContentBuilder::handlePathPaint(PathPaintOp paintOp, currentPath_.clear(); } -} // namespace pdfengine +} diff --git a/engine/src/parser/content_builder.hpp b/engine/src/parser/content_builder.hpp index ef0065c..f770951 100644 --- a/engine/src/parser/content_builder.hpp +++ b/engine/src/parser/content_builder.hpp @@ -16,12 +16,11 @@ public: std::vector> build(const std::vector& operations); private: - // Graphics State Tracker struct GraphicsState { std::string fontName; double fontSize = 0.0; double tm[6] = {1.0, 0.0, 0.0, 1.0, 0.0, 0.0}; - Matrix ctm; // Current Transformation Matrix + Matrix ctm; }; GraphicsState state_; @@ -31,7 +30,6 @@ private: void processOperation(const Operation& op, std::vector>& outObjects); - // Handlers void handleTf(const Operation& op); void handleTd(const Operation& op); void handleTj(const Operation& op, std::vector>& outObjects); @@ -42,4 +40,4 @@ private: void handlePathPaint(PathPaintOp paintOp, std::vector>& outObjects, bool closePath = false); }; -} // namespace pdfengine +} diff --git a/engine/src/parser/content_stream_parser.cpp b/engine/src/parser/content_stream_parser.cpp index 06c56f8..9bd0088 100644 --- a/engine/src/parser/content_stream_parser.cpp +++ b/engine/src/parser/content_stream_parser.cpp @@ -11,8 +11,6 @@ std::vector ContentStreamParser::tokenize(const std::string& stream for (size_t i = 0; i < stream.size(); ++i) { char c = stream[i]; - // Simplified tokenization: split by whitespace - // In a real PDF parser, we must handle arrays [], dicts <<>>, strings (), etc. if (std::isspace(c)) { if (!current_token.empty()) { tokens.push_back(current_token); @@ -46,10 +44,7 @@ void ContentStreamParser::parse(const std::string& contentStream, DisplayList& d std::vector operands; for (const auto& token : tokens) { - // Simple heuristic: if it starts with a letter (and isn't a PDF name starting with /) - // or is a known operator, treat as operator. Otherwise operand. if (!token.empty() && std::isalpha(token[0]) && token[0] != '/') { - // It's an operator if (token == "m") { if (operands.size() >= 2) { float y = std::stof(operands.back()); operands.pop_back(); @@ -93,26 +88,17 @@ void ContentStreamParser::parse(const std::string& contentStream, DisplayList& d displayList.strokePath(m_currentPath); m_currentPath.clear(); } else if (token == "Do") { - // Draw Image XObject if (!operands.empty()) { std::string imageName = operands.back(); operands.pop_back(); - // In a full implementation, we would look up 'imageName' in the - // page's /Resources /XObject dictionary, check if /Subtype is /Image, - // apply /Filter /DCTDecode (JPEG decompression), and extract raw pixels. - // For now, we emit a placeholder command if the name is found. - // Dummy ImageInfo for stub ImageInfo img; img.width = 100; img.height = 100; - // Usually the CTM (Current Transformation Matrix) defines the image bounds. - // We just emit a 1x1 image at origin, assuming SetTransformCommand handled bounds. Matrix m; displayList.drawImage(img, m, 1.0f); } } - // Clear operands for next operator operands.clear(); } else { operands.push_back(token); @@ -120,4 +106,4 @@ void ContentStreamParser::parse(const std::string& contentStream, DisplayList& d } } -} // namespace pdfengine +} diff --git a/engine/src/parser/content_stream_parser.hpp b/engine/src/parser/content_stream_parser.hpp index 6bff3f7..15bb384 100644 --- a/engine/src/parser/content_stream_parser.hpp +++ b/engine/src/parser/content_stream_parser.hpp @@ -7,22 +7,16 @@ namespace pdfengine { -// A lightweight parser for PDF content streams. -// In Phase 3, this interprets operators and builds the DisplayList. class ContentStreamParser { public: ContentStreamParser() = default; - // Parses the given content stream and appends commands to the display list. - // 'resources' could later be a map of names to images/fonts. void parse(const std::string& contentStream, DisplayList& displayList); private: - // Helper to tokenize the content stream std::vector tokenize(const std::string& stream); - // Current path state Path m_currentPath; }; -} // namespace pdfengine +} diff --git a/engine/src/parser/decoration_builder.cpp b/engine/src/parser/decoration_builder.cpp index c53ab53..2bc1d11 100644 --- a/engine/src/parser/decoration_builder.cpp +++ b/engine/src/parser/decoration_builder.cpp @@ -4,15 +4,12 @@ namespace pdfengine { Path DecorationBuilder::buildUnderline(float x, float y, float width, float thickness, float offset) { Path p; - // An underline is essentially a thin filled rectangle at (y + offset). - // The caller is responsible for supplying the correctly signed offset. p.addRect(x, y + offset, width, thickness); return p; } Path DecorationBuilder::buildStrikeout(float x, float y, float width, float thickness, float offset) { Path p; - // A strikeout is similarly a thin filled rectangle, positioned higher up. p.addRect(x, y + offset, width, thickness); return p; } @@ -28,14 +25,11 @@ Path DecorationBuilder::buildSquiggly(float x, float y, float width, float ampli float currentX = x; float endX = x + width; - // Create a jagged squiggly line using line segments. - // This is drawn as a stroked path rather than a filled rect. bool up = true; while (currentX < endX) { float nextX = currentX + (frequency / 2.0f); if (nextX > endX) { nextX = endX; - // Adjust the final Y to keep the slope somewhat consistent if chopped early float ratio = (nextX - currentX) / (frequency / 2.0f); float nextY = y + (up ? amplitude : -amplitude) * ratio; p.lineTo(nextX, nextY); @@ -52,4 +46,4 @@ Path DecorationBuilder::buildSquiggly(float x, float y, float width, float ampli return p; } -} // namespace pdfengine +} diff --git a/engine/src/parser/decoration_builder.hpp b/engine/src/parser/decoration_builder.hpp index d03dc41..5f7f17a 100644 --- a/engine/src/parser/decoration_builder.hpp +++ b/engine/src/parser/decoration_builder.hpp @@ -4,29 +4,13 @@ namespace pdfengine { -// A utility class to generate vector paths for text markup and decorations class DecorationBuilder { public: - // Builds a path representing a straight underline. - // x, y: starting coordinates (usually the baseline origin) - // width: length of the underline - // thickness: thickness of the line (used to build a thin rectangle) - // offset: vertical offset from y static Path buildUnderline(float x, float y, float width, float thickness = 1.0f, float offset = -2.0f); - // Builds a path representing a strikeout line. - // x, y: starting coordinates (baseline) - // width: length of the strikeout - // thickness: thickness of the line - // offset: vertical offset from y (typically goes up through the text) static Path buildStrikeout(float x, float y, float width, float thickness = 1.0f, float offset = 4.0f); - // Builds a path representing a squiggly underline (often used for spelling or grammar highlights). - // x, y: starting coordinates - // width: length of the squiggly - // amplitude: height of the squiggly waves - // frequency: horizontal width of a single wave cycle static Path buildSquiggly(float x, float y, float width, float amplitude = 2.0f, float frequency = 4.0f); }; -} // namespace pdfengine +} diff --git a/engine/src/parser/lexer.cpp b/engine/src/parser/lexer.cpp index 8446f15..4c29980 100644 --- a/engine/src/parser/lexer.cpp +++ b/engine/src/parser/lexer.cpp @@ -21,7 +21,7 @@ std::optional Lexer::nextToken() { skipWhitespaceAndComments(); if (isEOF()) { - return std::nullopt; // Or we can return EndOfStream token + return std::nullopt; } size_t startPos = pos_; @@ -55,7 +55,6 @@ void Lexer::skipWhitespaceAndComments() { if (isWhitespace(c)) { advance(); } else if (c == '%') { - // Skip until end of line while (!isEOF() && peek() != '\n' && peek() != '\r') { advance(); } @@ -67,7 +66,7 @@ void Lexer::skipWhitespaceAndComments() { std::optional Lexer::parseString() { size_t startPos = pos_; - advance(); // skip '(' + advance(); std::string str; int parenLevel = 1; @@ -95,13 +94,12 @@ std::optional Lexer::parseString() { case '(': str += '('; break; case ')': str += ')'; break; case '\\': str += '\\'; break; - case '\n': break; // ignored line break + case '\n': break; case '\r': if (peek() == '\n') advance(); break; default: if (n >= '0' && n <= '7') { - // Octal up to 3 digits int val = n - '0'; for (int i = 0; i < 2; ++i) { if (!isEOF() && peek() >= '0' && peek() <= '7') { @@ -112,7 +110,6 @@ std::optional Lexer::parseString() { } str += static_cast(val); } else { - // Unknown escape, just output the char str += n; } } @@ -126,7 +123,7 @@ std::optional Lexer::parseString() { std::optional Lexer::parseHexString() { size_t startPos = pos_; - advance(); // skip '<' + advance(); std::vector bytes; bool hasHigh = false; @@ -136,7 +133,6 @@ std::optional Lexer::parseHexString() { char c = advance(); if (c == '>') { if (hasHigh) { - // If odd number of hex digits, implicitly append 0 bytes.push_back(high << 4); } break; @@ -145,7 +141,6 @@ std::optional Lexer::parseHexString() { int val = hexDigitValue(c); if (val == -1) { - // Invalid char, usually stop or ignore. We'll ignore for now or break. continue; } @@ -163,7 +158,7 @@ std::optional Lexer::parseHexString() { std::optional Lexer::parseName() { size_t startPos = pos_; - advance(); // skip '/' + advance(); std::string name; while (!isEOF()) { @@ -203,10 +198,9 @@ std::optional Lexer::parseNumberOrOperator() { } if (val.empty()) { - return std::nullopt; // should not happen if we skip properly + return std::nullopt; } - // Is it a number? bool isNum = true; bool hasDot = false; for (size_t i = 0; i < val.size(); ++i) { @@ -221,7 +215,6 @@ std::optional Lexer::parseNumberOrOperator() { break; } } - // A single '+' or '-' or '.' is not a number if (val == "+" || val == "-" || val == ".") isNum = false; if (isNum) { @@ -239,23 +232,22 @@ std::optional Lexer::parseNumberOrOperator() { std::optional Lexer::parseDictOrLess() { size_t startPos = pos_; - advance(); // skip '<' + advance(); if (!isEOF() && peek() == '<') { - advance(); // skip second '<' + advance(); return Token{TokenType::DictStart, "", {}, 0.0, startPos, pos_}; } - // Shouldn't be called if it was HexString, handled in nextToken() - return Token{TokenType::Operator, "<", {}, 0.0, startPos, pos_}; // fallback + return Token{TokenType::Operator, "<", {}, 0.0, startPos, pos_}; } std::optional Lexer::parseDictOrGreater() { size_t startPos = pos_; - advance(); // skip '>' + advance(); if (!isEOF() && peek() == '>') { - advance(); // skip second '>' + advance(); return Token{TokenType::DictEnd, "", {}, 0.0, startPos, pos_}; } - return Token{TokenType::Operator, ">", {}, 0.0, startPos, pos_}; // fallback + return Token{TokenType::Operator, ">", {}, 0.0, startPos, pos_}; } char Lexer::peek(size_t offset) const { @@ -295,4 +287,4 @@ int Lexer::hexDigitValue(char c) { return -1; } -} // namespace pdfengine +} diff --git a/engine/src/parser/lexer.hpp b/engine/src/parser/lexer.hpp index a8d6b52..e9d4dff 100644 --- a/engine/src/parser/lexer.hpp +++ b/engine/src/parser/lexer.hpp @@ -11,10 +11,8 @@ class Lexer { public: explicit Lexer(std::string_view input); - // Retrieve all tokens in one pass std::vector tokenize(); - // Or retrieve tokens one by one std::optional nextToken(); private: @@ -39,4 +37,4 @@ private: static int hexDigitValue(char c); }; -} // namespace pdfengine +} diff --git a/engine/src/parser/parser.cpp b/engine/src/parser/parser.cpp index 868ab4d..6ac4eca 100644 --- a/engine/src/parser/parser.cpp +++ b/engine/src/parser/parser.cpp @@ -15,12 +15,10 @@ std::vector ContentParser::parse() { if (token->type == TokenType::Operator) { Operation op; op.op = token->stringValue; - // PDF stream operators consume whatever is on the operand stack op.operands = std::move(operandStack_); operandStack_.clear(); operations.push_back(std::move(op)); } else { - // It's an operand (or the start of a composite operand) auto node = parseNode(*token); if (node) { operandStack_.push_back(std::move(node)); @@ -74,8 +72,6 @@ std::shared_ptr ContentParser::parseNode(const Token& token) { case TokenType::DictEnd: case TokenType::EndOfStream: case TokenType::Operator: - // These should not be parsed as standalone operand nodes here - // Operators are handled in the main loop, Ends are handled in Array/Dict parsing return nullptr; } @@ -90,18 +86,15 @@ std::shared_ptr ContentParser::parseArray() { if (!token) break; if (token->type == TokenType::ArrayEnd) { - advance(); // consume ']' + advance(); break; } - // Cannot have Operator inside array if (token->type == TokenType::Operator) { - // PDF spec doesn't strictly allow operators inside arrays, - // but we gracefully break out or skip. We'll break out to avoid infinite loops. break; } - advance(); // consume item token + advance(); auto item = parseNode(*token); if (item) { node->arrayItems.push_back(std::move(item)); @@ -119,27 +112,24 @@ std::shared_ptr ContentParser::parseDictionary() { if (!keyToken) break; if (keyToken->type == TokenType::DictEnd) { - advance(); // consume '>>' + advance(); break; } if (keyToken->type != TokenType::Name) { - // Dictionaries must have Name keys. If not, this is a malformed dict. - // We just advance to avoid infinite loop. advance(); continue; } - advance(); // consume key + advance(); if (isEOF()) break; const Token* valToken = peek(); if (valToken->type == TokenType::DictEnd) { - // Incomplete key-value pair break; } - advance(); // consume value token + advance(); auto valNode = parseNode(*valToken); if (valNode) { node->dictItems[keyToken->stringValue] = std::move(valNode); @@ -167,4 +157,4 @@ bool ContentParser::isEOF() const { return pos_ >= tokens_.size() || tokens_[pos_].type == TokenType::EndOfStream; } -} // namespace pdfengine +} diff --git a/engine/src/parser/parser.hpp b/engine/src/parser/parser.hpp index 259bfbb..9b30f92 100644 --- a/engine/src/parser/parser.hpp +++ b/engine/src/parser/parser.hpp @@ -32,4 +32,4 @@ public: explicit ParserError(const std::string& msg) : std::runtime_error(msg) {} }; -} // namespace pdfengine +} diff --git a/engine/src/parser/pdfium_loader.cpp b/engine/src/parser/pdfium_loader.cpp index db5480e..f38677b 100644 --- a/engine/src/parser/pdfium_loader.cpp +++ b/engine/src/parser/pdfium_loader.cpp @@ -1,7 +1,6 @@ #include "parser/pdfium_loader.hpp" #ifdef PDFENGINE_WITH_PDFIUM -// The ONLY place in the codebase a raw PDFium header may be included (Rule R2). #include #endif @@ -17,7 +16,6 @@ bool pdfiumAvailable() noexcept { void pdfiumInitLibrary() { #ifdef PDFENGINE_WITH_PDFIUM - // PDFium expects this once per process before any document is opened. FPDF_InitLibrary(); #endif } @@ -28,4 +26,4 @@ void pdfiumDestroyLibrary() { #endif } -} // namespace pdfengine::parser +} diff --git a/engine/src/parser/pdfium_loader.hpp b/engine/src/parser/pdfium_loader.hpp index ec9a23a..6164aa4 100644 --- a/engine/src/parser/pdfium_loader.hpp +++ b/engine/src/parser/pdfium_loader.hpp @@ -1,20 +1,10 @@ -// pdfengine::parser — the PDFium boundary. -// -// Rule R2: this directory (engine/src/parser/) is the ONLY place in the entire -// codebase permitted to include PDFium headers or call raw FPDF_* APIs. CI -// enforces this with scripts/check_pdfium_boundary.* — keep it that way. #pragma once namespace pdfengine::parser { -// True if the engine was compiled with PDFENGINE_WITH_PDFIUM, i.e. linked -// against the PDFium static library. False in Phase 0 default builds. [[nodiscard]] bool pdfiumAvailable() noexcept; -// Global one-time initialisation / teardown of the PDFium library. -// No-ops when built without PDFium. Init must be called before, and destroy -// after, any document parsing. void pdfiumInitLibrary(); void pdfiumDestroyLibrary(); -} // namespace pdfengine::parser +} diff --git a/engine/src/parser/resource_resolver.hpp b/engine/src/parser/resource_resolver.hpp index 8396fb6..c3795aa 100644 --- a/engine/src/parser/resource_resolver.hpp +++ b/engine/src/parser/resource_resolver.hpp @@ -18,4 +18,4 @@ public: virtual ResolvedXObject resolveXObject(const std::string& name) = 0; }; -} // namespace pdfengine +} diff --git a/engine/src/qpdf/qpdf_extractor.cpp b/engine/src/qpdf/qpdf_extractor.cpp index 5f00668..5a5d6dc 100644 --- a/engine/src/qpdf/qpdf_extractor.cpp +++ b/engine/src/qpdf/qpdf_extractor.cpp @@ -38,7 +38,7 @@ StreamVerification verifyContentStream(const ExtractedStream& stream) { return v; } -} // namespace pdfengine +} namespace pdfengine::qpdf_layer { @@ -131,7 +131,6 @@ QpdfExtractor::extractFromQpdf(::QPDF& qpdf, int pageIndex) const { for (auto streamObj : streams) { if (!streamObj.isStream()) continue; - // Get filters QPDFObjectHandle dict = streamObj.getDict(); if (dict.hasKey("/Filter")) { QPDFObjectHandle filter = dict.getKey("/Filter"); @@ -149,7 +148,6 @@ QpdfExtractor::extractFromQpdf(::QPDF& qpdf, int pageIndex) const { } } - // Raw data try { Pl_Buffer rawPipeline("raw"); streamObj.pipeStreamData(&rawPipeline, 0, qpdf_dl_none, false); @@ -159,10 +157,8 @@ QpdfExtractor::extractFromQpdf(::QPDF& qpdf, int pageIndex) const { result.rawContent.append(reinterpret_cast(rawBuf->getBuffer()), rawBuf->getSize()); } } catch (...) { - // Ignore failure for raw } - // Decoded data try { Pl_Buffer decodedPipeline("decoded"); streamObj.pipeStreamData(&decodedPipeline, 0, qpdf_dl_all, false); @@ -191,4 +187,4 @@ QpdfExtractor::extractFromQpdf(::QPDF& qpdf, int pageIndex) const { #endif } -} // namespace pdfengine::qpdf_layer +} diff --git a/engine/src/qpdf/qpdf_extractor.hpp b/engine/src/qpdf/qpdf_extractor.hpp index 8590ef0..4cbba7d 100644 --- a/engine/src/qpdf/qpdf_extractor.hpp +++ b/engine/src/qpdf/qpdf_extractor.hpp @@ -35,4 +35,4 @@ private: extractFromQpdf(::QPDF& qpdf, int pageIndex) const; }; -} // namespace pdfengine::qpdf_layer +} diff --git a/engine/src/qpdf/qpdf_resource_resolver.cpp b/engine/src/qpdf/qpdf_resource_resolver.cpp index 7502626..69c91b8 100644 --- a/engine/src/qpdf/qpdf_resource_resolver.cpp +++ b/engine/src/qpdf/qpdf_resource_resolver.cpp @@ -42,4 +42,4 @@ ResolvedXObject QpdfResourceResolver::resolveXObject(const std::string& name) { return resolved; } -} // namespace pdfengine::qpdf_layer +} diff --git a/engine/src/qpdf/qpdf_resource_resolver.hpp b/engine/src/qpdf/qpdf_resource_resolver.hpp index ef319ba..887aeb6 100644 --- a/engine/src/qpdf/qpdf_resource_resolver.hpp +++ b/engine/src/qpdf/qpdf_resource_resolver.hpp @@ -15,4 +15,4 @@ private: QPDFObjectHandle resourcesDict_; }; -} // namespace pdfengine::qpdf_layer +} diff --git a/engine/src/qpdf/qpdf_writer.cpp b/engine/src/qpdf/qpdf_writer.cpp index cdc2ffc..229bf03 100644 --- a/engine/src/qpdf/qpdf_writer.cpp +++ b/engine/src/qpdf/qpdf_writer.cpp @@ -30,18 +30,12 @@ QpdfWriter::replacePageStreamAndSave(const std::string& sourcePath, QPDFPageObjectHelper& page = pages[pageIndex]; - // Create a new stream with the updated data QPDFObjectHandle newStream = QPDFObjectHandle::newStream(&pdf, newStreamData); - // Replace the page's contents stream - // According to QPDF specs, if we pass an array to newStream, it handles it, - // but it's simpler to just set the dictionary's /Contents to the new stream QPDFObjectHandle pageDict = page.getObjectHandle(); pageDict.replaceKey("/Contents", newStream); - // Write it out QPDFWriter writer(pdf, destPath.c_str()); - // For performance/size we usually compress streams writer.setStreamDataMode(qpdf_s_compress); writer.write(); @@ -52,4 +46,4 @@ QpdfWriter::replacePageStreamAndSave(const std::string& sourcePath, #endif } -} // namespace pdfengine::qpdf_layer +} diff --git a/engine/src/qpdf/qpdf_writer.hpp b/engine/src/qpdf/qpdf_writer.hpp index 120b356..d6b7859 100644 --- a/engine/src/qpdf/qpdf_writer.hpp +++ b/engine/src/qpdf/qpdf_writer.hpp @@ -12,7 +12,6 @@ class QpdfWriter { public: QpdfWriter() = default; - // Takes a source PDF, replaces the stream of the given page, and writes to a new destination [[nodiscard]] std::expected replacePageStreamAndSave(const std::string& sourcePath, @@ -21,4 +20,4 @@ public: const std::string& newStreamData) const; }; -} // namespace pdfengine::qpdf_layer +} diff --git a/engine/src/serializer/ast_serializer.cpp b/engine/src/serializer/ast_serializer.cpp index dbbd2bd..a3e447d 100644 --- a/engine/src/serializer/ast_serializer.cpp +++ b/engine/src/serializer/ast_serializer.cpp @@ -21,12 +21,10 @@ std::string AstSerializer::serialize(const std::vector& operations) c std::string AstSerializer::serializeNode(const AstNode& node) const { switch (node.type) { case AstNodeType::Number: { - // Need to drop trailing zeros for integers to save space and match standard PDF double intPart; if (std::modf(node.numberValue, &intPart) == 0.0) { return std::to_string(static_cast(node.numberValue)); } else { - // Round to 4 decimal places for cleanliness std::ostringstream out; out.precision(4); out << std::fixed << node.numberValue; @@ -37,7 +35,7 @@ std::string AstSerializer::serializeNode(const AstNode& node) const { } } case AstNodeType::Name: - return "/" + node.stringValue; // simplified, assumes no special chars requiring # hex encoding for now + return "/" + node.stringValue; case AstNodeType::String: return "(" + escapeString(node.stringValue) + ")"; @@ -90,4 +88,4 @@ std::string AstSerializer::escapeString(const std::string& str) const { return res; } -} // namespace pdfengine +} diff --git a/engine/src/serializer/ast_serializer.hpp b/engine/src/serializer/ast_serializer.hpp index aa56c74..10d0d65 100644 --- a/engine/src/serializer/ast_serializer.hpp +++ b/engine/src/serializer/ast_serializer.hpp @@ -17,4 +17,4 @@ private: std::string escapeString(const std::string& str) const; }; -} // namespace pdfengine +} diff --git a/engine/src/serializer/content_serializer.cpp b/engine/src/serializer/content_serializer.cpp index bb2f807..ace37f8 100644 --- a/engine/src/serializer/content_serializer.cpp +++ b/engine/src/serializer/content_serializer.cpp @@ -15,12 +15,10 @@ std::vector ContentSerializer::serialize(const std::vector& outOps) const { - // BT Operation opBT; opBT.op = "BT"; outOps.push_back(std::move(opBT)); - // /FontName FontSize Tf if (!textObj.fontName.empty() && textObj.fontSize > 0) { Operation opTf; opTf.op = "Tf"; @@ -36,7 +34,6 @@ void ContentSerializer::serializeText(const TextObject& textObj, std::vector(AstNodeType::String); @@ -54,10 +50,9 @@ void ContentSerializer::serializeText(const TextObject& textObj, std::vector& outOps) const; }; -} // namespace pdfengine +} diff --git a/engine/src/text/selection.cpp b/engine/src/text/selection.cpp index 95faaab..9f41a46 100644 --- a/engine/src/text/selection.cpp +++ b/engine/src/text/selection.cpp @@ -1,9 +1,3 @@ -// Adobe-grade hit-testing & text selection for PdfPage. -// -// Concrete implementations of PdfPage::orderedGlyphs / hitGlyph / selectRange, -// built on top of the (virtual) extractTextWithBounds(). All maths is in -// page-point space with a top-left origin — identical to the frontend -// TextSelectionModel, so engine and browser selections agree. #include "pdfengine/pdf_document.hpp" @@ -21,32 +15,28 @@ struct OGlyph { int line = 0; double right = 0.0; double bottom = 0.0; - double mid = 0.0; // horizontal centre + double mid = 0.0; }; struct LineBand { double top = 0.0; double bottom = 0.0; - double mid = 0.0; // vertical centre of the first glyph - int start = 0; // inclusive glyph index - int end = 0; // exclusive glyph index + double mid = 0.0; + int start = 0; + int end = 0; }; -// Uniform 2D grid over glyph bounding boxes — a true spatial index for the -// point→glyph query, independent of line assignment. This is what makes -// hit-testing exact on overlapping/dense content (diacritics, multi-column, -// rotated runs) where a line-bucketed search would miss. struct Grid { double minX = 0.0, minY = 0.0, cell = 1.0; int cols = 0, rows = 0; - std::vector> cells; // size cols*rows; glyph indices per cell + std::vector> cells; [[nodiscard]] bool empty() const { return cols == 0 || rows == 0; } [[nodiscard]] int at(int c, int r) const { return r * cols + c; } }; struct SelIndex { - std::vector glyphs; // reading order + std::vector glyphs; std::vector lines; Grid grid; }; @@ -58,8 +48,6 @@ bool isSpace(const std::string& t) { return true; } -// Cluster raw glyphs into lines (by vertical overlap) then sort each line L→R, -// flattening into a single reading-order array with cached line bands. SelIndex buildIndex(const std::vector& raw) { SelIndex idx; std::vector clean; @@ -121,7 +109,6 @@ SelIndex buildIndex(const std::vector& raw) { idx.lines.push_back(band); } - // Build the spatial grid over the final glyph set. Grid& grid = idx.grid; double minX = std::numeric_limits::max(); double minY = std::numeric_limits::max(); @@ -135,8 +122,6 @@ SelIndex buildIndex(const std::vector& raw) { maxY = std::max(maxY, og.bottom); sumH += og.g.h; } - // Cell ≈ average glyph height (~one text line), so most cells hold a handful - // of glyphs. Grow the cell if the grid would otherwise be unreasonably large. double cell = std::max(1.0, sumH / static_cast(idx.glyphs.size())); auto dim = [&](double lo, double hi) { return std::max(1, static_cast((hi - lo) / cell) + 1); @@ -167,15 +152,7 @@ SelIndex buildIndex(const std::vector& raw) { return idx; } -// --- spatial index queries (Adobe-grade, fast on dense/overlapping content) -- -// -// Point→glyph uses the 2D grid (exact regardless of line layout). Caret/line -// positioning uses the line bands via binary search (lines are in ascending -// vertical order; each line's glyphs are contiguous and x-sorted). Both stay -// cheap on pages with tens of thousands of glyphs. -// Exact glyph under a point via the spatial grid — never misses a containing -// glyph, even where text lines overlap. int glyphAt(const SelIndex& idx, double x, double y) { const Grid& grid = idx.grid; if (grid.empty() || x < grid.minX || y < grid.minY) return -1; @@ -193,7 +170,6 @@ int lineAt(const SelIndex& idx, double y) { const auto& lines = idx.lines; if (lines.empty()) return -1; - // Binary search for the last line whose top <= y. int lo = 0; int hi = static_cast(lines.size()); while (lo < hi) { @@ -201,11 +177,8 @@ int lineAt(const SelIndex& idx, double y) { if (lines[m].top <= y) lo = m + 1; else hi = m; } - const int cand = lo - 1; // -1 when y is above every line + const int cand = lo - 1; - // Check the immediate neighbourhood for true containment, else take the - // nearest band by centre. Bands can overlap slightly (sub/superscript - // tolerance), so a ±1 window around the search boundary is enough. int best = -1; double bestDist = std::numeric_limits::max(); for (int i = cand - 1; i <= cand + 1; ++i) { @@ -221,8 +194,6 @@ int lineAt(const SelIndex& idx, double y) { return cand < 0 ? 0 : static_cast(lines.size()) - 1; } -// Index of the last glyph in [start,end) whose left edge x <= queryX, or -// start-1 if queryX is left of the whole line. Glyphs are sorted by x. static int lastGlyphLeftOf(const SelIndex& idx, const LineBand& line, double x) { int lo = line.start; int hi = line.end; @@ -235,13 +206,11 @@ static int lastGlyphLeftOf(const SelIndex& idx, const LineBand& line, double x) } int caretAt(const SelIndex& idx, double x, double y) { - // If the point lands on a glyph, the caret sits on its near or far side. const int hit = glyphAt(idx, x, y); if (hit >= 0) { const OGlyph& g = idx.glyphs[hit]; return x < g.mid ? hit : hit + 1; } - // Otherwise position within the nearest line (gaps / between lines / margins). const int li = lineAt(idx, y); if (li < 0) return 0; const LineBand& line = idx.lines[li]; @@ -251,8 +220,8 @@ int caretAt(const SelIndex& idx, double x, double y) { const int cand = lastGlyphLeftOf(idx, line, x); if (cand < line.start) return line.start; const OGlyph& g = idx.glyphs[cand]; - if (x <= g.right) return x < g.mid ? cand : cand + 1; // inside the glyph - return cand + 1; // in the gap after it + if (x <= g.right) return x < g.mid ? cand : cand + 1; + return cand + 1; } std::string textOfRange(const SelIndex& idx, int start, int end) { @@ -278,8 +247,7 @@ std::string textOfRange(const SelIndex& idx, int start, int end) { std::vector rectsOfRange(const SelIndex& idx, int start, int end) { std::vector rects; if (start >= end) return rects; - // Group selected glyphs by line, preserving first-seen order. - std::map> spanByLine; // line -> {minX, maxRight} + std::map> spanByLine; std::vector order; for (int i = start; i < end; ++i) { const OGlyph& g = idx.glyphs[i]; @@ -307,7 +275,7 @@ std::vector rectsOfRange(const SelIndex& idx, int start, int end) { return rects; } -} // namespace +} std::expected, EngineError> PdfPage::orderedGlyphs() const { auto raw = extractTextWithBounds(); @@ -346,4 +314,4 @@ PdfPage::selectRange(double ax, double ay, double bx, double by) const { return sel; } -} // namespace pdfengine +} \ No newline at end of file diff --git a/engine/tests/content_builder_test.cpp b/engine/tests/content_builder_test.cpp index 98c16c4..b9648c8 100644 --- a/engine/tests/content_builder_test.cpp +++ b/engine/tests/content_builder_test.cpp @@ -32,7 +32,7 @@ const PathObject* requirePathObject(const std::unique_ptr& object return static_cast(object.get()); } -} // namespace +} TEST(ContentBuilderTest, SimpleTextState) { Lexer lexer("10 20 Td /F1 12 Tf (Hello) Tj"); @@ -67,7 +67,6 @@ TEST(ContentBuilderTest, KerningArrayTJ) { EXPECT_EQ(objects[0]->getType(), ContentObjectType::Text); auto* textObj = static_cast(objects[0].get()); - // -600 is less than -500, so it inserts a space EXPECT_EQ(textObj->text, "Hello World"); } @@ -202,7 +201,6 @@ TEST(ContentBuilderTest, IntegrationHelloWorld) { auto ops = parser.parse(); auto objects = builder.build(ops); - // hello_world.pdf has two text lines: "Hello, world!" and "Goodbye, world!" int textObjectCount = 0; bool foundHello = false; bool foundGoodbye = false; diff --git a/engine/tests/display_list_test.cpp b/engine/tests/display_list_test.cpp index 83e1179..afbc4cb 100644 --- a/engine/tests/display_list_test.cpp +++ b/engine/tests/display_list_test.cpp @@ -5,7 +5,6 @@ using namespace pdfengine; -// A simple visitor for testing that just records the sequence of visited commands. class MockVisitor : public CommandVisitor { public: std::vector calls; @@ -25,7 +24,6 @@ TEST(DisplayListTest, RecordAndReplay) { EXPECT_EQ(list.size(), 0); - // Record some commands list.saveState(); list.setTransform(Matrix(2.0f, 0.0f, 0.0f, 2.0f, 0.0f, 0.0f)); list.fillRect(10.0f, 10.0f, 100.0f, 50.0f); @@ -34,7 +32,6 @@ TEST(DisplayListTest, RecordAndReplay) { EXPECT_EQ(list.size(), 5); - // Replay to the mock visitor MockVisitor visitor; list.replay(visitor); diff --git a/engine/tests/document_test.cpp b/engine/tests/document_test.cpp index 54077eb..48f1cb0 100644 --- a/engine/tests/document_test.cpp +++ b/engine/tests/document_test.cpp @@ -264,7 +264,7 @@ TEST(PageRenderTest, InvalidPageIndexReturnsPageOutOfBounds) { auto docRes = PdfDocument::loadFromFile(path.string()); ASSERT_TRUE(docRes.has_value()); - auto pageRes = (*docRes)->getPage(1); // Page index 1 is out of bounds for 1-page doc + auto pageRes = (*docRes)->getPage(1); ASSERT_FALSE(pageRes.has_value()); EXPECT_EQ(pageRes.error(), EngineError::PageOutOfBounds); @@ -447,7 +447,6 @@ TEST(DocumentEditTest, ApplyRedactionAndFullSave) { ASSERT_TRUE(docRes.has_value()); auto doc = *docRes; - // Verify text exists initially { auto pageRes = doc->getPage(0); ASSERT_TRUE(pageRes.has_value()); @@ -456,7 +455,6 @@ TEST(DocumentEditTest, ApplyRedactionAndFullSave) { EXPECT_NE(textRes->find("Hello"), std::string::npos); } - // Redact the entire page bounds to remove all objects std::string editsJson = R"({ "version": "1.0", "operations": [ @@ -494,7 +492,6 @@ TEST(DocumentEditTest, ApplyRedactionAndFullSave) { auto textRes = newPage->extractText(); ASSERT_TRUE(textRes.has_value()); - // The text should be completely gone EXPECT_EQ(textRes->find("Hello"), std::string::npos); EXPECT_EQ(textRes->find("world"), std::string::npos); } @@ -562,7 +559,6 @@ TEST(DocumentEditTest, ApplyPageRotationAndIncrementalSave) { EXPECT_GT(origW, 0.0); EXPECT_GT(origH, origW); - // 1. Rotate by 90 degrees (90 total) std::string editsJson1 = R"({ "version": "1.0", "operations": [ @@ -580,7 +576,6 @@ TEST(DocumentEditTest, ApplyPageRotationAndIncrementalSave) { auto editRes1 = doc->applyEdits(editsJson1); ASSERT_TRUE(editRes1.has_value()); - // 2. Rotate by another 90 degrees (180 total) std::string editsJson2 = R"({ "version": "1.0", "operations": [ @@ -598,7 +593,6 @@ TEST(DocumentEditTest, ApplyPageRotationAndIncrementalSave) { auto editRes2 = doc->applyEdits(editsJson2); ASSERT_TRUE(editRes2.has_value()); - // Save and load back to verify 180 degree rotation (dimensions should be original again) auto saveRes = doc->saveIncremental(); ASSERT_TRUE(saveRes.has_value()); const auto& savedBytes = *saveRes; @@ -617,7 +611,6 @@ TEST(DocumentEditTest, ApplyPageRotationAndIncrementalSave) { EXPECT_NEAR(rotatedW, origW, 0.01); EXPECT_NEAR(rotatedH, origH, 0.01); - // 3. Now rotate by -90 degrees (back to 90 total) std::string editsJson3 = R"({ "version": "1.0", "operations": [ @@ -700,7 +693,6 @@ TEST(DocumentEditTest, ApplyPageReorderAndIncrementalSave) { auto doc = *docRes; EXPECT_EQ(doc->pageCount(), 2); - // Swap the pages: move page 1 (index 1) to page 0 (index 0) std::string editsJson = R"({ "version": "1.0", "operations": [ @@ -861,7 +853,6 @@ TEST(FontDiagnosticsTest, ConcurrencyThreadSafety) { TEST(FontDiagnosticsTest, DeepIntrospectionAndFontSizeVerification) { SKIP_IF_NO_PDFIUM(); - // Test Part 1: Introspection & Metadata verification using utf-8.pdf { auto path = getCorpusPath("fonts", "utf-8.pdf"); if (std::filesystem::exists(path)) { @@ -874,13 +865,11 @@ TEST(FontDiagnosticsTest, DeepIntrospectionAndFontSizeVerification) { auto fonts = *fontsRes; for (const auto& f : fonts) { - // Core fields EXPECT_FALSE(f.fontName.empty()); EXPECT_FALSE(f.type.empty()); EXPECT_FALSE(f.normalizedFamily.empty()); EXPECT_FALSE(f.internalFontId.empty()); - // Subset tagging consistency if (f.isSubset) { EXPECT_EQ(f.subsetTag.size(), 6); for (char c : f.subsetTag) { @@ -894,7 +883,6 @@ TEST(FontDiagnosticsTest, DeepIntrospectionAndFontSizeVerification) { EXPECT_EQ(f.internalFontId, f.fontName + "_" + f.type + "_" + std::to_string(f.flags)); } - // Source Type / Fallbacks & Substitutions consistency if (f.sourceType == "SystemFallback") { EXPECT_FALSE(f.isEmbedded); EXPECT_TRUE(f.substitutedFrom.empty()); @@ -909,7 +897,6 @@ TEST(FontDiagnosticsTest, DeepIntrospectionAndFontSizeVerification) { #endif } - // Check descriptor metrics are non-zero / reasonably set EXPECT_GT(f.ascent, 0.0); EXPECT_LT(f.descent, 0.0); EXPECT_GT(f.capHeight, 0.0); @@ -917,7 +904,6 @@ TEST(FontDiagnosticsTest, DeepIntrospectionAndFontSizeVerification) { } } - // Test Part 2: Vertical Writing Mode Detection { auto path = getCorpusPath("fonts", "vertical_text.pdf"); if (std::filesystem::exists(path)) { @@ -935,12 +921,10 @@ TEST(FontDiagnosticsTest, DeepIntrospectionAndFontSizeVerification) { EXPECT_TRUE(f.encoding.find("-V") != std::string::npos || f.cmapName.find("-V") != std::string::npos); } } - // Ensure at least one vertical font is found in vertical_text.pdf EXPECT_TRUE(foundVertical); } } - // Test Part 2b: Vertical Font Detection Heuristic explicit validation { auto path1 = getCorpusPath("fonts", "vertical_identity_v.pdf"); if (std::filesystem::exists(path1)) { @@ -957,7 +941,6 @@ TEST(FontDiagnosticsTest, DeepIntrospectionAndFontSizeVerification) { } } - // Test Part 3: Font Size and Glyph Bounds Handling { auto path = getCorpusPath("fonts", "utf-8.pdf"); if (!std::filesystem::exists(path)) { @@ -980,20 +963,18 @@ TEST(FontDiagnosticsTest, DeepIntrospectionAndFontSizeVerification) { std::vector uniqueSizes; for (const auto& glyph : glyphs) { - // Ensure glyph bounding box and font sizes are valid positive numbers if (glyph.text != " " && glyph.text != "\r" && glyph.text != "\n" && glyph.text != "\t") { EXPECT_GT(glyph.w, 0.0); EXPECT_GT(glyph.h, 0.0); } EXPECT_GT(glyph.fontSize, 0.0); - EXPECT_LT(glyph.fontSize, 100.0); // No absurdly large font sizes + EXPECT_LT(glyph.fontSize, 100.0); if (std::find(uniqueSizes.begin(), uniqueSizes.end(), glyph.fontSize) == uniqueSizes.end()) { uniqueSizes.push_back(glyph.fontSize); } } - // If it's utf-8.pdf, it should have multiple distinct font sizes if (path.filename().string() == "utf-8.pdf") { EXPECT_GE(uniqueSizes.size(), 2u); } @@ -1001,23 +982,10 @@ TEST(FontDiagnosticsTest, DeepIntrospectionAndFontSizeVerification) { } } -// ========================================================================= -// Tests validating real PDFium font dictionary introspection. -// These tests verify that isEmbedded, type, ascent, descent, capHeight, and -// hasToUnicode are now derived from actual PDF font objects rather than from -// font-name heuristics (the behaviour that predated this change). -// ========================================================================= -// Verify that embedded fonts report isEmbedded=true and that sourceType is -// set to "Embedded" from the real FPDFFont_GetIsEmbedded() result. -// A subset-embedded font (ABCDEF+FontName prefix) is the clearest case -// because the old heuristic relied solely on the prefix tag for embedding -// detection, while real PDFium checks for the /FontFile stream. TEST(FontDiagnosticsTest, RealPDFiumEmbeddingAndTypeAccuracy) { SKIP_IF_NO_PDFIUM(); - // text_font.pdf has an embedded subset TrueType font \u2014 best candidate for - // verifying that isEmbedded comes from the PDF font stream, not the name tag. auto path = getCorpusPath("fonts", "text_font.pdf"); if (!std::filesystem::exists(path)) { GTEST_SKIP() << "text_font.pdf not found in corpus."; @@ -1035,11 +1003,9 @@ TEST(FontDiagnosticsTest, RealPDFiumEmbeddingAndTypeAccuracy) { ASSERT_FALSE(fonts.empty()) << "text_font.pdf must expose at least one font"; for (const auto& f : fonts) { - // Core invariant: every font must have a non-empty name and type. EXPECT_FALSE(f.fontName.empty()); EXPECT_FALSE(f.type.empty()); - // type must be one of the four valid PDF font subtypes. static const std::vector kValidTypes = { "Type1", "TrueType", "CIDFontType0", "CIDFontType2" }; @@ -1047,8 +1013,6 @@ TEST(FontDiagnosticsTest, RealPDFiumEmbeddingAndTypeAccuracy) { != kValidTypes.end(); EXPECT_TRUE(typeValid) << "Unexpected type '" << f.type << "' for font '" << f.fontName << "'"; - // Subset-prefixed fonts MUST be reported as embedded by PDFium - // (the /FontFile stream is required by the PDF spec when a subset tag is present). if (f.isSubset) { EXPECT_TRUE(f.isEmbedded) << "Subset font '" << f.fontName @@ -1059,21 +1023,15 @@ TEST(FontDiagnosticsTest, RealPDFiumEmbeddingAndTypeAccuracy) { EXPECT_TRUE(f.substitutedTo.empty()); } - // isEmbedded=true and sourceType="Embedded" must be consistent. if (f.isEmbedded) { EXPECT_EQ(f.sourceType, "Embedded"); } } } -// Verify that font descriptor metrics (ascent, descent, capHeight) come from -// the real PDF FontDescriptor via FPDFFont_GetAscent/Descent(), not from the -// hardcoded fallback table. The critical invariant is sign correctness: -// ascent must be positive, descent must be negative. TEST(FontDiagnosticsTest, RealPDFiumMetricsAccuracy) { SKIP_IF_NO_PDFIUM(); - // Use the largest font corpus file; it contains the most diverse fonts. auto path = getCorpusPath("fonts", "text_font.pdf"); if (!std::filesystem::exists(path)) { GTEST_SKIP() << "text_font.pdf not found in corpus."; @@ -1085,9 +1043,6 @@ TEST(FontDiagnosticsTest, RealPDFiumMetricsAccuracy) { ASSERT_TRUE(fontsRes.has_value()); for (const auto& f : *fontsRes) { - // ascent and descent from FPDFFont_GetAscent/Descent(font, 1000.0f, …) - // are in PDF 1000-unit space. ascent is above the baseline (positive), - // descent is below (negative). EXPECT_GT(f.ascent, 0.0) << "ascent must be positive for font '" << f.fontName << "'"; EXPECT_LT(f.descent, 0.0) @@ -1095,30 +1050,17 @@ TEST(FontDiagnosticsTest, RealPDFiumMetricsAccuracy) { EXPECT_GT(f.capHeight, 0.0) << "capHeight must be positive for font '" << f.fontName << "'"; - // capHeight must not exceed ascent (sanity: caps never taller than ascender). - EXPECT_LE(f.capHeight, f.ascent + 1.0) // +1 for float rounding + EXPECT_LE(f.capHeight, f.ascent + 1.0) << "capHeight should not exceed ascent for font '" << f.fontName << "'"; - // Values must be in a plausible PDF 1000-unit-space range. - // Standard fonts typically have ascent in [400, 1200]. EXPECT_LT(f.ascent, 1500.0) << "Implausibly large ascent for '" << f.fontName << "'"; EXPECT_GT(f.descent, -1500.0) << "Implausibly deep descent for '" << f.fontName << "'"; } } -// Verify that hasToUnicode reflects actual Unicode decode capability rather -// than the old always-true heuristic. -// -// with_tounicode.pdf \u2014 PDF containing a font that has a /ToUnicode stream; -// PDFium should decode characters successfully. -// no_tounicode.pdf \u2014 PDF containing a font with no /ToUnicode stream and no -// standard encoding; PDFium cannot map char codes to Unicode. -// latin_extended.pdf \u2014 Standard Latin font; must decode to Unicode via built-in -// encoding (WinAnsiEncoding or similar). TEST(FontDiagnosticsTest, ToUnicodePresenceAccuracy) { SKIP_IF_NO_PDFIUM(); - // Case 1: font WITH ToUnicode \u2014 hasToUnicode must be true { auto path = getCorpusPath("fonts", "with_tounicode.pdf"); if (std::filesystem::exists(path)) { @@ -1140,7 +1082,6 @@ TEST(FontDiagnosticsTest, ToUnicodePresenceAccuracy) { } } - // Case 2: font WITHOUT ToUnicode or decodable encoding \u2014 hasToUnicode must be false { auto path = getCorpusPath("fonts", "no_tounicode.pdf"); if (std::filesystem::exists(path)) { @@ -1150,7 +1091,6 @@ TEST(FontDiagnosticsTest, ToUnicodePresenceAccuracy) { if (pageRes.has_value()) { auto fontsRes = (*pageRes)->getFonts(); if (fontsRes.has_value() && !fontsRes->empty()) { - // All fonts in a no-tounicode document should fail Unicode decode. for (const auto& f : *fontsRes) { EXPECT_FALSE(f.hasToUnicode) << "Font '" << f.fontName @@ -1162,7 +1102,6 @@ TEST(FontDiagnosticsTest, ToUnicodePresenceAccuracy) { } } - // Case 3: standard Latin font \u2014 must decode to Unicode via built-in encoding { auto path = getCorpusPath("fonts", "latin_extended.pdf"); if (std::filesystem::exists(path)) { @@ -1184,25 +1123,18 @@ TEST(FontDiagnosticsTest, ToUnicodePresenceAccuracy) { } } } -// ========================================================================= -// Tests for UTF-16 Surrogate Pairs (Emoji and CJK Ext-B) -// ========================================================================= TEST(UtfConversionTest, EmojiSurrogatePairs) { - // 😀 U+1F600 -> UTF-8: F0 9F 98 80 std::string utf8_grinning = "\xF0\x9F\x98\x80"; auto utf16 = pdfengine::parser::utf8_to_utf16le(utf8_grinning); - // Should be D83D DE00 + null terminator ASSERT_EQ(utf16.size(), 3); EXPECT_EQ(utf16[0], 0xD83D); EXPECT_EQ(utf16[1], 0xDE00); EXPECT_EQ(utf16[2], 0x0000); - // Convert back to UTF-8 std::string utf8_out = pdfengine::parser::utf16le_to_utf8(reinterpret_cast(utf16.data()), utf16.size() - 1); EXPECT_EQ(utf8_out, utf8_grinning); - // 🚀 U+1F680 -> UTF-8: F0 9F 9A 80 std::string utf8_rocket = "\xF0\x9F\x9A\x80"; utf16 = pdfengine::parser::utf8_to_utf16le(utf8_rocket); ASSERT_EQ(utf16.size(), 3); @@ -1215,76 +1147,58 @@ TEST(UtfConversionTest, EmojiSurrogatePairs) { } TEST(UtfConversionTest, CJKExtensionB) { - // 𠀀 U+20000 -> UTF-8: F0 A0 80 80 std::string utf8_cjk = "\xF0\xA0\x80\x80"; auto utf16 = pdfengine::parser::utf8_to_utf16le(utf8_cjk); - // Should be D840 DC00 + null terminator ASSERT_EQ(utf16.size(), 3); EXPECT_EQ(utf16[0], 0xD840); EXPECT_EQ(utf16[1], 0xDC00); EXPECT_EQ(utf16[2], 0x0000); - // Convert back to UTF-8 std::string utf8_out = pdfengine::parser::utf16le_to_utf8(reinterpret_cast(utf16.data()), utf16.size() - 1); EXPECT_EQ(utf8_out, utf8_cjk); } TEST(UtfConversionTest, RoundtripMixed) { - // "A😀B𠀀C" -> 41 F0 9F 98 80 42 F0 A0 80 80 43 std::string mixed = "A\xF0\x9F\x98\x80""B\xF0\xA0\x80\x80""C"; auto utf16 = pdfengine::parser::utf8_to_utf16le(mixed); std::string mixed_out = pdfengine::parser::utf16le_to_utf8(reinterpret_cast(utf16.data()), utf16.size() - 1); EXPECT_EQ(mixed_out, mixed); } -// ========================================================================= -// Tests for CJK CID Resolution -// ========================================================================= TEST(CjkResolutionTest, AdobeCNS1) { - // Basic mapping checks for the core Adobe-CNS1 block (Traditional Chinese) using pdfengine::fonts::pdf_fonts::CjkCollectionDB; - // Test existing block (100-130) - EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 100), 0x4E00); // 一 - EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 112), 0x4E2D); // 中 - EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 130), 0x4ED7); // 仗 + EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 100), 0x4E00); + EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 112), 0x4E2D); + EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 130), 0x4ED7); - // Test the newly expanded block (131-140) - EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 131), 0x4ED8); // 付 - EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 135), 0x4EDF); // 仟 - EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 140), 0x4F01); // 企 + EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 131), 0x4ED8); + EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 135), 0x4EDF); + EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 140), 0x4F01); - // Test alternative naming - EXPECT_EQ(CjkCollectionDB::resolveCID("Identity-H-CNS1", 137), 0x4EE3); // 代 + EXPECT_EQ(CjkCollectionDB::resolveCID("Identity-H-CNS1", 137), 0x4EE3); } TEST(CjkResolutionTest, AdobeKorea1) { using pdfengine::fonts::pdf_fonts::CjkCollectionDB; - // Test existing Korean block (101-150) - EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 101), 0xAC00); // 가 - EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 119), 0xAC1C); // 개 - EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 150), 0xAC90); // 겔 + EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 101), 0xAC00); + EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 119), 0xAC1C); + EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 150), 0xAC90); - // Test newly added Hangul block (151-160) - EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 151), 0xAC94); // 겝 - EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 156), 0xACA9); // 결 - EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 160), 0xACBD); // 겼 + EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 151), 0xAC94); + EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 156), 0xACA9); + EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 160), 0xACBD); - // Test alternative naming - EXPECT_EQ(CjkCollectionDB::resolveCID("UniKS-UTF16-H-Korea1", 153), 0xACA0); // 겠 + EXPECT_EQ(CjkCollectionDB::resolveCID("UniKS-UTF16-H-Korea1", 153), 0xACA0); } -// ========================================================================= -// GlyphCache Concurrency & Benchmark Test -// ========================================================================= TEST(GlyphCacheTest, ConcurrencyBench) { using namespace pdfengine::fonts; FontFace face; - // Load a common system font for testing cache keys bool loaded = face.loadFromFile("C:\\Windows\\Fonts\\arial.ttf"); if (!loaded) { GTEST_SKIP() << "Skipping benchmark: Arial font not found."; @@ -1323,7 +1237,7 @@ TEST(GlyphCacheTest, ConcurrencyBench) { return diff.count(); }; - run_benchmark(2, 1000); // warmup + run_benchmark(2, 1000); cache.clear(); double time_10 = run_benchmark(10, 10000); @@ -1334,7 +1248,7 @@ TEST(GlyphCacheTest, ConcurrencyBench) { double time_50 = run_benchmark(50, 10000); std::cout << "[ BENCHMARK ] 50 Threads Time: " << time_50 << " seconds (" << (500000.0 / time_50) << " ops/sec)\n"; - EXPECT_LE(cache.size(), 1000 + 16); // Accommodate shard capacity rounding + EXPECT_LE(cache.size(), 1000 + 16); } TEST(FontDiagnosticsTest, EmbeddedFontResolutionAndReloadingVerification) { @@ -1415,12 +1329,10 @@ TEST(FontDiagnosticsTest, EmbeddedFontResolutionAndReloadingVerification) { std::cout << std::endl; } - // Restore first charmap if (face->num_charmaps > 0) { FT_Set_Charmap(face, face->charmaps[0]); } - // Print all glyph names in the face std::cout << " Glyph names: "; for (int i = 0; i < face->num_glyphs; ++i) { char nameBuf[64] = {0}; @@ -1437,7 +1349,6 @@ TEST(FontDiagnosticsTest, EmbeddedFontResolutionAndReloadingVerification) { EXPECT_TRUE(resolvedFont->isEmbedded()); - // Let's test a few common characters: 'A' (65), 'a' (97), '0' (48), ' ' (32) std::vector testChars = {32, 48, 65, 97}; for (uint32_t cp : testChars) { bool hasG = resolvedFont->hasGlyph(cp); @@ -1446,23 +1357,19 @@ TEST(FontDiagnosticsTest, EmbeddedFontResolutionAndReloadingVerification) { << ", advanceWidth=" << w << std::endl; } - // Verify metrics returned are non-zero/valid auto metrics = resolvedFont->getMetrics(12.0); std::cout << " Metrics: ascent=" << metrics.ascent << ", descent=" << metrics.descent << ", capHeight=" << metrics.capHeight << std::endl; EXPECT_NE(metrics.ascent, 0.0); EXPECT_NE(metrics.descent, 0.0); EXPECT_NE(metrics.capHeight, 0.0); - // Specific verification for text_font.pdf where we mapped charcode 1 -> GID 1 if (fileName == "text_font.pdf") { - // hasGlyph(1) should return true because the charmap maps 1 -> 1 EXPECT_TRUE(resolvedFont->hasGlyph(1)); double w = resolvedFont->getAdvanceWidth(1, 12.0); EXPECT_GT(w, 0.0); std::cout << " [VERIFIED] text_font.pdf char(1): hasGlyph=yes, advanceWidth=" << w << std::endl; } - // Verify we can load glyphs directly by glyph index (0 to num_glyphs - 1) if (face->num_glyphs > 1) { bool foundNonZeroWidth = false; for (int gid = 1; gid < face->num_glyphs; ++gid) { @@ -1525,7 +1432,6 @@ TEST(DocumentEditTest, ReplaceTextMVPStandardFont) { if (i + 1 < objectIndices.size()) indicesStr += ","; } - // Flat format payload std::string editsJson = R"({ "version": "1.0", "operations": [ @@ -1604,7 +1510,6 @@ TEST(DocumentEditTest, ReplaceTextRuntimeFontEngine) { if (i + 1 < objectIndices.size()) indicesStr += ","; } - // JSON payload including internalFontId to resolve std::string editsJson = R"({ "version": "1.0", "operations": [ @@ -1659,7 +1564,6 @@ TEST(DocumentEditTest, ReplaceTextFontReuseAndEmbedding) { ASSERT_TRUE(modelRes.has_value()); const auto& model = *modelRes; - // Find the first text run std::vector objectIndices; std::string originalFontId = ""; for (const auto& p : model.paragraphs) { @@ -1684,7 +1588,6 @@ TEST(DocumentEditTest, ReplaceTextFontReuseAndEmbedding) { if (i + 1 < objectIndices.size()) indicesStr += ","; } - // JSON payload containing replacement using system font embedding std::string editsJson = R"({ "version": "1.0", "operations": [ @@ -1702,7 +1605,6 @@ TEST(DocumentEditTest, ReplaceTextFontReuseAndEmbedding) { auto editRes = doc->applyEdits(editsJson); ASSERT_TRUE(editRes.has_value()); - // Save and reload auto saveRes = doc->saveIncremental(); ASSERT_TRUE(saveRes.has_value()); const auto& savedBytes = *saveRes; @@ -1712,7 +1614,6 @@ TEST(DocumentEditTest, ReplaceTextFontReuseAndEmbedding) { ASSERT_TRUE(newDocRes.has_value()); auto newDoc = *newDocRes; - // Get page fonts to verify that Arial was successfully embedded in the new document auto fontsRes = newDoc->getFonts(0, 0); ASSERT_TRUE(fontsRes.has_value()); @@ -1745,7 +1646,6 @@ TEST(DocumentEditTest, ReplaceTextHarfBuzzShapingAndReflow) { ASSERT_TRUE(modelRes.has_value()); const auto& model = *modelRes; - // Find a line that has at least 2 runs, where the first run uses Roboto-Regular std::vector targetIndices; std::string originalFontId = ""; std::string runBText = ""; @@ -1782,7 +1682,6 @@ TEST(DocumentEditTest, ReplaceTextHarfBuzzShapingAndReflow) { if (i + 1 < targetIndices.size()) indicesStr += ","; } - // JSON payload: replacing runA with a very long text to trigger significant shift std::string editsJson = R"({ "version": "1.0", "operations": [ @@ -1800,7 +1699,6 @@ TEST(DocumentEditTest, ReplaceTextHarfBuzzShapingAndReflow) { auto editRes = doc->applyEdits(editsJson); ASSERT_TRUE(editRes.has_value()); - // Save and reload auto saveRes = doc->saveIncremental(); ASSERT_TRUE(saveRes.has_value()); const auto& savedBytes = *saveRes; @@ -1818,7 +1716,6 @@ TEST(DocumentEditTest, ReplaceTextHarfBuzzShapingAndReflow) { ASSERT_TRUE(newModelRes.has_value()); const auto& newModel = *newModelRes; - // Find runB in the new document model and verify its X coordinate has shifted to the right bool foundRunB = false; double runBNewX = 0.0; for (const auto& p : newModel.paragraphs) { @@ -1841,6 +1738,4 @@ TEST(DocumentEditTest, ReplaceTextHarfBuzzShapingAndReflow) { std::cout << "Reflow Engine verified: '" << runBText << "' shifted from X=" << runBOrigX << " to X=" << runBNewX << std::endl; } -} - - +} \ No newline at end of file diff --git a/engine/tests/fonts_test.cpp b/engine/tests/fonts_test.cpp index 4a6f8a1..c252e6b 100644 --- a/engine/tests/fonts_test.cpp +++ b/engine/tests/fonts_test.cpp @@ -22,9 +22,6 @@ namespace { -// Case-insensitive substring check. System font filenames differ in case -// across platforms (e.g. macOS ships "Times.ttc", Windows "times.ttf"), so the -// font-fallback assertions match without regard to case. bool containsCI(const std::string& haystack, const std::string& needle) { auto it = std::search( haystack.begin(), haystack.end(), needle.begin(), needle.end(), @@ -50,14 +47,12 @@ bool saveGlyphAsPGM(const pdfengine::fonts::GlyphBitmap& bitmap, const std::stri std::string getSystemFontPath() { #if defined(_WIN32) - // Common Windows fonts std::vector paths = { "C:\\Windows\\Fonts\\arial.ttf", "C:\\Windows\\Fonts\\consola.ttf", "C:\\Windows\\Fonts\\tahoma.ttf" }; #elif defined(__APPLE__) - // Common macOS fonts std::vector paths = { "/Library/Fonts/Arial.ttf", "/System/Library/Fonts/Geneva.ttf", @@ -65,7 +60,6 @@ std::string getSystemFontPath() { "/System/Library/Fonts/Supplemental/Arial.ttf" }; #else - // Common Linux fonts std::vector paths = { "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", @@ -81,7 +75,7 @@ std::string getSystemFontPath() { return ""; } -} // namespace +} namespace pdfengine::fonts { @@ -107,12 +101,10 @@ TEST(FontTest, FontFaceMoveSemantics) { FT_Face rawFace = face1.getFace(); ASSERT_NE(rawFace, nullptr); - // Move construction FontFace face2(std::move(face1)); EXPECT_EQ(face1.getFace(), nullptr); EXPECT_EQ(face2.getFace(), rawFace); - // Move assignment FontFace face3; face3 = std::move(face2); EXPECT_EQ(face2.getFace(), nullptr); @@ -134,7 +126,7 @@ TEST(FontTest, HbShaperEmptyInput) { } TEST(FontTest, HbShaperNullFace) { - FontFace face; // Null face + FontFace face; HbShaper shaper; auto glyphs = shaper.shapeRun("Hello", face, 16); EXPECT_TRUE(glyphs.empty()); @@ -155,20 +147,15 @@ TEST(FontTest, HbShaperShapeTextSuccess) { std::string testText = "Hello World!"; auto glyphs = shaper.shapeRun(testText, face, 16); - // Validate that some glyphs were shaped. - // Note that the number of glyphs doesn't strictly have to match testText.length() (e.g. ligatures), - // but for simple English it's usually 1:1. EXPECT_FALSE(glyphs.empty()); for (const auto& g : glyphs) { - // Glyph index should be non-zero for valid glyphs (0 is usually .notdef) - // Note: some fonts might not map all characters, but Arial/DejaVu/Consolas should map ASCII. EXPECT_GT(g.advanceX, 0.0); } } TEST(FontTest, FontFaceRenderGlyphNullFace) { - FontFace face; // Null face + FontFace face; auto glyph = face.renderGlyph(0, 16); EXPECT_FALSE(glyph.has_value()); } @@ -183,11 +170,9 @@ TEST(FontTest, FontFaceRenderGlyphSuccess) { ASSERT_TRUE(face.loadFromFile(fontPath)); ASSERT_NE(face.getFace(), nullptr); - // Get the glyph index for character 'A'. unsigned int glyphIndex = FT_Get_Char_Index(face.getFace(), 'A'); - ASSERT_GT(glyphIndex, 0u); // Ensure it's not the undefined glyph + ASSERT_GT(glyphIndex, 0u); - // Render it at 24px. auto glyphOpt = face.renderGlyph(glyphIndex, 24); ASSERT_TRUE(glyphOpt.has_value()); @@ -211,22 +196,18 @@ TEST(FontTest, GlyphCacheBasicGetInsert) { EXPECT_EQ(cache.capacity(), 10u); EXPECT_EQ(cache.size(), 0u); - // Initial check: cache miss auto miss = cache.get(face, 12, 16); EXPECT_FALSE(miss.has_value()); - // Create a dummy GlyphBitmap GlyphBitmap bitmap; bitmap.width = 10; bitmap.height = 12; bitmap.pixels = std::vector(120, 255); bitmap.advance = 8.5; - // Insert cache.insert(face, 12, 16, bitmap); EXPECT_EQ(cache.size(), 1u); - // Cache hit auto hit = cache.get(face, 12, 16); ASSERT_TRUE(hit.has_value()); EXPECT_EQ(hit->width, 10); @@ -244,7 +225,6 @@ TEST(FontTest, GlyphCacheEvictionPolicy) { FontFace face; ASSERT_TRUE(face.loadFromFile(fontPath)); - // Capacity 2 GlyphCache cache(2); GlyphBitmap bmp1{ .width = 1 }; @@ -255,7 +235,6 @@ TEST(FontTest, GlyphCacheEvictionPolicy) { cache.insert(face, 2, 16, bmp2); EXPECT_EQ(cache.size(), 2u); - // Insert third one: should evict the oldest (1, 16) cache.insert(face, 3, 16, bmp3); EXPECT_EQ(cache.size(), 2u); @@ -282,11 +261,9 @@ TEST(FontTest, GlyphCacheLRUPolicy) { cache.insert(face, 1, 16, bmp1); cache.insert(face, 2, 16, bmp2); - // Access 1 to make it most recently used auto hit = cache.get(face, 1, 16); ASSERT_TRUE(hit.has_value()); - // Insert 3: since 2 is the oldest (least recently used), 2 should be evicted and 1 should remain cache.insert(face, 3, 16, bmp3); EXPECT_EQ(cache.size(), 2u); @@ -343,18 +320,15 @@ TEST(FontTest, UnicodeAndRtlShaping) { HbShaper shaper; unsigned int fontSize = 16; - // Test A: Arabic (RTL) - "سلام" { std::string arabicText = "سلام"; auto glyphs = shaper.shapeRun(arabicText, face, fontSize); EXPECT_FALSE(glyphs.empty()); for (const auto& g : glyphs) { - // Validate that shaping executed and returned valid layout metrics EXPECT_TRUE(g.advanceX != 0.0 || g.advanceY != 0.0 || g.offsetX != 0.0 || g.offsetY != 0.0 || g.glyphIndex != 999999u); } } - // Test B: Hindi - "नमस्ते" { std::string hindiText = "नमस्ते"; auto glyphs = shaper.shapeRun(hindiText, face, fontSize); @@ -364,7 +338,6 @@ TEST(FontTest, UnicodeAndRtlShaping) { } } - // Test C: Ligatures - "office" { std::string ligatureText = "office"; auto glyphs = shaper.shapeRun(ligatureText, face, fontSize); @@ -541,20 +514,14 @@ TEST(FontTest, CacheRecencyStress) { ASSERT_TRUE(cache.get(face, 2, 16).has_value()); cache.insert(face, 5, 16, bmps[5]); - // 1 should be evicted because it was the oldest EXPECT_FALSE(cache.get(face, 1, 16).has_value()); - // 5 was just inserted, should be at the front EXPECT_TRUE(cache.get(face, 5, 16).has_value()); - // Access 3 to promote it to the front ASSERT_TRUE(cache.get(face, 3, 16).has_value()); - // Insert 6. With cache.get lookups, 4 is now the oldest (since 3, 5, 2, 0 have been looked up recently) cache.insert(face, 6, 16, bmps[6]); - // 4 should be evicted EXPECT_FALSE(cache.get(face, 4, 16).has_value()); - // The rest should remain EXPECT_TRUE(cache.get(face, 0, 16).has_value()); EXPECT_TRUE(cache.get(face, 2, 16).has_value()); EXPECT_TRUE(cache.get(face, 3, 16).has_value()); @@ -568,7 +535,6 @@ TEST(FontLoaderTest, FontFaceLoadFromMemorySuccess) { GTEST_SKIP() << "No system font found to run load from memory success test."; } - // Read the entire file into a buffer std::ifstream file(fontPath, std::ios::binary | std::ios::ate); ASSERT_TRUE(file.is_open()); std::streamsize size = file.tellg(); @@ -577,12 +543,10 @@ TEST(FontLoaderTest, FontFaceLoadFromMemorySuccess) { std::vector buffer(size); ASSERT_TRUE(file.read(reinterpret_cast(buffer.data()), size)); - // Load from memory FontFace face; ASSERT_TRUE(face.loadFromMemory(buffer)); ASSERT_NE(face.getFace(), nullptr); - // Validate that glyph rendering and metrics are valid unsigned int glyphIndex = FT_Get_Char_Index(face.getFace(), 'M'); ASSERT_GT(glyphIndex, 0u); @@ -595,12 +559,10 @@ TEST(FontLoaderTest, FontFaceLoadFromMemorySuccess) { TEST(FontLoaderTest, FontFaceLoadFromMemoryInvalid) { FontFace face; - // Empty vector std::vector emptyData; EXPECT_FALSE(face.loadFromMemory(emptyData)); EXPECT_EQ(face.getFace(), nullptr); - // Corrupt garbage data std::vector corruptData = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66}; EXPECT_FALSE(face.loadFromMemory(corruptData)); EXPECT_EQ(face.getFace(), nullptr); @@ -619,14 +581,12 @@ TEST(FontLoaderTest, FontLoaderTrueTypeSuccess) { std::vector buffer(size); ASSERT_TRUE(file.read(reinterpret_cast(buffer.data()), size)); - // Use factory loader auto pdfFont = pdfengine::fonts::pdf_fonts::FontLoader::loadTrueTypeFromMemory("Arial", buffer); ASSERT_NE(pdfFont, nullptr); EXPECT_EQ(pdfFont->getBaseFont(), "Arial"); EXPECT_EQ(pdfFont->getType(), pdfengine::fonts::pdf_fonts::FontType::TrueType); EXPECT_TRUE(pdfFont->isEmbedded()); - // Shaping via the loaded FontFace HbShaper shaper; auto glyphs = shaper.shapeRun("Test Memory Load", pdfFont->getFontFace(), 16); EXPECT_FALSE(glyphs.empty()); @@ -685,28 +645,22 @@ TEST(FontDescriptorTest, DescriptorParsingSuccess) { EXPECT_DOUBLE_EQ(desc.getCapHeight(), 728.0); EXPECT_DOUBLE_EQ(desc.getStemV(), 94.0); - // Check flags EXPECT_FALSE(desc.isFixedPitch()); - EXPECT_TRUE(desc.isNonsymbolic()); // 32 + EXPECT_TRUE(desc.isNonsymbolic()); EXPECT_FALSE(desc.isItalic()); } TEST(FontDescriptorTest, DescriptorParsingMalformed) { pdfengine::fonts::pdf_fonts::FontDescriptor desc; - // Missing << EXPECT_FALSE(desc.parseFromDictionaryString("/Flags 32 >>")); - // Unmatched >> EXPECT_FALSE(desc.parseFromDictionaryString("<< /Flags 32")); - // Malformed BBox array (missing urx, ury) EXPECT_FALSE(desc.parseFromDictionaryString("<< /FontBBox [-166 -225] >>")); - // Malformed double conversion EXPECT_FALSE(desc.parseFromDictionaryString("<< /Ascent abc >>")); - // Key without value EXPECT_FALSE(desc.parseFromDictionaryString("<< /Ascent >>")); } @@ -723,14 +677,12 @@ TEST(FontDescriptorTest, FontLoaderWithDescriptor) { std::vector buffer(size); ASSERT_TRUE(file.read(reinterpret_cast(buffer.data()), size)); - // Create descriptor auto descriptor = std::make_unique(); descriptor->setFontName("Arial-BoldMT"); - descriptor->setFlags(96); // Nonsymbolic (32) | Italic (64) + descriptor->setFlags(96); descriptor->setAscent(905.0); descriptor->setDescent(-211.0); - // Load with descriptor auto pdfFont = pdfengine::fonts::pdf_fonts::FontLoader::loadTrueTypeFromMemory("Arial-Bold", buffer, std::move(descriptor)); ASSERT_NE(pdfFont, nullptr); EXPECT_EQ(pdfFont->getBaseFont(), "Arial-Bold"); @@ -749,27 +701,24 @@ TEST(FontDescriptorTest, FontLoaderWithDescriptor) { TEST(EncodingTest, PredefinedEncodingTest) { using namespace pdfengine::fonts::pdf_fonts; - // WinAnsiEncoding PredefinedEncoding winAnsi(SimpleEncodingType::WinAnsi); EXPECT_EQ(winAnsi.getType(), SimpleEncodingType::WinAnsi); - EXPECT_EQ(winAnsi.decode(65), 65); // 'A' - EXPECT_EQ(winAnsi.decode(128), 0x20AC); // Euro symbol exception - EXPECT_EQ(winAnsi.decode(169), 169); // copyright symbol (standard ISO-8859-1) - EXPECT_EQ(winAnsi.decode(300), 0); // Out of bounds + EXPECT_EQ(winAnsi.decode(65), 65); + EXPECT_EQ(winAnsi.decode(128), 0x20AC); + EXPECT_EQ(winAnsi.decode(169), 169); + EXPECT_EQ(winAnsi.decode(300), 0); - // MacRomanEncoding PredefinedEncoding macRoman(SimpleEncodingType::MacRoman); EXPECT_EQ(macRoman.getType(), SimpleEncodingType::MacRoman); - EXPECT_EQ(macRoman.decode(65), 65); // 'A' - EXPECT_EQ(macRoman.decode(128), 0x00C4); // High page lookup exception (A-dieresis) - EXPECT_EQ(macRoman.decode(300), 0); // Out of bounds + EXPECT_EQ(macRoman.decode(65), 65); + EXPECT_EQ(macRoman.decode(128), 0x00C4); + EXPECT_EQ(macRoman.decode(300), 0); - // Identity encoding (pass-through) PredefinedEncoding identity(SimpleEncodingType::Identity); EXPECT_EQ(identity.getType(), SimpleEncodingType::Identity); EXPECT_EQ(identity.decode(65), 65); EXPECT_EQ(identity.decode(128), 128); - EXPECT_EQ(identity.decode(1000), 1000); // Beyond 255 pass-through + EXPECT_EQ(identity.decode(1000), 1000); } TEST(EncodingTest, CustomEncodingWithDifferences) { @@ -778,24 +727,19 @@ TEST(EncodingTest, CustomEncodingWithDifferences) { auto baseEncoding = std::make_unique(SimpleEncodingType::WinAnsi); CustomEncoding custom(std::move(baseEncoding)); - // Fallback to base EXPECT_EQ(custom.decode(65), 65); - // Standard glyph name difference mapping custom.addDifference(120, "quotesingle"); EXPECT_EQ(custom.decode(120), 0x0027); - // Unicode-like glyph name (uniXXXX) difference mapping custom.addDifference(121, "uni0041"); - EXPECT_EQ(custom.decode(121), 0x0041); // 'A' + EXPECT_EQ(custom.decode(121), 0x0041); - // uXXXX representation custom.addDifference(122, "u0042"); - EXPECT_EQ(custom.decode(122), 0x0042); // 'B' + EXPECT_EQ(custom.decode(122), 0x0042); - // Non-existent or unresolved glyph name custom.addDifference(123, "nonexistentglyphname123"); - EXPECT_EQ(custom.decode(123), 123); // Falls back to base (WinAnsi maps 123 to 123 '{') + EXPECT_EQ(custom.decode(123), 123); } TEST(EncodingTest, ToUnicodeCMapbfchar) { @@ -822,9 +766,9 @@ TEST(EncodingTest, ToUnicodeCMapbfchar) { "end\n"; ASSERT_TRUE(cmap.parseCMapStream(cmapStream)); - EXPECT_EQ(cmap.decode(1), 0x0041); // 'A' - EXPECT_EQ(cmap.decode(2), 0x0042); // 'B' - EXPECT_EQ(cmap.decode(3), 0); // Missing + EXPECT_EQ(cmap.decode(1), 0x0041); + EXPECT_EQ(cmap.decode(2), 0x0042); + EXPECT_EQ(cmap.decode(3), 0); } TEST(EncodingTest, ToUnicodeCMapbfrange) { @@ -835,22 +779,20 @@ TEST(EncodingTest, ToUnicodeCMapbfrange) { std::string cmapStream = "begincmap\n" "2 beginbfrange\n" - "<0001> <0005> <0041>\n" // Sequential base mapping (<0001> to <0005> starting at <0041>) - "<0010> <0012> [<0061> <0062> <0063>]\n" // Array mapping + "<0001> <0005> <0041>\n" + "<0010> <0012> [<0061> <0062> <0063>]\n" "endbfrange\n" "endcmap\n"; ASSERT_TRUE(cmap.parseCMapStream(cmapStream)); - // Assert sequential - EXPECT_EQ(cmap.decode(1), 0x0041); // 'A' - EXPECT_EQ(cmap.decode(3), 0x0043); // 'C' - EXPECT_EQ(cmap.decode(5), 0x0045); // 'E' + EXPECT_EQ(cmap.decode(1), 0x0041); + EXPECT_EQ(cmap.decode(3), 0x0043); + EXPECT_EQ(cmap.decode(5), 0x0045); - // Assert array - EXPECT_EQ(cmap.decode(0x10), 0x0061); // 'a' - EXPECT_EQ(cmap.decode(0x11), 0x0062); // 'b' - EXPECT_EQ(cmap.decode(0x12), 0x0063); // 'c' + EXPECT_EQ(cmap.decode(0x10), 0x0061); + EXPECT_EQ(cmap.decode(0x11), 0x0062); + EXPECT_EQ(cmap.decode(0x12), 0x0063); } TEST(EncodingTest, ToUnicodeMalformedCMap) { @@ -858,17 +800,15 @@ TEST(EncodingTest, ToUnicodeMalformedCMap) { ToUnicodeCMap cmap; - // Completely invalid/garbage content std::string garbageStream = "This is a garbage string with no valid CMap elements"; EXPECT_FALSE(cmap.parseCMapStream(garbageStream)); - // Partially valid CMap - should recover parsed entries std::string partialStream = "begincmap\n" "beginbfchar\n" - "<0001> <0041>\n" // Valid - "<0002> /invalid\n" // Invalid/missing dest (non-hex) - "<0003> <0043>\n" // Valid + "<0001> <0041>\n" + "<0002> /invalid\n" + "<0003> <0043>\n" "endbfchar\n" "endcmap\n"; @@ -891,12 +831,10 @@ TEST(EncodingTest, FontLoaderWithEncoding) { std::vector buffer(size); ASSERT_TRUE(file.read(reinterpret_cast(buffer.data()), size)); - // Create predefined encoding (WinAnsi) auto encoding = std::make_unique( pdfengine::fonts::pdf_fonts::SimpleEncodingType::WinAnsi ); - // Load font with encoding auto pdfFont = pdfengine::fonts::pdf_fonts::FontLoader::loadTrueTypeFromMemory( "Arial-With-Encoding", buffer, nullptr, std::move(encoding) ); @@ -907,7 +845,6 @@ TEST(EncodingTest, FontLoaderWithEncoding) { const auto* retrievedEncoding = pdfFont->getEncoding(); ASSERT_NE(retrievedEncoding, nullptr); - // Verify it translates Euro symbol exception properly EXPECT_EQ(retrievedEncoding->decode(128), 0x20AC); } @@ -960,14 +897,12 @@ TEST(CIDFontTest, CompositeFontInitializationAndTypes) { auto desc = std::make_unique(); desc->setFontName("SimSun-Descriptor"); - // Test CIDFontType0 CIDFont font0("SimSun", FontType::CIDFontType0, false, std::move(desc)); EXPECT_EQ(font0.getBaseFont(), "SimSun"); EXPECT_EQ(font0.getType(), FontType::CIDFontType0); EXPECT_FALSE(font0.isEmbedded()); EXPECT_EQ(font0.getDescriptor()->getFontName(), "SimSun-Descriptor"); - // Test CIDFontType2 CIDFont font2("MS-Gothic", FontType::CIDFontType2, true); EXPECT_EQ(font2.getBaseFont(), "MS-Gothic"); EXPECT_EQ(font2.getType(), FontType::CIDFontType2); @@ -979,12 +914,10 @@ TEST(CIDFontTest, CIDToGIDTranslations) { CIDFont font("SimSun", FontType::CIDFontType2, false); - // By default, it should be an identity mapping EXPECT_TRUE(font.isIdentityMap()); EXPECT_EQ(font.mapCIDToGID(100), 100); EXPECT_EQ(font.mapCIDToGID(5000), 5000); - // Set custom mapping std::unordered_map customMap = { {10, 100}, {20, 200}, @@ -995,9 +928,8 @@ TEST(CIDFontTest, CIDToGIDTranslations) { EXPECT_EQ(font.mapCIDToGID(10), 100); EXPECT_EQ(font.mapCIDToGID(20), 200); EXPECT_EQ(font.mapCIDToGID(30), 300); - EXPECT_EQ(font.mapCIDToGID(40), 0); // Undefined / missing + EXPECT_EQ(font.mapCIDToGID(40), 0); - // Set back to identity font.setIdentityCIDToGIDMap(); EXPECT_TRUE(font.isIdentityMap()); EXPECT_EQ(font.mapCIDToGID(10), 10); @@ -1006,22 +938,18 @@ TEST(CIDFontTest, CIDToGIDTranslations) { TEST(CIDFontTest, NonEmbeddedCIDFontSystemFallback) { using namespace pdfengine::fonts::pdf_fonts; - // Load Chinese Simplified CJK fallback auto fontSimSun = FontLoader::loadCIDFontSystemFallback("SimSun", FontType::CIDFontType2); ASSERT_NE(fontSimSun, nullptr); EXPECT_EQ(fontSimSun->getBaseFont(), "SimSun"); EXPECT_EQ(fontSimSun->getType(), FontType::CIDFontType2); EXPECT_FALSE(fontSimSun->isEmbedded()); - // Load Japanese CJK fallback auto fontGothic = FontLoader::loadCIDFontSystemFallback("HeiseiMin-W3", FontType::CIDFontType0); ASSERT_NE(fontGothic, nullptr); EXPECT_EQ(fontGothic->getBaseFont(), "HeiseiMin-W3"); EXPECT_EQ(fontGothic->getType(), FontType::CIDFontType0); EXPECT_FALSE(fontGothic->isEmbedded()); - // Verify it resolved to a valid system font that can render and shape text - // E.g. we can shape a basic CJK run with SimSun or MS Gothic (like Japanese characters) HbShaper shaper; auto glyphs = shaper.shapeRun("日本語漢字", fontGothic->getFontFace(), 16); EXPECT_FALSE(glyphs.empty()); @@ -1040,30 +968,30 @@ TEST(FontFallbackTest, StandardFontFallbacks) { auto& fallback = FontFallback::getInstance(); - // Helvetica resolves to its sans-serif substitute for the host platform. std::string path1 = fallback.getFallbackFontPath("Helvetica"); EXPECT_FALSE(path1.empty()); EXPECT_TRUE(std::filesystem::exists(path1)); #if defined(_WIN32) EXPECT_TRUE(containsCI(path1, "arial") || containsCI(path1, "liberationsans")); #elif defined(__APPLE__) - // Arial may not be installed; the resolver then falls back to Helvetica. EXPECT_TRUE(containsCI(path1, "arial") || containsCI(path1, "helvetica") || containsCI(path1, "liberationsans")); #else EXPECT_TRUE(containsCI(path1, "liberationsans") || containsCI(path1, "dejavusans")); #endif - // Times resolves to its serif substitute for the host platform. std::string path2 = fallback.getFallbackFontPath("Times-Roman"); EXPECT_FALSE(path2.empty()); EXPECT_TRUE(std::filesystem::exists(path2)); #if defined(_WIN32) - EXPECT_TRUE(containsCI(path2, "times") || containsCI(path2, "liberationserif")); + EXPECT_TRUE(containsCI(path2, "times") || containsCI(path2, "liberationserif") || + containsCI(path2, "tinos")); #elif defined(__APPLE__) - EXPECT_TRUE(containsCI(path2, "times") || containsCI(path2, "liberationserif")); + EXPECT_TRUE(containsCI(path2, "times") || containsCI(path2, "liberationserif") || + containsCI(path2, "tinos")); #else - EXPECT_TRUE(containsCI(path2, "liberationserif") || containsCI(path2, "dejavuserif")); + EXPECT_TRUE(containsCI(path2, "liberationserif") || containsCI(path2, "dejavuserif") || + containsCI(path2, "tinos")); #endif } @@ -1072,22 +1000,20 @@ TEST(FontFallbackTest, StyleModifierResolutions) { auto& fallback = FontFallback::getInstance(); - // Bold Helvetica should map to a bold sans-serif substitute. std::string pathBold = fallback.getFallbackFontPath("Helvetica", true, false); EXPECT_FALSE(pathBold.empty()); EXPECT_TRUE(std::filesystem::exists(pathBold)); #if defined(_WIN32) - // Windows ships the styled variants, so assert the exact bold face. EXPECT_TRUE(containsCI(pathBold, "arialbd") || containsCI(pathBold, "liberationsans-bold")); #endif - // Bold-italic Times should map to a bold-italic serif substitute. std::string pathBoldItalic = fallback.getFallbackFontPath("Times", true, true); EXPECT_FALSE(pathBoldItalic.empty()); EXPECT_TRUE(std::filesystem::exists(pathBoldItalic)); #if defined(_WIN32) EXPECT_TRUE(containsCI(pathBoldItalic, "timesbi") || - containsCI(pathBoldItalic, "liberationserif-bolditalic")); + containsCI(pathBoldItalic, "liberationserif-bolditalic") || + containsCI(pathBoldItalic, "tinos-bolditalic")); #endif } @@ -1097,11 +1023,8 @@ TEST(FontFallbackTest, CustomFallbackRegistration) { auto& fallback = FontFallback::getInstance(); fallback.resetToDefaults(); - // Lookup the default substitute path for Helvetica. std::string standardPath = fallback.getFallbackFontPath("Helvetica"); - // The resolver only returns an override whose file actually exists on disk, - // so register a real temp file rather than a hardcoded OS-specific path. std::filesystem::path overrideFont = std::filesystem::temp_directory_path() / "pdfengine_custom_fallback.ttf"; { std::ofstream(overrideFont) << "stub-font"; } @@ -1111,7 +1034,6 @@ TEST(FontFallbackTest, CustomFallbackRegistration) { std::string overridenPath = fallback.getFallbackFontPath("Helvetica"); EXPECT_EQ(overridenPath, overrideFont.string()); - // Reset back to defaults and confirm the original substitute returns. fallback.resetToDefaults(); std::string restoredPath = fallback.getFallbackFontPath("Helvetica"); EXPECT_EQ(restoredPath, standardPath); @@ -1128,7 +1050,6 @@ TEST(FontSubsetTest, SubsetTagParsingAndStripping) { EXPECT_EQ(FontSubset::getSubsetPrefix(subsetName), "KTJHQO"); EXPECT_EQ(FontSubset::stripSubsetPrefix(subsetName), "Arial"); - // Standard naming (no prefix) std::string normalName = "Arial"; EXPECT_FALSE(FontSubset::hasSubsetPrefix(normalName)); EXPECT_EQ(FontSubset::getSubsetPrefix(normalName), ""); @@ -1138,20 +1059,16 @@ TEST(FontSubsetTest, SubsetTagParsingAndStripping) { TEST(FontSubsetTest, PrefixFormatValidation) { using namespace pdfengine::fonts::pdf_fonts; - // Prefixes must be exactly 6 UPPERCASE letters followed by '+' EXPECT_TRUE(FontSubset::hasSubsetPrefix("ABCDEF+Helvetica")); - // Lowercase should fail EXPECT_FALSE(FontSubset::hasSubsetPrefix("abcDEF+Helvetica")); EXPECT_FALSE(FontSubset::hasSubsetPrefix("ABCdef+Helvetica")); - // Numbers/Special should fail EXPECT_FALSE(FontSubset::hasSubsetPrefix("ABC123+Helvetica")); EXPECT_FALSE(FontSubset::hasSubsetPrefix("ABCDE_+Helvetica")); - // Length must be exactly 6 characters - EXPECT_FALSE(FontSubset::hasSubsetPrefix("ABCDE+Helvetica")); // 5 chars - EXPECT_FALSE(FontSubset::hasSubsetPrefix("ABCDEFG+Helvetica")); // 7 chars + EXPECT_FALSE(FontSubset::hasSubsetPrefix("ABCDE+Helvetica")); + EXPECT_FALSE(FontSubset::hasSubsetPrefix("ABCDEFG+Helvetica")); } TEST(FontSubsetTest, GIDRemappingTranslations) { @@ -1163,14 +1080,12 @@ TEST(FontSubsetTest, GIDRemappingTranslations) { EXPECT_EQ(subset.getBaseFontName(), "Arial"); EXPECT_EQ(subset.getPrefix(), "KTJHQO"); - // Default pass-through lookup EXPECT_EQ(subset.mapSubsetToOriginal(5), 5); EXPECT_FALSE(subset.hasGlyphMapping(5)); - // Register glyph ID translations - subset.addGlyphMapping(1, 41); // 'A' - subset.addGlyphMapping(2, 42); // 'B' - subset.addGlyphMapping(3, 43); // 'C' + subset.addGlyphMapping(1, 41); + subset.addGlyphMapping(2, 42); + subset.addGlyphMapping(3, 43); EXPECT_EQ(subset.getMappingCount(), 3u); EXPECT_TRUE(subset.hasGlyphMapping(1)); @@ -1178,13 +1093,12 @@ TEST(FontSubsetTest, GIDRemappingTranslations) { EXPECT_EQ(subset.mapSubsetToOriginal(1), 41); EXPECT_EQ(subset.mapSubsetToOriginal(2), 42); EXPECT_EQ(subset.mapSubsetToOriginal(3), 43); - EXPECT_EQ(subset.mapSubsetToOriginal(4), 4); // Falls back to standard GID + EXPECT_EQ(subset.mapSubsetToOriginal(4), 4); } TEST(FontSubsetTest, CoreFontSubsettingIntegration) { using namespace pdfengine::fonts::pdf_fonts; - // Test TrueType Font integration TrueTypeFont fontTT("KTJHQO+Arial", false); const auto* ttSubset = fontTT.getSubsetInfo(); ASSERT_NE(ttSubset, nullptr); @@ -1192,7 +1106,6 @@ TEST(FontSubsetTest, CoreFontSubsettingIntegration) { EXPECT_EQ(ttSubset->getBaseFontName(), "Arial"); EXPECT_EQ(ttSubset->getPrefix(), "KTJHQO"); - // Test Type1 Font integration Type1Font fontT1("SUBSET+Courier", false); const auto* t1Subset = fontT1.getSubsetInfo(); ASSERT_NE(t1Subset, nullptr); @@ -1200,7 +1113,6 @@ TEST(FontSubsetTest, CoreFontSubsettingIntegration) { EXPECT_EQ(t1Subset->getBaseFontName(), "Courier"); EXPECT_EQ(t1Subset->getPrefix(), "SUBSET"); - // Test CID Font integration CIDFont fontCID("CJKTAG+SimSun", FontType::CIDFontType2, false); const auto* cidSubset = fontCID.getSubsetInfo(); ASSERT_NE(cidSubset, nullptr); @@ -1330,36 +1242,28 @@ TEST(TextExtractionLayerTest, StringUtf8Conversion) { TEST(FontSubstitutionAndWidthsTest, WidthMatchingAndSubstitutionVerification) { using namespace pdfengine::fonts::pdf_fonts; - // Load non-embedded Helvetica font, which triggers substitution auto font = FontLoader::loadType1SystemFallback("Helvetica"); ASSERT_NE(font, nullptr); - // Confirm that the font does not have widths set yet EXPECT_FALSE(font->hasWidths()); - // Original widths for character codes 65 to 68 ('A' to 'D') from PDF /Widths array - // E.g., 'A'=600, 'B'=500, 'C'=550, 'D'=400 std::vector pdfWidths = { 600.0, 500.0, 550.0, 400.0 }; font->setWidths(65, 68, pdfWidths); EXPECT_TRUE(font->hasWidths()); - // Font size context: 12.0 double fontSize = 12.0; - // Expected widths = (W / 1000.0) * fontSize - double expectedWidthA = (600.0 / 1000.0) * fontSize; // 7.2 - double expectedWidthB = (500.0 / 1000.0) * fontSize; // 6.0 - double expectedWidthC = (550.0 / 1000.0) * fontSize; // 6.6 - double expectedWidthD = (400.0 / 1000.0) * fontSize; // 4.8 + double expectedWidthA = (600.0 / 1000.0) * fontSize; + double expectedWidthB = (500.0 / 1000.0) * fontSize; + double expectedWidthC = (550.0 / 1000.0) * fontSize; + double expectedWidthD = (400.0 / 1000.0) * fontSize; - // Verify mapped widths match original PDF widths with 0% error (well under 5%) EXPECT_NEAR(font->getCharWidth(65, fontSize), expectedWidthA, 1e-5); EXPECT_NEAR(font->getCharWidth(66, fontSize), expectedWidthB, 1e-5); EXPECT_NEAR(font->getCharWidth(67, fontSize), expectedWidthC, 1e-5); EXPECT_NEAR(font->getCharWidth(68, fontSize), expectedWidthD, 1e-5); - // Verify out-of-range character falls back safely (returns 0.0 or descriptor missing width) EXPECT_EQ(font->getCharWidth(999, fontSize), 0.0); } @@ -1377,12 +1281,10 @@ TEST(CIDAdvancedMappingTest, VerticalMetricsResolution) { auto font = FontLoader::loadType1SystemFallback("Helvetica"); ASSERT_NE(font, nullptr); - // Default vertical advance metrics: 1.0em = font size context double fontSize = 12.0; EXPECT_EQ(font->isVertical(), false); EXPECT_EQ(font->getCharHeight(65, fontSize), fontSize); - // Turn vertical metrics ON and verify custom heights font->setVertical(true); EXPECT_EQ(font->isVertical(), true); @@ -1390,39 +1292,32 @@ TEST(CIDAdvancedMappingTest, VerticalMetricsResolution) { font->setVerticalMetrics(65, 67, verticalAdvances); EXPECT_TRUE(font->hasVerticalMetrics()); - // Expected heights: (Adv / 1000.0) * fontSize - EXPECT_NEAR(font->getCharHeight(65, fontSize), 12.0, 1e-5); // (1000/1000) * 12 - EXPECT_NEAR(font->getCharHeight(66, fontSize), 9.6, 1e-5); // (800/1000) * 12 - EXPECT_NEAR(font->getCharHeight(67, fontSize), 10.8, 1e-5); // (900/1000) * 12 - EXPECT_NEAR(font->getCharHeight(999, fontSize), 12.0, 1e-5); // Fallback to 12.0 + EXPECT_NEAR(font->getCharHeight(65, fontSize), 12.0, 1e-5); + EXPECT_NEAR(font->getCharHeight(66, fontSize), 9.6, 1e-5); + EXPECT_NEAR(font->getCharHeight(67, fontSize), 10.8, 1e-5); + EXPECT_NEAR(font->getCharHeight(999, fontSize), 12.0, 1e-5); } TEST(CIDAdvancedMappingTest, CjkCollectionResolutionDB) { using namespace pdfengine::fonts::pdf_fonts; - // Standard Adobe-Japan1 Hiragana CIDs - EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1010), 0x3041); // Hiragana 'ぁ' - EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1092), 0x3093); // Hiragana 'ん' + EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1010), 0x3041); + EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1092), 0x3093); - // Standard Adobe-Japan1 Katakana CIDs - EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1125), 0x30A1); // Katakana 'ァ' - EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1205), 0x30F6); // Katakana 'ヶ' + EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1125), 0x30A1); + EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1205), 0x30F6); - // Core Kanji - EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1206), 0x4E00); // Kanji '一' + EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1206), 0x4E00); - // GB1 Chinese simplified ideographic marks - EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-GB1", 1), 0x3000); // space - EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-GB1", 2), 0x3001); // comma + EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-GB1", 1), 0x3000); + EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-GB1", 2), 0x3001); } TEST(CIDAdvancedMappingTest, HarfBuzzVerticalShapingSignature) { using namespace pdfengine::fonts; - // Validate WritingMode configurations EXPECT_EQ(static_cast(HbShaper::WritingMode::Horizontal), 0); EXPECT_EQ(static_cast(HbShaper::WritingMode::Vertical), 1); } -} // namespace pdfengine::fonts - +} \ No newline at end of file diff --git a/engine/tests/graphics_state_test.cpp b/engine/tests/graphics_state_test.cpp index 95a7ee4..f974e24 100644 --- a/engine/tests/graphics_state_test.cpp +++ b/engine/tests/graphics_state_test.cpp @@ -32,33 +32,28 @@ TEST(MatrixTest, TransformPoint) { float y = 5.0f; m.transform(x, y); - EXPECT_FLOAT_EQ(x, 20.0f); // 2*5 + 10 - EXPECT_FLOAT_EQ(y, 35.0f); // 3*5 + 20 + EXPECT_FLOAT_EQ(x, 20.0f); + EXPECT_FLOAT_EQ(y, 35.0f); } TEST(GraphicsStateStackTest, PushPop) { GraphicsStateStack stack; - // Initial state stack.current().lineWidth = 5.0f; - // Push new state stack.push(); EXPECT_FLOAT_EQ(stack.current().lineWidth, 5.0f); - // Modify current state stack.current().lineWidth = 10.0f; EXPECT_FLOAT_EQ(stack.current().lineWidth, 10.0f); - // Pop back to initial stack.pop(); EXPECT_FLOAT_EQ(stack.current().lineWidth, 5.0f); } TEST(GraphicsStateStackTest, PopEmptyProtection) { GraphicsStateStack stack; - // Attempting to pop the root state should be safe (ignored) stack.pop(); - stack.current().lineWidth = 2.0f; // Should still be valid + stack.current().lineWidth = 2.0f; EXPECT_FLOAT_EQ(stack.current().lineWidth, 2.0f); } diff --git a/engine/tests/image_xobject_test.cpp b/engine/tests/image_xobject_test.cpp index 53cde7a..216bbfb 100644 --- a/engine/tests/image_xobject_test.cpp +++ b/engine/tests/image_xobject_test.cpp @@ -406,7 +406,7 @@ std::vector rgb(std::initializer_list values) { return std::vector(values); } -} // namespace +} TEST(ImageXObjectVerification, JpegLogoAndPhotoDecodeAndMatchPdfiumRender) { const auto logoJpeg = encodeJpegRgb(rgb({ diff --git a/engine/tests/lexer_test.cpp b/engine/tests/lexer_test.cpp index b600515..9d92ea3 100644 --- a/engine/tests/lexer_test.cpp +++ b/engine/tests/lexer_test.cpp @@ -40,7 +40,7 @@ TEST(LexerTest, Strings) { EXPECT_EQ(tokens[2].stringValue, "Escapes \n \t \\ ()"); EXPECT_EQ(tokens[3].type, TokenType::String); - EXPECT_EQ(tokens[3].stringValue, "Octal +"); // \053 is '+' + EXPECT_EQ(tokens[3].stringValue, "Octal +"); } TEST(LexerTest, HexStrings) { @@ -49,12 +49,10 @@ TEST(LexerTest, HexStrings) { ASSERT_EQ(tokens.size(), 2); EXPECT_EQ(tokens[0].type, TokenType::HexString); - // "Hello" std::vector expected1 = {0x48, 0x65, 0x6C, 0x6C, 0x6F}; EXPECT_EQ(tokens[0].bytesValue, expected1); EXPECT_EQ(tokens[1].type, TokenType::HexString); - // "4A5" padded to "4A50" std::vector expected2 = {0x4A, 0x50}; EXPECT_EQ(tokens[1].bytesValue, expected2); } @@ -113,15 +111,11 @@ TEST(LexerTest, IntegrationHelloWorld) { Lexer lexer(stream->decodedContent); auto tokens = lexer.tokenize(); - // We expect something like: BT /F1 12 Tf (Hello) Tj ET - // plus any graphics state like 0 0 0 rg, etc. - // Let's just find the text block. bool foundHello = false; for (size_t i = 0; i < tokens.size(); ++i) { if (tokens[i].type == TokenType::String && tokens[i].stringValue == "Hello, world!") { foundHello = true; - // The next token should be Tj or TJ ASSERT_LT(i + 1, tokens.size()); EXPECT_EQ(tokens[i+1].type, TokenType::Operator); EXPECT_TRUE(tokens[i+1].stringValue == "Tj" || tokens[i+1].stringValue == "TJ"); diff --git a/engine/tests/parser_test.cpp b/engine/tests/parser_test.cpp index 82c9135..050651e 100644 --- a/engine/tests/parser_test.cpp +++ b/engine/tests/parser_test.cpp @@ -37,27 +37,21 @@ TEST(ParserTest, ArraysAndDicts) { ContentParser parser(tokens); auto ops = parser.parse(); - // The dictionary and array and string are ALL pushed onto the operand stack - // until the operator 'Tj' is encountered. - // Tj will consume all of them. ASSERT_EQ(ops.size(), 1); EXPECT_EQ(ops[0].op, "Tj"); ASSERT_EQ(ops[0].operands.size(), 3); - // First operand: Dict auto dictNode = ops[0].operands[0]; EXPECT_EQ(dictNode->type, AstNodeType::Dictionary); ASSERT_TRUE(dictNode->dictItems.find("Type") != dictNode->dictItems.end()); EXPECT_EQ(dictNode->dictItems["Type"]->stringValue, "Page"); - // Second operand: Array auto arrayNode = ops[0].operands[1]; EXPECT_EQ(arrayNode->type, AstNodeType::Array); ASSERT_EQ(arrayNode->arrayItems.size(), 2); EXPECT_DOUBLE_EQ(arrayNode->arrayItems[0]->numberValue, 1.0); EXPECT_DOUBLE_EQ(arrayNode->arrayItems[1]->numberValue, 2.0); - // Third operand: String auto strNode = ops[0].operands[2]; EXPECT_EQ(strNode->type, AstNodeType::String); EXPECT_EQ(strNode->stringValue, "Text"); diff --git a/engine/tests/qpdf_extractor_test.cpp b/engine/tests/qpdf_extractor_test.cpp index 5a450d6..beb7edf 100644 --- a/engine/tests/qpdf_extractor_test.cpp +++ b/engine/tests/qpdf_extractor_test.cpp @@ -39,7 +39,7 @@ TEST_F(QpdfExtractorTest, ExtractFromMemory) { auto stream = extractor.extractPageStream(path.string(), 0); ASSERT_TRUE(stream.has_value()); EXPECT_EQ(stream->pageIndex, 0); - EXPECT_FALSE(stream->compressed); // Or true depending on qpdf, but we only verify success here + EXPECT_FALSE(stream->compressed); StreamVerification v = verifyContentStream(stream.value()); EXPECT_TRUE(v.hasBT); @@ -70,7 +70,6 @@ TEST_F(QpdfExtractorTest, CorruptPdf) { } TEST_F(QpdfExtractorTest, EmptyContents) { - // about_blank.pdf usually has an empty page or no text auto path = getCorpusPath("basic", "about_blank.pdf"); auto stream = extractor.extractPageStream(path.string(), 0); ASSERT_TRUE(stream.has_value()); @@ -81,7 +80,6 @@ TEST_F(QpdfExtractorTest, EmptyContents) { } TEST_F(QpdfExtractorTest, VerifyNoText) { - // black.pdf or rectangles.pdf has no text, just graphics auto path = getCorpusPath("basic", "black.pdf"); auto stream = extractor.extractPageStream(path.string(), 0); ASSERT_TRUE(stream.has_value()); diff --git a/engine/tests/qpdf_writer_test.cpp b/engine/tests/qpdf_writer_test.cpp index af3b847..c54ec1a 100644 --- a/engine/tests/qpdf_writer_test.cpp +++ b/engine/tests/qpdf_writer_test.cpp @@ -20,12 +20,10 @@ TEST(QpdfWriterTest, IntegrationReadModifyWrite) { std::filesystem::path sourcePath = std::filesystem::path(TEST_CORPUS_DIR) / "basic" / "hello_world.pdf"; std::filesystem::path destPath = std::filesystem::path(TEST_CORPUS_DIR) / "basic" / "hello_world_modified.pdf"; - // 1. Extract QpdfExtractor extractor; auto stream = extractor.extractPageStream(sourcePath.string(), 0); ASSERT_TRUE(stream.has_value()); - // 2. Lex, Parse, Build Lexer lexer(stream->decodedContent); auto tokens = lexer.tokenize(); ContentParser parser(tokens); @@ -33,7 +31,6 @@ TEST(QpdfWriterTest, IntegrationReadModifyWrite) { ContentBuilder builder; auto objects = builder.build(ops); - // 3. Modify Text bool foundAndModified = false; for (auto& obj : objects) { if (obj->getType() == ContentObjectType::Text) { @@ -47,20 +44,16 @@ TEST(QpdfWriterTest, IntegrationReadModifyWrite) { } ASSERT_TRUE(foundAndModified) << "Could not find 'Hello, world!' to modify"; - // 4. Serialize back to Ops ContentSerializer contentSerializer; auto newOps = contentSerializer.serialize(objects); - // 5. Serialize to raw bytes AstSerializer astSerializer; std::string newRawStream = astSerializer.serialize(newOps); - // 6. Write and Save PDF QpdfWriter writer; auto writeRes = writer.replacePageStreamAndSave(sourcePath.string(), destPath.string(), 0, newRawStream); ASSERT_TRUE(writeRes.has_value()) << writeRes.error(); - // 7. Re-open and verify modification auto verifyStream = extractor.extractPageStream(destPath.string(), 0); ASSERT_TRUE(verifyStream.has_value()); @@ -83,6 +76,5 @@ TEST(QpdfWriterTest, IntegrationReadModifyWrite) { EXPECT_TRUE(verifiedModification) << "Modified string was not successfully saved and reloaded!"; - // Cleanup std::filesystem::remove(destPath); } diff --git a/engine/tests/skia_renderer_test.cpp b/engine/tests/skia_renderer_test.cpp index c1dfc74..d70a800 100644 --- a/engine/tests/skia_renderer_test.cpp +++ b/engine/tests/skia_renderer_test.cpp @@ -17,21 +17,18 @@ TEST(SkiaRendererTest, RenderSimpleDisplayList) { canvas.clear(SK_ColorWHITE); DisplayList dl; - // Push state and set fill color to red dl.saveState(); - dl.fillRect(10, 10, 50, 50); // Will be filled with red (we need a SetFillColorCommand, but right now GraphicsState doesn't expose it directly via Command yet. It will use default black) + dl.fillRect(10, 10, 50, 50); dl.restoreState(); SkiaRenderer renderer(&canvas); renderer.render(dl); - // Let's verify that a pixel at (20,20) was drawn. Since default color is black (0,0,0), we check that. SkColor c = bitmap.getColor(20, 20); EXPECT_EQ(SkColorGetR(c), 0); EXPECT_EQ(SkColorGetG(c), 0); EXPECT_EQ(SkColorGetB(c), 0); - // Check outside the rect SkColor bg = bitmap.getColor(5, 5); EXPECT_EQ(SkColorGetR(bg), 255); EXPECT_EQ(SkColorGetG(bg), 255); diff --git a/engine/tests/smoke_test.cpp b/engine/tests/smoke_test.cpp index 141c14f..eab0762 100644 --- a/engine/tests/smoke_test.cpp +++ b/engine/tests/smoke_test.cpp @@ -1,6 +1,3 @@ -// Phase 0 smoke test: proves the engine library compiles, links against its -// vcpkg dependencies, and is callable. This is what Gate G0 ("infrastructure -// compiles on all platforms") checks in CI. #include #include #include @@ -28,6 +25,5 @@ TEST(EngineSmoke, BuildInfoConsistentWithSkiaLinkage) { } TEST(EngineSmoke, LogBuildInfoDoesNotThrow) { - // Exercises the spdlog dependency end to end (compile + link + call). EXPECT_NO_THROW(pdfengine::engineLogBuildInfo()); } diff --git a/engine/tests/wasm_hello.cpp b/engine/tests/wasm_hello.cpp index ea402ca..6a8dac0 100644 --- a/engine/tests/wasm_hello.cpp +++ b/engine/tests/wasm_hello.cpp @@ -3,13 +3,11 @@ extern "C" { -// Exported function: add two numbers EMSCRIPTEN_KEEPALIVE int add(int a, int b) { return a + b; } -// Exported function: print hello message EMSCRIPTEN_KEEPALIVE void hello() { std::cout << "Hello from C++ WASM!" << std::endl; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0e330e0..cc37359 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -30,20 +30,16 @@ function App() { const [documents, setDocuments] = useState([]); const [activeDoc, setActiveDoc] = useState(null); - // Document history powers undo/redo: each edit produces a new document id. const [hist, setHist] = useState<{ stack: string[]; index: number }>({ stack: [], index: -1 }); const selectedDocId = hist.index >= 0 ? hist.stack[hist.index] : ''; const canUndo = hist.index > 0; const canRedo = hist.index < hist.stack.length - 1; const preservePageRef = useRef(false); - // Effective PDF permissions for the active document (carried forward across edits). - // `can` defaults to allowed when permissions are unknown / unencrypted. const permissions = activeDoc?.permissions ?? null; const can = (flag: keyof PDFPermissions) => !permissions || permissions[flag] !== false; const denyToast = (label: string) => toast(`${label} is not permitted by this document's restrictions`, 'error'); - // Tools the active document's permissions forbid (greyed out in the rail). const disabledTools = new Set(); if (!can('canAnnotate')) (['highlight', 'underline', 'strikeout', 'squiggly', 'draw', 'comment', 'textbox', 'stamp', 'signature'] as ToolId[]).forEach((t) => disabledTools.add(t)); @@ -51,14 +47,12 @@ function App() { const disabledToolsRef = useRef(disabledTools); disabledToolsRef.current = disabledTools; - // View / tools const [zoom, setZoom] = useState(1.0); const [activeTool, setActiveTool] = useState('select'); const [toolSettings, setToolSettings] = useState(DEFAULT_TOOL_SETTINGS); const [currentPage, setCurrentPage] = useState(0); const [isInspectorOpen, setIsInspectorOpen] = useState(true); - // Data const [annotations, setAnnotations] = useState([]); const [metadata, setMetadata] = useState(null); const [fonts, setFonts] = useState([]); @@ -69,7 +63,6 @@ function App() { const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); - // Tool aux state const [pendingSignature, setPendingSignature] = useState<{ url: string; aspect: number } | null>(null); const [signatureModalOpen, setSignatureModalOpen] = useState(false); const [aboutModalOpen, setAboutModalOpen] = useState(false); @@ -77,14 +70,12 @@ function App() { const [activeStamp, setActiveStamp] = useState<{ label: string; color: string } | null>(null); const [confirmState, setConfirmState] = useState<(CustomConfirmationOptions & { onConfirm: () => void }) | null>(null); - // Search const [searchQuery, setSearchQuery] = useState(''); const [searchCaseSensitive, setSearchCaseSensitive] = useState(false); const [searchWholeWords, setSearchWholeWords] = useState(false); const [searchResults, setSearchResults] = useState([]); const [searchCurrentMatch, setSearchCurrentMatch] = useState(0); - /* ----------------------------------------------------- history helpers */ const openDocument = useCallback((id: string) => setHist({ stack: [id], index: 0 }), []); const pushHistory = (id: string) => setHist((h) => ({ stack: [...h.stack.slice(0, h.index + 1), id], index: h.index + 1 })); @@ -101,10 +92,7 @@ function App() { toast('Redo', 'info', 1200); }; - /* ------------------------------------------------------ initial loads */ useEffect(() => { - // Healthy = the gateway answered. Engine availability is a separate concern - // (gateway can be up while the C++ engine bindings aren't built yet). gatewayService.getHealth() .then((h) => { setBackendHealthy(true); setEngineReady(!!h.engine_available); }) .catch(() => setBackendHealthy(false)); @@ -125,7 +113,6 @@ function App() { })(); }, [openDocument]); - // Load metadata, annotations, fonts when the active document version changes. useEffect(() => { if (!selectedDocId) return; let active = true; @@ -153,7 +140,6 @@ function App() { content: a.content, timestamp: a.timestamp, pageIndex: a.pageIndex, - // Ink stroke geometry (top-left page points) so the overlay can redraw it interactively. paths: Array.isArray(a.paths) && a.paths.length > 0 ? a.paths : undefined, fieldName: a.fieldName, fieldValue: a.fieldValue, @@ -172,7 +158,6 @@ function App() { return () => { active = false; }; }, [selectedDocId]); - /* ---------------------------------------------------------- live search */ useEffect(() => { const t = setTimeout(async () => { if (!searchQuery || !selectedDocId) { @@ -198,7 +183,6 @@ function App() { viewerRef.current?.scrollToPage(searchResults[i].pageIndex); }; - /* ------------------------------------------------- keyboard shortcuts */ useEffect(() => { const onKey = (e: KeyboardEvent) => { const target = e.target as HTMLElement; @@ -223,9 +207,6 @@ function App() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [canUndo, canRedo, pendingSignature]); - /* -------------------------------------------------------- edit pipeline */ - // Adopt a new document version produced by an edit (reflow, annotations, AND Raw Text via P2a): - // keep the current page visible, push it onto the undo/redo history, and refresh the doc list. const adoptNewDocument = (newDocumentId: string) => { preservePageRef.current = true; pushHistory(newDocumentId); @@ -251,7 +232,6 @@ function App() { const pageHeightPts = (pageIndex: number) => activeDoc?.pages?.[pageIndex]?.height ?? activeDoc?.pageHeight ?? 792; - /* -------------------------------------- annotation creation (optimistic) */ const handleAnnotationAdded = (a: Annotation) => { if (!can('canAnnotate')) { denyToast('Annotations'); return; } setAnnotations((prev) => [...prev, a]); @@ -285,17 +265,13 @@ function App() { } }; - /* ---------------------------------------------- new overlay placements */ const handleDecorateText = (pageIndex: number, lines: Rect[], type: 'underline' | 'strikeout' | 'squiggly', color: string) => { if (!can('canAnnotate')) { denyToast('Text decorations'); return; } - // One markup annotation covering all selected lines. quadPoints are in top-left - // points (line px ÷ zoom); the engine flips to bottom-up, mirroring highlight. const quadPoints = lines.map((line) => { const lx = line.x / zoom, ly = line.y / zoom, lw = line.width / zoom, lh = line.height / zoom; return { x1: lx, y1: ly + lh, x2: lx + lw, y2: ly + lh, x3: lx + lw, y3: ly, x4: lx, y4: ly }; }); - // Optimistically show local overlays until the edit round-trips. const newAnnos = lines.map(line => ({ id: rid('locdec'), type, @@ -319,8 +295,6 @@ function App() { setActiveTool('select'); }; - // Rewrite existing page text in place via replace_text, which targets stable - // page-object indices (no coordinates) and reflows the rest of the line. const handleEditText = (pageIndex: number, run: EditableRun, newText: string) => { if (!can('canModify')) { denyToast('Editing text'); setActiveTool('select'); return; } applyOps([{ @@ -335,7 +309,6 @@ function App() { setActiveTool('select'); }; - // Whole-paragraph reflow (Word-style wrap + push-down) for multi-line paragraph edits. const handleReflowParagraph = (pageIndex: number, payload: ReflowParagraphPayload) => { if (!can('canModify')) { denyToast('Editing text'); setActiveTool('select'); return; } applyOps([{ id: rid('reflow'), type: 'reflow_paragraph', pageIndex, data: payload }], 'Text reflowed'); @@ -368,7 +341,6 @@ function App() { setActiveTool('select'); }; - /* ---------------------------------------------------------- page ops */ const handleRotate = () => { if (!activeDoc) return; if (!can('canAssemble')) { denyToast('Rotating pages'); return; } @@ -415,7 +387,6 @@ function App() { }); }; - /* ----------------------------------------------------------- doc-level */ const handleUpload = async (file: File, password = '') => { try { setIsLoading(true); @@ -426,7 +397,6 @@ function App() { setPasswordPrompt(null); } catch (e) { if (e instanceof PasswordError) { - // Needs a password (missing or wrong) — prompt and retry. setPasswordPrompt({ file, filename: file.name, error: password ? 'Incorrect password — please try again.' : undefined }); } else { console.error('Upload failed', e); @@ -466,8 +436,6 @@ function App() { setTimeout(() => { iframe.contentWindow?.focus(); iframe.contentWindow?.print(); - // Optional: cleanup after a delay - // setTimeout(() => document.body.removeChild(iframe), 10000); }, 100); }; document.body.appendChild(iframe); @@ -493,7 +461,7 @@ function App() { const fitWidth = () => { const w = activeDoc?.pageWidth || 612; const inspectorWidth = isInspectorOpen ? 322 : 0; - const avail = window.innerWidth - inspectorWidth - 48 /*spacing leeway*/; + const avail = window.innerWidth - inspectorWidth - 48 ; setZoom(Math.max(0.25, Math.min(3, avail / w))); }; @@ -502,7 +470,6 @@ function App() { }; const handleDeleteAnnotation = (a: Annotation) => { - // Optimistically drop it from the overlay; the save round-trip makes it authoritative. setAnnotations((prev) => prev.filter((x) => x.id !== a.id)); applyOps([{ id: rid('delanno'), type: 'delete_annotation', pageIndex: a.pageIndex ?? currentPage, @@ -511,7 +478,6 @@ function App() { }; const handleUpdateAnnotation = (a: Annotation) => { - // Optimistically update setAnnotations((prev) => prev.map((x) => x.id === a.id ? a : x)); applyOps([{ id: rid('updanno'), type: 'update_annotation', pageIndex: a.pageIndex ?? currentPage, @@ -528,7 +494,6 @@ function App() { }], 'Annotation updated'); }; - /* -------------------------------------------------------------- render */ return (
= (p) => { return (