Merge pull request 'fix: code cleanup' (#67) from furqan into dev

Reviewed-on: https://gitea.maskantech.in/gitea_admin/pdf/pulls/67
This commit is contained in:
furqan
2026-06-22 09:50:27 +00:00
141 changed files with 554 additions and 1735 deletions
+1 -8
View File
@@ -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/hardened_limits.h"
#include "pdfengine/pdf_document.hpp" #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.metadata();
(void)d.extractOutline(); (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) { for (int i = 0; i < limit; ++i) {
auto page = d.getPage(i); auto page = d.getPage(i);
if (!page) continue; if (!page) continue;
+1 -3
View File
@@ -27,11 +27,9 @@ public:
double numberValue = 0.0; double numberValue = 0.0;
bool boolValue = false; bool boolValue = false;
// We use a vector of shared_ptr for recursive data structures so the node is easily copyable/movable
std::vector<std::shared_ptr<AstNode>> arrayItems; std::vector<std::shared_ptr<AstNode>> arrayItems;
std::unordered_map<std::string, std::shared_ptr<AstNode>> dictItems; std::unordered_map<std::string, std::shared_ptr<AstNode>> dictItems;
// Constructors for convenience
AstNode() = default; AstNode() = default;
explicit AstNode(AstNodeType t) : type(t) {} explicit AstNode(AstNodeType t) : type(t) {}
}; };
@@ -41,4 +39,4 @@ struct Operation {
std::vector<std::shared_ptr<AstNode>> operands; std::vector<std::shared_ptr<AstNode>> operands;
}; };
} // namespace pdfengine }
+4 -8
View File
@@ -32,12 +32,10 @@ class TextObject : public ContentObject {
public: public:
ContentObjectType getType() const override { return ContentObjectType::Text; } ContentObjectType getType() const override { return ContentObjectType::Text; }
std::string text; // The decoded text string std::string text;
std::string fontName; // Font resource name (e.g. "F1") std::string fontName;
double fontSize = 0.0; // Font size 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}; double tm[6] = {1.0, 0.0, 0.0, 1.0, 0.0, 0.0};
}; };
@@ -53,10 +51,8 @@ public:
int bitsPerComponent = 8; int bitsPerComponent = 8;
bool hasSoftMask = false; bool hasSoftMask = false;
// Decoded raw pixels (RGBA format for Skia)
std::vector<uint8_t> pixelData; std::vector<uint8_t> pixelData;
// The Current Transformation Matrix (CTM) at the time the 'Do' operator was invoked
Matrix transform; Matrix transform;
}; };
@@ -75,4 +71,4 @@ public:
Matrix transform; Matrix transform;
}; };
} // namespace pdfengine }
+3 -11
View File
@@ -5,12 +5,6 @@
namespace pdfengine { 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 { struct ExtractedStream {
std::string rawContent; std::string rawContent;
std::string decodedContent; std::string decodedContent;
@@ -20,16 +14,14 @@ struct ExtractedStream {
bool multiStream = false; 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 { struct StreamVerification {
bool hasBT = false; bool hasBT = false;
bool hasET = false; bool hasET = false;
bool hasTf = false; bool hasTf = false;
bool hasTj = false; // Tj or TJ bool hasTj = false;
bool multiStream = false; // page had multiple /Contents streams bool multiStream = false;
}; };
StreamVerification verifyContentStream(const ExtractedStream& stream); StreamVerification verifyContentStream(const ExtractedStream& stream);
} // namespace pdfengine }
+1 -11
View File
@@ -10,13 +10,11 @@ namespace pdfengine {
class CommandVisitor; class CommandVisitor;
// Base class for all drawing commands
struct Command { struct Command {
virtual ~Command() = default; virtual ~Command() = default;
virtual void accept(CommandVisitor& visitor) const = 0; virtual void accept(CommandVisitor& visitor) const = 0;
}; };
// --- Specific Command Types ---
struct SaveStateCommand : public Command { struct SaveStateCommand : public Command {
void accept(CommandVisitor& visitor) const override; void accept(CommandVisitor& visitor) const override;
@@ -41,7 +39,6 @@ struct FillRectCommand : public Command {
struct DrawTextCommand : public Command { struct DrawTextCommand : public Command {
std::string text; std::string text;
float x, y; 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) {} DrawTextCommand(std::string text, float x, float y) : text(std::move(text)), x(x), y(y) {}
void accept(CommandVisitor& visitor) const override; 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 { class CommandVisitor {
public: public:
virtual ~CommandVisitor() = default; virtual ~CommandVisitor() = default;
@@ -85,20 +80,15 @@ public:
virtual void visit(const DrawImageCommand& cmd) = 0; virtual void visit(const DrawImageCommand& cmd) = 0;
}; };
// --- Display List Container ---
// A container that stores a sequence of drawing commands.
class DisplayList { class DisplayList {
public: public:
DisplayList() = default; DisplayList() = default;
// Add commands directly
void addCommand(std::unique_ptr<Command> cmd); void addCommand(std::unique_ptr<Command> cmd);
// Replay the commands to a visitor (renderer)
void replay(CommandVisitor& visitor) const; void replay(CommandVisitor& visitor) const;
// Helper methods to easily append common commands
void saveState(); void saveState();
void restoreState(); void restoreState();
void setTransform(const Matrix& m); void setTransform(const Matrix& m);
@@ -115,4 +105,4 @@ private:
std::vector<std::unique_ptr<Command>> m_commands; std::vector<std::unique_ptr<Command>> m_commands;
}; };
} // namespace pdfengine }
+2 -18
View File
@@ -5,10 +5,6 @@
namespace pdfengine { namespace pdfengine {
// 2D Affine Transformation Matrix (3x3 matrix optimized for 2D)
// [ a b 0 ]
// [ c d 0 ]
// [ e f 1 ]
struct Matrix { struct Matrix {
float a = 1.0f, b = 0.0f; float a = 1.0f, b = 0.0f;
float c = 0.0f, d = 1.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) 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) {} : 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; [[nodiscard]] Matrix multiply(const Matrix& other) const noexcept;
// Transform a 2D point using this matrix
void transform(float& x, float& y) const noexcept; void transform(float& x, float& y) const noexcept;
}; };
@@ -31,32 +25,22 @@ struct Color {
float b = 0.0f; float b = 0.0f;
}; };
// Represents the current graphics state in a PDF document
struct GraphicsState { struct GraphicsState {
Matrix ctm; // Current Transformation Matrix Matrix ctm;
Color fillColor; Color fillColor;
Color strokeColor; Color strokeColor;
float lineWidth = 1.0f; 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 { class GraphicsStateStack {
public: public:
GraphicsStateStack(); GraphicsStateStack();
// Corresponds to the 'q' operator (save graphics state)
void push(); void push();
// Corresponds to the 'Q' operator (restore graphics state)
void pop(); void pop();
// Access the current active graphics state
[[nodiscard]] GraphicsState& current(); [[nodiscard]] GraphicsState& current();
[[nodiscard]] const GraphicsState& current() const; [[nodiscard]] const GraphicsState& current() const;
@@ -64,4 +48,4 @@ private:
std::vector<GraphicsState> m_stack; std::vector<GraphicsState> m_stack;
}; };
} // namespace pdfengine }
+3 -27
View File
@@ -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 #ifndef PDFENGINE_HARDENED_LIMITS_H
#define PDFENGINE_HARDENED_LIMITS_H #define PDFENGINE_HARDENED_LIMITS_H
@@ -17,33 +5,21 @@
namespace pdfengine::limits { namespace pdfengine::limits {
// Largest input document we will even attempt to parse (1 GiB).
inline constexpr std::uint64_t kMaxDocumentBytes = 1ull << 30; 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 inline constexpr double kMaxPageDimensionPt = 200'000.0;
// generous multiple of that to tolerate odd-but-real documents.
inline constexpr double kMaxPageDimensionPt = 200'000.0; // ~2,777 inches
// No legitimate document has this many pages; stops runaway iteration.
inline constexpr int kMaxPageCount = 100'000; 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; 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; 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 { inline constexpr bool pageDimensionsOk(double widthPt, double heightPt) noexcept {
return widthPt > 0.0 && heightPt > 0.0 && widthPt <= kMaxPageDimensionPt && return widthPt > 0.0 && heightPt > 0.0 && widthPt <= kMaxPageDimensionPt &&
heightPt <= 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 { inline constexpr bool rasterSizeOk(std::int64_t widthPx, std::int64_t heightPx) noexcept {
if (widthPx <= 0 || heightPx <= 0) return false; if (widthPx <= 0 || heightPx <= 0) return false;
if (widthPx > kMaxRasterPixels || heightPx > kMaxRasterPixels) 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; return objects >= 0 && objects <= kMaxObjects;
} }
} // namespace pdfengine::limits }
#endif // PDFENGINE_HARDENED_LIMITS_H #endif
+3 -5
View File
@@ -5,7 +5,6 @@
namespace pdfengine { namespace pdfengine {
// Basic color spaces we might encounter
enum class ColorSpace { enum class ColorSpace {
DeviceGray, DeviceGray,
DeviceRGB, DeviceRGB,
@@ -13,12 +12,11 @@ enum class ColorSpace {
Indexed Indexed
}; };
// Holds decoded image data ready for the display list (typically RGBA)
struct ImageInfo { struct ImageInfo {
int width = 0; int width = 0;
int height = 0; int height = 0;
int channels = 4; // 4 = RGBA int channels = 4;
std::vector<uint8_t> pixelData; // Decoded raw pixels std::vector<uint8_t> pixelData;
}; };
} // namespace pdfengine }
+2 -5
View File
@@ -4,13 +4,11 @@
namespace pdfengine { namespace pdfengine {
// Basic point structure
struct Point { struct Point {
float x = 0.0f; float x = 0.0f;
float y = 0.0f; float y = 0.0f;
}; };
// Represents a 2D vector path constructed from basic drawing commands.
class Path { class Path {
public: public:
enum class Verb { enum class Verb {
@@ -22,7 +20,7 @@ public:
struct Segment { struct Segment {
Verb verb; Verb verb;
Point points[3]; // Up to 3 points depending on verb (e.g., Cubic bezier) Point points[3];
}; };
Path() = default; Path() = default;
@@ -43,7 +41,6 @@ public:
m_segments.push_back({Verb::Close, {{}, {}, {}}}); m_segments.push_back({Verb::Close, {{}, {}, {}}});
} }
// Helper for 're' (rectangle) operator
void addRect(float x, float y, float w, float h) { void addRect(float x, float y, float w, float h) {
moveTo(x, y); moveTo(x, y);
lineTo(x + w, y); lineTo(x + w, y);
@@ -63,4 +60,4 @@ private:
std::vector<Segment> m_segments; std::vector<Segment> m_segments;
}; };
} // namespace pdfengine }
+1 -11
View File
@@ -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 #pragma once
#include <pdfengine/version.hpp> #include <pdfengine/version.hpp>
@@ -10,19 +5,14 @@
namespace pdfengine { namespace pdfengine {
// Human-readable engine version, e.g. "0.1.0".
[[nodiscard]] std::string_view engineVersion() noexcept; [[nodiscard]] std::string_view engineVersion() noexcept;
// One-line build descriptor, e.g. "pdfengine 0.1.0 (pdfium=off)".
[[nodiscard]] std::string_view engineBuildInfo() noexcept; [[nodiscard]] std::string_view engineBuildInfo() noexcept;
// True if this build was compiled and linked against the PDFium parser core.
[[nodiscard]] bool engineHasPdfium() noexcept; [[nodiscard]] bool engineHasPdfium() noexcept;
// True if this build was compiled and linked against the Skia graphics core.
[[nodiscard]] bool engineHasSkia() noexcept; [[nodiscard]] bool engineHasSkia() noexcept;
// Emits engineBuildInfo() through spdlog at info level.
void engineLogBuildInfo(); void engineLogBuildInfo();
} // namespace pdfengine }
+1 -5
View File
@@ -7,11 +7,8 @@ class SkCanvas;
namespace pdfengine { namespace pdfengine {
// A visitor that replays a DisplayList onto a Skia canvas.
class SkiaRenderer : public CommandVisitor { class SkiaRenderer : public CommandVisitor {
public: public:
// Takes a pointer to an external SkCanvas.
// The caller is responsible for the canvas's lifecycle.
explicit SkiaRenderer(SkCanvas* canvas); explicit SkiaRenderer(SkCanvas* canvas);
void visit(const SaveStateCommand& cmd) override; void visit(const SaveStateCommand& cmd) override;
@@ -22,7 +19,6 @@ public:
void visit(const FillPathCommand& cmd) override; void visit(const FillPathCommand& cmd) override;
void visit(const StrokePathCommand& cmd) override; void visit(const StrokePathCommand& cmd) override;
void visit(const DrawImageCommand& cmd) override; void visit(const DrawImageCommand& cmd) override;
// Renders the entire display list to the canvas
void render(const DisplayList& displayList); void render(const DisplayList& displayList);
private: private:
@@ -30,4 +26,4 @@ private:
GraphicsStateStack m_stateStack; GraphicsStateStack m_stateStack;
}; };
} // namespace pdfengine }
+16 -17
View File
@@ -7,29 +7,28 @@
namespace pdfengine { namespace pdfengine {
enum class TokenType { enum class TokenType {
Operator, // e.g., "Tj", "BT", "ET", "Tf", "re", "f" Operator,
String, // e.g., "(Hello World)", fully unescaped String,
HexString, // e.g., "<48656C6C6F>", fully decoded to bytes HexString,
Name, // e.g., "/F1" (without the slash, unescaped) Name,
Number, // e.g., "12.3", "-4", stored as string/double Number,
ArrayStart, // "[" ArrayStart,
ArrayEnd, // "]" ArrayEnd,
DictStart, // "<<" DictStart,
DictEnd, // ">>" DictEnd,
Boolean, // "true", "false" Boolean,
Null, // "null" Null,
EndOfStream // EOF marker EndOfStream
}; };
struct Token { struct Token {
TokenType type; TokenType type;
std::string stringValue; // Used for Operator, String, Name std::string stringValue;
std::vector<uint8_t> bytesValue; // Used for HexString std::vector<uint8_t> bytesValue;
double numberValue = 0.0; // Used for Number double numberValue = 0.0;
// Position tracking for error reporting (optional but helpful)
size_t startOffset = 0; size_t startOffset = 0;
size_t endOffset = 0; size_t endOffset = 0;
}; };
} // namespace pdfengine }
+1 -1
View File
@@ -55,4 +55,4 @@ void DisplayList::drawImage(const ImageInfo& image, const Matrix& m, float opaci
addCommand(std::make_unique<DrawImageCommand>(image, m, opacity)); addCommand(std::make_unique<DrawImageCommand>(image, m, opacity));
} }
} // namespace pdfengine }
+1 -1
View File
@@ -37,4 +37,4 @@ void engineLogBuildInfo() {
spdlog::info("{}", engineBuildInfo()); spdlog::info("{}", engineBuildInfo());
} }
} // namespace pdfengine }
+1 -8
View File
@@ -21,12 +21,10 @@ void Matrix::transform(float& x, float& y) const noexcept {
} }
GraphicsStateStack::GraphicsStateStack() { GraphicsStateStack::GraphicsStateStack() {
// A PDF always starts with one default graphics state on the stack.
m_stack.emplace_back(); m_stack.emplace_back();
} }
void GraphicsStateStack::push() { void GraphicsStateStack::push() {
// Duplicate the current state and push it onto the stack.
if (!m_stack.empty()) { if (!m_stack.empty()) {
m_stack.push_back(m_stack.back()); m_stack.push_back(m_stack.back());
} else { } else {
@@ -35,14 +33,9 @@ void GraphicsStateStack::push() {
} }
void GraphicsStateStack::pop() { 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) { if (m_stack.size() > 1) {
m_stack.pop_back(); m_stack.pop_back();
} else { } 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(); return m_stack.back();
} }
} // namespace pdfengine }
+2 -4
View File
@@ -49,7 +49,7 @@ void jpegErrorExit(j_common_ptr cinfo) {
longjmp(manager->jump, 1); longjmp(manager->jump, 1);
} }
} // namespace }
namespace pdfengine { namespace pdfengine {
@@ -291,8 +291,6 @@ std::vector<uint8_t> ImageDecoder::convertCmykToRgba(const std::vector<unsigned
float g = 255.0f; float g = 255.0f;
float b = 255.0f; float b = 255.0f;
// Match PDFium's default DeviceCMYK process primaries closely enough for
// unprofiled print images until ICCBased color management lands.
r = applyInk(r, 0.0f, c); r = applyInk(r, 0.0f, c);
g = applyInk(g, 174.0f, c); g = applyInk(g, 174.0f, c);
b = applyInk(b, 239.0f, c); b = applyInk(b, 239.0f, c);
@@ -323,4 +321,4 @@ std::vector<uint8_t> ImageDecoder::convertCmykToRgba(const std::vector<unsigned
return rgba; return rgba;
} }
} // namespace pdfengine }
+1 -6
View File
@@ -9,8 +9,6 @@ namespace pdfengine {
class ImageDecoder { class ImageDecoder {
public: public:
// Decodes a PDF Image XObject into RGBA raw pixels.
// Supports FlateDecode (raw) and DCTDecode (JPEG).
static std::vector<uint8_t> decode(QPDFObjectHandle imageStream, static std::vector<uint8_t> decode(QPDFObjectHandle imageStream,
const std::string& colorSpace, const std::string& colorSpace,
int width, int height, int width, int height,
@@ -21,14 +19,11 @@ private:
static std::vector<uint8_t> decodeJpeg(const std::vector<unsigned char>& jpegBytes); static std::vector<uint8_t> decodeJpeg(const std::vector<unsigned char>& jpegBytes);
static void applySoftMask(QPDFObjectHandle imageStream, std::vector<uint8_t>& rgba, int width, int height); static void applySoftMask(QPDFObjectHandle imageStream, std::vector<uint8_t>& rgba, int width, int height);
// Converts raw DeviceGray (1 byte per pixel) to RGBA (4 bytes per pixel)
static std::vector<uint8_t> convertGrayToRgba(const std::vector<unsigned char>& rawBytes, int width, int height); static std::vector<uint8_t> convertGrayToRgba(const std::vector<unsigned char>& rawBytes, int width, int height);
// Converts raw DeviceRGB (3 bytes per pixel) to RGBA (4 bytes per pixel)
static std::vector<uint8_t> convertRgbToRgba(const std::vector<unsigned char>& rawBytes, int width, int height); static std::vector<uint8_t> convertRgbToRgba(const std::vector<unsigned char>& rawBytes, int width, int height);
// Converts raw DeviceCMYK (4 bytes per pixel) to RGBA (4 bytes per pixel)
static std::vector<uint8_t> convertCmykToRgba(const std::vector<unsigned char>& rawBytes, int width, int height); static std::vector<uint8_t> convertCmykToRgba(const std::vector<unsigned char>& rawBytes, int width, int height);
}; };
} // namespace pdfengine }
+3 -13
View File
@@ -19,7 +19,6 @@ SkiaRenderer::SkiaRenderer(SkCanvas* canvas) : m_canvas(canvas) {}
void SkiaRenderer::render(const DisplayList& displayList) { void SkiaRenderer::render(const DisplayList& displayList) {
if (!m_canvas) return; if (!m_canvas) return;
// Iterate and accept commands
displayList.replay(*this); displayList.replay(*this);
} }
@@ -46,10 +45,6 @@ void SkiaRenderer::visit(const SetTransformCommand& cmd) {
#ifdef PDFENGINE_WITH_SKIA #ifdef PDFENGINE_WITH_SKIA
if (m_canvas) { if (m_canvas) {
SkMatrix skMatrix; 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( skMatrix.setAll(
cmd.matrix.a, cmd.matrix.c, cmd.matrix.e, cmd.matrix.a, cmd.matrix.c, cmd.matrix.e,
cmd.matrix.b, cmd.matrix.d, cmd.matrix.f, cmd.matrix.b, cmd.matrix.d, cmd.matrix.f,
@@ -58,7 +53,6 @@ void SkiaRenderer::visit(const SetTransformCommand& cmd) {
m_canvas->concat(skMatrix); m_canvas->concat(skMatrix);
} }
#endif #endif
// Also update internal state if we need it later
m_stateStack.current().ctm = m_stateStack.current().ctm.multiply(cmd.matrix); m_stateStack.current().ctm = m_stateStack.current().ctm.multiply(cmd.matrix);
} }
@@ -95,7 +89,6 @@ void SkiaRenderer::visit(const DrawTextCommand& cmd) {
static_cast<uint8_t>(color.g * 255), static_cast<uint8_t>(color.g * 255),
static_cast<uint8_t>(color.b * 255))); static_cast<uint8_t>(color.b * 255)));
// For phase 0/1 testing, we use a default typeface
SkFont font(nullptr, 12.0f); SkFont font(nullptr, 12.0f);
m_canvas->drawString(cmd.text.c_str(), cmd.x, cmd.y, font, paint); 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.setAntiAlias(true);
paint.setStyle(SkPaint::kStroke_Style); 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; const auto& color = m_stateStack.current().fillColor;
paint.setColor(SkColorSetARGB(255, paint.setColor(SkColorSetARGB(255,
static_cast<uint8_t>(color.r * 255), static_cast<uint8_t>(color.r * 255),
static_cast<uint8_t>(color.g * 255), static_cast<uint8_t>(color.g * 255),
static_cast<uint8_t>(color.b * 255))); static_cast<uint8_t>(color.b * 255)));
paint.setStrokeWidth(1.0f); // Default for now paint.setStrokeWidth(1.0f);
m_canvas->drawPath(skPath, paint); m_canvas->drawPath(skPath, paint);
#endif #endif
@@ -191,7 +183,6 @@ void SkiaRenderer::visit(const DrawImageCommand& cmd) {
#ifdef PDFENGINE_WITH_SKIA #ifdef PDFENGINE_WITH_SKIA
if (!m_canvas || cmd.image.pixelData.empty()) return; if (!m_canvas || cmd.image.pixelData.empty()) return;
// Create an SkImage from the raw RGBA pixels
SkImageInfo info = SkImageInfo::Make( SkImageInfo info = SkImageInfo::Make(
cmd.image.width, cmd.image.width,
cmd.image.height, cmd.image.height,
@@ -207,7 +198,7 @@ void SkiaRenderer::visit(const DrawImageCommand& cmd) {
sk_sp<SkImage> skImage = SkImages::RasterFromData( sk_sp<SkImage> skImage = SkImages::RasterFromData(
info, info,
std::move(data), std::move(data),
cmd.image.width * 4 // rowBytes cmd.image.width * 4
); );
if (skImage) { if (skImage) {
@@ -221,7 +212,6 @@ void SkiaRenderer::visit(const DrawImageCommand& cmd) {
); );
m_canvas->concat(skMatrix); 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); SkRect destRect = SkRect::MakeXYWH(0, 0, 1.0f, 1.0f);
SkPaint paint; SkPaint paint;
@@ -239,4 +229,4 @@ void SkiaRenderer::visit(const DrawImageCommand& cmd) {
#endif #endif
} }
} // namespace pdfengine }
+7 -7
View File
@@ -5,12 +5,12 @@
namespace pdfengine::fonts { namespace pdfengine::fonts {
struct GlyphBitmap { struct GlyphBitmap {
std::vector<unsigned char> pixels; // 8-bit grayscale pixels (0 = transparent, 255 = fully opaque) std::vector<unsigned char> pixels;
int width = 0; // Width of the glyph bitmap in pixels int width = 0;
int height = 0; // Height of the glyph bitmap in pixels int height = 0;
int bearingX = 0; // Horizontal bearing X (bitmap_left) in pixels int bearingX = 0;
int bearingY = 0; // Horizontal bearing Y (bitmap_top) in pixels int bearingY = 0;
double advance = 0.0; // Horizontal advance in pixels double advance = 0.0;
}; };
} // namespace pdfengine::fonts }
+2 -7
View File
@@ -26,11 +26,10 @@ std::optional<GlyphBitmap> GlyphCache::get(const FontFace& fontFace, unsigned in
auto it = shard.cache_map_.find(key); auto it = shard.cache_map_.find(key);
if (it == shard.cache_map_.end()) { if (it == shard.cache_map_.end()) {
shard.misses_++; shard.misses_++;
return std::nullopt; // Cache miss return std::nullopt;
} }
shard.hits_++; 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); shard.lru_list_.splice(shard.lru_list_.begin(), shard.lru_list_, it->second.second);
return it->second.first; return it->second.first;
@@ -49,23 +48,19 @@ void GlyphCache::insert(const FontFace& fontFace, unsigned int glyphIndex, unsig
std::lock_guard<std::mutex> lock(shard.mutex_); std::lock_guard<std::mutex> lock(shard.mutex_);
auto it = shard.cache_map_.find(key); auto it = shard.cache_map_.find(key);
if (it != shard.cache_map_.end()) { if (it != shard.cache_map_.end()) {
// Element already exists: update bitmap and move it to the front
it->second.first = bitmap; it->second.first = bitmap;
shard.lru_list_.splice(shard.lru_list_.begin(), shard.lru_list_, it->second.second); shard.lru_list_.splice(shard.lru_list_.begin(), shard.lru_list_, it->second.second);
return; return;
} }
// Capacity per shard
std::size_t shard_capacity = (capacity_ + shards_.size() - 1) / shards_.size(); 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) { if (shard.cache_map_.size() >= shard_capacity && shard_capacity > 0) {
GlyphCacheKey oldest = shard.lru_list_.back(); GlyphCacheKey oldest = shard.lru_list_.back();
shard.cache_map_.erase(oldest); shard.cache_map_.erase(oldest);
shard.lru_list_.pop_back(); shard.lru_list_.pop_back();
} }
// Insert new element
if (shard_capacity > 0) { if (shard_capacity > 0) {
shard.lru_list_.push_front(key); shard.lru_list_.push_front(key);
shard.cache_map_[key] = std::make_pair(bitmap, shard.lru_list_.begin()); shard.cache_map_[key] = std::make_pair(bitmap, shard.lru_list_.begin());
@@ -120,4 +115,4 @@ void GlyphCache::resetStats() {
} }
} }
} // namespace pdfengine::fonts }
+1 -11
View File
@@ -28,7 +28,6 @@ struct GlyphCacheKeyHash {
std::size_t h1 = std::hash<uint64_t>{}(key.fontId); std::size_t h1 = std::hash<uint64_t>{}(key.fontId);
std::size_t h2 = std::hash<unsigned int>{}(key.glyphIndex); std::size_t h2 = std::hash<unsigned int>{}(key.glyphIndex);
std::size_t h3 = std::hash<unsigned int>{}(key.fontSize); std::size_t h3 = std::hash<unsigned int>{}(key.fontSize);
// Combine hashes using standard boost hash_combine algorithm
return h1 ^ (h2 + 0x9e3779b9 + (h1 << 6) + (h1 >> 2)) ^ (h3 + 0x9e3779b9 + (h2 << 6) + (h2 >> 2)); 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); explicit GlyphCache(std::size_t capacity);
~GlyphCache(); ~GlyphCache();
// Cache is move-only to prevent copying internal list iterators
GlyphCache(const GlyphCache&) = delete; GlyphCache(const GlyphCache&) = delete;
GlyphCache& operator=(const GlyphCache&) = delete; GlyphCache& operator=(const GlyphCache&) = delete;
GlyphCache(GlyphCache&&) noexcept = default; GlyphCache(GlyphCache&&) noexcept = default;
GlyphCache& operator=(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<GlyphBitmap> get(const FontFace& fontFace, unsigned int glyphIndex, unsigned int fontSize); std::optional<GlyphBitmap> 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); 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; std::size_t size() const;
// Returns the maximum capacity of the cache
std::size_t capacity() const; std::size_t capacity() const;
// Clears all elements from the cache
void clear(); void clear();
// Returns the cache hit rate (hits / (hits + misses)). Returns 0.0 if no lookups have occurred.
double hitRate() const; double hitRate() const;
// Resets hit and miss counters
void resetStats(); void resetStats();
private: private:
@@ -85,10 +76,9 @@ private:
static constexpr std::size_t NUM_SHARDS = 16; static constexpr std::size_t NUM_SHARDS = 16;
std::vector<std::unique_ptr<Shard>> shards_; std::vector<std::unique_ptr<Shard>> shards_;
// Helper to get shard index based on key hash
std::size_t getShardIndex(const GlyphCacheKey& key) const { std::size_t getShardIndex(const GlyphCacheKey& key) const {
return GlyphCacheKeyHash{}(key) % shards_.size(); return GlyphCacheKeyHash{}(key) % shards_.size();
} }
}; };
} // namespace pdfengine::fonts }
+2 -12
View File
@@ -65,8 +65,6 @@ bool FontFace::loadFromFile(const std::string& path) {
return false; 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); FT_Set_Pixel_Sizes(face_, 0, 16);
return true; return true;
@@ -83,7 +81,6 @@ bool FontFace::loadFromMemory(const std::vector<uint8_t>& data) {
face_ = nullptr; face_ = nullptr;
} }
// Copy to internal buffer to guarantee its lifetime aligns with face_
font_data_ = data; font_data_ = data;
if (FT_New_Memory_Face( if (FT_New_Memory_Face(
@@ -98,8 +95,6 @@ bool FontFace::loadFromMemory(const std::vector<uint8_t>& data) {
return false; 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); FT_Set_Pixel_Sizes(face_, 0, 16);
return true; return true;
@@ -113,10 +108,9 @@ bool FontFace::coversUnicode(uint32_t codepoint) const {
if (!face_) return false; if (!face_) return false;
std::lock_guard<std::mutex> lock(*mutex_); std::lock_guard<std::mutex> lock(*mutex_);
FT_CharMap prev = face_->charmap; 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_Select_Charmap(face_, FT_ENCODING_UNICODE);
FT_UInt gid = FT_Get_Char_Index(face_, codepoint); 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; return gid != 0;
} }
@@ -135,12 +129,10 @@ std::optional<GlyphBitmap> FontFace::renderGlyph(unsigned int glyphIndex, unsign
std::lock_guard<std::mutex> lock(*mutex_); std::lock_guard<std::mutex> lock(*mutex_);
// Set font size in pixels.
if (FT_Set_Pixel_Sizes(face_, 0, fontSize)) { if (FT_Set_Pixel_Sizes(face_, 0, fontSize)) {
return std::nullopt; return std::nullopt;
} }
// Load and render the glyph bitmap into the face->glyph slot.
if (FT_Load_Glyph(face_, glyphIndex, FT_LOAD_RENDER)) { if (FT_Load_Glyph(face_, glyphIndex, FT_LOAD_RENDER)) {
return std::nullopt; return std::nullopt;
} }
@@ -154,10 +146,8 @@ std::optional<GlyphBitmap> FontFace::renderGlyph(unsigned int glyphIndex, unsign
glyph_bitmap.bearingX = slot->bitmap_left; glyph_bitmap.bearingX = slot->bitmap_left;
glyph_bitmap.bearingY = slot->bitmap_top; glyph_bitmap.bearingY = slot->bitmap_top;
// Advance is in 26.6 fractional pixels. Convert to double.
glyph_bitmap.advance = static_cast<double>(slot->advance.x) / 64.0; glyph_bitmap.advance = static_cast<double>(slot->advance.x) / 64.0;
// Extract the pixels. Pitch specifies bytes per row.
if (glyph_bitmap.width > 0 && glyph_bitmap.height > 0) { if (glyph_bitmap.width > 0 && glyph_bitmap.height > 0) {
glyph_bitmap.pixels.resize(glyph_bitmap.width * glyph_bitmap.height); glyph_bitmap.pixels.resize(glyph_bitmap.width * glyph_bitmap.height);
for (int r = 0; r < glyph_bitmap.height; ++r) { for (int r = 0; r < glyph_bitmap.height; ++r) {
@@ -172,4 +162,4 @@ std::optional<GlyphBitmap> FontFace::renderGlyph(unsigned int glyphIndex, unsign
return glyph_bitmap; return glyph_bitmap;
} }
} // namespace pdfengine::fonts }
+2 -8
View File
@@ -18,7 +18,6 @@ public:
FontFace(); FontFace();
~FontFace(); ~FontFace();
// FontFace is move-only to avoid double freeing FreeType resources.
FontFace(const FontFace&) = delete; FontFace(const FontFace&) = delete;
FontFace& operator=(const FontFace&) = delete; FontFace& operator=(const FontFace&) = delete;
FontFace(FontFace&& other) noexcept; FontFace(FontFace&& other) noexcept;
@@ -31,20 +30,15 @@ public:
uint64_t getId() const; uint64_t getId() const;
std::mutex& getMutex() 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; bool coversUnicode(uint32_t codepoint) const;
// Renders a glyph by index and size, returning a GlyphBitmap on success.
std::optional<GlyphBitmap> renderGlyph(unsigned int glyphIndex, unsigned int fontSize); std::optional<GlyphBitmap> renderGlyph(unsigned int glyphIndex, unsigned int fontSize);
private: private:
uint64_t font_id_; uint64_t font_id_;
FT_Face face_; FT_Face face_;
std::unique_ptr<std::mutex> mutex_; std::unique_ptr<std::mutex> mutex_;
std::vector<uint8_t> font_data_; // Keeps the loaded memory buffer alive for FT_Face std::vector<uint8_t> font_data_;
}; };
} // namespace pdfengine::fonts }
+1 -1
View File
@@ -25,4 +25,4 @@ FT_Library FreeTypeManager::getLibrary() const {
return library_; return library_;
} }
} // namespace pdfengine::fonts }
+1 -2
View File
@@ -11,7 +11,6 @@ public:
FT_Library getLibrary() const; FT_Library getLibrary() const;
// Delete copy/move constructors and assignment operators for singleton
FreeTypeManager(const FreeTypeManager&) = delete; FreeTypeManager(const FreeTypeManager&) = delete;
FreeTypeManager& operator=(const FreeTypeManager&) = delete; FreeTypeManager& operator=(const FreeTypeManager&) = delete;
FreeTypeManager(FreeTypeManager&&) = delete; FreeTypeManager(FreeTypeManager&&) = delete;
@@ -24,4 +23,4 @@ private:
FT_Library library_; FT_Library library_;
}; };
} // namespace pdfengine::fonts }
+1 -5
View File
@@ -21,7 +21,6 @@ std::expected<std::unique_ptr<pdf_fonts::Font>, std::string> FontResolver::resol
const auto& bytes = dataRes.value(); const auto& bytes = dataRes.value();
// Build FontDescriptor from FontInfo
auto descriptor = std::make_unique<pdf_fonts::FontDescriptor>(); auto descriptor = std::make_unique<pdf_fonts::FontDescriptor>();
descriptor->setFontName(fontInfo.fontName); descriptor->setFontName(fontInfo.fontName);
descriptor->setFlags(fontInfo.flags); descriptor->setFlags(fontInfo.flags);
@@ -29,7 +28,6 @@ std::expected<std::unique_ptr<pdf_fonts::Font>, std::string> FontResolver::resol
descriptor->setDescent(fontInfo.descent); descriptor->setDescent(fontInfo.descent);
descriptor->setCapHeight(fontInfo.capHeight); descriptor->setCapHeight(fontInfo.capHeight);
// Route to the appropriate FontLoader method based on FontInfo type
if (fontInfo.type == "TrueType") { if (fontInfo.type == "TrueType") {
auto font = pdf_fonts::FontLoader::loadTrueTypeFromMemory(fontInfo.normalizedFamily, bytes, std::move(descriptor)); auto font = pdf_fonts::FontLoader::loadTrueTypeFromMemory(fontInfo.normalizedFamily, bytes, std::move(descriptor));
if (font) return font; if (font) return font;
@@ -47,10 +45,8 @@ std::expected<std::unique_ptr<pdf_fonts::Font>, std::string> FontResolver::resol
return std::unexpected("Failed to parse extracted font data"); return std::unexpected("Failed to parse extracted font data");
} else { } else {
// Handle System Fallback
spdlog::info("Resolving system fallback font for: {}", fontInfo.fontName); spdlog::info("Resolving system fallback font for: {}", fontInfo.fontName);
// Build FontDescriptor from FontInfo
auto descriptor = std::make_unique<pdf_fonts::FontDescriptor>(); auto descriptor = std::make_unique<pdf_fonts::FontDescriptor>();
descriptor->setFontName(fontInfo.fontName); descriptor->setFontName(fontInfo.fontName);
descriptor->setFlags(fontInfo.flags); descriptor->setFlags(fontInfo.flags);
@@ -73,4 +69,4 @@ std::expected<std::unique_ptr<pdf_fonts::Font>, std::string> FontResolver::resol
} }
} }
} // namespace pdfengine::fonts::loader }
+1 -4
View File
@@ -12,13 +12,10 @@ class FontResolver {
public: public:
explicit FontResolver(std::shared_ptr<PdfDocument> document); explicit FontResolver(std::shared_ptr<PdfDocument> 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::unique_ptr<pdf_fonts::Font>, std::string> resolveFont(const FontInfo& fontInfo); std::expected<std::unique_ptr<pdf_fonts::Font>, std::string> resolveFont(const FontInfo& fontInfo);
private: private:
std::shared_ptr<PdfDocument> document_; std::shared_ptr<PdfDocument> document_;
}; };
} // namespace pdfengine::fonts::loader }
@@ -5,230 +5,218 @@ namespace pdfengine::fonts::pdf_fonts {
uint32_t CjkCollectionDB::resolveCID(const std::string& collection, uint32_t cid) { uint32_t CjkCollectionDB::resolveCID(const std::string& collection, uint32_t cid) {
if (collection == "Adobe-Japan1" || collection.find("Japan") != std::string::npos) { 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) { if (cid >= 1010 && cid <= 1092) {
return 0x3041 + (cid - 1010); 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 >= 1125 && cid <= 1205) {
if (cid == 1205) return 0x30F6; if (cid == 1205) return 0x30F6;
return 0x30A1 + (cid - 1125); 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) { if (cid >= 1206 && cid <= 1255) {
switch (cid) { switch (cid) {
case 1206: return 0x4E00; // 一 case 1206: return 0x4E00;
case 1207: return 0x4E01; // 丁 case 1207: return 0x4E01;
case 1208: return 0x4E03; // 七 case 1208: return 0x4E03;
case 1209: return 0x4E07; // 万 case 1209: return 0x4E07;
case 1210: return 0x4E08; // 丈 case 1210: return 0x4E08;
case 1211: return 0x4E09; // 三 case 1211: return 0x4E09;
case 1212: return 0x4E0A; // 上 case 1212: return 0x4E0A;
case 1213: return 0x4E0B; // 下 case 1213: return 0x4E0B;
case 1214: return 0x4E0D; // 不 case 1214: return 0x4E0D;
case 1215: return 0x4E0E; // 与 case 1215: return 0x4E0E;
case 1216: return 0x4E10; // 丐 case 1216: return 0x4E10;
case 1217: return 0x4E11; // 丑 case 1217: return 0x4E11;
case 1218: return 0x4E14; // 且 case 1218: return 0x4E14;
case 1219: return 0x4E15; // 丕 case 1219: return 0x4E15;
case 1220: return 0x4E16; // 世 case 1220: return 0x4E16;
case 1221: return 0x4E17; // 丘 case 1221: return 0x4E17;
case 1222: return 0x4E18; // 丙 case 1222: return 0x4E18;
case 1223: return 0x4E19; // 両 case 1223: return 0x4E19;
case 1224: return 0x4E1D; // 丞 case 1224: return 0x4E1D;
case 1225: return 0x4E2D; // 中 case 1225: return 0x4E2D;
case 1226: return 0x4E32; // 串 case 1226: return 0x4E32;
case 1227: return 0x4E38; // 丸 case 1227: return 0x4E38;
case 1228: return 0x4E39; // 丹 case 1228: return 0x4E39;
case 1229: return 0x4E3B; // 主 case 1229: return 0x4E3B;
case 1230: return 0x4E3C; // 丼 case 1230: return 0x4E3C;
case 1231: return 0x4E3F; // 丿 case 1231: return 0x4E3F;
case 1232: return 0x4E42; // 乂 case 1232: return 0x4E42;
case 1233: return 0x4E43; // 乃 case 1233: return 0x4E43;
case 1234: return 0x4E45; // 久 case 1234: return 0x4E45;
case 1235: return 0x4E4B; // 之 case 1235: return 0x4E4B;
case 1236: return 0x4E4D; // 乍 case 1236: return 0x4E4D;
case 1237: return 0x4E4E; // 乎 case 1237: return 0x4E4E;
case 1238: return 0x4E4F; // 乏 case 1238: return 0x4E4F;
case 1239: return 0x4E56; // 乖 case 1239: return 0x4E56;
case 1240: return 0x4E57; // 乗 case 1240: return 0x4E57;
case 1241: return 0x4E58; // 乘 case 1241: return 0x4E58;
case 1242: return 0x4E59; // 乙 case 1242: return 0x4E59;
case 1243: return 0x4E5D; // 九 case 1243: return 0x4E5D;
case 1244: return 0x4E5E; // 乞 case 1244: return 0x4E5E;
case 1245: return 0x4E5F; // 也 case 1245: return 0x4E5F;
case 1246: return 0x4E62; // 乱 case 1246: return 0x4E62;
case 1247: return 0x4E73; // 乳 case 1247: return 0x4E73;
case 1248: return 0x4E7E; // 乾 case 1248: return 0x4E7E;
case 1249: return 0x4E82; // 亂 case 1249: return 0x4E82;
case 1250: return 0x4E86; // 了 case 1250: return 0x4E86;
case 1251: return 0x4E88; // 予 case 1251: return 0x4E88;
case 1252: return 0x4E89; // 争 case 1252: return 0x4E89;
case 1253: return 0x4E8B; // 事 case 1253: return 0x4E8B;
case 1254: return 0x4E8C; // 二 case 1254: return 0x4E8C;
case 1255: return 0x4E8E; // 于 case 1255: return 0x4E8E;
default: break; default: break;
} }
} }
} }
else if (collection == "Adobe-Korea1" || collection.find("Korea") != std::string::npos) { 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) { if (cid >= 101 && cid <= 160) {
switch (cid) { switch (cid) {
case 101: return 0xAC00; // 가 case 101: return 0xAC00;
case 102: return 0xAC01; // 각 case 102: return 0xAC01;
case 103: return 0xAC04; // 간 case 103: return 0xAC04;
case 104: return 0xAC07; // 갇 case 104: return 0xAC07;
case 105: return 0xAC08; // 갈 case 105: return 0xAC08;
case 106: return 0xAC09; // 갉 case 106: return 0xAC09;
case 107: return 0xAC0A; // 갊 case 107: return 0xAC0A;
case 108: return 0xAC10; // 감 case 108: return 0xAC10;
case 109: return 0xAC11; // 갑 case 109: return 0xAC11;
case 110: return 0xAC12; // 값 case 110: return 0xAC12;
case 111: return 0xAC13; // 갓 case 111: return 0xAC13;
case 112: return 0xAC14; // 갔 case 112: return 0xAC14;
case 113: return 0xAC15; // 강 case 113: return 0xAC15;
case 114: return 0xAC16; // 갖 case 114: return 0xAC16;
case 115: return 0xAC17; // 갗 case 115: return 0xAC17;
case 116: return 0xAC19; // 같 case 116: return 0xAC19;
case 117: return 0xAC1A; // 갚 case 117: return 0xAC1A;
case 118: return 0xAC1B; // 갛 case 118: return 0xAC1B;
case 119: return 0xAC1C; // 개 case 119: return 0xAC1C;
case 120: return 0xAC1D; // 객 case 120: return 0xAC1D;
case 121: return 0xAC20; // 갠 case 121: return 0xAC20;
case 122: return 0xAC24; // 갤 case 122: return 0xAC24;
case 123: return 0xAC2C; // 갬 case 123: return 0xAC2C;
case 124: return 0xAC2D; // 갭 case 124: return 0xAC2D;
case 125: return 0xAC2F; // 갯 case 125: return 0xAC2F;
case 126: return 0xAC30; // 갰 case 126: return 0xAC30;
case 127: return 0xAC31; // 갱 case 127: return 0xAC31;
case 128: return 0xAC38; // 갸 case 128: return 0xAC38;
case 129: return 0xAC39; // 갹 case 129: return 0xAC39;
case 130: return 0xAC3C; // 갼 case 130: return 0xAC3C;
case 131: return 0xAC40; // 걀 case 131: return 0xAC40;
case 132: return 0xAC48; // 걈 case 132: return 0xAC48;
case 133: return 0xAC49; // 걉 case 133: return 0xAC49;
case 134: return 0xAC4B; // 걋 case 134: return 0xAC4B;
case 135: return 0xAC4C; // 걍 case 135: return 0xAC4C;
case 136: return 0xAC54; // 개의 case 136: return 0xAC54;
case 137: return 0xAC70; // 거 case 137: return 0xAC70;
case 138: return 0xAC71; // 걱 case 138: return 0xAC71;
case 139: return 0xAC74; // 건 case 139: return 0xAC74;
case 140: return 0xAC77; // 걷 case 140: return 0xAC77;
case 141: return 0xAC78; // 걸 case 141: return 0xAC78;
case 142: return 0xAC7A; // 걺 case 142: return 0xAC7A;
case 143: return 0xAC80; // 검 case 143: return 0xAC80;
case 144: return 0xAC81; // 겁 case 144: return 0xAC81;
case 145: return 0xAC83; // 것 case 145: return 0xAC83;
case 146: return 0xAC84; // 겄 case 146: return 0xAC84;
case 147: return 0xAC85; // 겡 case 147: return 0xAC85;
case 148: return 0xAC8C; // 게 case 148: return 0xAC8C;
case 149: return 0xAC8D; // 겐 case 149: return 0xAC8D;
case 150: return 0xAC90; // 겔 case 150: return 0xAC90;
case 151: return 0xAC94; // 겝 case 151: return 0xAC94;
case 152: return 0xAC9F; // 겟 case 152: return 0xAC9F;
case 153: return 0xACA0; // 겠 case 153: return 0xACA0;
case 154: return 0xACA1; // 겡 case 154: return 0xACA1;
case 155: return 0xACA8; // 겯 case 155: return 0xACA8;
case 156: return 0xACA9; // 결 case 156: return 0xACA9;
case 157: return 0xACB8; // 겸 case 157: return 0xACB8;
case 158: return 0xACB9; // 겹 case 158: return 0xACB9;
case 159: return 0xACBC; // 겻 case 159: return 0xACBC;
case 160: return 0xACBD; // 겼 case 160: return 0xACBD;
default: break; default: break;
} }
} }
} }
else if (collection == "Adobe-CNS1" || collection.find("CNS1") != std::string::npos) { 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) { if (cid >= 100 && cid <= 140) {
switch (cid) { switch (cid) {
case 100: return 0x4E00; // 一 case 100: return 0x4E00;
case 101: return 0x4E03; // 七 case 101: return 0x4E03;
case 102: return 0x4E07; // 万 case 102: return 0x4E07;
case 103: return 0x4E09; // 三 case 103: return 0x4E09;
case 104: return 0x4E0A; // 上 case 104: return 0x4E0A;
case 105: return 0x4E0B; // 下 case 105: return 0x4E0B;
case 106: return 0x4E10; // 丐 case 106: return 0x4E10;
case 107: return 0x4E11; // 丑 case 107: return 0x4E11;
case 108: return 0x4E14; // 且 case 108: return 0x4E14;
case 109: return 0x4E15; // 丕 case 109: return 0x4E15;
case 110: return 0x4E16; // 世 case 110: return 0x4E16;
case 111: return 0x4E18; // 丙 case 111: return 0x4E18;
case 112: return 0x4E2D; // 中 case 112: return 0x4E2D;
case 113: return 0x4E86; // 了 case 113: return 0x4E86;
case 114: return 0x4E92; // 互 case 114: return 0x4E92;
case 115: return 0x4E95; // 井 case 115: return 0x4E95;
case 116: return 0x4E99; // 亥 case 116: return 0x4E99;
case 117: return 0x4EBA; // 人 case 117: return 0x4EBA;
case 118: return 0x4EC0; // 什 case 118: return 0x4EC0;
case 119: return 0x4EC1; // 仁 case 119: return 0x4EC1;
case 120: return 0x4EC4; // 仃 case 120: return 0x4EC4;
case 121: return 0x4EC6; // 仄 case 121: return 0x4EC6;
case 122: return 0x4EC7; // 仇 case 122: return 0x4EC7;
case 123: return 0x4ECA; // 今 case 123: return 0x4ECA;
case 124: return 0x4ECB; // 介 case 124: return 0x4ECB;
case 125: return 0x4ECD; // 仍 case 125: return 0x4ECD;
case 126: return 0x4ECE; // 从 case 126: return 0x4ECE;
case 127: return 0x4ED4; // 仔 case 127: return 0x4ED4;
case 128: return 0x4ED5; // 仕 case 128: return 0x4ED5;
case 129: return 0x4ED6; // 他 case 129: return 0x4ED6;
case 130: return 0x4ED7; // 仗 case 130: return 0x4ED7;
case 131: return 0x4ED8; // 付 case 131: return 0x4ED8;
case 132: return 0x4ED9; // 仙 case 132: return 0x4ED9;
case 133: return 0x4EDD; // 仝 case 133: return 0x4EDD;
case 134: return 0x4EDE; // 仞 case 134: return 0x4EDE;
case 135: return 0x4EDF; // 仟 case 135: return 0x4EDF;
case 136: return 0x4EE1; // 仡 case 136: return 0x4EE1;
case 137: return 0x4EE3; // 代 case 137: return 0x4EE3;
case 138: return 0x4EE4; // 令 case 138: return 0x4EE4;
case 139: return 0x4EE5; // 以 case 139: return 0x4EE5;
case 140: return 0x4F01; // 企 case 140: return 0x4F01;
default: break; default: break;
} }
} }
} }
else if (collection == "Adobe-GB1" || collection.find("GB1") != std::string::npos) { else if (collection == "Adobe-GB1" || collection.find("GB1") != std::string::npos) {
// Standard GB simplified Chinese maps if (cid == 1) return 0x3000;
if (cid == 1) return 0x3000; // Ideographic space if (cid == 2) return 0x3001;
if (cid == 2) return 0x3001; // Ideographic comma if (cid == 3) return 0x3002;
if (cid == 3) return 0x3002; // Ideographic full stop
// Additional common GB1 characters
if (cid >= 100 && cid <= 120) { if (cid >= 100 && cid <= 120) {
switch (cid) { switch (cid) {
case 100: return 0x4E00; // 一 case 100: return 0x4E00;
case 101: return 0x4E03; // 七 case 101: return 0x4E03;
case 102: return 0x4E07; // 万 case 102: return 0x4E07;
case 103: return 0x4E09; // 三 case 103: return 0x4E09;
case 104: return 0x4E0A; // 上 case 104: return 0x4E0A;
case 105: return 0x4E0B; // 下 case 105: return 0x4E0B;
case 106: return 0x4E10; // 丐 case 106: return 0x4E10;
case 107: return 0x4E11; // 丑 case 107: return 0x4E11;
case 108: return 0x4E14; // 且 case 108: return 0x4E14;
case 109: return 0x4E15; // 丕 case 109: return 0x4E15;
case 110: return 0x4E16; // 世 case 110: return 0x4E16;
case 111: return 0x4E18; // 丙 case 111: return 0x4E18;
case 112: return 0x4E2D; // 中 case 112: return 0x4E2D;
case 113: return 0x4E86; // 了 case 113: return 0x4E86;
case 114: return 0x4E92; // 互 case 114: return 0x4E92;
case 115: return 0x4E95; // 井 case 115: return 0x4E95;
case 116: return 0x4E99; // 亥 case 116: return 0x4E99;
case 117: return 0x4EBA; // 人 case 117: return 0x4EBA;
case 118: return 0x4EC0; // 什 case 118: return 0x4EC0;
case 119: return 0x4EC1; // 仁 case 119: return 0x4EC1;
case 120: return 0x4EC4; // 仃 case 120: return 0x4EC4;
default: break; default: break;
} }
} }
} }
return 0; // fallback return 0;
} }
} // namespace pdfengine::fonts::pdf_fonts }
@@ -7,9 +7,7 @@ namespace pdfengine::fonts::pdf_fonts {
class CjkCollectionDB { class CjkCollectionDB {
public: 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); static uint32_t resolveCID(const std::string& collection, uint32_t cid);
}; };
} // namespace pdfengine::fonts::pdf_fonts }
@@ -10,61 +10,58 @@ namespace pdfengine::fonts::pdf_fonts {
namespace { namespace {
// Maps Windows CP-1252 exceptions (0x80 - 0x9F) for WinAnsiEncoding
uint32_t getWinAnsiException(uint32_t code) { uint32_t getWinAnsiException(uint32_t code) {
switch (code) { switch (code) {
case 128: return 0x20AC; // Euro case 128: return 0x20AC;
case 130: return 0x201A; // Single low-9 quote case 130: return 0x201A;
case 131: return 0x0192; // Florin case 131: return 0x0192;
case 132: return 0x201E; // Double low-9 quote case 132: return 0x201E;
case 133: return 0x2026; // Ellipsis case 133: return 0x2026;
case 134: return 0x2020; // Dagger case 134: return 0x2020;
case 135: return 0x2021; // Double Dagger case 135: return 0x2021;
case 136: return 0x02C6; // Circumflex case 136: return 0x02C6;
case 137: return 0x2030; // Per mille case 137: return 0x2030;
case 138: return 0x0160; // S Caron case 138: return 0x0160;
case 139: return 0x2039; // Single guillemet left case 139: return 0x2039;
case 140: return 0x0152; // OE case 140: return 0x0152;
case 142: return 0x017D; // Z Caron case 142: return 0x017D;
case 145: return 0x2018; // Single quote left case 145: return 0x2018;
case 146: return 0x2019; // Single quote right case 146: return 0x2019;
case 147: return 0x201C; // Double quote left case 147: return 0x201C;
case 148: return 0x201D; // Double quote right case 148: return 0x201D;
case 149: return 0x2022; // Bullet case 149: return 0x2022;
case 150: return 0x2013; // En dash case 150: return 0x2013;
case 151: return 0x2014; // Em dash case 151: return 0x2014;
case 152: return 0x02DC; // Tilde case 152: return 0x02DC;
case 153: return 0x2122; // Trademark case 153: return 0x2122;
case 154: return 0x0161; // s Caron case 154: return 0x0161;
case 155: return 0x203A; // Single guillemet right case 155: return 0x203A;
case 156: return 0x0153; // oe case 156: return 0x0153;
case 158: return 0x017E; // z Caron case 158: return 0x017E;
case 159: return 0x0178; // Y Dieresis case 159: return 0x0178;
default: return 0; default: return 0;
} }
} }
// Maps Standard MacRoman characters from 128 to 255
const uint32_t kMacRomanHighPage[128] = { const uint32_t kMacRomanHighPage[128] = {
0x00C4, 0x00C5, 0x00C7, 0x00C9, 0x00D1, 0x00D6, 0x00DC, 0x00E1, // 128-135 0x00C4, 0x00C5, 0x00C7, 0x00C9, 0x00D1, 0x00D6, 0x00DC, 0x00E1,
0x00E0, 0x00E2, 0x00E4, 0x00E3, 0x00E5, 0x00E7, 0x00E9, 0x00E8, // 136-143 0x00E0, 0x00E2, 0x00E4, 0x00E3, 0x00E5, 0x00E7, 0x00E9, 0x00E8,
0x00EA, 0x00EB, 0x00ED, 0x00EC, 0x00EE, 0x00EF, 0x00F1, 0x00F3, // 144-151 0x00EA, 0x00EB, 0x00ED, 0x00EC, 0x00EE, 0x00EF, 0x00F1, 0x00F3,
0x00F2, 0x00F4, 0x00F6, 0x00F5, 0x00FA, 0x00F9, 0x00FB, 0x00FC, // 152-159 0x00F2, 0x00F4, 0x00F6, 0x00F5, 0x00FA, 0x00F9, 0x00FB, 0x00FC,
0x2020, 0x00B0, 0x00A2, 0x00A3, 0x00A7, 0x2022, 0x00B6, 0x00DF, // 160-167 0x2020, 0x00B0, 0x00A2, 0x00A3, 0x00A7, 0x2022, 0x00B6, 0x00DF,
0x00AE, 0x00A9, 0x2122, 0x00B4, 0x00A8, 0x2260, 0x00C6, 0x00D8, // 168-175 0x00AE, 0x00A9, 0x2122, 0x00B4, 0x00A8, 0x2260, 0x00C6, 0x00D8,
0x221E, 0x00B1, 0x2264, 0x2265, 0x00A5, 0x00B5, 0x2202, 0x2211, // 176-183 0x221E, 0x00B1, 0x2264, 0x2265, 0x00A5, 0x00B5, 0x2202, 0x2211,
0x220F, 0x03C0, 0x222B, 0x00AA, 0x00BA, 0x2126, 0x00E6, 0x00F8, // 184-191 0x220F, 0x03C0, 0x222B, 0x00AA, 0x00BA, 0x2126, 0x00E6, 0x00F8,
0x00BF, 0x00A1, 0x00AC, 0x221A, 0x0192, 0x2248, 0x2206, 0x00AB, // 192-199 0x00BF, 0x00A1, 0x00AC, 0x221A, 0x0192, 0x2248, 0x2206, 0x00AB,
0x00BB, 0x2026, 0x00A0, 0x00C0, 0x00C3, 0x00D5, 0x0152, 0x0153, // 200-207 0x00BB, 0x2026, 0x00A0, 0x00C0, 0x00C3, 0x00D5, 0x0152, 0x0153,
0x2013, 0x2014, 0x201C, 0x201D, 0x2018, 0x2019, 0x00F7, 0x25CA, // 208-215 0x2013, 0x2014, 0x201C, 0x201D, 0x2018, 0x2019, 0x00F7, 0x25CA,
0x00FF, 0x0178, 0x2044, 0x00A4, 0x2039, 0x203A, 0xFB01, 0xFB02, // 216-223 0x00FF, 0x0178, 0x2044, 0x00A4, 0x2039, 0x203A, 0xFB01, 0xFB02,
0x2021, 0x00B7, 0x201A, 0x201E, 0x2030, 0x00C2, 0x00CA, 0x00C1, // 224-231 0x2021, 0x00B7, 0x201A, 0x201E, 0x2030, 0x00C2, 0x00CA, 0x00C1,
0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF, 0x00CC, 0x00D3, 0x00D4, // 232-239 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF, 0x00CC, 0x00D3, 0x00D4,
0xF8FF, 0x00D2, 0x00DA, 0x00DB, 0x00D9, 0x0131, 0x02C6, 0x02DC, // 240-247 0xF8FF, 0x00D2, 0x00DA, 0x00DB, 0x00D9, 0x0131, 0x02C6, 0x02DC,
0x00AF, 0x02D8, 0x02D9, 0x02DA, 0x00B8, 0x02DD, 0x02DB, 0x02C7 // 248-255 0x00AF, 0x02D8, 0x02D9, 0x02DA, 0x00B8, 0x02DD, 0x02DB, 0x02C7
}; };
// Safe hexadecimal digit conversions
bool parseHexValue(const std::string& hexStr, uint32_t& value) { bool parseHexValue(const std::string& hexStr, uint32_t& value) {
if (hexStr.empty()) return false; if (hexStr.empty()) return false;
std::string cleanHex; 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) {} PredefinedEncoding::PredefinedEncoding(SimpleEncodingType type) : type_(type) {}
uint32_t PredefinedEncoding::decode(uint32_t charCode) const { uint32_t PredefinedEncoding::decode(uint32_t charCode) const {
@@ -115,9 +109,6 @@ uint32_t PredefinedEncoding::decode(uint32_t charCode) const {
return charCode; return charCode;
} }
// ==========================================
// CustomEncoding Implementation
// ==========================================
CustomEncoding::CustomEncoding(std::unique_ptr<Encoding> baseEncoding) CustomEncoding::CustomEncoding(std::unique_ptr<Encoding> baseEncoding)
: base_(std::move(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 { uint32_t ToUnicodeCMap::decode(uint32_t charCode) const {
auto it = cmap_.find(charCode); auto it = cmap_.find(charCode);
if (it != cmap_.end()) { if (it != cmap_.end()) {
@@ -151,9 +139,6 @@ bool ToUnicodeCMap::parseCMapStream(const std::string& streamStr) {
return ToUnicodeParser::parse(streamStr, cmap_); return ToUnicodeParser::parse(streamStr, cmap_);
} }
// ==========================================
// Adobe Glyph List (AGL) Implementation
// ==========================================
uint32_t resolveGlyphNameToUnicode(const std::string& name) { uint32_t resolveGlyphNameToUnicode(const std::string& name) {
if (name.empty()) return 0; if (name.empty()) return 0;
@@ -209,4 +194,4 @@ uint32_t resolveGlyphNameToUnicode(const std::string& name) {
return 0; return 0;
} }
} // namespace pdfengine::fonts::pdf_fonts }
@@ -17,16 +17,13 @@ enum class SimpleEncodingType {
Identity_V Identity_V
}; };
// Base interface for PDF character code to Unicode codepoint translation
class Encoding { class Encoding {
public: public:
virtual ~Encoding() = default; 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; virtual uint32_t decode(uint32_t charCode) const = 0;
}; };
// Standard predefined simple encodings
class PredefinedEncoding : public Encoding { class PredefinedEncoding : public Encoding {
public: public:
explicit PredefinedEncoding(SimpleEncodingType type); explicit PredefinedEncoding(SimpleEncodingType type);
@@ -39,7 +36,6 @@ private:
SimpleEncodingType type_; SimpleEncodingType type_;
}; };
// Custom encodings constructed with a base predefined encoding and a set of `/Differences`
class CustomEncoding : public Encoding { class CustomEncoding : public Encoding {
public: public:
explicit CustomEncoding(std::unique_ptr<Encoding> baseEncoding); explicit CustomEncoding(std::unique_ptr<Encoding> baseEncoding);
@@ -47,7 +43,6 @@ public:
uint32_t decode(uint32_t charCode) const override; 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); void addDifference(uint32_t code, const std::string& glyphName);
private: private:
@@ -55,7 +50,6 @@ private:
std::unordered_map<uint32_t, uint32_t> custom_map_; std::unordered_map<uint32_t, uint32_t> custom_map_;
}; };
// Advanced CMaps for CJK/CID /ToUnicode translation streams
class ToUnicodeCMap : public Encoding { class ToUnicodeCMap : public Encoding {
public: public:
ToUnicodeCMap() = default; ToUnicodeCMap() = default;
@@ -63,10 +57,8 @@ public:
uint32_t decode(uint32_t charCode) const override; uint32_t decode(uint32_t charCode) const override;
// Parses a `/ToUnicode` CMap definition from a PDF stream string
bool parseCMapStream(const std::string& streamStr); bool parseCMapStream(const std::string& streamStr);
// Explicitly add mapping for testing
void addMapping(uint32_t code, uint32_t unicode) { void addMapping(uint32_t code, uint32_t unicode) {
cmap_[code] = unicode; cmap_[code] = unicode;
} }
@@ -75,7 +67,6 @@ private:
std::unordered_map<uint32_t, uint32_t> cmap_; std::unordered_map<uint32_t, uint32_t> cmap_;
}; };
// Adobe Glyph List resolver: translates a standard PostScript glyph name to a Unicode codepoint.
uint32_t resolveGlyphNameToUnicode(const std::string& name); uint32_t resolveGlyphNameToUnicode(const std::string& name);
} // namespace pdfengine::fonts::pdf_fonts }
@@ -9,7 +9,6 @@ namespace pdfengine::fonts::pdf_fonts {
namespace { namespace {
// Helper to convert hex strings (e.g. "<001E>") to uint32_t
bool parseHexValue(const std::string& hexStr, uint32_t& value) { bool parseHexValue(const std::string& hexStr, uint32_t& value) {
if (hexStr.empty()) return false; if (hexStr.empty()) return false;
std::string cleanHex; 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<std::string> tokenizeCMap(const std::string& input) { std::vector<std::string> tokenizeCMap(const std::string& input) {
std::vector<std::string> tokens; std::vector<std::string> tokens;
size_t i = 0; size_t i = 0;
size_t len = input.length(); size_t len = input.length();
while (i < len) { while (i < len) {
// Skip whitespaces
if (std::isspace(static_cast<unsigned char>(input[i])) || input[i] == '\0') { if (std::isspace(static_cast<unsigned char>(input[i])) || input[i] == '\0') {
i++; i++;
continue; continue;
} }
// Bypassing comments starting with %
if (input[i] == '%') { if (input[i] == '%') {
while (i < len && input[i] != '\n' && input[i] != '\r') { while (i < len && input[i] != '\n' && input[i] != '\r') {
i++; i++;
@@ -49,7 +45,6 @@ std::vector<std::string> tokenizeCMap(const std::string& input) {
continue; continue;
} }
// Check for double angle brackets << and >> (delimiters)
if (i + 1 < len && input[i] == '<' && input[i + 1] == '<') { if (i + 1 < len && input[i] == '<' && input[i + 1] == '<') {
tokens.push_back("<<"); tokens.push_back("<<");
i += 2; i += 2;
@@ -61,7 +56,6 @@ std::vector<std::string> tokenizeCMap(const std::string& input) {
continue; continue;
} }
// Parse hex strings <001E>
if (input[i] == '<') { if (input[i] == '<') {
std::string hexToken; std::string hexToken;
hexToken += input[i++]; hexToken += input[i++];
@@ -75,7 +69,6 @@ std::vector<std::string> tokenizeCMap(const std::string& input) {
continue; continue;
} }
// Check for other standalone delimiters
if (input[i] == '[' || input[i] == ']' || input[i] == '>') { if (input[i] == '[' || input[i] == ']' || input[i] == '>') {
std::string delimToken(1, input[i]); std::string delimToken(1, input[i]);
tokens.push_back(delimToken); tokens.push_back(delimToken);
@@ -83,7 +76,6 @@ std::vector<std::string> tokenizeCMap(const std::string& input) {
continue; continue;
} }
// Standard token parsing
std::string token; std::string token;
while (i < len && !std::isspace(static_cast<unsigned char>(input[i])) && while (i < len && !std::isspace(static_cast<unsigned char>(input[i])) &&
input[i] != '\0' && input[i] != '%' && input[i] != '<' && input[i] != '>' && input[i] != '\0' && input[i] != '%' && input[i] != '<' && input[i] != '>' &&
@@ -97,7 +89,7 @@ std::vector<std::string> tokenizeCMap(const std::string& input) {
return tokens; return tokens;
} }
} // namespace }
bool ToUnicodeParser::parse(const std::string& streamStr, std::unordered_map<uint32_t, uint32_t>& outMap) { bool ToUnicodeParser::parse(const std::string& streamStr, std::unordered_map<uint32_t, uint32_t>& outMap) {
std::vector<std::string> tokens = tokenizeCMap(streamStr); std::vector<std::string> tokens = tokenizeCMap(streamStr);
@@ -141,8 +133,7 @@ bool ToUnicodeParser::parse(const std::string& streamStr, std::unordered_map<uin
if (parseHexValue(startToken, startCode) && parseHexValue(endToken, endCode)) { if (parseHexValue(startToken, startCode) && parseHexValue(endToken, endCode)) {
if (destToken == "[") { if (destToken == "[") {
// Array mapping: e.g. <0001> <0003> [<0041> <0042> <0043>] idx += 3;
idx += 3; // skip start, end, "["
uint32_t currentCode = startCode; uint32_t currentCode = startCode;
while (idx < size && tokens[idx] != "]" && currentCode <= endCode) { while (idx < size && tokens[idx] != "]" && currentCode <= endCode) {
uint32_t destVal = 0; uint32_t destVal = 0;
@@ -159,7 +150,6 @@ bool ToUnicodeParser::parse(const std::string& streamStr, std::unordered_map<uin
continue; continue;
} }
else { else {
// Sequential base mapping: e.g. <0001> <0003> <0041>
uint32_t destStart = 0; uint32_t destStart = 0;
if (parseHexValue(destToken, destStart)) { if (parseHexValue(destToken, destStart)) {
for (uint32_t code = startCode; code <= endCode; ++code) { for (uint32_t code = startCode; code <= endCode; ++code) {
@@ -182,4 +172,4 @@ bool ToUnicodeParser::parse(const std::string& streamStr, std::unordered_map<uin
return parsedAny; return parsedAny;
} }
} // namespace pdfengine::fonts::pdf_fonts }
@@ -8,9 +8,7 @@ namespace pdfengine::fonts::pdf_fonts {
class ToUnicodeParser { class ToUnicodeParser {
public: public:
// Parses a ToUnicode CMap stream and populates the provided mapping
// Returns true if at least one valid mapping was parsed, false otherwise
static bool parse(const std::string& streamStr, std::unordered_map<uint32_t, uint32_t>& outMap); static bool parse(const std::string& streamStr, std::unordered_map<uint32_t, uint32_t>& outMap);
}; };
} // namespace pdfengine::fonts::pdf_fonts }
+2 -14
View File
@@ -40,19 +40,15 @@ public:
virtual FontType getType() const = 0; virtual FontType getType() const = 0;
virtual bool isEmbedded() const = 0; virtual bool isEmbedded() const = 0;
// Gets reference to underlying FontFace rendering object
virtual pdfengine::fonts::FontFace& getFontFace() = 0; virtual pdfengine::fonts::FontFace& getFontFace() = 0;
virtual const pdfengine::fonts::FontFace& getFontFace() const = 0; virtual const pdfengine::fonts::FontFace& getFontFace() const = 0;
// Gets the font descriptor (returns nullptr if none exists)
virtual const FontDescriptor* getDescriptor() const = 0; virtual const FontDescriptor* getDescriptor() const = 0;
// Gets the font encoding (returns nullptr if none exists)
virtual const Encoding* getEncoding() const = 0; virtual const Encoding* getEncoding() const = 0;
virtual const FontSubset* getSubsetInfo() const = 0; virtual const FontSubset* getSubsetInfo() const = 0;
// Advanced Layout APIs
virtual FontSource getSourceType() const { return source_type_; } virtual FontSource getSourceType() const { return source_type_; }
virtual void setSourceType(FontSource source) { source_type_ = source; } virtual void setSourceType(FontSource source) { source_type_ = source; }
@@ -87,17 +83,15 @@ public:
FT_Face rawFace = getFontFace().getFace(); FT_Face rawFace = getFontFace().getFace();
FT_Set_Pixel_Sizes(rawFace, 0, static_cast<FT_UInt>(fontSize)); FT_Set_Pixel_Sizes(rawFace, 0, static_cast<FT_UInt>(fontSize));
// Convert from 26.6 to double
metrics_cache_.ascent = static_cast<double>(rawFace->size->metrics.ascender) / 64.0; metrics_cache_.ascent = static_cast<double>(rawFace->size->metrics.ascender) / 64.0;
metrics_cache_.descent = static_cast<double>(rawFace->size->metrics.descender) / 64.0; metrics_cache_.descent = static_cast<double>(rawFace->size->metrics.descender) / 64.0;
metrics_cache_.lineGap = static_cast<double>(rawFace->size->metrics.height - rawFace->size->metrics.ascender + rawFace->size->metrics.descender) / 64.0; metrics_cache_.lineGap = static_cast<double>(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'); FT_UInt hIndex = FT_Get_Char_Index(rawFace, 'H');
if (hIndex > 0 && FT_Load_Glyph(rawFace, hIndex, FT_LOAD_DEFAULT) == 0) { if (hIndex > 0 && FT_Load_Glyph(rawFace, hIndex, FT_LOAD_DEFAULT) == 0) {
metrics_cache_.capHeight = static_cast<double>(rawFace->glyph->metrics.horiBearingY) / 64.0; metrics_cache_.capHeight = static_cast<double>(rawFace->glyph->metrics.horiBearingY) / 64.0;
} else { } else {
metrics_cache_.capHeight = metrics_cache_.ascent * 0.7; // Fallback metrics_cache_.capHeight = metrics_cache_.ascent * 0.7;
} }
metrics_cached_ = true; metrics_cached_ = true;
@@ -106,13 +100,10 @@ public:
return metrics_cache_; return metrics_cache_;
} }
// Translates a raw character code to a Unicode codepoint
virtual uint32_t decodeToUnicode(uint32_t charCode) const = 0; 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<uint32_t>& charCodes) const = 0; virtual std::string decodeStringToUnicode(const std::vector<uint32_t>& charCodes) const = 0;
// Widths methods
virtual void setWidths(uint32_t firstChar, uint32_t lastChar, const std::vector<double>& widths) { virtual void setWidths(uint32_t firstChar, uint32_t lastChar, const std::vector<double>& widths) {
first_char_ = firstChar; first_char_ = firstChar;
last_char_ = lastChar; last_char_ = lastChar;
@@ -131,12 +122,9 @@ public:
if (charCode >= first_char_ && charCode <= last_char_) { if (charCode >= first_char_ && charCode <= last_char_) {
size_t index = charCode - first_char_; size_t index = charCode - first_char_;
if (index < widths_.size()) { if (index < widths_.size()) {
// PDF widths are in 1/1000 units of text space.
// Scale to requested fontSize.
return (widths_[index] / 1000.0) * fontSize; return (widths_[index] / 1000.0) * fontSize;
} }
} }
// Fallback to missing width if descriptor is available
const auto* desc = getDescriptor(); const auto* desc = getDescriptor();
if (desc && desc->getMissingWidth() > 0.0) { if (desc && desc->getMissingWidth() > 0.0) {
return (desc->getMissingWidth() / 1000.0) * fontSize; return (desc->getMissingWidth() / 1000.0) * fontSize;
@@ -198,4 +186,4 @@ protected:
bool has_vertical_metrics_ = false; bool has_vertical_metrics_ = false;
}; };
} // namespace pdfengine::fonts::pdf_fonts }
+14 -15
View File
@@ -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') { if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\0') {
pos++; pos++;
} else if (c == '%') { } else if (c == '%') {
// comment, skip to end of line
while (pos < str.size() && str[pos] != '\n' && str[pos] != '\r') { while (pos < str.size() && str[pos] != '\n' && str[pos] != '\r') {
pos++; pos++;
} }
@@ -23,32 +22,32 @@ void skipWhitespace(const std::string& str, size_t& pos) {
} }
} }
} // namespace }
bool FontDescriptor::parseFromDictionaryString(const std::string& dictStr) { bool FontDescriptor::parseFromDictionaryString(const std::string& dictStr) {
size_t pos = 0; size_t pos = 0;
skipWhitespace(dictStr, pos); skipWhitespace(dictStr, pos);
if (pos + 2 > dictStr.size() || dictStr[pos] != '<' || dictStr[pos+1] != '<') { if (pos + 2 > dictStr.size() || dictStr[pos] != '<' || dictStr[pos+1] != '<') {
return false; // must start with << return false;
} }
pos += 2; pos += 2;
while (true) { while (true) {
skipWhitespace(dictStr, pos); skipWhitespace(dictStr, pos);
if (pos >= dictStr.size()) { if (pos >= dictStr.size()) {
return false; // missing closing >> return false;
} }
if (pos + 2 <= dictStr.size() && dictStr[pos] == '>' && dictStr[pos+1] == '>') { if (pos + 2 <= dictStr.size() && dictStr[pos] == '>' && dictStr[pos+1] == '>') {
pos += 2; pos += 2;
break; // successfully reached closing >> break;
} }
if (dictStr[pos] != '/') { if (dictStr[pos] != '/') {
return false; // key must start with / return false;
} }
pos++; // skip '/' pos++;
size_t keyStart = pos; size_t keyStart = pos;
while (pos < dictStr.size()) { while (pos < dictStr.size()) {
@@ -61,19 +60,19 @@ bool FontDescriptor::parseFromDictionaryString(const std::string& dictStr) {
pos++; pos++;
} }
if (pos == keyStart) { if (pos == keyStart) {
return false; // empty key name return false;
} }
std::string key = dictStr.substr(keyStart, pos - keyStart); std::string key = dictStr.substr(keyStart, pos - keyStart);
skipWhitespace(dictStr, pos); skipWhitespace(dictStr, pos);
if (pos >= dictStr.size()) { if (pos >= dictStr.size()) {
return false; // key with no value return false;
} }
std::string valueStr; std::string valueStr;
if (dictStr[pos] == '[') { if (dictStr[pos] == '[') {
size_t arrStart = pos; size_t arrStart = pos;
pos++; // skip '[' pos++;
int depth = 1; int depth = 1;
while (pos < dictStr.size() && depth > 0) { while (pos < dictStr.size() && depth > 0) {
if (dictStr[pos] == '[') depth++; if (dictStr[pos] == '[') depth++;
@@ -81,11 +80,11 @@ bool FontDescriptor::parseFromDictionaryString(const std::string& dictStr) {
pos++; pos++;
} }
if (depth > 0) { if (depth > 0) {
return false; // unmatched brackets return false;
} }
valueStr = dictStr.substr(arrStart, pos - arrStart); valueStr = dictStr.substr(arrStart, pos - arrStart);
} else if (dictStr[pos] == '/') { } else if (dictStr[pos] == '/') {
pos++; // skip '/' pos++;
size_t valStart = pos; size_t valStart = pos;
while (pos < dictStr.size()) { while (pos < dictStr.size()) {
char c = dictStr[pos]; char c = dictStr[pos];
@@ -157,7 +156,7 @@ bool FontDescriptor::parseFromDictionaryString(const std::string& dictStr) {
idx += processed; idx += processed;
} }
if (nums.size() != 4) { if (nums.size() != 4) {
return false; // FontBBox must have 4 coordinates return false;
} }
font_bbox_.llx = static_cast<int>(nums[0]); font_bbox_.llx = static_cast<int>(nums[0]);
font_bbox_.lly = static_cast<int>(nums[1]); font_bbox_.lly = static_cast<int>(nums[1]);
@@ -165,11 +164,11 @@ bool FontDescriptor::parseFromDictionaryString(const std::string& dictStr) {
font_bbox_.ury = static_cast<int>(nums[3]); font_bbox_.ury = static_cast<int>(nums[3]);
} }
} catch (const std::exception&) { } catch (const std::exception&) {
return false; // parsing or range exception return false;
} }
} }
return true; return true;
} }
} // namespace pdfengine::fonts::pdf_fonts }
@@ -21,7 +21,6 @@ public:
FontDescriptor() = default; FontDescriptor() = default;
~FontDescriptor() = default; ~FontDescriptor() = default;
// Getters and Setters
std::string getFontName() const { return font_name_; } std::string getFontName() const { return font_name_; }
void setFontName(const std::string& name) { font_name_ = name; } void setFontName(const std::string& name) { font_name_ = name; }
@@ -64,7 +63,6 @@ public:
double getMissingWidth() const { return missing_width_; } double getMissingWidth() const { return missing_width_; }
void setMissingWidth(double missing_width) { missing_width_ = 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 isFixedPitch() const { return (flags_ & 1) != 0; }
bool isSerif() const { return (flags_ & 2) != 0; } bool isSerif() const { return (flags_ & 2) != 0; }
bool isSymbolic() const { return (flags_ & 4) != 0; } bool isSymbolic() const { return (flags_ & 4) != 0; }
@@ -75,7 +73,6 @@ public:
bool isSmallCap() const { return (flags_ & 131072) != 0; } bool isSmallCap() const { return (flags_ & 131072) != 0; }
bool isForceBold() const { return (flags_ & 262144) != 0; } bool isForceBold() const { return (flags_ & 262144) != 0; }
// Parses a PDF dictionary string representing a FontDescriptor.
bool parseFromDictionaryString(const std::string& dictStr); bool parseFromDictionaryString(const std::string& dictStr);
private: private:
@@ -95,4 +92,4 @@ private:
double missing_width_ = 0.0; double missing_width_ = 0.0;
}; };
} // namespace pdfengine::fonts::pdf_fonts }
+1 -6
View File
@@ -13,7 +13,7 @@ std::string toLower(const std::string& str) {
}); });
return lower; return lower;
} }
} // namespace }
FontFallback& FontFallback::getInstance() { FontFallback& FontFallback::getInstance() {
static FontFallback instance; 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 #ifdef PDFENGINE_FONT_DIR
{ {
auto has = [&](const char* s) { return lowerName.find(s) != std::string::npos; }; auto has = [&](const char* s) { return lowerName.find(s) != std::string::npos; };
+1 -1
View File
@@ -36,4 +36,4 @@ private:
mutable std::mutex rules_mutex_; mutable std::mutex rules_mutex_;
}; };
} // namespace pdfengine::fonts::pdf_fonts }
+1 -5
View File
@@ -41,7 +41,6 @@ std::unique_ptr<Font> FontLoader::loadType1SystemFallback(
std::unique_ptr<FontDescriptor> descriptor, std::unique_ptr<FontDescriptor> descriptor,
std::unique_ptr<Encoding> encoding std::unique_ptr<Encoding> encoding
) { ) {
// Determine bold/italic style modifiers from name
std::string lowerName = baseFont; std::string lowerName = baseFont;
std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(), [](unsigned char c) { std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(), [](unsigned char c) {
return static_cast<char>(std::tolower(c)); return static_cast<char>(std::tolower(c));
@@ -50,7 +49,6 @@ std::unique_ptr<Font> FontLoader::loadType1SystemFallback(
bool bold = (lowerName.find("bold") != std::string::npos); bool bold = (lowerName.find("bold") != std::string::npos);
bool italic = (lowerName.find("italic") != std::string::npos || lowerName.find("oblique") != 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); std::string fontPath = FontFallback::getInstance().getFallbackFontPath(baseFont, bold, italic);
auto font = std::make_unique<Type1Font>(baseFont, false, std::move(descriptor), std::move(encoding)); auto font = std::make_unique<Type1Font>(baseFont, false, std::move(descriptor), std::move(encoding));
@@ -80,7 +78,6 @@ std::unique_ptr<Font> FontLoader::loadCIDFontSystemFallback(
std::unique_ptr<FontDescriptor> descriptor, std::unique_ptr<FontDescriptor> descriptor,
std::unique_ptr<Encoding> encoding std::unique_ptr<Encoding> encoding
) { ) {
// Determine CJK style modifiers if any
std::string lowerName = baseFont; std::string lowerName = baseFont;
std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(), [](unsigned char c) { std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(), [](unsigned char c) {
return static_cast<char>(std::tolower(c)); return static_cast<char>(std::tolower(c));
@@ -88,7 +85,6 @@ std::unique_ptr<Font> FontLoader::loadCIDFontSystemFallback(
bool bold = (lowerName.find("bold") != std::string::npos); bool bold = (lowerName.find("bold") != std::string::npos);
bool italic = (lowerName.find("italic") != std::string::npos || lowerName.find("oblique") != 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); std::string fontPath = FontFallback::getInstance().getFallbackFontPath(baseFont, bold, italic);
auto font = std::make_unique<CIDFont>(baseFont, subtype, false, std::move(descriptor), std::move(encoding)); auto font = std::make_unique<CIDFont>(baseFont, subtype, false, std::move(descriptor), std::move(encoding));
@@ -98,4 +94,4 @@ std::unique_ptr<Font> FontLoader::loadCIDFontSystemFallback(
return font; return font;
} }
} // namespace pdfengine::fonts::pdf_fonts }
+2 -7
View File
@@ -11,7 +11,6 @@ namespace pdfengine::fonts::pdf_fonts {
class FontLoader { class FontLoader {
public: public:
// Factory method to load an embedded TrueType font from its raw stream bytes
static std::unique_ptr<Font> loadTrueTypeFromMemory( static std::unique_ptr<Font> loadTrueTypeFromMemory(
const std::string& baseFont, const std::string& baseFont,
const std::vector<uint8_t>& streamData, const std::vector<uint8_t>& streamData,
@@ -19,7 +18,6 @@ public:
std::unique_ptr<Encoding> encoding = nullptr std::unique_ptr<Encoding> encoding = nullptr
); );
// Factory method to load an embedded Type1 font from raw memory bytes
static std::unique_ptr<Font> loadType1FromMemory( static std::unique_ptr<Font> loadType1FromMemory(
const std::string& baseFont, const std::string& baseFont,
const std::vector<uint8_t>& streamData, const std::vector<uint8_t>& streamData,
@@ -27,26 +25,23 @@ public:
std::unique_ptr<Encoding> encoding = nullptr std::unique_ptr<Encoding> encoding = nullptr
); );
// Factory method to load a non-embedded standard Type1 font (resolves to standard system fallback)
static std::unique_ptr<Font> loadType1SystemFallback( static std::unique_ptr<Font> loadType1SystemFallback(
const std::string& baseFont, const std::string& baseFont,
std::unique_ptr<FontDescriptor> descriptor = nullptr, std::unique_ptr<FontDescriptor> descriptor = nullptr,
std::unique_ptr<Encoding> encoding = nullptr std::unique_ptr<Encoding> encoding = nullptr
); );
// Factory method to load an embedded CIDFont from raw stream bytes
static std::unique_ptr<Font> loadCIDFontFromMemory( static std::unique_ptr<Font> loadCIDFontFromMemory(
const std::string& baseFont, const std::string& baseFont,
FontType subtype, // CIDFontType0 or CIDFontType2 FontType subtype,
const std::vector<uint8_t>& streamData, const std::vector<uint8_t>& streamData,
std::unique_ptr<FontDescriptor> descriptor = nullptr, std::unique_ptr<FontDescriptor> descriptor = nullptr,
std::unique_ptr<Encoding> encoding = nullptr std::unique_ptr<Encoding> encoding = nullptr
); );
// Factory method to load a non-embedded CJK CIDFont (resolves to CJK system fallbacks)
static std::unique_ptr<Font> loadCIDFontSystemFallback( static std::unique_ptr<Font> loadCIDFontSystemFallback(
const std::string& baseFont, const std::string& baseFont,
FontType subtype, // CIDFontType0 or CIDFontType2 FontType subtype,
std::unique_ptr<FontDescriptor> descriptor = nullptr, std::unique_ptr<FontDescriptor> descriptor = nullptr,
std::unique_ptr<Encoding> encoding = nullptr std::unique_ptr<Encoding> encoding = nullptr
); );
+2 -18
View File
@@ -16,8 +16,6 @@ void FontSubset::populateFromFace(FontSubset& subset, void* ftFace) {
FT_UInt gindex; FT_UInt gindex;
FT_ULong charcode = FT_Get_First_Char(face, &gindex); FT_ULong charcode = FT_Get_First_Char(face, &gindex);
while (gindex != 0) { 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<uint32_t>(charcode)); subset.addGlyphMapping(gindex, static_cast<uint32_t>(charcode));
charcode = FT_Get_Next_Char(face, charcode, &gindex); charcode = FT_Get_Next_Char(face, charcode, &gindex);
} }
@@ -55,10 +53,8 @@ std::vector<uint8_t> FontSubset::buildSubset(const std::vector<uint8_t>& origina
hb_set_add(glyph_set, gid); hb_set_add(glyph_set, gid);
} }
// Always keep GID 0 (the .notdef glyph)
hb_set_add(glyph_set, 0); 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_subset_input_set_flags(input, HB_SUBSET_FLAGS_RETAIN_GIDS);
hb_face_t* subset_face = hb_subset_or_fail(face, input); hb_face_t* subset_face = hb_subset_or_fail(face, input);
@@ -81,12 +77,6 @@ std::vector<uint8_t> FontSubset::buildSubset(const std::vector<uint8_t>& origina
return result; 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<uint8_t>& font, uint64_t seed) { static void makeFontNameUnique(std::vector<uint8_t>& 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 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; }; 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<uint8_t>& font, uint64_t seed) {
uint32_t len = rd16(rec + 8); uint32_t len = rd16(rec + 8);
size_t s = storageOff + rd16(rec + 10); size_t s = storageOff + rd16(rec + 10);
for (uint32_t b = 0; b < len && b < 8; ++b) { for (uint32_t b = 0; b < len && b < 8; ++b) {
if (s + b < font.size()) font[s + b] = static_cast<uint8_t>('A' + ((seed >> ((b % 8) * 4)) & 0x0F)); // 'A'..'P' if (s + b < font.size()) font[s + b] = static_cast<uint8_t>('A' + ((seed >> ((b % 8) * 4)) & 0x0F));
} }
} }
} }
@@ -117,11 +107,6 @@ static void makeFontNameUnique(std::vector<uint8_t>& font, uint64_t seed) {
std::vector<uint8_t> FontSubset::buildSubsetByUnicode(const std::vector<uint8_t>& originalStream, std::vector<uint8_t> FontSubset::buildSubsetByUnicode(const std::vector<uint8_t>& originalStream,
const std::vector<uint32_t>& codepoints) { const std::vector<uint32_t>& 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<uint8_t> result; std::vector<uint8_t> result;
if (originalStream.empty() || codepoints.empty()) { if (originalStream.empty() || codepoints.empty()) {
return result; return result;
@@ -152,7 +137,6 @@ std::vector<uint8_t> FontSubset::buildSubsetByUnicode(const std::vector<uint8_t>
hb_set_add(unicodes, cp); 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_face_t* subset_face = hb_subset_or_fail(face, input);
hb_subset_input_destroy(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(); return subset_to_original_map_.find(subsetGid) != subset_to_original_map_.end();
} }
} // namespace pdfengine::fonts::pdf_fonts }
+1 -5
View File
@@ -16,14 +16,10 @@ public:
static std::string getSubsetPrefix(const std::string& fontName); 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); static void populateFromFace(FontSubset& subset, void* ftFace);
// Rebuilds the TTF/CID stream keeping only the specified GIDs using HarfBuzz
static std::vector<uint8_t> buildSubset(const std::vector<uint8_t>& originalStream, const std::vector<uint32_t>& glyphIdsToKeep); static std::vector<uint8_t> buildSubset(const std::vector<uint8_t>& originalStream, const std::vector<uint32_t>& 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<uint8_t> buildSubsetByUnicode(const std::vector<uint8_t>& originalStream, const std::vector<uint32_t>& codepoints); static std::vector<uint8_t> buildSubsetByUnicode(const std::vector<uint8_t>& originalStream, const std::vector<uint32_t>& codepoints);
explicit FontSubset(const std::string& fontName); explicit FontSubset(const std::string& fontName);
@@ -45,4 +41,4 @@ private:
std::unordered_map<uint32_t, uint32_t> subset_to_original_map_; std::unordered_map<uint32_t, uint32_t> subset_to_original_map_;
}; };
} // namespace pdfengine::fonts::pdf_fonts }
@@ -33,7 +33,7 @@ std::string unicodeToUtf8(uint32_t codepoint) {
return utf8; return utf8;
} }
} // namespace }
CIDFont::CIDFont( CIDFont::CIDFont(
const std::string& baseFont, 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_) { if (descriptor_) {
std::string fontName = descriptor_->getFontName(); std::string fontName = descriptor_->getFontName();
std::string lowerName = fontName; std::string lowerName = fontName;
@@ -171,7 +170,6 @@ uint32_t CIDFont::decodeToUnicode(uint32_t charCode) const {
} }
if (registry != "None") { if (registry != "None") {
// Resolve CJK collections (e.g. matching standard Japanese font mappings)
uint32_t cjkResolved = CjkCollectionDB::resolveCID(registry, charCode); uint32_t cjkResolved = CjkCollectionDB::resolveCID(registry, charCode);
if (cjkResolved != 0) { if (cjkResolved != 0) {
return cjkResolved; return cjkResolved;
@@ -226,4 +224,4 @@ void CIDFont::buildGidToUnicodeMap() const {
is_gid_to_unicode_map_built_ = true; is_gid_to_unicode_map_built_ = true;
} }
} // namespace pdfengine::fonts::pdf_fonts }
@@ -17,17 +17,15 @@ class CIDFont : public Font {
public: public:
CIDFont( CIDFont(
const std::string& baseFont, const std::string& baseFont,
FontType subtype, // CIDFontType0 or CIDFontType2 FontType subtype,
bool isEmbedded, bool isEmbedded,
std::unique_ptr<FontDescriptor> descriptor = nullptr, std::unique_ptr<FontDescriptor> descriptor = nullptr,
std::unique_ptr<Encoding> encoding = nullptr std::unique_ptr<Encoding> encoding = nullptr
); );
~CIDFont() override; ~CIDFont() override;
// Load the font from raw embedded stream bytes
bool loadFromStream(const std::vector<uint8_t>& streamData); bool loadFromStream(const std::vector<uint8_t>& streamData);
// Load the font from a system or CJK fallback file path
bool loadFromFile(const std::string& filePath); bool loadFromFile(const std::string& filePath);
std::string getBaseFont() const override; std::string getBaseFont() const override;
@@ -44,7 +42,6 @@ public:
uint32_t decodeToUnicode(uint32_t charCode) const override; uint32_t decodeToUnicode(uint32_t charCode) const override;
std::string decodeStringToUnicode(const std::vector<uint32_t>& charCodes) const override; std::string decodeStringToUnicode(const std::vector<uint32_t>& charCodes) const override;
// CID to GID translation methods
uint32_t mapCIDToGID(uint32_t cid) const; uint32_t mapCIDToGID(uint32_t cid) const;
void setCIDToGIDMap(std::unordered_map<uint32_t, uint32_t> cidToGid); void setCIDToGIDMap(std::unordered_map<uint32_t, uint32_t> cidToGid);
void setIdentityCIDToGIDMap(); void setIdentityCIDToGIDMap();
@@ -68,4 +65,4 @@ private:
void buildGidToUnicodeMap() const; void buildGidToUnicodeMap() const;
}; };
} // namespace pdfengine::fonts::pdf_fonts }
@@ -31,7 +31,7 @@ std::string unicodeToUtf8(uint32_t codepoint) {
return utf8; return utf8;
} }
} // namespace }
TrueTypeFont::TrueTypeFont( TrueTypeFont::TrueTypeFont(
const std::string& baseFont, const std::string& baseFont,
@@ -130,4 +130,4 @@ std::string TrueTypeFont::decodeStringToUnicode(const std::vector<uint32_t>& cha
return result; return result;
} }
} // namespace pdfengine::fonts::pdf_fonts }
@@ -20,7 +20,6 @@ public:
); );
~TrueTypeFont() override; ~TrueTypeFont() override;
// Load the font from raw embedded stream bytes
bool loadFromStream(const std::vector<uint8_t>& streamData); bool loadFromStream(const std::vector<uint8_t>& streamData);
std::string getBaseFont() const override; std::string getBaseFont() const override;
@@ -46,4 +45,4 @@ private:
std::unique_ptr<FontSubset> subset_info_; std::unique_ptr<FontSubset> subset_info_;
}; };
} // namespace pdfengine::fonts::pdf_fonts }
@@ -31,7 +31,7 @@ std::string unicodeToUtf8(uint32_t codepoint) {
return utf8; return utf8;
} }
} // namespace }
Type1Font::Type1Font( Type1Font::Type1Font(
const std::string& baseFont, const std::string& baseFont,
@@ -134,4 +134,4 @@ std::string Type1Font::decodeStringToUnicode(const std::vector<uint32_t>& charCo
return result; return result;
} }
} // namespace pdfengine::fonts::pdf_fonts }
@@ -21,10 +21,8 @@ public:
); );
~Type1Font() override; ~Type1Font() override;
// Load the font from raw embedded stream bytes
bool loadFromStream(const std::vector<uint8_t>& streamData); bool loadFromStream(const std::vector<uint8_t>& streamData);
// Load the font from a system or fallback file path
bool loadFromFile(const std::string& filePath); bool loadFromFile(const std::string& filePath);
std::string getBaseFont() const override; std::string getBaseFont() const override;
@@ -50,4 +48,4 @@ private:
std::unique_ptr<FontSubset> subset_info_; std::unique_ptr<FontSubset> subset_info_;
}; };
} // namespace pdfengine::fonts::pdf_fonts }
+1 -17
View File
@@ -22,60 +22,47 @@ std::vector<ShapedGlyph> HbShaper::shapeRun(
return result; return result;
} }
// Acquire lock to prevent concurrent mutation of FT_Face's active pixel size
std::lock_guard<std::mutex> lock(font.getMutex()); std::lock_guard<std::mutex> 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)) { if (FT_Set_Pixel_Sizes(ftFace, 0, fontSize)) {
return result; return result;
} }
// Create a HarfBuzz font wrapper around the FreeType face.
hb_font_t* hbFont = hb_ft_font_create_referenced(ftFace); hb_font_t* hbFont = hb_ft_font_create_referenced(ftFace);
if (!hbFont) { if (!hbFont) {
return result; return result;
} }
// Sync HarfBuzz font with FreeType face changes.
hb_ft_font_changed(hbFont); hb_ft_font_changed(hbFont);
// Create a text buffer.
hb_buffer_t* hbBuffer = hb_buffer_create(); hb_buffer_t* hbBuffer = hb_buffer_create();
if (!hbBuffer) { if (!hbBuffer) {
hb_font_destroy(hbFont); hb_font_destroy(hbFont);
return result; return result;
} }
// Add text to buffer as UTF-8.
hb_buffer_add_utf8(hbBuffer, text.c_str(), static_cast<int>(text.length()), 0, -1); hb_buffer_add_utf8(hbBuffer, text.c_str(), static_cast<int>(text.length()), 0, -1);
// Let HarfBuzz guess direction, script, and language properties.
hb_buffer_guess_segment_properties(hbBuffer); hb_buffer_guess_segment_properties(hbBuffer);
if (writingMode == WritingMode::Vertical) { if (writingMode == WritingMode::Vertical) {
hb_buffer_set_direction(hbBuffer, HB_DIRECTION_TTB); hb_buffer_set_direction(hbBuffer, HB_DIRECTION_TTB);
} }
// Enable professional typography features (Kerning and Ligatures)
hb_feature_t features[2]; hb_feature_t features[2];
// Enable kerning
features[0].tag = HB_TAG('k', 'e', 'r', 'n'); features[0].tag = HB_TAG('k', 'e', 'r', 'n');
features[0].value = 1; features[0].value = 1;
features[0].start = 0; features[0].start = 0;
features[0].end = static_cast<unsigned int>(-1); features[0].end = static_cast<unsigned int>(-1);
// Enable standard ligatures
features[1].tag = HB_TAG('l', 'i', 'g', 'a'); features[1].tag = HB_TAG('l', 'i', 'g', 'a');
features[1].value = 1; features[1].value = 1;
features[1].start = 0; features[1].start = 0;
features[1].end = static_cast<unsigned int>(-1); features[1].end = static_cast<unsigned int>(-1);
// Shape the text inside the buffer using the font.
hb_shape(hbFont, hbBuffer, features, 2); hb_shape(hbFont, hbBuffer, features, 2);
// Retrieve the results.
unsigned int glyphCount = 0; unsigned int glyphCount = 0;
hb_glyph_info_t* glyphInfos = hb_buffer_get_glyph_infos(hbBuffer, &glyphCount); hb_glyph_info_t* glyphInfos = hb_buffer_get_glyph_infos(hbBuffer, &glyphCount);
hb_glyph_position_t* glyphPositions = hb_buffer_get_glyph_positions(hbBuffer, &glyphCount); hb_glyph_position_t* glyphPositions = hb_buffer_get_glyph_positions(hbBuffer, &glyphCount);
@@ -85,8 +72,6 @@ std::vector<ShapedGlyph> HbShaper::shapeRun(
for (unsigned int i = 0; i < glyphCount; ++i) { for (unsigned int i = 0; i < glyphCount; ++i) {
ShapedGlyph sg; ShapedGlyph sg;
sg.glyphIndex = glyphInfos[i].codepoint; 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<double>(glyphPositions[i].x_advance) / 64.0; sg.advanceX = static_cast<double>(glyphPositions[i].x_advance) / 64.0;
sg.advanceY = static_cast<double>(glyphPositions[i].y_advance) / 64.0; sg.advanceY = static_cast<double>(glyphPositions[i].y_advance) / 64.0;
sg.offsetX = static_cast<double>(glyphPositions[i].x_offset) / 64.0; sg.offsetX = static_cast<double>(glyphPositions[i].x_offset) / 64.0;
@@ -96,11 +81,10 @@ std::vector<ShapedGlyph> HbShaper::shapeRun(
} }
} }
// Clean up HarfBuzz resources.
hb_buffer_destroy(hbBuffer); hb_buffer_destroy(hbBuffer);
hb_font_destroy(hbFont); hb_font_destroy(hbFont);
return result; return result;
} }
} // namespace pdfengine::fonts }
+1 -3
View File
@@ -31,8 +31,6 @@ public:
Vertical Vertical
}; };
// Shapes the input UTF-8 text run using the given FontFace and fontSize.
// Returns a vector of shaped glyphs.
std::vector<ShapedGlyph> shapeRun( std::vector<ShapedGlyph> shapeRun(
const std::string& text, const std::string& text,
FontFace& font, FontFace& font,
@@ -41,4 +39,4 @@ public:
); );
}; };
} // namespace pdfengine::fonts }
+2 -4
View File
@@ -33,7 +33,7 @@ void ContentBuilder::processOperation(const Operation& op, std::vector<std::uniq
handleTf(op); handleTf(op);
} else if (op.op == "Td" || op.op == "TD" || op.op == "Tm") { } else if (op.op == "Td" || op.op == "TD" || op.op == "Tm") {
handleTd(op); handleTd(op);
} else if (op.op == "Tj" || op.op == "'") { // ' is equivalent to T* Tj } else if (op.op == "Tj" || op.op == "'") {
handleTj(op, outObjects); handleTj(op, outObjects);
} else if (op.op == "TJ") { } else if (op.op == "TJ") {
handleTJ_Array(op, outObjects); handleTJ_Array(op, outObjects);
@@ -72,7 +72,6 @@ void ContentBuilder::handleTf(const Operation& op) {
void ContentBuilder::handleTd(const Operation& op) { void ContentBuilder::handleTd(const Operation& op) {
if (op.op == "Tm" && op.operands.size() >= 6) { if (op.op == "Tm" && op.operands.size() >= 6) {
// Tm takes 6 operands: a b c d e f
auto it = op.operands.end(); auto it = op.operands.end();
auto fNode = *(--it); auto fNode = *(--it);
auto eNode = *(--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 (eNode->type == AstNodeType::Number) state_.tm[4] = eNode->numberValue;
if (fNode->type == AstNodeType::Number) state_.tm[5] = fNode->numberValue; if (fNode->type == AstNodeType::Number) state_.tm[5] = fNode->numberValue;
} else if ((op.op == "Td" || op.op == "TD") && op.operands.size() >= 2) { } 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 it = op.operands.end();
auto yNode = *(--it); auto yNode = *(--it);
auto xNode = *(--it); auto xNode = *(--it);
@@ -306,4 +304,4 @@ void ContentBuilder::handlePathPaint(PathPaintOp paintOp,
currentPath_.clear(); currentPath_.clear();
} }
} // namespace pdfengine }
+2 -4
View File
@@ -16,12 +16,11 @@ public:
std::vector<std::unique_ptr<ContentObject>> build(const std::vector<Operation>& operations); std::vector<std::unique_ptr<ContentObject>> build(const std::vector<Operation>& operations);
private: private:
// Graphics State Tracker
struct GraphicsState { struct GraphicsState {
std::string fontName; std::string fontName;
double fontSize = 0.0; double fontSize = 0.0;
double tm[6] = {1.0, 0.0, 0.0, 1.0, 0.0, 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_; GraphicsState state_;
@@ -31,7 +30,6 @@ private:
void processOperation(const Operation& op, std::vector<std::unique_ptr<ContentObject>>& outObjects); void processOperation(const Operation& op, std::vector<std::unique_ptr<ContentObject>>& outObjects);
// Handlers
void handleTf(const Operation& op); void handleTf(const Operation& op);
void handleTd(const Operation& op); void handleTd(const Operation& op);
void handleTj(const Operation& op, std::vector<std::unique_ptr<ContentObject>>& outObjects); void handleTj(const Operation& op, std::vector<std::unique_ptr<ContentObject>>& outObjects);
@@ -42,4 +40,4 @@ private:
void handlePathPaint(PathPaintOp paintOp, std::vector<std::unique_ptr<ContentObject>>& outObjects, bool closePath = false); void handlePathPaint(PathPaintOp paintOp, std::vector<std::unique_ptr<ContentObject>>& outObjects, bool closePath = false);
}; };
} // namespace pdfengine }
+1 -15
View File
@@ -11,8 +11,6 @@ std::vector<std::string> ContentStreamParser::tokenize(const std::string& stream
for (size_t i = 0; i < stream.size(); ++i) { for (size_t i = 0; i < stream.size(); ++i) {
char c = stream[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 (std::isspace(c)) {
if (!current_token.empty()) { if (!current_token.empty()) {
tokens.push_back(current_token); tokens.push_back(current_token);
@@ -46,10 +44,7 @@ void ContentStreamParser::parse(const std::string& contentStream, DisplayList& d
std::vector<std::string> operands; std::vector<std::string> operands;
for (const auto& token : tokens) { 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] != '/') { if (!token.empty() && std::isalpha(token[0]) && token[0] != '/') {
// It's an operator
if (token == "m") { if (token == "m") {
if (operands.size() >= 2) { if (operands.size() >= 2) {
float y = std::stof(operands.back()); operands.pop_back(); 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); displayList.strokePath(m_currentPath);
m_currentPath.clear(); m_currentPath.clear();
} else if (token == "Do") { } else if (token == "Do") {
// Draw Image XObject
if (!operands.empty()) { if (!operands.empty()) {
std::string imageName = operands.back(); operands.pop_back(); 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; ImageInfo img;
img.width = 100; img.width = 100;
img.height = 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; Matrix m;
displayList.drawImage(img, m, 1.0f); displayList.drawImage(img, m, 1.0f);
} }
} }
// Clear operands for next operator
operands.clear(); operands.clear();
} else { } else {
operands.push_back(token); operands.push_back(token);
@@ -120,4 +106,4 @@ void ContentStreamParser::parse(const std::string& contentStream, DisplayList& d
} }
} }
} // namespace pdfengine }
+1 -7
View File
@@ -7,22 +7,16 @@
namespace pdfengine { namespace pdfengine {
// A lightweight parser for PDF content streams.
// In Phase 3, this interprets operators and builds the DisplayList.
class ContentStreamParser { class ContentStreamParser {
public: public:
ContentStreamParser() = default; 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); void parse(const std::string& contentStream, DisplayList& displayList);
private: private:
// Helper to tokenize the content stream
std::vector<std::string> tokenize(const std::string& stream); std::vector<std::string> tokenize(const std::string& stream);
// Current path state
Path m_currentPath; Path m_currentPath;
}; };
} // namespace pdfengine }
+1 -7
View File
@@ -4,15 +4,12 @@ namespace pdfengine {
Path DecorationBuilder::buildUnderline(float x, float y, float width, float thickness, float offset) { Path DecorationBuilder::buildUnderline(float x, float y, float width, float thickness, float offset) {
Path p; 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); p.addRect(x, y + offset, width, thickness);
return p; return p;
} }
Path DecorationBuilder::buildStrikeout(float x, float y, float width, float thickness, float offset) { Path DecorationBuilder::buildStrikeout(float x, float y, float width, float thickness, float offset) {
Path p; Path p;
// A strikeout is similarly a thin filled rectangle, positioned higher up.
p.addRect(x, y + offset, width, thickness); p.addRect(x, y + offset, width, thickness);
return p; return p;
} }
@@ -28,14 +25,11 @@ Path DecorationBuilder::buildSquiggly(float x, float y, float width, float ampli
float currentX = x; float currentX = x;
float endX = x + width; 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; bool up = true;
while (currentX < endX) { while (currentX < endX) {
float nextX = currentX + (frequency / 2.0f); float nextX = currentX + (frequency / 2.0f);
if (nextX > endX) { if (nextX > endX) {
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 ratio = (nextX - currentX) / (frequency / 2.0f);
float nextY = y + (up ? amplitude : -amplitude) * ratio; float nextY = y + (up ? amplitude : -amplitude) * ratio;
p.lineTo(nextX, nextY); p.lineTo(nextX, nextY);
@@ -52,4 +46,4 @@ Path DecorationBuilder::buildSquiggly(float x, float y, float width, float ampli
return p; return p;
} }
} // namespace pdfengine }
+1 -17
View File
@@ -4,29 +4,13 @@
namespace pdfengine { namespace pdfengine {
// A utility class to generate vector paths for text markup and decorations
class DecorationBuilder { class DecorationBuilder {
public: 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); 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); 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); static Path buildSquiggly(float x, float y, float width, float amplitude = 2.0f, float frequency = 4.0f);
}; };
} // namespace pdfengine }
+13 -21
View File
@@ -21,7 +21,7 @@ std::optional<Token> Lexer::nextToken() {
skipWhitespaceAndComments(); skipWhitespaceAndComments();
if (isEOF()) { if (isEOF()) {
return std::nullopt; // Or we can return EndOfStream token return std::nullopt;
} }
size_t startPos = pos_; size_t startPos = pos_;
@@ -55,7 +55,6 @@ void Lexer::skipWhitespaceAndComments() {
if (isWhitespace(c)) { if (isWhitespace(c)) {
advance(); advance();
} else if (c == '%') { } else if (c == '%') {
// Skip until end of line
while (!isEOF() && peek() != '\n' && peek() != '\r') { while (!isEOF() && peek() != '\n' && peek() != '\r') {
advance(); advance();
} }
@@ -67,7 +66,7 @@ void Lexer::skipWhitespaceAndComments() {
std::optional<Token> Lexer::parseString() { std::optional<Token> Lexer::parseString() {
size_t startPos = pos_; size_t startPos = pos_;
advance(); // skip '(' advance();
std::string str; std::string str;
int parenLevel = 1; int parenLevel = 1;
@@ -95,13 +94,12 @@ std::optional<Token> Lexer::parseString() {
case '(': str += '('; break; case '(': str += '('; break;
case ')': str += ')'; break; case ')': str += ')'; break;
case '\\': str += '\\'; break; case '\\': str += '\\'; break;
case '\n': break; // ignored line break case '\n': break;
case '\r': case '\r':
if (peek() == '\n') advance(); if (peek() == '\n') advance();
break; break;
default: default:
if (n >= '0' && n <= '7') { if (n >= '0' && n <= '7') {
// Octal up to 3 digits
int val = n - '0'; int val = n - '0';
for (int i = 0; i < 2; ++i) { for (int i = 0; i < 2; ++i) {
if (!isEOF() && peek() >= '0' && peek() <= '7') { if (!isEOF() && peek() >= '0' && peek() <= '7') {
@@ -112,7 +110,6 @@ std::optional<Token> Lexer::parseString() {
} }
str += static_cast<char>(val); str += static_cast<char>(val);
} else { } else {
// Unknown escape, just output the char
str += n; str += n;
} }
} }
@@ -126,7 +123,7 @@ std::optional<Token> Lexer::parseString() {
std::optional<Token> Lexer::parseHexString() { std::optional<Token> Lexer::parseHexString() {
size_t startPos = pos_; size_t startPos = pos_;
advance(); // skip '<' advance();
std::vector<uint8_t> bytes; std::vector<uint8_t> bytes;
bool hasHigh = false; bool hasHigh = false;
@@ -136,7 +133,6 @@ std::optional<Token> Lexer::parseHexString() {
char c = advance(); char c = advance();
if (c == '>') { if (c == '>') {
if (hasHigh) { if (hasHigh) {
// If odd number of hex digits, implicitly append 0
bytes.push_back(high << 4); bytes.push_back(high << 4);
} }
break; break;
@@ -145,7 +141,6 @@ std::optional<Token> Lexer::parseHexString() {
int val = hexDigitValue(c); int val = hexDigitValue(c);
if (val == -1) { if (val == -1) {
// Invalid char, usually stop or ignore. We'll ignore for now or break.
continue; continue;
} }
@@ -163,7 +158,7 @@ std::optional<Token> Lexer::parseHexString() {
std::optional<Token> Lexer::parseName() { std::optional<Token> Lexer::parseName() {
size_t startPos = pos_; size_t startPos = pos_;
advance(); // skip '/' advance();
std::string name; std::string name;
while (!isEOF()) { while (!isEOF()) {
@@ -203,10 +198,9 @@ std::optional<Token> Lexer::parseNumberOrOperator() {
} }
if (val.empty()) { if (val.empty()) {
return std::nullopt; // should not happen if we skip properly return std::nullopt;
} }
// Is it a number?
bool isNum = true; bool isNum = true;
bool hasDot = false; bool hasDot = false;
for (size_t i = 0; i < val.size(); ++i) { for (size_t i = 0; i < val.size(); ++i) {
@@ -221,7 +215,6 @@ std::optional<Token> Lexer::parseNumberOrOperator() {
break; break;
} }
} }
// A single '+' or '-' or '.' is not a number
if (val == "+" || val == "-" || val == ".") isNum = false; if (val == "+" || val == "-" || val == ".") isNum = false;
if (isNum) { if (isNum) {
@@ -239,23 +232,22 @@ std::optional<Token> Lexer::parseNumberOrOperator() {
std::optional<Token> Lexer::parseDictOrLess() { std::optional<Token> Lexer::parseDictOrLess() {
size_t startPos = pos_; size_t startPos = pos_;
advance(); // skip '<' advance();
if (!isEOF() && peek() == '<') { if (!isEOF() && peek() == '<') {
advance(); // skip second '<' advance();
return Token{TokenType::DictStart, "", {}, 0.0, startPos, pos_}; 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_};
return Token{TokenType::Operator, "<", {}, 0.0, startPos, pos_}; // fallback
} }
std::optional<Token> Lexer::parseDictOrGreater() { std::optional<Token> Lexer::parseDictOrGreater() {
size_t startPos = pos_; size_t startPos = pos_;
advance(); // skip '>' advance();
if (!isEOF() && peek() == '>') { if (!isEOF() && peek() == '>') {
advance(); // skip second '>' advance();
return Token{TokenType::DictEnd, "", {}, 0.0, startPos, pos_}; 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 { char Lexer::peek(size_t offset) const {
@@ -295,4 +287,4 @@ int Lexer::hexDigitValue(char c) {
return -1; return -1;
} }
} // namespace pdfengine }
+1 -3
View File
@@ -11,10 +11,8 @@ class Lexer {
public: public:
explicit Lexer(std::string_view input); explicit Lexer(std::string_view input);
// Retrieve all tokens in one pass
std::vector<Token> tokenize(); std::vector<Token> tokenize();
// Or retrieve tokens one by one
std::optional<Token> nextToken(); std::optional<Token> nextToken();
private: private:
@@ -39,4 +37,4 @@ private:
static int hexDigitValue(char c); static int hexDigitValue(char c);
}; };
} // namespace pdfengine }
+6 -16
View File
@@ -15,12 +15,10 @@ std::vector<Operation> ContentParser::parse() {
if (token->type == TokenType::Operator) { if (token->type == TokenType::Operator) {
Operation op; Operation op;
op.op = token->stringValue; op.op = token->stringValue;
// PDF stream operators consume whatever is on the operand stack
op.operands = std::move(operandStack_); op.operands = std::move(operandStack_);
operandStack_.clear(); operandStack_.clear();
operations.push_back(std::move(op)); operations.push_back(std::move(op));
} else { } else {
// It's an operand (or the start of a composite operand)
auto node = parseNode(*token); auto node = parseNode(*token);
if (node) { if (node) {
operandStack_.push_back(std::move(node)); operandStack_.push_back(std::move(node));
@@ -74,8 +72,6 @@ std::shared_ptr<AstNode> ContentParser::parseNode(const Token& token) {
case TokenType::DictEnd: case TokenType::DictEnd:
case TokenType::EndOfStream: case TokenType::EndOfStream:
case TokenType::Operator: 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; return nullptr;
} }
@@ -90,18 +86,15 @@ std::shared_ptr<AstNode> ContentParser::parseArray() {
if (!token) break; if (!token) break;
if (token->type == TokenType::ArrayEnd) { if (token->type == TokenType::ArrayEnd) {
advance(); // consume ']' advance();
break; break;
} }
// Cannot have Operator inside array
if (token->type == TokenType::Operator) { 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; break;
} }
advance(); // consume item token advance();
auto item = parseNode(*token); auto item = parseNode(*token);
if (item) { if (item) {
node->arrayItems.push_back(std::move(item)); node->arrayItems.push_back(std::move(item));
@@ -119,27 +112,24 @@ std::shared_ptr<AstNode> ContentParser::parseDictionary() {
if (!keyToken) break; if (!keyToken) break;
if (keyToken->type == TokenType::DictEnd) { if (keyToken->type == TokenType::DictEnd) {
advance(); // consume '>>' advance();
break; break;
} }
if (keyToken->type != TokenType::Name) { 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(); advance();
continue; continue;
} }
advance(); // consume key advance();
if (isEOF()) break; if (isEOF()) break;
const Token* valToken = peek(); const Token* valToken = peek();
if (valToken->type == TokenType::DictEnd) { if (valToken->type == TokenType::DictEnd) {
// Incomplete key-value pair
break; break;
} }
advance(); // consume value token advance();
auto valNode = parseNode(*valToken); auto valNode = parseNode(*valToken);
if (valNode) { if (valNode) {
node->dictItems[keyToken->stringValue] = std::move(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; return pos_ >= tokens_.size() || tokens_[pos_].type == TokenType::EndOfStream;
} }
} // namespace pdfengine }
+1 -1
View File
@@ -32,4 +32,4 @@ public:
explicit ParserError(const std::string& msg) : std::runtime_error(msg) {} explicit ParserError(const std::string& msg) : std::runtime_error(msg) {}
}; };
} // namespace pdfengine }
+1 -3
View File
@@ -1,7 +1,6 @@
#include "parser/pdfium_loader.hpp" #include "parser/pdfium_loader.hpp"
#ifdef PDFENGINE_WITH_PDFIUM #ifdef PDFENGINE_WITH_PDFIUM
// The ONLY place in the codebase a raw PDFium header may be included (Rule R2).
#include <fpdfview.h> #include <fpdfview.h>
#endif #endif
@@ -17,7 +16,6 @@ bool pdfiumAvailable() noexcept {
void pdfiumInitLibrary() { void pdfiumInitLibrary() {
#ifdef PDFENGINE_WITH_PDFIUM #ifdef PDFENGINE_WITH_PDFIUM
// PDFium expects this once per process before any document is opened.
FPDF_InitLibrary(); FPDF_InitLibrary();
#endif #endif
} }
@@ -28,4 +26,4 @@ void pdfiumDestroyLibrary() {
#endif #endif
} }
} // namespace pdfengine::parser }
+1 -11
View File
@@ -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 #pragma once
namespace pdfengine::parser { 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; [[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 pdfiumInitLibrary();
void pdfiumDestroyLibrary(); void pdfiumDestroyLibrary();
} // namespace pdfengine::parser }
+1 -1
View File
@@ -18,4 +18,4 @@ public:
virtual ResolvedXObject resolveXObject(const std::string& name) = 0; virtual ResolvedXObject resolveXObject(const std::string& name) = 0;
}; };
} // namespace pdfengine }
+2 -6
View File
@@ -38,7 +38,7 @@ StreamVerification verifyContentStream(const ExtractedStream& stream) {
return v; return v;
} }
} // namespace pdfengine }
namespace pdfengine::qpdf_layer { namespace pdfengine::qpdf_layer {
@@ -131,7 +131,6 @@ QpdfExtractor::extractFromQpdf(::QPDF& qpdf, int pageIndex) const {
for (auto streamObj : streams) { for (auto streamObj : streams) {
if (!streamObj.isStream()) continue; if (!streamObj.isStream()) continue;
// Get filters
QPDFObjectHandle dict = streamObj.getDict(); QPDFObjectHandle dict = streamObj.getDict();
if (dict.hasKey("/Filter")) { if (dict.hasKey("/Filter")) {
QPDFObjectHandle filter = dict.getKey("/Filter"); QPDFObjectHandle filter = dict.getKey("/Filter");
@@ -149,7 +148,6 @@ QpdfExtractor::extractFromQpdf(::QPDF& qpdf, int pageIndex) const {
} }
} }
// Raw data
try { try {
Pl_Buffer rawPipeline("raw"); Pl_Buffer rawPipeline("raw");
streamObj.pipeStreamData(&rawPipeline, 0, qpdf_dl_none, false); streamObj.pipeStreamData(&rawPipeline, 0, qpdf_dl_none, false);
@@ -159,10 +157,8 @@ QpdfExtractor::extractFromQpdf(::QPDF& qpdf, int pageIndex) const {
result.rawContent.append(reinterpret_cast<const char*>(rawBuf->getBuffer()), rawBuf->getSize()); result.rawContent.append(reinterpret_cast<const char*>(rawBuf->getBuffer()), rawBuf->getSize());
} }
} catch (...) { } catch (...) {
// Ignore failure for raw
} }
// Decoded data
try { try {
Pl_Buffer decodedPipeline("decoded"); Pl_Buffer decodedPipeline("decoded");
streamObj.pipeStreamData(&decodedPipeline, 0, qpdf_dl_all, false); streamObj.pipeStreamData(&decodedPipeline, 0, qpdf_dl_all, false);
@@ -191,4 +187,4 @@ QpdfExtractor::extractFromQpdf(::QPDF& qpdf, int pageIndex) const {
#endif #endif
} }
} // namespace pdfengine::qpdf_layer }
+1 -1
View File
@@ -35,4 +35,4 @@ private:
extractFromQpdf(::QPDF& qpdf, int pageIndex) const; extractFromQpdf(::QPDF& qpdf, int pageIndex) const;
}; };
} // namespace pdfengine::qpdf_layer }
+1 -1
View File
@@ -42,4 +42,4 @@ ResolvedXObject QpdfResourceResolver::resolveXObject(const std::string& name) {
return resolved; return resolved;
} }
} // namespace pdfengine::qpdf_layer }
+1 -1
View File
@@ -15,4 +15,4 @@ private:
QPDFObjectHandle resourcesDict_; QPDFObjectHandle resourcesDict_;
}; };
} // namespace pdfengine::qpdf_layer }
+1 -7
View File
@@ -30,18 +30,12 @@ QpdfWriter::replacePageStreamAndSave(const std::string& sourcePath,
QPDFPageObjectHelper& page = pages[pageIndex]; QPDFPageObjectHelper& page = pages[pageIndex];
// Create a new stream with the updated data
QPDFObjectHandle newStream = QPDFObjectHandle::newStream(&pdf, newStreamData); 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(); QPDFObjectHandle pageDict = page.getObjectHandle();
pageDict.replaceKey("/Contents", newStream); pageDict.replaceKey("/Contents", newStream);
// Write it out
QPDFWriter writer(pdf, destPath.c_str()); QPDFWriter writer(pdf, destPath.c_str());
// For performance/size we usually compress streams
writer.setStreamDataMode(qpdf_s_compress); writer.setStreamDataMode(qpdf_s_compress);
writer.write(); writer.write();
@@ -52,4 +46,4 @@ QpdfWriter::replacePageStreamAndSave(const std::string& sourcePath,
#endif #endif
} }
} // namespace pdfengine::qpdf_layer }
+1 -2
View File
@@ -12,7 +12,6 @@ class QpdfWriter {
public: public:
QpdfWriter() = default; QpdfWriter() = default;
// Takes a source PDF, replaces the stream of the given page, and writes to a new destination
[[nodiscard]] [[nodiscard]]
std::expected<void, std::string> std::expected<void, std::string>
replacePageStreamAndSave(const std::string& sourcePath, replacePageStreamAndSave(const std::string& sourcePath,
@@ -21,4 +20,4 @@ public:
const std::string& newStreamData) const; const std::string& newStreamData) const;
}; };
} // namespace pdfengine::qpdf_layer }
+2 -4
View File
@@ -21,12 +21,10 @@ std::string AstSerializer::serialize(const std::vector<Operation>& operations) c
std::string AstSerializer::serializeNode(const AstNode& node) const { std::string AstSerializer::serializeNode(const AstNode& node) const {
switch (node.type) { switch (node.type) {
case AstNodeType::Number: { case AstNodeType::Number: {
// Need to drop trailing zeros for integers to save space and match standard PDF
double intPart; double intPart;
if (std::modf(node.numberValue, &intPart) == 0.0) { if (std::modf(node.numberValue, &intPart) == 0.0) {
return std::to_string(static_cast<long long>(node.numberValue)); return std::to_string(static_cast<long long>(node.numberValue));
} else { } else {
// Round to 4 decimal places for cleanliness
std::ostringstream out; std::ostringstream out;
out.precision(4); out.precision(4);
out << std::fixed << node.numberValue; out << std::fixed << node.numberValue;
@@ -37,7 +35,7 @@ std::string AstSerializer::serializeNode(const AstNode& node) const {
} }
} }
case AstNodeType::Name: case AstNodeType::Name:
return "/" + node.stringValue; // simplified, assumes no special chars requiring # hex encoding for now return "/" + node.stringValue;
case AstNodeType::String: case AstNodeType::String:
return "(" + escapeString(node.stringValue) + ")"; return "(" + escapeString(node.stringValue) + ")";
@@ -90,4 +88,4 @@ std::string AstSerializer::escapeString(const std::string& str) const {
return res; return res;
} }
} // namespace pdfengine }
+1 -1
View File
@@ -17,4 +17,4 @@ private:
std::string escapeString(const std::string& str) const; std::string escapeString(const std::string& str) const;
}; };
} // namespace pdfengine }
+1 -6
View File
@@ -15,12 +15,10 @@ std::vector<Operation> ContentSerializer::serialize(const std::vector<std::uniqu
} }
void ContentSerializer::serializeText(const TextObject& textObj, std::vector<Operation>& outOps) const { void ContentSerializer::serializeText(const TextObject& textObj, std::vector<Operation>& outOps) const {
// BT
Operation opBT; Operation opBT;
opBT.op = "BT"; opBT.op = "BT";
outOps.push_back(std::move(opBT)); outOps.push_back(std::move(opBT));
// /FontName FontSize Tf
if (!textObj.fontName.empty() && textObj.fontSize > 0) { if (!textObj.fontName.empty() && textObj.fontSize > 0) {
Operation opTf; Operation opTf;
opTf.op = "Tf"; opTf.op = "Tf";
@@ -36,7 +34,6 @@ void ContentSerializer::serializeText(const TextObject& textObj, std::vector<Ope
outOps.push_back(std::move(opTf)); outOps.push_back(std::move(opTf));
} }
// a b c d e f Tm
Operation opTm; Operation opTm;
opTm.op = "Tm"; opTm.op = "Tm";
for (int i = 0; i < 6; ++i) { for (int i = 0; i < 6; ++i) {
@@ -46,7 +43,6 @@ void ContentSerializer::serializeText(const TextObject& textObj, std::vector<Ope
} }
outOps.push_back(std::move(opTm)); outOps.push_back(std::move(opTm));
// (text) Tj
Operation opTj; Operation opTj;
opTj.op = "Tj"; opTj.op = "Tj";
auto strNode = std::make_shared<AstNode>(AstNodeType::String); auto strNode = std::make_shared<AstNode>(AstNodeType::String);
@@ -54,10 +50,9 @@ void ContentSerializer::serializeText(const TextObject& textObj, std::vector<Ope
opTj.operands.push_back(std::move(strNode)); opTj.operands.push_back(std::move(strNode));
outOps.push_back(std::move(opTj)); outOps.push_back(std::move(opTj));
// ET
Operation opET; Operation opET;
opET.op = "ET"; opET.op = "ET";
outOps.push_back(std::move(opET)); outOps.push_back(std::move(opET));
} }
} // namespace pdfengine }
+1 -1
View File
@@ -17,4 +17,4 @@ private:
void serializeText(const TextObject& textObj, std::vector<Operation>& outOps) const; void serializeText(const TextObject& textObj, std::vector<Operation>& outOps) const;
}; };
} // namespace pdfengine }
+12 -44
View File
@@ -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" #include "pdfengine/pdf_document.hpp"
@@ -21,32 +15,28 @@ struct OGlyph {
int line = 0; int line = 0;
double right = 0.0; double right = 0.0;
double bottom = 0.0; double bottom = 0.0;
double mid = 0.0; // horizontal centre double mid = 0.0;
}; };
struct LineBand { struct LineBand {
double top = 0.0; double top = 0.0;
double bottom = 0.0; double bottom = 0.0;
double mid = 0.0; // vertical centre of the first glyph double mid = 0.0;
int start = 0; // inclusive glyph index int start = 0;
int end = 0; // exclusive glyph index 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 { struct Grid {
double minX = 0.0, minY = 0.0, cell = 1.0; double minX = 0.0, minY = 0.0, cell = 1.0;
int cols = 0, rows = 0; int cols = 0, rows = 0;
std::vector<std::vector<int>> cells; // size cols*rows; glyph indices per cell std::vector<std::vector<int>> cells;
[[nodiscard]] bool empty() const { return cols == 0 || rows == 0; } [[nodiscard]] bool empty() const { return cols == 0 || rows == 0; }
[[nodiscard]] int at(int c, int r) const { return r * cols + c; } [[nodiscard]] int at(int c, int r) const { return r * cols + c; }
}; };
struct SelIndex { struct SelIndex {
std::vector<OGlyph> glyphs; // reading order std::vector<OGlyph> glyphs;
std::vector<LineBand> lines; std::vector<LineBand> lines;
Grid grid; Grid grid;
}; };
@@ -58,8 +48,6 @@ bool isSpace(const std::string& t) {
return true; 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<GlyphBounds>& raw) { SelIndex buildIndex(const std::vector<GlyphBounds>& raw) {
SelIndex idx; SelIndex idx;
std::vector<GlyphBounds> clean; std::vector<GlyphBounds> clean;
@@ -121,7 +109,6 @@ SelIndex buildIndex(const std::vector<GlyphBounds>& raw) {
idx.lines.push_back(band); idx.lines.push_back(band);
} }
// Build the spatial grid over the final glyph set.
Grid& grid = idx.grid; Grid& grid = idx.grid;
double minX = std::numeric_limits<double>::max(); double minX = std::numeric_limits<double>::max();
double minY = std::numeric_limits<double>::max(); double minY = std::numeric_limits<double>::max();
@@ -135,8 +122,6 @@ SelIndex buildIndex(const std::vector<GlyphBounds>& raw) {
maxY = std::max(maxY, og.bottom); maxY = std::max(maxY, og.bottom);
sumH += og.g.h; 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<double>(idx.glyphs.size())); double cell = std::max(1.0, sumH / static_cast<double>(idx.glyphs.size()));
auto dim = [&](double lo, double hi) { auto dim = [&](double lo, double hi) {
return std::max(1, static_cast<int>((hi - lo) / cell) + 1); return std::max(1, static_cast<int>((hi - lo) / cell) + 1);
@@ -167,15 +152,7 @@ SelIndex buildIndex(const std::vector<GlyphBounds>& raw) {
return idx; 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) { int glyphAt(const SelIndex& idx, double x, double y) {
const Grid& grid = idx.grid; const Grid& grid = idx.grid;
if (grid.empty() || x < grid.minX || y < grid.minY) return -1; 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; const auto& lines = idx.lines;
if (lines.empty()) return -1; if (lines.empty()) return -1;
// Binary search for the last line whose top <= y.
int lo = 0; int lo = 0;
int hi = static_cast<int>(lines.size()); int hi = static_cast<int>(lines.size());
while (lo < hi) { while (lo < hi) {
@@ -201,11 +177,8 @@ int lineAt(const SelIndex& idx, double y) {
if (lines[m].top <= y) lo = m + 1; if (lines[m].top <= y) lo = m + 1;
else hi = m; 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; int best = -1;
double bestDist = std::numeric_limits<double>::max(); double bestDist = std::numeric_limits<double>::max();
for (int i = cand - 1; i <= cand + 1; ++i) { 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<int>(lines.size()) - 1; return cand < 0 ? 0 : static_cast<int>(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) { static int lastGlyphLeftOf(const SelIndex& idx, const LineBand& line, double x) {
int lo = line.start; int lo = line.start;
int hi = line.end; 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) { 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); const int hit = glyphAt(idx, x, y);
if (hit >= 0) { if (hit >= 0) {
const OGlyph& g = idx.glyphs[hit]; const OGlyph& g = idx.glyphs[hit];
return x < g.mid ? hit : hit + 1; return x < g.mid ? hit : hit + 1;
} }
// Otherwise position within the nearest line (gaps / between lines / margins).
const int li = lineAt(idx, y); const int li = lineAt(idx, y);
if (li < 0) return 0; if (li < 0) return 0;
const LineBand& line = idx.lines[li]; 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); const int cand = lastGlyphLeftOf(idx, line, x);
if (cand < line.start) return line.start; if (cand < line.start) return line.start;
const OGlyph& g = idx.glyphs[cand]; const OGlyph& g = idx.glyphs[cand];
if (x <= g.right) return x < g.mid ? cand : cand + 1; // inside the glyph if (x <= g.right) return x < g.mid ? cand : cand + 1;
return cand + 1; // in the gap after it return cand + 1;
} }
std::string textOfRange(const SelIndex& idx, int start, int end) { 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<GlyphBounds> rectsOfRange(const SelIndex& idx, int start, int end) { std::vector<GlyphBounds> rectsOfRange(const SelIndex& idx, int start, int end) {
std::vector<GlyphBounds> rects; std::vector<GlyphBounds> rects;
if (start >= end) return rects; if (start >= end) return rects;
// Group selected glyphs by line, preserving first-seen order. std::map<int, std::pair<double, double>> spanByLine;
std::map<int, std::pair<double, double>> spanByLine; // line -> {minX, maxRight}
std::vector<int> order; std::vector<int> order;
for (int i = start; i < end; ++i) { for (int i = start; i < end; ++i) {
const OGlyph& g = idx.glyphs[i]; const OGlyph& g = idx.glyphs[i];
@@ -307,7 +275,7 @@ std::vector<GlyphBounds> rectsOfRange(const SelIndex& idx, int start, int end) {
return rects; return rects;
} }
} // namespace }
std::expected<std::vector<GlyphBounds>, EngineError> PdfPage::orderedGlyphs() const { std::expected<std::vector<GlyphBounds>, EngineError> PdfPage::orderedGlyphs() const {
auto raw = extractTextWithBounds(); auto raw = extractTextWithBounds();
@@ -346,4 +314,4 @@ PdfPage::selectRange(double ax, double ay, double bx, double by) const {
return sel; return sel;
} }
} // namespace pdfengine }
+1 -3
View File
@@ -32,7 +32,7 @@ const PathObject* requirePathObject(const std::unique_ptr<ContentObject>& object
return static_cast<const PathObject*>(object.get()); return static_cast<const PathObject*>(object.get());
} }
} // namespace }
TEST(ContentBuilderTest, SimpleTextState) { TEST(ContentBuilderTest, SimpleTextState) {
Lexer lexer("10 20 Td /F1 12 Tf (Hello) Tj"); Lexer lexer("10 20 Td /F1 12 Tf (Hello) Tj");
@@ -67,7 +67,6 @@ TEST(ContentBuilderTest, KerningArrayTJ) {
EXPECT_EQ(objects[0]->getType(), ContentObjectType::Text); EXPECT_EQ(objects[0]->getType(), ContentObjectType::Text);
auto* textObj = static_cast<TextObject*>(objects[0].get()); auto* textObj = static_cast<TextObject*>(objects[0].get());
// -600 is less than -500, so it inserts a space
EXPECT_EQ(textObj->text, "Hello World"); EXPECT_EQ(textObj->text, "Hello World");
} }
@@ -202,7 +201,6 @@ TEST(ContentBuilderTest, IntegrationHelloWorld) {
auto ops = parser.parse(); auto ops = parser.parse();
auto objects = builder.build(ops); auto objects = builder.build(ops);
// hello_world.pdf has two text lines: "Hello, world!" and "Goodbye, world!"
int textObjectCount = 0; int textObjectCount = 0;
bool foundHello = false; bool foundHello = false;
bool foundGoodbye = false; bool foundGoodbye = false;
-3
View File
@@ -5,7 +5,6 @@
using namespace pdfengine; using namespace pdfengine;
// A simple visitor for testing that just records the sequence of visited commands.
class MockVisitor : public CommandVisitor { class MockVisitor : public CommandVisitor {
public: public:
std::vector<std::string> calls; std::vector<std::string> calls;
@@ -25,7 +24,6 @@ TEST(DisplayListTest, RecordAndReplay) {
EXPECT_EQ(list.size(), 0); EXPECT_EQ(list.size(), 0);
// Record some commands
list.saveState(); list.saveState();
list.setTransform(Matrix(2.0f, 0.0f, 0.0f, 2.0f, 0.0f, 0.0f)); 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); list.fillRect(10.0f, 10.0f, 100.0f, 50.0f);
@@ -34,7 +32,6 @@ TEST(DisplayListTest, RecordAndReplay) {
EXPECT_EQ(list.size(), 5); EXPECT_EQ(list.size(), 5);
// Replay to the mock visitor
MockVisitor visitor; MockVisitor visitor;
list.replay(visitor); list.replay(visitor);
+20 -125
View File
@@ -264,7 +264,7 @@ TEST(PageRenderTest, InvalidPageIndexReturnsPageOutOfBounds) {
auto docRes = PdfDocument::loadFromFile(path.string()); auto docRes = PdfDocument::loadFromFile(path.string());
ASSERT_TRUE(docRes.has_value()); 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()); ASSERT_FALSE(pageRes.has_value());
EXPECT_EQ(pageRes.error(), EngineError::PageOutOfBounds); EXPECT_EQ(pageRes.error(), EngineError::PageOutOfBounds);
@@ -447,7 +447,6 @@ TEST(DocumentEditTest, ApplyRedactionAndFullSave) {
ASSERT_TRUE(docRes.has_value()); ASSERT_TRUE(docRes.has_value());
auto doc = *docRes; auto doc = *docRes;
// Verify text exists initially
{ {
auto pageRes = doc->getPage(0); auto pageRes = doc->getPage(0);
ASSERT_TRUE(pageRes.has_value()); ASSERT_TRUE(pageRes.has_value());
@@ -456,7 +455,6 @@ TEST(DocumentEditTest, ApplyRedactionAndFullSave) {
EXPECT_NE(textRes->find("Hello"), std::string::npos); EXPECT_NE(textRes->find("Hello"), std::string::npos);
} }
// Redact the entire page bounds to remove all objects
std::string editsJson = R"({ std::string editsJson = R"({
"version": "1.0", "version": "1.0",
"operations": [ "operations": [
@@ -494,7 +492,6 @@ TEST(DocumentEditTest, ApplyRedactionAndFullSave) {
auto textRes = newPage->extractText(); auto textRes = newPage->extractText();
ASSERT_TRUE(textRes.has_value()); ASSERT_TRUE(textRes.has_value());
// The text should be completely gone
EXPECT_EQ(textRes->find("Hello"), std::string::npos); EXPECT_EQ(textRes->find("Hello"), std::string::npos);
EXPECT_EQ(textRes->find("world"), 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(origW, 0.0);
EXPECT_GT(origH, origW); EXPECT_GT(origH, origW);
// 1. Rotate by 90 degrees (90 total)
std::string editsJson1 = R"({ std::string editsJson1 = R"({
"version": "1.0", "version": "1.0",
"operations": [ "operations": [
@@ -580,7 +576,6 @@ TEST(DocumentEditTest, ApplyPageRotationAndIncrementalSave) {
auto editRes1 = doc->applyEdits(editsJson1); auto editRes1 = doc->applyEdits(editsJson1);
ASSERT_TRUE(editRes1.has_value()); ASSERT_TRUE(editRes1.has_value());
// 2. Rotate by another 90 degrees (180 total)
std::string editsJson2 = R"({ std::string editsJson2 = R"({
"version": "1.0", "version": "1.0",
"operations": [ "operations": [
@@ -598,7 +593,6 @@ TEST(DocumentEditTest, ApplyPageRotationAndIncrementalSave) {
auto editRes2 = doc->applyEdits(editsJson2); auto editRes2 = doc->applyEdits(editsJson2);
ASSERT_TRUE(editRes2.has_value()); ASSERT_TRUE(editRes2.has_value());
// Save and load back to verify 180 degree rotation (dimensions should be original again)
auto saveRes = doc->saveIncremental(); auto saveRes = doc->saveIncremental();
ASSERT_TRUE(saveRes.has_value()); ASSERT_TRUE(saveRes.has_value());
const auto& savedBytes = *saveRes; const auto& savedBytes = *saveRes;
@@ -617,7 +611,6 @@ TEST(DocumentEditTest, ApplyPageRotationAndIncrementalSave) {
EXPECT_NEAR(rotatedW, origW, 0.01); EXPECT_NEAR(rotatedW, origW, 0.01);
EXPECT_NEAR(rotatedH, origH, 0.01); EXPECT_NEAR(rotatedH, origH, 0.01);
// 3. Now rotate by -90 degrees (back to 90 total)
std::string editsJson3 = R"({ std::string editsJson3 = R"({
"version": "1.0", "version": "1.0",
"operations": [ "operations": [
@@ -700,7 +693,6 @@ TEST(DocumentEditTest, ApplyPageReorderAndIncrementalSave) {
auto doc = *docRes; auto doc = *docRes;
EXPECT_EQ(doc->pageCount(), 2); EXPECT_EQ(doc->pageCount(), 2);
// Swap the pages: move page 1 (index 1) to page 0 (index 0)
std::string editsJson = R"({ std::string editsJson = R"({
"version": "1.0", "version": "1.0",
"operations": [ "operations": [
@@ -861,7 +853,6 @@ TEST(FontDiagnosticsTest, ConcurrencyThreadSafety) {
TEST(FontDiagnosticsTest, DeepIntrospectionAndFontSizeVerification) { TEST(FontDiagnosticsTest, DeepIntrospectionAndFontSizeVerification) {
SKIP_IF_NO_PDFIUM(); SKIP_IF_NO_PDFIUM();
// Test Part 1: Introspection & Metadata verification using utf-8.pdf
{ {
auto path = getCorpusPath("fonts", "utf-8.pdf"); auto path = getCorpusPath("fonts", "utf-8.pdf");
if (std::filesystem::exists(path)) { if (std::filesystem::exists(path)) {
@@ -874,13 +865,11 @@ TEST(FontDiagnosticsTest, DeepIntrospectionAndFontSizeVerification) {
auto fonts = *fontsRes; auto fonts = *fontsRes;
for (const auto& f : fonts) { for (const auto& f : fonts) {
// Core fields
EXPECT_FALSE(f.fontName.empty()); EXPECT_FALSE(f.fontName.empty());
EXPECT_FALSE(f.type.empty()); EXPECT_FALSE(f.type.empty());
EXPECT_FALSE(f.normalizedFamily.empty()); EXPECT_FALSE(f.normalizedFamily.empty());
EXPECT_FALSE(f.internalFontId.empty()); EXPECT_FALSE(f.internalFontId.empty());
// Subset tagging consistency
if (f.isSubset) { if (f.isSubset) {
EXPECT_EQ(f.subsetTag.size(), 6); EXPECT_EQ(f.subsetTag.size(), 6);
for (char c : f.subsetTag) { 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)); EXPECT_EQ(f.internalFontId, f.fontName + "_" + f.type + "_" + std::to_string(f.flags));
} }
// Source Type / Fallbacks & Substitutions consistency
if (f.sourceType == "SystemFallback") { if (f.sourceType == "SystemFallback") {
EXPECT_FALSE(f.isEmbedded); EXPECT_FALSE(f.isEmbedded);
EXPECT_TRUE(f.substitutedFrom.empty()); EXPECT_TRUE(f.substitutedFrom.empty());
@@ -909,7 +897,6 @@ TEST(FontDiagnosticsTest, DeepIntrospectionAndFontSizeVerification) {
#endif #endif
} }
// Check descriptor metrics are non-zero / reasonably set
EXPECT_GT(f.ascent, 0.0); EXPECT_GT(f.ascent, 0.0);
EXPECT_LT(f.descent, 0.0); EXPECT_LT(f.descent, 0.0);
EXPECT_GT(f.capHeight, 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"); auto path = getCorpusPath("fonts", "vertical_text.pdf");
if (std::filesystem::exists(path)) { 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); 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); EXPECT_TRUE(foundVertical);
} }
} }
// Test Part 2b: Vertical Font Detection Heuristic explicit validation
{ {
auto path1 = getCorpusPath("fonts", "vertical_identity_v.pdf"); auto path1 = getCorpusPath("fonts", "vertical_identity_v.pdf");
if (std::filesystem::exists(path1)) { 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"); auto path = getCorpusPath("fonts", "utf-8.pdf");
if (!std::filesystem::exists(path)) { if (!std::filesystem::exists(path)) {
@@ -980,20 +963,18 @@ TEST(FontDiagnosticsTest, DeepIntrospectionAndFontSizeVerification) {
std::vector<double> uniqueSizes; std::vector<double> uniqueSizes;
for (const auto& glyph : glyphs) { 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") { if (glyph.text != " " && glyph.text != "\r" && glyph.text != "\n" && glyph.text != "\t") {
EXPECT_GT(glyph.w, 0.0); EXPECT_GT(glyph.w, 0.0);
EXPECT_GT(glyph.h, 0.0); EXPECT_GT(glyph.h, 0.0);
} }
EXPECT_GT(glyph.fontSize, 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()) { if (std::find(uniqueSizes.begin(), uniqueSizes.end(), glyph.fontSize) == uniqueSizes.end()) {
uniqueSizes.push_back(glyph.fontSize); 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") { if (path.filename().string() == "utf-8.pdf") {
EXPECT_GE(uniqueSizes.size(), 2u); 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) { TEST(FontDiagnosticsTest, RealPDFiumEmbeddingAndTypeAccuracy) {
SKIP_IF_NO_PDFIUM(); 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"); auto path = getCorpusPath("fonts", "text_font.pdf");
if (!std::filesystem::exists(path)) { if (!std::filesystem::exists(path)) {
GTEST_SKIP() << "text_font.pdf not found in corpus."; 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"; ASSERT_FALSE(fonts.empty()) << "text_font.pdf must expose at least one font";
for (const auto& f : fonts) { 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.fontName.empty());
EXPECT_FALSE(f.type.empty()); EXPECT_FALSE(f.type.empty());
// type must be one of the four valid PDF font subtypes.
static const std::vector<std::string> kValidTypes = { static const std::vector<std::string> kValidTypes = {
"Type1", "TrueType", "CIDFontType0", "CIDFontType2" "Type1", "TrueType", "CIDFontType0", "CIDFontType2"
}; };
@@ -1047,8 +1013,6 @@ TEST(FontDiagnosticsTest, RealPDFiumEmbeddingAndTypeAccuracy) {
!= kValidTypes.end(); != kValidTypes.end();
EXPECT_TRUE(typeValid) << "Unexpected type '" << f.type << "' for font '" << f.fontName << "'"; 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) { if (f.isSubset) {
EXPECT_TRUE(f.isEmbedded) EXPECT_TRUE(f.isEmbedded)
<< "Subset font '" << f.fontName << "Subset font '" << f.fontName
@@ -1059,21 +1023,15 @@ TEST(FontDiagnosticsTest, RealPDFiumEmbeddingAndTypeAccuracy) {
EXPECT_TRUE(f.substitutedTo.empty()); EXPECT_TRUE(f.substitutedTo.empty());
} }
// isEmbedded=true and sourceType="Embedded" must be consistent.
if (f.isEmbedded) { if (f.isEmbedded) {
EXPECT_EQ(f.sourceType, "Embedded"); 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) { TEST(FontDiagnosticsTest, RealPDFiumMetricsAccuracy) {
SKIP_IF_NO_PDFIUM(); SKIP_IF_NO_PDFIUM();
// Use the largest font corpus file; it contains the most diverse fonts.
auto path = getCorpusPath("fonts", "text_font.pdf"); auto path = getCorpusPath("fonts", "text_font.pdf");
if (!std::filesystem::exists(path)) { if (!std::filesystem::exists(path)) {
GTEST_SKIP() << "text_font.pdf not found in corpus."; GTEST_SKIP() << "text_font.pdf not found in corpus.";
@@ -1085,9 +1043,6 @@ TEST(FontDiagnosticsTest, RealPDFiumMetricsAccuracy) {
ASSERT_TRUE(fontsRes.has_value()); ASSERT_TRUE(fontsRes.has_value());
for (const auto& f : *fontsRes) { 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) EXPECT_GT(f.ascent, 0.0)
<< "ascent must be positive for font '" << f.fontName << "'"; << "ascent must be positive for font '" << f.fontName << "'";
EXPECT_LT(f.descent, 0.0) EXPECT_LT(f.descent, 0.0)
@@ -1095,30 +1050,17 @@ TEST(FontDiagnosticsTest, RealPDFiumMetricsAccuracy) {
EXPECT_GT(f.capHeight, 0.0) EXPECT_GT(f.capHeight, 0.0)
<< "capHeight must be positive for font '" << f.fontName << "'"; << "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)
EXPECT_LE(f.capHeight, f.ascent + 1.0) // +1 for float rounding
<< "capHeight should not exceed ascent for font '" << f.fontName << "'"; << "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_LT(f.ascent, 1500.0) << "Implausibly large ascent for '" << f.fontName << "'";
EXPECT_GT(f.descent, -1500.0) << "Implausibly deep descent 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) { TEST(FontDiagnosticsTest, ToUnicodePresenceAccuracy) {
SKIP_IF_NO_PDFIUM(); SKIP_IF_NO_PDFIUM();
// Case 1: font WITH ToUnicode \u2014 hasToUnicode must be true
{ {
auto path = getCorpusPath("fonts", "with_tounicode.pdf"); auto path = getCorpusPath("fonts", "with_tounicode.pdf");
if (std::filesystem::exists(path)) { 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"); auto path = getCorpusPath("fonts", "no_tounicode.pdf");
if (std::filesystem::exists(path)) { if (std::filesystem::exists(path)) {
@@ -1150,7 +1091,6 @@ TEST(FontDiagnosticsTest, ToUnicodePresenceAccuracy) {
if (pageRes.has_value()) { if (pageRes.has_value()) {
auto fontsRes = (*pageRes)->getFonts(); auto fontsRes = (*pageRes)->getFonts();
if (fontsRes.has_value() && !fontsRes->empty()) { if (fontsRes.has_value() && !fontsRes->empty()) {
// All fonts in a no-tounicode document should fail Unicode decode.
for (const auto& f : *fontsRes) { for (const auto& f : *fontsRes) {
EXPECT_FALSE(f.hasToUnicode) EXPECT_FALSE(f.hasToUnicode)
<< "Font '" << f.fontName << "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"); auto path = getCorpusPath("fonts", "latin_extended.pdf");
if (std::filesystem::exists(path)) { 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) { TEST(UtfConversionTest, EmojiSurrogatePairs) {
// 😀 U+1F600 -> UTF-8: F0 9F 98 80
std::string utf8_grinning = "\xF0\x9F\x98\x80"; std::string utf8_grinning = "\xF0\x9F\x98\x80";
auto utf16 = pdfengine::parser::utf8_to_utf16le(utf8_grinning); auto utf16 = pdfengine::parser::utf8_to_utf16le(utf8_grinning);
// Should be D83D DE00 + null terminator
ASSERT_EQ(utf16.size(), 3); ASSERT_EQ(utf16.size(), 3);
EXPECT_EQ(utf16[0], 0xD83D); EXPECT_EQ(utf16[0], 0xD83D);
EXPECT_EQ(utf16[1], 0xDE00); EXPECT_EQ(utf16[1], 0xDE00);
EXPECT_EQ(utf16[2], 0x0000); EXPECT_EQ(utf16[2], 0x0000);
// Convert back to UTF-8
std::string utf8_out = pdfengine::parser::utf16le_to_utf8(reinterpret_cast<const char16_t*>(utf16.data()), utf16.size() - 1); std::string utf8_out = pdfengine::parser::utf16le_to_utf8(reinterpret_cast<const char16_t*>(utf16.data()), utf16.size() - 1);
EXPECT_EQ(utf8_out, utf8_grinning); EXPECT_EQ(utf8_out, utf8_grinning);
// 🚀 U+1F680 -> UTF-8: F0 9F 9A 80
std::string utf8_rocket = "\xF0\x9F\x9A\x80"; std::string utf8_rocket = "\xF0\x9F\x9A\x80";
utf16 = pdfengine::parser::utf8_to_utf16le(utf8_rocket); utf16 = pdfengine::parser::utf8_to_utf16le(utf8_rocket);
ASSERT_EQ(utf16.size(), 3); ASSERT_EQ(utf16.size(), 3);
@@ -1215,76 +1147,58 @@ TEST(UtfConversionTest, EmojiSurrogatePairs) {
} }
TEST(UtfConversionTest, CJKExtensionB) { TEST(UtfConversionTest, CJKExtensionB) {
// 𠀀 U+20000 -> UTF-8: F0 A0 80 80
std::string utf8_cjk = "\xF0\xA0\x80\x80"; std::string utf8_cjk = "\xF0\xA0\x80\x80";
auto utf16 = pdfengine::parser::utf8_to_utf16le(utf8_cjk); auto utf16 = pdfengine::parser::utf8_to_utf16le(utf8_cjk);
// Should be D840 DC00 + null terminator
ASSERT_EQ(utf16.size(), 3); ASSERT_EQ(utf16.size(), 3);
EXPECT_EQ(utf16[0], 0xD840); EXPECT_EQ(utf16[0], 0xD840);
EXPECT_EQ(utf16[1], 0xDC00); EXPECT_EQ(utf16[1], 0xDC00);
EXPECT_EQ(utf16[2], 0x0000); EXPECT_EQ(utf16[2], 0x0000);
// Convert back to UTF-8
std::string utf8_out = pdfengine::parser::utf16le_to_utf8(reinterpret_cast<const char16_t*>(utf16.data()), utf16.size() - 1); std::string utf8_out = pdfengine::parser::utf16le_to_utf8(reinterpret_cast<const char16_t*>(utf16.data()), utf16.size() - 1);
EXPECT_EQ(utf8_out, utf8_cjk); EXPECT_EQ(utf8_out, utf8_cjk);
} }
TEST(UtfConversionTest, RoundtripMixed) { 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"; std::string mixed = "A\xF0\x9F\x98\x80""B\xF0\xA0\x80\x80""C";
auto utf16 = pdfengine::parser::utf8_to_utf16le(mixed); auto utf16 = pdfengine::parser::utf8_to_utf16le(mixed);
std::string mixed_out = pdfengine::parser::utf16le_to_utf8(reinterpret_cast<const char16_t*>(utf16.data()), utf16.size() - 1); std::string mixed_out = pdfengine::parser::utf16le_to_utf8(reinterpret_cast<const char16_t*>(utf16.data()), utf16.size() - 1);
EXPECT_EQ(mixed_out, mixed); EXPECT_EQ(mixed_out, mixed);
} }
// =========================================================================
// Tests for CJK CID Resolution
// =========================================================================
TEST(CjkResolutionTest, AdobeCNS1) { TEST(CjkResolutionTest, AdobeCNS1) {
// Basic mapping checks for the core Adobe-CNS1 block (Traditional Chinese)
using pdfengine::fonts::pdf_fonts::CjkCollectionDB; 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", 100), 0x4E00); // 一 EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 112), 0x4E2D);
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 112), 0x4E2D); // 中 EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 130), 0x4ED7);
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", 131), 0x4ED8); // 付 EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 135), 0x4EDF);
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 135), 0x4EDF); // 仟 EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 140), 0x4F01);
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) { TEST(CjkResolutionTest, AdobeKorea1) {
using pdfengine::fonts::pdf_fonts::CjkCollectionDB; 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", 101), 0xAC00); // 가 EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 119), 0xAC1C);
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 119), 0xAC1C); // 개 EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 150), 0xAC90);
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", 151), 0xAC94); // 겝 EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 156), 0xACA9);
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 156), 0xACA9); // 결 EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 160), 0xACBD);
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) { TEST(GlyphCacheTest, ConcurrencyBench) {
using namespace pdfengine::fonts; using namespace pdfengine::fonts;
FontFace face; FontFace face;
// Load a common system font for testing cache keys
bool loaded = face.loadFromFile("C:\\Windows\\Fonts\\arial.ttf"); bool loaded = face.loadFromFile("C:\\Windows\\Fonts\\arial.ttf");
if (!loaded) { if (!loaded) {
GTEST_SKIP() << "Skipping benchmark: Arial font not found."; GTEST_SKIP() << "Skipping benchmark: Arial font not found.";
@@ -1323,7 +1237,7 @@ TEST(GlyphCacheTest, ConcurrencyBench) {
return diff.count(); return diff.count();
}; };
run_benchmark(2, 1000); // warmup run_benchmark(2, 1000);
cache.clear(); cache.clear();
double time_10 = run_benchmark(10, 10000); double time_10 = run_benchmark(10, 10000);
@@ -1334,7 +1248,7 @@ TEST(GlyphCacheTest, ConcurrencyBench) {
double time_50 = run_benchmark(50, 10000); double time_50 = run_benchmark(50, 10000);
std::cout << "[ BENCHMARK ] 50 Threads Time: " << time_50 << " seconds (" << (500000.0 / time_50) << " ops/sec)\n"; 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) { TEST(FontDiagnosticsTest, EmbeddedFontResolutionAndReloadingVerification) {
@@ -1415,12 +1329,10 @@ TEST(FontDiagnosticsTest, EmbeddedFontResolutionAndReloadingVerification) {
std::cout << std::endl; std::cout << std::endl;
} }
// Restore first charmap
if (face->num_charmaps > 0) { if (face->num_charmaps > 0) {
FT_Set_Charmap(face, face->charmaps[0]); FT_Set_Charmap(face, face->charmaps[0]);
} }
// Print all glyph names in the face
std::cout << " Glyph names: "; std::cout << " Glyph names: ";
for (int i = 0; i < face->num_glyphs; ++i) { for (int i = 0; i < face->num_glyphs; ++i) {
char nameBuf[64] = {0}; char nameBuf[64] = {0};
@@ -1437,7 +1349,6 @@ TEST(FontDiagnosticsTest, EmbeddedFontResolutionAndReloadingVerification) {
EXPECT_TRUE(resolvedFont->isEmbedded()); EXPECT_TRUE(resolvedFont->isEmbedded());
// Let's test a few common characters: 'A' (65), 'a' (97), '0' (48), ' ' (32)
std::vector<uint32_t> testChars = {32, 48, 65, 97}; std::vector<uint32_t> testChars = {32, 48, 65, 97};
for (uint32_t cp : testChars) { for (uint32_t cp : testChars) {
bool hasG = resolvedFont->hasGlyph(cp); bool hasG = resolvedFont->hasGlyph(cp);
@@ -1446,23 +1357,19 @@ TEST(FontDiagnosticsTest, EmbeddedFontResolutionAndReloadingVerification) {
<< ", advanceWidth=" << w << std::endl; << ", advanceWidth=" << w << std::endl;
} }
// Verify metrics returned are non-zero/valid
auto metrics = resolvedFont->getMetrics(12.0); auto metrics = resolvedFont->getMetrics(12.0);
std::cout << " Metrics: ascent=" << metrics.ascent << ", descent=" << metrics.descent << ", capHeight=" << metrics.capHeight << std::endl; std::cout << " Metrics: ascent=" << metrics.ascent << ", descent=" << metrics.descent << ", capHeight=" << metrics.capHeight << std::endl;
EXPECT_NE(metrics.ascent, 0.0); EXPECT_NE(metrics.ascent, 0.0);
EXPECT_NE(metrics.descent, 0.0); EXPECT_NE(metrics.descent, 0.0);
EXPECT_NE(metrics.capHeight, 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") { if (fileName == "text_font.pdf") {
// hasGlyph(1) should return true because the charmap maps 1 -> 1
EXPECT_TRUE(resolvedFont->hasGlyph(1)); EXPECT_TRUE(resolvedFont->hasGlyph(1));
double w = resolvedFont->getAdvanceWidth(1, 12.0); double w = resolvedFont->getAdvanceWidth(1, 12.0);
EXPECT_GT(w, 0.0); EXPECT_GT(w, 0.0);
std::cout << " [VERIFIED] text_font.pdf char(1): hasGlyph=yes, advanceWidth=" << w << std::endl; 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) { if (face->num_glyphs > 1) {
bool foundNonZeroWidth = false; bool foundNonZeroWidth = false;
for (int gid = 1; gid < face->num_glyphs; ++gid) { for (int gid = 1; gid < face->num_glyphs; ++gid) {
@@ -1525,7 +1432,6 @@ TEST(DocumentEditTest, ReplaceTextMVPStandardFont) {
if (i + 1 < objectIndices.size()) indicesStr += ","; if (i + 1 < objectIndices.size()) indicesStr += ",";
} }
// Flat format payload
std::string editsJson = R"({ std::string editsJson = R"({
"version": "1.0", "version": "1.0",
"operations": [ "operations": [
@@ -1604,7 +1510,6 @@ TEST(DocumentEditTest, ReplaceTextRuntimeFontEngine) {
if (i + 1 < objectIndices.size()) indicesStr += ","; if (i + 1 < objectIndices.size()) indicesStr += ",";
} }
// JSON payload including internalFontId to resolve
std::string editsJson = R"({ std::string editsJson = R"({
"version": "1.0", "version": "1.0",
"operations": [ "operations": [
@@ -1659,7 +1564,6 @@ TEST(DocumentEditTest, ReplaceTextFontReuseAndEmbedding) {
ASSERT_TRUE(modelRes.has_value()); ASSERT_TRUE(modelRes.has_value());
const auto& model = *modelRes; const auto& model = *modelRes;
// Find the first text run
std::vector<int> objectIndices; std::vector<int> objectIndices;
std::string originalFontId = ""; std::string originalFontId = "";
for (const auto& p : model.paragraphs) { for (const auto& p : model.paragraphs) {
@@ -1684,7 +1588,6 @@ TEST(DocumentEditTest, ReplaceTextFontReuseAndEmbedding) {
if (i + 1 < objectIndices.size()) indicesStr += ","; if (i + 1 < objectIndices.size()) indicesStr += ",";
} }
// JSON payload containing replacement using system font embedding
std::string editsJson = R"({ std::string editsJson = R"({
"version": "1.0", "version": "1.0",
"operations": [ "operations": [
@@ -1702,7 +1605,6 @@ TEST(DocumentEditTest, ReplaceTextFontReuseAndEmbedding) {
auto editRes = doc->applyEdits(editsJson); auto editRes = doc->applyEdits(editsJson);
ASSERT_TRUE(editRes.has_value()); ASSERT_TRUE(editRes.has_value());
// Save and reload
auto saveRes = doc->saveIncremental(); auto saveRes = doc->saveIncremental();
ASSERT_TRUE(saveRes.has_value()); ASSERT_TRUE(saveRes.has_value());
const auto& savedBytes = *saveRes; const auto& savedBytes = *saveRes;
@@ -1712,7 +1614,6 @@ TEST(DocumentEditTest, ReplaceTextFontReuseAndEmbedding) {
ASSERT_TRUE(newDocRes.has_value()); ASSERT_TRUE(newDocRes.has_value());
auto newDoc = *newDocRes; auto newDoc = *newDocRes;
// Get page fonts to verify that Arial was successfully embedded in the new document
auto fontsRes = newDoc->getFonts(0, 0); auto fontsRes = newDoc->getFonts(0, 0);
ASSERT_TRUE(fontsRes.has_value()); ASSERT_TRUE(fontsRes.has_value());
@@ -1745,7 +1646,6 @@ TEST(DocumentEditTest, ReplaceTextHarfBuzzShapingAndReflow) {
ASSERT_TRUE(modelRes.has_value()); ASSERT_TRUE(modelRes.has_value());
const auto& model = *modelRes; const auto& model = *modelRes;
// Find a line that has at least 2 runs, where the first run uses Roboto-Regular
std::vector<int> targetIndices; std::vector<int> targetIndices;
std::string originalFontId = ""; std::string originalFontId = "";
std::string runBText = ""; std::string runBText = "";
@@ -1782,7 +1682,6 @@ TEST(DocumentEditTest, ReplaceTextHarfBuzzShapingAndReflow) {
if (i + 1 < targetIndices.size()) indicesStr += ","; if (i + 1 < targetIndices.size()) indicesStr += ",";
} }
// JSON payload: replacing runA with a very long text to trigger significant shift
std::string editsJson = R"({ std::string editsJson = R"({
"version": "1.0", "version": "1.0",
"operations": [ "operations": [
@@ -1800,7 +1699,6 @@ TEST(DocumentEditTest, ReplaceTextHarfBuzzShapingAndReflow) {
auto editRes = doc->applyEdits(editsJson); auto editRes = doc->applyEdits(editsJson);
ASSERT_TRUE(editRes.has_value()); ASSERT_TRUE(editRes.has_value());
// Save and reload
auto saveRes = doc->saveIncremental(); auto saveRes = doc->saveIncremental();
ASSERT_TRUE(saveRes.has_value()); ASSERT_TRUE(saveRes.has_value());
const auto& savedBytes = *saveRes; const auto& savedBytes = *saveRes;
@@ -1818,7 +1716,6 @@ TEST(DocumentEditTest, ReplaceTextHarfBuzzShapingAndReflow) {
ASSERT_TRUE(newModelRes.has_value()); ASSERT_TRUE(newModelRes.has_value());
const auto& newModel = *newModelRes; const auto& newModel = *newModelRes;
// Find runB in the new document model and verify its X coordinate has shifted to the right
bool foundRunB = false; bool foundRunB = false;
double runBNewX = 0.0; double runBNewX = 0.0;
for (const auto& p : newModel.paragraphs) { 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; std::cout << "Reflow Engine verified: '" << runBText << "' shifted from X=" << runBOrigX << " to X=" << runBNewX << std::endl;
} }
} }
+62 -167
View File
@@ -22,9 +22,6 @@
namespace { 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) { bool containsCI(const std::string& haystack, const std::string& needle) {
auto it = std::search( auto it = std::search(
haystack.begin(), haystack.end(), needle.begin(), needle.end(), 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() { std::string getSystemFontPath() {
#if defined(_WIN32) #if defined(_WIN32)
// Common Windows fonts
std::vector<std::string> paths = { std::vector<std::string> paths = {
"C:\\Windows\\Fonts\\arial.ttf", "C:\\Windows\\Fonts\\arial.ttf",
"C:\\Windows\\Fonts\\consola.ttf", "C:\\Windows\\Fonts\\consola.ttf",
"C:\\Windows\\Fonts\\tahoma.ttf" "C:\\Windows\\Fonts\\tahoma.ttf"
}; };
#elif defined(__APPLE__) #elif defined(__APPLE__)
// Common macOS fonts
std::vector<std::string> paths = { std::vector<std::string> paths = {
"/Library/Fonts/Arial.ttf", "/Library/Fonts/Arial.ttf",
"/System/Library/Fonts/Geneva.ttf", "/System/Library/Fonts/Geneva.ttf",
@@ -65,7 +60,6 @@ std::string getSystemFontPath() {
"/System/Library/Fonts/Supplemental/Arial.ttf" "/System/Library/Fonts/Supplemental/Arial.ttf"
}; };
#else #else
// Common Linux fonts
std::vector<std::string> paths = { std::vector<std::string> paths = {
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
@@ -81,7 +75,7 @@ std::string getSystemFontPath() {
return ""; return "";
} }
} // namespace }
namespace pdfengine::fonts { namespace pdfengine::fonts {
@@ -107,12 +101,10 @@ TEST(FontTest, FontFaceMoveSemantics) {
FT_Face rawFace = face1.getFace(); FT_Face rawFace = face1.getFace();
ASSERT_NE(rawFace, nullptr); ASSERT_NE(rawFace, nullptr);
// Move construction
FontFace face2(std::move(face1)); FontFace face2(std::move(face1));
EXPECT_EQ(face1.getFace(), nullptr); EXPECT_EQ(face1.getFace(), nullptr);
EXPECT_EQ(face2.getFace(), rawFace); EXPECT_EQ(face2.getFace(), rawFace);
// Move assignment
FontFace face3; FontFace face3;
face3 = std::move(face2); face3 = std::move(face2);
EXPECT_EQ(face2.getFace(), nullptr); EXPECT_EQ(face2.getFace(), nullptr);
@@ -134,7 +126,7 @@ TEST(FontTest, HbShaperEmptyInput) {
} }
TEST(FontTest, HbShaperNullFace) { TEST(FontTest, HbShaperNullFace) {
FontFace face; // Null face FontFace face;
HbShaper shaper; HbShaper shaper;
auto glyphs = shaper.shapeRun("Hello", face, 16); auto glyphs = shaper.shapeRun("Hello", face, 16);
EXPECT_TRUE(glyphs.empty()); EXPECT_TRUE(glyphs.empty());
@@ -155,20 +147,15 @@ TEST(FontTest, HbShaperShapeTextSuccess) {
std::string testText = "Hello World!"; std::string testText = "Hello World!";
auto glyphs = shaper.shapeRun(testText, face, 16); 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()); EXPECT_FALSE(glyphs.empty());
for (const auto& g : glyphs) { 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); EXPECT_GT(g.advanceX, 0.0);
} }
} }
TEST(FontTest, FontFaceRenderGlyphNullFace) { TEST(FontTest, FontFaceRenderGlyphNullFace) {
FontFace face; // Null face FontFace face;
auto glyph = face.renderGlyph(0, 16); auto glyph = face.renderGlyph(0, 16);
EXPECT_FALSE(glyph.has_value()); EXPECT_FALSE(glyph.has_value());
} }
@@ -183,11 +170,9 @@ TEST(FontTest, FontFaceRenderGlyphSuccess) {
ASSERT_TRUE(face.loadFromFile(fontPath)); ASSERT_TRUE(face.loadFromFile(fontPath));
ASSERT_NE(face.getFace(), nullptr); ASSERT_NE(face.getFace(), nullptr);
// Get the glyph index for character 'A'.
unsigned int glyphIndex = FT_Get_Char_Index(face.getFace(), '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); auto glyphOpt = face.renderGlyph(glyphIndex, 24);
ASSERT_TRUE(glyphOpt.has_value()); ASSERT_TRUE(glyphOpt.has_value());
@@ -211,22 +196,18 @@ TEST(FontTest, GlyphCacheBasicGetInsert) {
EXPECT_EQ(cache.capacity(), 10u); EXPECT_EQ(cache.capacity(), 10u);
EXPECT_EQ(cache.size(), 0u); EXPECT_EQ(cache.size(), 0u);
// Initial check: cache miss
auto miss = cache.get(face, 12, 16); auto miss = cache.get(face, 12, 16);
EXPECT_FALSE(miss.has_value()); EXPECT_FALSE(miss.has_value());
// Create a dummy GlyphBitmap
GlyphBitmap bitmap; GlyphBitmap bitmap;
bitmap.width = 10; bitmap.width = 10;
bitmap.height = 12; bitmap.height = 12;
bitmap.pixels = std::vector<unsigned char>(120, 255); bitmap.pixels = std::vector<unsigned char>(120, 255);
bitmap.advance = 8.5; bitmap.advance = 8.5;
// Insert
cache.insert(face, 12, 16, bitmap); cache.insert(face, 12, 16, bitmap);
EXPECT_EQ(cache.size(), 1u); EXPECT_EQ(cache.size(), 1u);
// Cache hit
auto hit = cache.get(face, 12, 16); auto hit = cache.get(face, 12, 16);
ASSERT_TRUE(hit.has_value()); ASSERT_TRUE(hit.has_value());
EXPECT_EQ(hit->width, 10); EXPECT_EQ(hit->width, 10);
@@ -244,7 +225,6 @@ TEST(FontTest, GlyphCacheEvictionPolicy) {
FontFace face; FontFace face;
ASSERT_TRUE(face.loadFromFile(fontPath)); ASSERT_TRUE(face.loadFromFile(fontPath));
// Capacity 2
GlyphCache cache(2); GlyphCache cache(2);
GlyphBitmap bmp1{ .width = 1 }; GlyphBitmap bmp1{ .width = 1 };
@@ -255,7 +235,6 @@ TEST(FontTest, GlyphCacheEvictionPolicy) {
cache.insert(face, 2, 16, bmp2); cache.insert(face, 2, 16, bmp2);
EXPECT_EQ(cache.size(), 2u); EXPECT_EQ(cache.size(), 2u);
// Insert third one: should evict the oldest (1, 16)
cache.insert(face, 3, 16, bmp3); cache.insert(face, 3, 16, bmp3);
EXPECT_EQ(cache.size(), 2u); EXPECT_EQ(cache.size(), 2u);
@@ -282,11 +261,9 @@ TEST(FontTest, GlyphCacheLRUPolicy) {
cache.insert(face, 1, 16, bmp1); cache.insert(face, 1, 16, bmp1);
cache.insert(face, 2, 16, bmp2); cache.insert(face, 2, 16, bmp2);
// Access 1 to make it most recently used
auto hit = cache.get(face, 1, 16); auto hit = cache.get(face, 1, 16);
ASSERT_TRUE(hit.has_value()); 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); cache.insert(face, 3, 16, bmp3);
EXPECT_EQ(cache.size(), 2u); EXPECT_EQ(cache.size(), 2u);
@@ -343,18 +320,15 @@ TEST(FontTest, UnicodeAndRtlShaping) {
HbShaper shaper; HbShaper shaper;
unsigned int fontSize = 16; unsigned int fontSize = 16;
// Test A: Arabic (RTL) - "سلام"
{ {
std::string arabicText = "سلام"; std::string arabicText = "سلام";
auto glyphs = shaper.shapeRun(arabicText, face, fontSize); auto glyphs = shaper.shapeRun(arabicText, face, fontSize);
EXPECT_FALSE(glyphs.empty()); EXPECT_FALSE(glyphs.empty());
for (const auto& g : glyphs) { 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); 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 = "नमस्ते"; std::string hindiText = "नमस्ते";
auto glyphs = shaper.shapeRun(hindiText, face, fontSize); auto glyphs = shaper.shapeRun(hindiText, face, fontSize);
@@ -364,7 +338,6 @@ TEST(FontTest, UnicodeAndRtlShaping) {
} }
} }
// Test C: Ligatures - "office"
{ {
std::string ligatureText = "office"; std::string ligatureText = "office";
auto glyphs = shaper.shapeRun(ligatureText, face, fontSize); auto glyphs = shaper.shapeRun(ligatureText, face, fontSize);
@@ -541,20 +514,14 @@ TEST(FontTest, CacheRecencyStress) {
ASSERT_TRUE(cache.get(face, 2, 16).has_value()); ASSERT_TRUE(cache.get(face, 2, 16).has_value());
cache.insert(face, 5, 16, bmps[5]); 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()); 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()); 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()); 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]); cache.insert(face, 6, 16, bmps[6]);
// 4 should be evicted
EXPECT_FALSE(cache.get(face, 4, 16).has_value()); 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, 0, 16).has_value());
EXPECT_TRUE(cache.get(face, 2, 16).has_value()); EXPECT_TRUE(cache.get(face, 2, 16).has_value());
EXPECT_TRUE(cache.get(face, 3, 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."; 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); std::ifstream file(fontPath, std::ios::binary | std::ios::ate);
ASSERT_TRUE(file.is_open()); ASSERT_TRUE(file.is_open());
std::streamsize size = file.tellg(); std::streamsize size = file.tellg();
@@ -577,12 +543,10 @@ TEST(FontLoaderTest, FontFaceLoadFromMemorySuccess) {
std::vector<uint8_t> buffer(size); std::vector<uint8_t> buffer(size);
ASSERT_TRUE(file.read(reinterpret_cast<char*>(buffer.data()), size)); ASSERT_TRUE(file.read(reinterpret_cast<char*>(buffer.data()), size));
// Load from memory
FontFace face; FontFace face;
ASSERT_TRUE(face.loadFromMemory(buffer)); ASSERT_TRUE(face.loadFromMemory(buffer));
ASSERT_NE(face.getFace(), nullptr); ASSERT_NE(face.getFace(), nullptr);
// Validate that glyph rendering and metrics are valid
unsigned int glyphIndex = FT_Get_Char_Index(face.getFace(), 'M'); unsigned int glyphIndex = FT_Get_Char_Index(face.getFace(), 'M');
ASSERT_GT(glyphIndex, 0u); ASSERT_GT(glyphIndex, 0u);
@@ -595,12 +559,10 @@ TEST(FontLoaderTest, FontFaceLoadFromMemorySuccess) {
TEST(FontLoaderTest, FontFaceLoadFromMemoryInvalid) { TEST(FontLoaderTest, FontFaceLoadFromMemoryInvalid) {
FontFace face; FontFace face;
// Empty vector
std::vector<uint8_t> emptyData; std::vector<uint8_t> emptyData;
EXPECT_FALSE(face.loadFromMemory(emptyData)); EXPECT_FALSE(face.loadFromMemory(emptyData));
EXPECT_EQ(face.getFace(), nullptr); EXPECT_EQ(face.getFace(), nullptr);
// Corrupt garbage data
std::vector<uint8_t> corruptData = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66}; std::vector<uint8_t> corruptData = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66};
EXPECT_FALSE(face.loadFromMemory(corruptData)); EXPECT_FALSE(face.loadFromMemory(corruptData));
EXPECT_EQ(face.getFace(), nullptr); EXPECT_EQ(face.getFace(), nullptr);
@@ -619,14 +581,12 @@ TEST(FontLoaderTest, FontLoaderTrueTypeSuccess) {
std::vector<uint8_t> buffer(size); std::vector<uint8_t> buffer(size);
ASSERT_TRUE(file.read(reinterpret_cast<char*>(buffer.data()), size)); ASSERT_TRUE(file.read(reinterpret_cast<char*>(buffer.data()), size));
// Use factory loader
auto pdfFont = pdfengine::fonts::pdf_fonts::FontLoader::loadTrueTypeFromMemory("Arial", buffer); auto pdfFont = pdfengine::fonts::pdf_fonts::FontLoader::loadTrueTypeFromMemory("Arial", buffer);
ASSERT_NE(pdfFont, nullptr); ASSERT_NE(pdfFont, nullptr);
EXPECT_EQ(pdfFont->getBaseFont(), "Arial"); EXPECT_EQ(pdfFont->getBaseFont(), "Arial");
EXPECT_EQ(pdfFont->getType(), pdfengine::fonts::pdf_fonts::FontType::TrueType); EXPECT_EQ(pdfFont->getType(), pdfengine::fonts::pdf_fonts::FontType::TrueType);
EXPECT_TRUE(pdfFont->isEmbedded()); EXPECT_TRUE(pdfFont->isEmbedded());
// Shaping via the loaded FontFace
HbShaper shaper; HbShaper shaper;
auto glyphs = shaper.shapeRun("Test Memory Load", pdfFont->getFontFace(), 16); auto glyphs = shaper.shapeRun("Test Memory Load", pdfFont->getFontFace(), 16);
EXPECT_FALSE(glyphs.empty()); EXPECT_FALSE(glyphs.empty());
@@ -685,28 +645,22 @@ TEST(FontDescriptorTest, DescriptorParsingSuccess) {
EXPECT_DOUBLE_EQ(desc.getCapHeight(), 728.0); EXPECT_DOUBLE_EQ(desc.getCapHeight(), 728.0);
EXPECT_DOUBLE_EQ(desc.getStemV(), 94.0); EXPECT_DOUBLE_EQ(desc.getStemV(), 94.0);
// Check flags
EXPECT_FALSE(desc.isFixedPitch()); EXPECT_FALSE(desc.isFixedPitch());
EXPECT_TRUE(desc.isNonsymbolic()); // 32 EXPECT_TRUE(desc.isNonsymbolic());
EXPECT_FALSE(desc.isItalic()); EXPECT_FALSE(desc.isItalic());
} }
TEST(FontDescriptorTest, DescriptorParsingMalformed) { TEST(FontDescriptorTest, DescriptorParsingMalformed) {
pdfengine::fonts::pdf_fonts::FontDescriptor desc; pdfengine::fonts::pdf_fonts::FontDescriptor desc;
// Missing <<
EXPECT_FALSE(desc.parseFromDictionaryString("/Flags 32 >>")); EXPECT_FALSE(desc.parseFromDictionaryString("/Flags 32 >>"));
// Unmatched >>
EXPECT_FALSE(desc.parseFromDictionaryString("<< /Flags 32")); EXPECT_FALSE(desc.parseFromDictionaryString("<< /Flags 32"));
// Malformed BBox array (missing urx, ury)
EXPECT_FALSE(desc.parseFromDictionaryString("<< /FontBBox [-166 -225] >>")); EXPECT_FALSE(desc.parseFromDictionaryString("<< /FontBBox [-166 -225] >>"));
// Malformed double conversion
EXPECT_FALSE(desc.parseFromDictionaryString("<< /Ascent abc >>")); EXPECT_FALSE(desc.parseFromDictionaryString("<< /Ascent abc >>"));
// Key without value
EXPECT_FALSE(desc.parseFromDictionaryString("<< /Ascent >>")); EXPECT_FALSE(desc.parseFromDictionaryString("<< /Ascent >>"));
} }
@@ -723,14 +677,12 @@ TEST(FontDescriptorTest, FontLoaderWithDescriptor) {
std::vector<uint8_t> buffer(size); std::vector<uint8_t> buffer(size);
ASSERT_TRUE(file.read(reinterpret_cast<char*>(buffer.data()), size)); ASSERT_TRUE(file.read(reinterpret_cast<char*>(buffer.data()), size));
// Create descriptor
auto descriptor = std::make_unique<pdfengine::fonts::pdf_fonts::FontDescriptor>(); auto descriptor = std::make_unique<pdfengine::fonts::pdf_fonts::FontDescriptor>();
descriptor->setFontName("Arial-BoldMT"); descriptor->setFontName("Arial-BoldMT");
descriptor->setFlags(96); // Nonsymbolic (32) | Italic (64) descriptor->setFlags(96);
descriptor->setAscent(905.0); descriptor->setAscent(905.0);
descriptor->setDescent(-211.0); descriptor->setDescent(-211.0);
// Load with descriptor
auto pdfFont = pdfengine::fonts::pdf_fonts::FontLoader::loadTrueTypeFromMemory("Arial-Bold", buffer, std::move(descriptor)); auto pdfFont = pdfengine::fonts::pdf_fonts::FontLoader::loadTrueTypeFromMemory("Arial-Bold", buffer, std::move(descriptor));
ASSERT_NE(pdfFont, nullptr); ASSERT_NE(pdfFont, nullptr);
EXPECT_EQ(pdfFont->getBaseFont(), "Arial-Bold"); EXPECT_EQ(pdfFont->getBaseFont(), "Arial-Bold");
@@ -749,27 +701,24 @@ TEST(FontDescriptorTest, FontLoaderWithDescriptor) {
TEST(EncodingTest, PredefinedEncodingTest) { TEST(EncodingTest, PredefinedEncodingTest) {
using namespace pdfengine::fonts::pdf_fonts; using namespace pdfengine::fonts::pdf_fonts;
// WinAnsiEncoding
PredefinedEncoding winAnsi(SimpleEncodingType::WinAnsi); PredefinedEncoding winAnsi(SimpleEncodingType::WinAnsi);
EXPECT_EQ(winAnsi.getType(), SimpleEncodingType::WinAnsi); EXPECT_EQ(winAnsi.getType(), SimpleEncodingType::WinAnsi);
EXPECT_EQ(winAnsi.decode(65), 65); // 'A' EXPECT_EQ(winAnsi.decode(65), 65);
EXPECT_EQ(winAnsi.decode(128), 0x20AC); // Euro symbol exception EXPECT_EQ(winAnsi.decode(128), 0x20AC);
EXPECT_EQ(winAnsi.decode(169), 169); // copyright symbol (standard ISO-8859-1) EXPECT_EQ(winAnsi.decode(169), 169);
EXPECT_EQ(winAnsi.decode(300), 0); // Out of bounds EXPECT_EQ(winAnsi.decode(300), 0);
// MacRomanEncoding
PredefinedEncoding macRoman(SimpleEncodingType::MacRoman); PredefinedEncoding macRoman(SimpleEncodingType::MacRoman);
EXPECT_EQ(macRoman.getType(), SimpleEncodingType::MacRoman); EXPECT_EQ(macRoman.getType(), SimpleEncodingType::MacRoman);
EXPECT_EQ(macRoman.decode(65), 65); // 'A' EXPECT_EQ(macRoman.decode(65), 65);
EXPECT_EQ(macRoman.decode(128), 0x00C4); // High page lookup exception (A-dieresis) EXPECT_EQ(macRoman.decode(128), 0x00C4);
EXPECT_EQ(macRoman.decode(300), 0); // Out of bounds EXPECT_EQ(macRoman.decode(300), 0);
// Identity encoding (pass-through)
PredefinedEncoding identity(SimpleEncodingType::Identity); PredefinedEncoding identity(SimpleEncodingType::Identity);
EXPECT_EQ(identity.getType(), SimpleEncodingType::Identity); EXPECT_EQ(identity.getType(), SimpleEncodingType::Identity);
EXPECT_EQ(identity.decode(65), 65); EXPECT_EQ(identity.decode(65), 65);
EXPECT_EQ(identity.decode(128), 128); 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) { TEST(EncodingTest, CustomEncodingWithDifferences) {
@@ -778,24 +727,19 @@ TEST(EncodingTest, CustomEncodingWithDifferences) {
auto baseEncoding = std::make_unique<PredefinedEncoding>(SimpleEncodingType::WinAnsi); auto baseEncoding = std::make_unique<PredefinedEncoding>(SimpleEncodingType::WinAnsi);
CustomEncoding custom(std::move(baseEncoding)); CustomEncoding custom(std::move(baseEncoding));
// Fallback to base
EXPECT_EQ(custom.decode(65), 65); EXPECT_EQ(custom.decode(65), 65);
// Standard glyph name difference mapping
custom.addDifference(120, "quotesingle"); custom.addDifference(120, "quotesingle");
EXPECT_EQ(custom.decode(120), 0x0027); EXPECT_EQ(custom.decode(120), 0x0027);
// Unicode-like glyph name (uniXXXX) difference mapping
custom.addDifference(121, "uni0041"); custom.addDifference(121, "uni0041");
EXPECT_EQ(custom.decode(121), 0x0041); // 'A' EXPECT_EQ(custom.decode(121), 0x0041);
// uXXXX representation
custom.addDifference(122, "u0042"); 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"); 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) { TEST(EncodingTest, ToUnicodeCMapbfchar) {
@@ -822,9 +766,9 @@ TEST(EncodingTest, ToUnicodeCMapbfchar) {
"end\n"; "end\n";
ASSERT_TRUE(cmap.parseCMapStream(cmapStream)); ASSERT_TRUE(cmap.parseCMapStream(cmapStream));
EXPECT_EQ(cmap.decode(1), 0x0041); // 'A' EXPECT_EQ(cmap.decode(1), 0x0041);
EXPECT_EQ(cmap.decode(2), 0x0042); // 'B' EXPECT_EQ(cmap.decode(2), 0x0042);
EXPECT_EQ(cmap.decode(3), 0); // Missing EXPECT_EQ(cmap.decode(3), 0);
} }
TEST(EncodingTest, ToUnicodeCMapbfrange) { TEST(EncodingTest, ToUnicodeCMapbfrange) {
@@ -835,22 +779,20 @@ TEST(EncodingTest, ToUnicodeCMapbfrange) {
std::string cmapStream = std::string cmapStream =
"begincmap\n" "begincmap\n"
"2 beginbfrange\n" "2 beginbfrange\n"
"<0001> <0005> <0041>\n" // Sequential base mapping (<0001> to <0005> starting at <0041>) "<0001> <0005> <0041>\n"
"<0010> <0012> [<0061> <0062> <0063>]\n" // Array mapping "<0010> <0012> [<0061> <0062> <0063>]\n"
"endbfrange\n" "endbfrange\n"
"endcmap\n"; "endcmap\n";
ASSERT_TRUE(cmap.parseCMapStream(cmapStream)); ASSERT_TRUE(cmap.parseCMapStream(cmapStream));
// Assert sequential EXPECT_EQ(cmap.decode(1), 0x0041);
EXPECT_EQ(cmap.decode(1), 0x0041); // 'A' EXPECT_EQ(cmap.decode(3), 0x0043);
EXPECT_EQ(cmap.decode(3), 0x0043); // 'C' EXPECT_EQ(cmap.decode(5), 0x0045);
EXPECT_EQ(cmap.decode(5), 0x0045); // 'E'
// Assert array EXPECT_EQ(cmap.decode(0x10), 0x0061);
EXPECT_EQ(cmap.decode(0x10), 0x0061); // 'a' EXPECT_EQ(cmap.decode(0x11), 0x0062);
EXPECT_EQ(cmap.decode(0x11), 0x0062); // 'b' EXPECT_EQ(cmap.decode(0x12), 0x0063);
EXPECT_EQ(cmap.decode(0x12), 0x0063); // 'c'
} }
TEST(EncodingTest, ToUnicodeMalformedCMap) { TEST(EncodingTest, ToUnicodeMalformedCMap) {
@@ -858,17 +800,15 @@ TEST(EncodingTest, ToUnicodeMalformedCMap) {
ToUnicodeCMap cmap; ToUnicodeCMap cmap;
// Completely invalid/garbage content
std::string garbageStream = "This is a garbage string with no valid CMap elements"; std::string garbageStream = "This is a garbage string with no valid CMap elements";
EXPECT_FALSE(cmap.parseCMapStream(garbageStream)); EXPECT_FALSE(cmap.parseCMapStream(garbageStream));
// Partially valid CMap - should recover parsed entries
std::string partialStream = std::string partialStream =
"begincmap\n" "begincmap\n"
"beginbfchar\n" "beginbfchar\n"
"<0001> <0041>\n" // Valid "<0001> <0041>\n"
"<0002> /invalid\n" // Invalid/missing dest (non-hex) "<0002> /invalid\n"
"<0003> <0043>\n" // Valid "<0003> <0043>\n"
"endbfchar\n" "endbfchar\n"
"endcmap\n"; "endcmap\n";
@@ -891,12 +831,10 @@ TEST(EncodingTest, FontLoaderWithEncoding) {
std::vector<uint8_t> buffer(size); std::vector<uint8_t> buffer(size);
ASSERT_TRUE(file.read(reinterpret_cast<char*>(buffer.data()), size)); ASSERT_TRUE(file.read(reinterpret_cast<char*>(buffer.data()), size));
// Create predefined encoding (WinAnsi)
auto encoding = std::make_unique<pdfengine::fonts::pdf_fonts::PredefinedEncoding>( auto encoding = std::make_unique<pdfengine::fonts::pdf_fonts::PredefinedEncoding>(
pdfengine::fonts::pdf_fonts::SimpleEncodingType::WinAnsi pdfengine::fonts::pdf_fonts::SimpleEncodingType::WinAnsi
); );
// Load font with encoding
auto pdfFont = pdfengine::fonts::pdf_fonts::FontLoader::loadTrueTypeFromMemory( auto pdfFont = pdfengine::fonts::pdf_fonts::FontLoader::loadTrueTypeFromMemory(
"Arial-With-Encoding", buffer, nullptr, std::move(encoding) "Arial-With-Encoding", buffer, nullptr, std::move(encoding)
); );
@@ -907,7 +845,6 @@ TEST(EncodingTest, FontLoaderWithEncoding) {
const auto* retrievedEncoding = pdfFont->getEncoding(); const auto* retrievedEncoding = pdfFont->getEncoding();
ASSERT_NE(retrievedEncoding, nullptr); ASSERT_NE(retrievedEncoding, nullptr);
// Verify it translates Euro symbol exception properly
EXPECT_EQ(retrievedEncoding->decode(128), 0x20AC); EXPECT_EQ(retrievedEncoding->decode(128), 0x20AC);
} }
@@ -960,14 +897,12 @@ TEST(CIDFontTest, CompositeFontInitializationAndTypes) {
auto desc = std::make_unique<FontDescriptor>(); auto desc = std::make_unique<FontDescriptor>();
desc->setFontName("SimSun-Descriptor"); desc->setFontName("SimSun-Descriptor");
// Test CIDFontType0
CIDFont font0("SimSun", FontType::CIDFontType0, false, std::move(desc)); CIDFont font0("SimSun", FontType::CIDFontType0, false, std::move(desc));
EXPECT_EQ(font0.getBaseFont(), "SimSun"); EXPECT_EQ(font0.getBaseFont(), "SimSun");
EXPECT_EQ(font0.getType(), FontType::CIDFontType0); EXPECT_EQ(font0.getType(), FontType::CIDFontType0);
EXPECT_FALSE(font0.isEmbedded()); EXPECT_FALSE(font0.isEmbedded());
EXPECT_EQ(font0.getDescriptor()->getFontName(), "SimSun-Descriptor"); EXPECT_EQ(font0.getDescriptor()->getFontName(), "SimSun-Descriptor");
// Test CIDFontType2
CIDFont font2("MS-Gothic", FontType::CIDFontType2, true); CIDFont font2("MS-Gothic", FontType::CIDFontType2, true);
EXPECT_EQ(font2.getBaseFont(), "MS-Gothic"); EXPECT_EQ(font2.getBaseFont(), "MS-Gothic");
EXPECT_EQ(font2.getType(), FontType::CIDFontType2); EXPECT_EQ(font2.getType(), FontType::CIDFontType2);
@@ -979,12 +914,10 @@ TEST(CIDFontTest, CIDToGIDTranslations) {
CIDFont font("SimSun", FontType::CIDFontType2, false); CIDFont font("SimSun", FontType::CIDFontType2, false);
// By default, it should be an identity mapping
EXPECT_TRUE(font.isIdentityMap()); EXPECT_TRUE(font.isIdentityMap());
EXPECT_EQ(font.mapCIDToGID(100), 100); EXPECT_EQ(font.mapCIDToGID(100), 100);
EXPECT_EQ(font.mapCIDToGID(5000), 5000); EXPECT_EQ(font.mapCIDToGID(5000), 5000);
// Set custom mapping
std::unordered_map<uint32_t, uint32_t> customMap = { std::unordered_map<uint32_t, uint32_t> customMap = {
{10, 100}, {10, 100},
{20, 200}, {20, 200},
@@ -995,9 +928,8 @@ TEST(CIDFontTest, CIDToGIDTranslations) {
EXPECT_EQ(font.mapCIDToGID(10), 100); EXPECT_EQ(font.mapCIDToGID(10), 100);
EXPECT_EQ(font.mapCIDToGID(20), 200); EXPECT_EQ(font.mapCIDToGID(20), 200);
EXPECT_EQ(font.mapCIDToGID(30), 300); 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(); font.setIdentityCIDToGIDMap();
EXPECT_TRUE(font.isIdentityMap()); EXPECT_TRUE(font.isIdentityMap());
EXPECT_EQ(font.mapCIDToGID(10), 10); EXPECT_EQ(font.mapCIDToGID(10), 10);
@@ -1006,22 +938,18 @@ TEST(CIDFontTest, CIDToGIDTranslations) {
TEST(CIDFontTest, NonEmbeddedCIDFontSystemFallback) { TEST(CIDFontTest, NonEmbeddedCIDFontSystemFallback) {
using namespace pdfengine::fonts::pdf_fonts; using namespace pdfengine::fonts::pdf_fonts;
// Load Chinese Simplified CJK fallback
auto fontSimSun = FontLoader::loadCIDFontSystemFallback("SimSun", FontType::CIDFontType2); auto fontSimSun = FontLoader::loadCIDFontSystemFallback("SimSun", FontType::CIDFontType2);
ASSERT_NE(fontSimSun, nullptr); ASSERT_NE(fontSimSun, nullptr);
EXPECT_EQ(fontSimSun->getBaseFont(), "SimSun"); EXPECT_EQ(fontSimSun->getBaseFont(), "SimSun");
EXPECT_EQ(fontSimSun->getType(), FontType::CIDFontType2); EXPECT_EQ(fontSimSun->getType(), FontType::CIDFontType2);
EXPECT_FALSE(fontSimSun->isEmbedded()); EXPECT_FALSE(fontSimSun->isEmbedded());
// Load Japanese CJK fallback
auto fontGothic = FontLoader::loadCIDFontSystemFallback("HeiseiMin-W3", FontType::CIDFontType0); auto fontGothic = FontLoader::loadCIDFontSystemFallback("HeiseiMin-W3", FontType::CIDFontType0);
ASSERT_NE(fontGothic, nullptr); ASSERT_NE(fontGothic, nullptr);
EXPECT_EQ(fontGothic->getBaseFont(), "HeiseiMin-W3"); EXPECT_EQ(fontGothic->getBaseFont(), "HeiseiMin-W3");
EXPECT_EQ(fontGothic->getType(), FontType::CIDFontType0); EXPECT_EQ(fontGothic->getType(), FontType::CIDFontType0);
EXPECT_FALSE(fontGothic->isEmbedded()); 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; HbShaper shaper;
auto glyphs = shaper.shapeRun("日本語漢字", fontGothic->getFontFace(), 16); auto glyphs = shaper.shapeRun("日本語漢字", fontGothic->getFontFace(), 16);
EXPECT_FALSE(glyphs.empty()); EXPECT_FALSE(glyphs.empty());
@@ -1040,30 +968,30 @@ TEST(FontFallbackTest, StandardFontFallbacks) {
auto& fallback = FontFallback::getInstance(); auto& fallback = FontFallback::getInstance();
// Helvetica resolves to its sans-serif substitute for the host platform.
std::string path1 = fallback.getFallbackFontPath("Helvetica"); std::string path1 = fallback.getFallbackFontPath("Helvetica");
EXPECT_FALSE(path1.empty()); EXPECT_FALSE(path1.empty());
EXPECT_TRUE(std::filesystem::exists(path1)); EXPECT_TRUE(std::filesystem::exists(path1));
#if defined(_WIN32) #if defined(_WIN32)
EXPECT_TRUE(containsCI(path1, "arial") || containsCI(path1, "liberationsans")); EXPECT_TRUE(containsCI(path1, "arial") || containsCI(path1, "liberationsans"));
#elif defined(__APPLE__) #elif defined(__APPLE__)
// Arial may not be installed; the resolver then falls back to Helvetica.
EXPECT_TRUE(containsCI(path1, "arial") || containsCI(path1, "helvetica") || EXPECT_TRUE(containsCI(path1, "arial") || containsCI(path1, "helvetica") ||
containsCI(path1, "liberationsans")); containsCI(path1, "liberationsans"));
#else #else
EXPECT_TRUE(containsCI(path1, "liberationsans") || containsCI(path1, "dejavusans")); EXPECT_TRUE(containsCI(path1, "liberationsans") || containsCI(path1, "dejavusans"));
#endif #endif
// Times resolves to its serif substitute for the host platform.
std::string path2 = fallback.getFallbackFontPath("Times-Roman"); std::string path2 = fallback.getFallbackFontPath("Times-Roman");
EXPECT_FALSE(path2.empty()); EXPECT_FALSE(path2.empty());
EXPECT_TRUE(std::filesystem::exists(path2)); EXPECT_TRUE(std::filesystem::exists(path2));
#if defined(_WIN32) #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__) #elif defined(__APPLE__)
EXPECT_TRUE(containsCI(path2, "times") || containsCI(path2, "liberationserif")); EXPECT_TRUE(containsCI(path2, "times") || containsCI(path2, "liberationserif") ||
containsCI(path2, "tinos"));
#else #else
EXPECT_TRUE(containsCI(path2, "liberationserif") || containsCI(path2, "dejavuserif")); EXPECT_TRUE(containsCI(path2, "liberationserif") || containsCI(path2, "dejavuserif") ||
containsCI(path2, "tinos"));
#endif #endif
} }
@@ -1072,22 +1000,20 @@ TEST(FontFallbackTest, StyleModifierResolutions) {
auto& fallback = FontFallback::getInstance(); auto& fallback = FontFallback::getInstance();
// Bold Helvetica should map to a bold sans-serif substitute.
std::string pathBold = fallback.getFallbackFontPath("Helvetica", true, false); std::string pathBold = fallback.getFallbackFontPath("Helvetica", true, false);
EXPECT_FALSE(pathBold.empty()); EXPECT_FALSE(pathBold.empty());
EXPECT_TRUE(std::filesystem::exists(pathBold)); EXPECT_TRUE(std::filesystem::exists(pathBold));
#if defined(_WIN32) #if defined(_WIN32)
// Windows ships the styled variants, so assert the exact bold face.
EXPECT_TRUE(containsCI(pathBold, "arialbd") || containsCI(pathBold, "liberationsans-bold")); EXPECT_TRUE(containsCI(pathBold, "arialbd") || containsCI(pathBold, "liberationsans-bold"));
#endif #endif
// Bold-italic Times should map to a bold-italic serif substitute.
std::string pathBoldItalic = fallback.getFallbackFontPath("Times", true, true); std::string pathBoldItalic = fallback.getFallbackFontPath("Times", true, true);
EXPECT_FALSE(pathBoldItalic.empty()); EXPECT_FALSE(pathBoldItalic.empty());
EXPECT_TRUE(std::filesystem::exists(pathBoldItalic)); EXPECT_TRUE(std::filesystem::exists(pathBoldItalic));
#if defined(_WIN32) #if defined(_WIN32)
EXPECT_TRUE(containsCI(pathBoldItalic, "timesbi") || EXPECT_TRUE(containsCI(pathBoldItalic, "timesbi") ||
containsCI(pathBoldItalic, "liberationserif-bolditalic")); containsCI(pathBoldItalic, "liberationserif-bolditalic") ||
containsCI(pathBoldItalic, "tinos-bolditalic"));
#endif #endif
} }
@@ -1097,11 +1023,8 @@ TEST(FontFallbackTest, CustomFallbackRegistration) {
auto& fallback = FontFallback::getInstance(); auto& fallback = FontFallback::getInstance();
fallback.resetToDefaults(); fallback.resetToDefaults();
// Lookup the default substitute path for Helvetica.
std::string standardPath = fallback.getFallbackFontPath("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::path overrideFont =
std::filesystem::temp_directory_path() / "pdfengine_custom_fallback.ttf"; std::filesystem::temp_directory_path() / "pdfengine_custom_fallback.ttf";
{ std::ofstream(overrideFont) << "stub-font"; } { std::ofstream(overrideFont) << "stub-font"; }
@@ -1111,7 +1034,6 @@ TEST(FontFallbackTest, CustomFallbackRegistration) {
std::string overridenPath = fallback.getFallbackFontPath("Helvetica"); std::string overridenPath = fallback.getFallbackFontPath("Helvetica");
EXPECT_EQ(overridenPath, overrideFont.string()); EXPECT_EQ(overridenPath, overrideFont.string());
// Reset back to defaults and confirm the original substitute returns.
fallback.resetToDefaults(); fallback.resetToDefaults();
std::string restoredPath = fallback.getFallbackFontPath("Helvetica"); std::string restoredPath = fallback.getFallbackFontPath("Helvetica");
EXPECT_EQ(restoredPath, standardPath); EXPECT_EQ(restoredPath, standardPath);
@@ -1128,7 +1050,6 @@ TEST(FontSubsetTest, SubsetTagParsingAndStripping) {
EXPECT_EQ(FontSubset::getSubsetPrefix(subsetName), "KTJHQO"); EXPECT_EQ(FontSubset::getSubsetPrefix(subsetName), "KTJHQO");
EXPECT_EQ(FontSubset::stripSubsetPrefix(subsetName), "Arial"); EXPECT_EQ(FontSubset::stripSubsetPrefix(subsetName), "Arial");
// Standard naming (no prefix)
std::string normalName = "Arial"; std::string normalName = "Arial";
EXPECT_FALSE(FontSubset::hasSubsetPrefix(normalName)); EXPECT_FALSE(FontSubset::hasSubsetPrefix(normalName));
EXPECT_EQ(FontSubset::getSubsetPrefix(normalName), ""); EXPECT_EQ(FontSubset::getSubsetPrefix(normalName), "");
@@ -1138,20 +1059,16 @@ TEST(FontSubsetTest, SubsetTagParsingAndStripping) {
TEST(FontSubsetTest, PrefixFormatValidation) { TEST(FontSubsetTest, PrefixFormatValidation) {
using namespace pdfengine::fonts::pdf_fonts; using namespace pdfengine::fonts::pdf_fonts;
// Prefixes must be exactly 6 UPPERCASE letters followed by '+'
EXPECT_TRUE(FontSubset::hasSubsetPrefix("ABCDEF+Helvetica")); EXPECT_TRUE(FontSubset::hasSubsetPrefix("ABCDEF+Helvetica"));
// Lowercase should fail
EXPECT_FALSE(FontSubset::hasSubsetPrefix("abcDEF+Helvetica")); EXPECT_FALSE(FontSubset::hasSubsetPrefix("abcDEF+Helvetica"));
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("ABC123+Helvetica"));
EXPECT_FALSE(FontSubset::hasSubsetPrefix("ABCDE_+Helvetica")); EXPECT_FALSE(FontSubset::hasSubsetPrefix("ABCDE_+Helvetica"));
// Length must be exactly 6 characters EXPECT_FALSE(FontSubset::hasSubsetPrefix("ABCDE+Helvetica"));
EXPECT_FALSE(FontSubset::hasSubsetPrefix("ABCDE+Helvetica")); // 5 chars EXPECT_FALSE(FontSubset::hasSubsetPrefix("ABCDEFG+Helvetica"));
EXPECT_FALSE(FontSubset::hasSubsetPrefix("ABCDEFG+Helvetica")); // 7 chars
} }
TEST(FontSubsetTest, GIDRemappingTranslations) { TEST(FontSubsetTest, GIDRemappingTranslations) {
@@ -1163,14 +1080,12 @@ TEST(FontSubsetTest, GIDRemappingTranslations) {
EXPECT_EQ(subset.getBaseFontName(), "Arial"); EXPECT_EQ(subset.getBaseFontName(), "Arial");
EXPECT_EQ(subset.getPrefix(), "KTJHQO"); EXPECT_EQ(subset.getPrefix(), "KTJHQO");
// Default pass-through lookup
EXPECT_EQ(subset.mapSubsetToOriginal(5), 5); EXPECT_EQ(subset.mapSubsetToOriginal(5), 5);
EXPECT_FALSE(subset.hasGlyphMapping(5)); EXPECT_FALSE(subset.hasGlyphMapping(5));
// Register glyph ID translations subset.addGlyphMapping(1, 41);
subset.addGlyphMapping(1, 41); // 'A' subset.addGlyphMapping(2, 42);
subset.addGlyphMapping(2, 42); // 'B' subset.addGlyphMapping(3, 43);
subset.addGlyphMapping(3, 43); // 'C'
EXPECT_EQ(subset.getMappingCount(), 3u); EXPECT_EQ(subset.getMappingCount(), 3u);
EXPECT_TRUE(subset.hasGlyphMapping(1)); EXPECT_TRUE(subset.hasGlyphMapping(1));
@@ -1178,13 +1093,12 @@ TEST(FontSubsetTest, GIDRemappingTranslations) {
EXPECT_EQ(subset.mapSubsetToOriginal(1), 41); EXPECT_EQ(subset.mapSubsetToOriginal(1), 41);
EXPECT_EQ(subset.mapSubsetToOriginal(2), 42); EXPECT_EQ(subset.mapSubsetToOriginal(2), 42);
EXPECT_EQ(subset.mapSubsetToOriginal(3), 43); 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) { TEST(FontSubsetTest, CoreFontSubsettingIntegration) {
using namespace pdfengine::fonts::pdf_fonts; using namespace pdfengine::fonts::pdf_fonts;
// Test TrueType Font integration
TrueTypeFont fontTT("KTJHQO+Arial", false); TrueTypeFont fontTT("KTJHQO+Arial", false);
const auto* ttSubset = fontTT.getSubsetInfo(); const auto* ttSubset = fontTT.getSubsetInfo();
ASSERT_NE(ttSubset, nullptr); ASSERT_NE(ttSubset, nullptr);
@@ -1192,7 +1106,6 @@ TEST(FontSubsetTest, CoreFontSubsettingIntegration) {
EXPECT_EQ(ttSubset->getBaseFontName(), "Arial"); EXPECT_EQ(ttSubset->getBaseFontName(), "Arial");
EXPECT_EQ(ttSubset->getPrefix(), "KTJHQO"); EXPECT_EQ(ttSubset->getPrefix(), "KTJHQO");
// Test Type1 Font integration
Type1Font fontT1("SUBSET+Courier", false); Type1Font fontT1("SUBSET+Courier", false);
const auto* t1Subset = fontT1.getSubsetInfo(); const auto* t1Subset = fontT1.getSubsetInfo();
ASSERT_NE(t1Subset, nullptr); ASSERT_NE(t1Subset, nullptr);
@@ -1200,7 +1113,6 @@ TEST(FontSubsetTest, CoreFontSubsettingIntegration) {
EXPECT_EQ(t1Subset->getBaseFontName(), "Courier"); EXPECT_EQ(t1Subset->getBaseFontName(), "Courier");
EXPECT_EQ(t1Subset->getPrefix(), "SUBSET"); EXPECT_EQ(t1Subset->getPrefix(), "SUBSET");
// Test CID Font integration
CIDFont fontCID("CJKTAG+SimSun", FontType::CIDFontType2, false); CIDFont fontCID("CJKTAG+SimSun", FontType::CIDFontType2, false);
const auto* cidSubset = fontCID.getSubsetInfo(); const auto* cidSubset = fontCID.getSubsetInfo();
ASSERT_NE(cidSubset, nullptr); ASSERT_NE(cidSubset, nullptr);
@@ -1330,36 +1242,28 @@ TEST(TextExtractionLayerTest, StringUtf8Conversion) {
TEST(FontSubstitutionAndWidthsTest, WidthMatchingAndSubstitutionVerification) { TEST(FontSubstitutionAndWidthsTest, WidthMatchingAndSubstitutionVerification) {
using namespace pdfengine::fonts::pdf_fonts; using namespace pdfengine::fonts::pdf_fonts;
// Load non-embedded Helvetica font, which triggers substitution
auto font = FontLoader::loadType1SystemFallback("Helvetica"); auto font = FontLoader::loadType1SystemFallback("Helvetica");
ASSERT_NE(font, nullptr); ASSERT_NE(font, nullptr);
// Confirm that the font does not have widths set yet
EXPECT_FALSE(font->hasWidths()); 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<double> pdfWidths = { 600.0, 500.0, 550.0, 400.0 }; std::vector<double> pdfWidths = { 600.0, 500.0, 550.0, 400.0 };
font->setWidths(65, 68, pdfWidths); font->setWidths(65, 68, pdfWidths);
EXPECT_TRUE(font->hasWidths()); EXPECT_TRUE(font->hasWidths());
// Font size context: 12.0
double fontSize = 12.0; double fontSize = 12.0;
// Expected widths = (W / 1000.0) * fontSize double expectedWidthA = (600.0 / 1000.0) * fontSize;
double expectedWidthA = (600.0 / 1000.0) * fontSize; // 7.2 double expectedWidthB = (500.0 / 1000.0) * fontSize;
double expectedWidthB = (500.0 / 1000.0) * fontSize; // 6.0 double expectedWidthC = (550.0 / 1000.0) * fontSize;
double expectedWidthC = (550.0 / 1000.0) * fontSize; // 6.6 double expectedWidthD = (400.0 / 1000.0) * fontSize;
double expectedWidthD = (400.0 / 1000.0) * fontSize; // 4.8
// 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(65, fontSize), expectedWidthA, 1e-5);
EXPECT_NEAR(font->getCharWidth(66, fontSize), expectedWidthB, 1e-5); EXPECT_NEAR(font->getCharWidth(66, fontSize), expectedWidthB, 1e-5);
EXPECT_NEAR(font->getCharWidth(67, fontSize), expectedWidthC, 1e-5); EXPECT_NEAR(font->getCharWidth(67, fontSize), expectedWidthC, 1e-5);
EXPECT_NEAR(font->getCharWidth(68, fontSize), expectedWidthD, 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); EXPECT_EQ(font->getCharWidth(999, fontSize), 0.0);
} }
@@ -1377,12 +1281,10 @@ TEST(CIDAdvancedMappingTest, VerticalMetricsResolution) {
auto font = FontLoader::loadType1SystemFallback("Helvetica"); auto font = FontLoader::loadType1SystemFallback("Helvetica");
ASSERT_NE(font, nullptr); ASSERT_NE(font, nullptr);
// Default vertical advance metrics: 1.0em = font size context
double fontSize = 12.0; double fontSize = 12.0;
EXPECT_EQ(font->isVertical(), false); EXPECT_EQ(font->isVertical(), false);
EXPECT_EQ(font->getCharHeight(65, fontSize), fontSize); EXPECT_EQ(font->getCharHeight(65, fontSize), fontSize);
// Turn vertical metrics ON and verify custom heights
font->setVertical(true); font->setVertical(true);
EXPECT_EQ(font->isVertical(), true); EXPECT_EQ(font->isVertical(), true);
@@ -1390,39 +1292,32 @@ TEST(CIDAdvancedMappingTest, VerticalMetricsResolution) {
font->setVerticalMetrics(65, 67, verticalAdvances); font->setVerticalMetrics(65, 67, verticalAdvances);
EXPECT_TRUE(font->hasVerticalMetrics()); EXPECT_TRUE(font->hasVerticalMetrics());
// Expected heights: (Adv / 1000.0) * fontSize EXPECT_NEAR(font->getCharHeight(65, fontSize), 12.0, 1e-5);
EXPECT_NEAR(font->getCharHeight(65, fontSize), 12.0, 1e-5); // (1000/1000) * 12 EXPECT_NEAR(font->getCharHeight(66, fontSize), 9.6, 1e-5);
EXPECT_NEAR(font->getCharHeight(66, fontSize), 9.6, 1e-5); // (800/1000) * 12 EXPECT_NEAR(font->getCharHeight(67, fontSize), 10.8, 1e-5);
EXPECT_NEAR(font->getCharHeight(67, fontSize), 10.8, 1e-5); // (900/1000) * 12 EXPECT_NEAR(font->getCharHeight(999, fontSize), 12.0, 1e-5);
EXPECT_NEAR(font->getCharHeight(999, fontSize), 12.0, 1e-5); // Fallback to 12.0
} }
TEST(CIDAdvancedMappingTest, CjkCollectionResolutionDB) { TEST(CIDAdvancedMappingTest, CjkCollectionResolutionDB) {
using namespace pdfengine::fonts::pdf_fonts; using namespace pdfengine::fonts::pdf_fonts;
// Standard Adobe-Japan1 Hiragana CIDs EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1010), 0x3041);
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1010), 0x3041); // Hiragana 'ぁ' EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1092), 0x3093);
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1092), 0x3093); // Hiragana 'ん'
// Standard Adobe-Japan1 Katakana CIDs EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1125), 0x30A1);
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1125), 0x30A1); // Katakana 'ァ' EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1205), 0x30F6);
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1205), 0x30F6); // Katakana 'ヶ'
// Core Kanji EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1206), 0x4E00);
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1206), 0x4E00); // Kanji '一'
// GB1 Chinese simplified ideographic marks EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-GB1", 1), 0x3000);
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-GB1", 1), 0x3000); // space EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-GB1", 2), 0x3001);
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-GB1", 2), 0x3001); // comma
} }
TEST(CIDAdvancedMappingTest, HarfBuzzVerticalShapingSignature) { TEST(CIDAdvancedMappingTest, HarfBuzzVerticalShapingSignature) {
using namespace pdfengine::fonts; using namespace pdfengine::fonts;
// Validate WritingMode configurations
EXPECT_EQ(static_cast<int>(HbShaper::WritingMode::Horizontal), 0); EXPECT_EQ(static_cast<int>(HbShaper::WritingMode::Horizontal), 0);
EXPECT_EQ(static_cast<int>(HbShaper::WritingMode::Vertical), 1); EXPECT_EQ(static_cast<int>(HbShaper::WritingMode::Vertical), 1);
} }
} // namespace pdfengine::fonts }
+3 -8
View File
@@ -32,33 +32,28 @@ TEST(MatrixTest, TransformPoint) {
float y = 5.0f; float y = 5.0f;
m.transform(x, y); m.transform(x, y);
EXPECT_FLOAT_EQ(x, 20.0f); // 2*5 + 10 EXPECT_FLOAT_EQ(x, 20.0f);
EXPECT_FLOAT_EQ(y, 35.0f); // 3*5 + 20 EXPECT_FLOAT_EQ(y, 35.0f);
} }
TEST(GraphicsStateStackTest, PushPop) { TEST(GraphicsStateStackTest, PushPop) {
GraphicsStateStack stack; GraphicsStateStack stack;
// Initial state
stack.current().lineWidth = 5.0f; stack.current().lineWidth = 5.0f;
// Push new state
stack.push(); stack.push();
EXPECT_FLOAT_EQ(stack.current().lineWidth, 5.0f); EXPECT_FLOAT_EQ(stack.current().lineWidth, 5.0f);
// Modify current state
stack.current().lineWidth = 10.0f; stack.current().lineWidth = 10.0f;
EXPECT_FLOAT_EQ(stack.current().lineWidth, 10.0f); EXPECT_FLOAT_EQ(stack.current().lineWidth, 10.0f);
// Pop back to initial
stack.pop(); stack.pop();
EXPECT_FLOAT_EQ(stack.current().lineWidth, 5.0f); EXPECT_FLOAT_EQ(stack.current().lineWidth, 5.0f);
} }
TEST(GraphicsStateStackTest, PopEmptyProtection) { TEST(GraphicsStateStackTest, PopEmptyProtection) {
GraphicsStateStack stack; GraphicsStateStack stack;
// Attempting to pop the root state should be safe (ignored)
stack.pop(); stack.pop();
stack.current().lineWidth = 2.0f; // Should still be valid stack.current().lineWidth = 2.0f;
EXPECT_FLOAT_EQ(stack.current().lineWidth, 2.0f); EXPECT_FLOAT_EQ(stack.current().lineWidth, 2.0f);
} }
+1 -1
View File
@@ -406,7 +406,7 @@ std::vector<uint8_t> rgb(std::initializer_list<uint8_t> values) {
return std::vector<uint8_t>(values); return std::vector<uint8_t>(values);
} }
} // namespace }
TEST(ImageXObjectVerification, JpegLogoAndPhotoDecodeAndMatchPdfiumRender) { TEST(ImageXObjectVerification, JpegLogoAndPhotoDecodeAndMatchPdfiumRender) {
const auto logoJpeg = encodeJpegRgb(rgb({ const auto logoJpeg = encodeJpegRgb(rgb({
+1 -7
View File
@@ -40,7 +40,7 @@ TEST(LexerTest, Strings) {
EXPECT_EQ(tokens[2].stringValue, "Escapes \n \t \\ ()"); EXPECT_EQ(tokens[2].stringValue, "Escapes \n \t \\ ()");
EXPECT_EQ(tokens[3].type, TokenType::String); EXPECT_EQ(tokens[3].type, TokenType::String);
EXPECT_EQ(tokens[3].stringValue, "Octal +"); // \053 is '+' EXPECT_EQ(tokens[3].stringValue, "Octal +");
} }
TEST(LexerTest, HexStrings) { TEST(LexerTest, HexStrings) {
@@ -49,12 +49,10 @@ TEST(LexerTest, HexStrings) {
ASSERT_EQ(tokens.size(), 2); ASSERT_EQ(tokens.size(), 2);
EXPECT_EQ(tokens[0].type, TokenType::HexString); EXPECT_EQ(tokens[0].type, TokenType::HexString);
// "Hello"
std::vector<uint8_t> expected1 = {0x48, 0x65, 0x6C, 0x6C, 0x6F}; std::vector<uint8_t> expected1 = {0x48, 0x65, 0x6C, 0x6C, 0x6F};
EXPECT_EQ(tokens[0].bytesValue, expected1); EXPECT_EQ(tokens[0].bytesValue, expected1);
EXPECT_EQ(tokens[1].type, TokenType::HexString); EXPECT_EQ(tokens[1].type, TokenType::HexString);
// "4A5" padded to "4A50"
std::vector<uint8_t> expected2 = {0x4A, 0x50}; std::vector<uint8_t> expected2 = {0x4A, 0x50};
EXPECT_EQ(tokens[1].bytesValue, expected2); EXPECT_EQ(tokens[1].bytesValue, expected2);
} }
@@ -113,15 +111,11 @@ TEST(LexerTest, IntegrationHelloWorld) {
Lexer lexer(stream->decodedContent); Lexer lexer(stream->decodedContent);
auto tokens = lexer.tokenize(); 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; bool foundHello = false;
for (size_t i = 0; i < tokens.size(); ++i) { for (size_t i = 0; i < tokens.size(); ++i) {
if (tokens[i].type == TokenType::String && tokens[i].stringValue == "Hello, world!") { if (tokens[i].type == TokenType::String && tokens[i].stringValue == "Hello, world!") {
foundHello = true; foundHello = true;
// The next token should be Tj or TJ
ASSERT_LT(i + 1, tokens.size()); ASSERT_LT(i + 1, tokens.size());
EXPECT_EQ(tokens[i+1].type, TokenType::Operator); EXPECT_EQ(tokens[i+1].type, TokenType::Operator);
EXPECT_TRUE(tokens[i+1].stringValue == "Tj" || tokens[i+1].stringValue == "TJ"); EXPECT_TRUE(tokens[i+1].stringValue == "Tj" || tokens[i+1].stringValue == "TJ");
-6
View File
@@ -37,27 +37,21 @@ TEST(ParserTest, ArraysAndDicts) {
ContentParser parser(tokens); ContentParser parser(tokens);
auto ops = parser.parse(); 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); ASSERT_EQ(ops.size(), 1);
EXPECT_EQ(ops[0].op, "Tj"); EXPECT_EQ(ops[0].op, "Tj");
ASSERT_EQ(ops[0].operands.size(), 3); ASSERT_EQ(ops[0].operands.size(), 3);
// First operand: Dict
auto dictNode = ops[0].operands[0]; auto dictNode = ops[0].operands[0];
EXPECT_EQ(dictNode->type, AstNodeType::Dictionary); EXPECT_EQ(dictNode->type, AstNodeType::Dictionary);
ASSERT_TRUE(dictNode->dictItems.find("Type") != dictNode->dictItems.end()); ASSERT_TRUE(dictNode->dictItems.find("Type") != dictNode->dictItems.end());
EXPECT_EQ(dictNode->dictItems["Type"]->stringValue, "Page"); EXPECT_EQ(dictNode->dictItems["Type"]->stringValue, "Page");
// Second operand: Array
auto arrayNode = ops[0].operands[1]; auto arrayNode = ops[0].operands[1];
EXPECT_EQ(arrayNode->type, AstNodeType::Array); EXPECT_EQ(arrayNode->type, AstNodeType::Array);
ASSERT_EQ(arrayNode->arrayItems.size(), 2); ASSERT_EQ(arrayNode->arrayItems.size(), 2);
EXPECT_DOUBLE_EQ(arrayNode->arrayItems[0]->numberValue, 1.0); EXPECT_DOUBLE_EQ(arrayNode->arrayItems[0]->numberValue, 1.0);
EXPECT_DOUBLE_EQ(arrayNode->arrayItems[1]->numberValue, 2.0); EXPECT_DOUBLE_EQ(arrayNode->arrayItems[1]->numberValue, 2.0);
// Third operand: String
auto strNode = ops[0].operands[2]; auto strNode = ops[0].operands[2];
EXPECT_EQ(strNode->type, AstNodeType::String); EXPECT_EQ(strNode->type, AstNodeType::String);
EXPECT_EQ(strNode->stringValue, "Text"); EXPECT_EQ(strNode->stringValue, "Text");
+1 -3
View File
@@ -39,7 +39,7 @@ TEST_F(QpdfExtractorTest, ExtractFromMemory) {
auto stream = extractor.extractPageStream(path.string(), 0); auto stream = extractor.extractPageStream(path.string(), 0);
ASSERT_TRUE(stream.has_value()); ASSERT_TRUE(stream.has_value());
EXPECT_EQ(stream->pageIndex, 0); 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()); StreamVerification v = verifyContentStream(stream.value());
EXPECT_TRUE(v.hasBT); EXPECT_TRUE(v.hasBT);
@@ -70,7 +70,6 @@ TEST_F(QpdfExtractorTest, CorruptPdf) {
} }
TEST_F(QpdfExtractorTest, EmptyContents) { TEST_F(QpdfExtractorTest, EmptyContents) {
// about_blank.pdf usually has an empty page or no text
auto path = getCorpusPath("basic", "about_blank.pdf"); auto path = getCorpusPath("basic", "about_blank.pdf");
auto stream = extractor.extractPageStream(path.string(), 0); auto stream = extractor.extractPageStream(path.string(), 0);
ASSERT_TRUE(stream.has_value()); ASSERT_TRUE(stream.has_value());
@@ -81,7 +80,6 @@ TEST_F(QpdfExtractorTest, EmptyContents) {
} }
TEST_F(QpdfExtractorTest, VerifyNoText) { TEST_F(QpdfExtractorTest, VerifyNoText) {
// black.pdf or rectangles.pdf has no text, just graphics
auto path = getCorpusPath("basic", "black.pdf"); auto path = getCorpusPath("basic", "black.pdf");
auto stream = extractor.extractPageStream(path.string(), 0); auto stream = extractor.extractPageStream(path.string(), 0);
ASSERT_TRUE(stream.has_value()); ASSERT_TRUE(stream.has_value());
-8
View File
@@ -20,12 +20,10 @@ TEST(QpdfWriterTest, IntegrationReadModifyWrite) {
std::filesystem::path sourcePath = std::filesystem::path(TEST_CORPUS_DIR) / "basic" / "hello_world.pdf"; 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"; std::filesystem::path destPath = std::filesystem::path(TEST_CORPUS_DIR) / "basic" / "hello_world_modified.pdf";
// 1. Extract
QpdfExtractor extractor; QpdfExtractor extractor;
auto stream = extractor.extractPageStream(sourcePath.string(), 0); auto stream = extractor.extractPageStream(sourcePath.string(), 0);
ASSERT_TRUE(stream.has_value()); ASSERT_TRUE(stream.has_value());
// 2. Lex, Parse, Build
Lexer lexer(stream->decodedContent); Lexer lexer(stream->decodedContent);
auto tokens = lexer.tokenize(); auto tokens = lexer.tokenize();
ContentParser parser(tokens); ContentParser parser(tokens);
@@ -33,7 +31,6 @@ TEST(QpdfWriterTest, IntegrationReadModifyWrite) {
ContentBuilder builder; ContentBuilder builder;
auto objects = builder.build(ops); auto objects = builder.build(ops);
// 3. Modify Text
bool foundAndModified = false; bool foundAndModified = false;
for (auto& obj : objects) { for (auto& obj : objects) {
if (obj->getType() == ContentObjectType::Text) { if (obj->getType() == ContentObjectType::Text) {
@@ -47,20 +44,16 @@ TEST(QpdfWriterTest, IntegrationReadModifyWrite) {
} }
ASSERT_TRUE(foundAndModified) << "Could not find 'Hello, world!' to modify"; ASSERT_TRUE(foundAndModified) << "Could not find 'Hello, world!' to modify";
// 4. Serialize back to Ops
ContentSerializer contentSerializer; ContentSerializer contentSerializer;
auto newOps = contentSerializer.serialize(objects); auto newOps = contentSerializer.serialize(objects);
// 5. Serialize to raw bytes
AstSerializer astSerializer; AstSerializer astSerializer;
std::string newRawStream = astSerializer.serialize(newOps); std::string newRawStream = astSerializer.serialize(newOps);
// 6. Write and Save PDF
QpdfWriter writer; QpdfWriter writer;
auto writeRes = writer.replacePageStreamAndSave(sourcePath.string(), destPath.string(), 0, newRawStream); auto writeRes = writer.replacePageStreamAndSave(sourcePath.string(), destPath.string(), 0, newRawStream);
ASSERT_TRUE(writeRes.has_value()) << writeRes.error(); ASSERT_TRUE(writeRes.has_value()) << writeRes.error();
// 7. Re-open and verify modification
auto verifyStream = extractor.extractPageStream(destPath.string(), 0); auto verifyStream = extractor.extractPageStream(destPath.string(), 0);
ASSERT_TRUE(verifyStream.has_value()); ASSERT_TRUE(verifyStream.has_value());
@@ -83,6 +76,5 @@ TEST(QpdfWriterTest, IntegrationReadModifyWrite) {
EXPECT_TRUE(verifiedModification) << "Modified string was not successfully saved and reloaded!"; EXPECT_TRUE(verifiedModification) << "Modified string was not successfully saved and reloaded!";
// Cleanup
std::filesystem::remove(destPath); std::filesystem::remove(destPath);
} }
+1 -4
View File
@@ -17,21 +17,18 @@ TEST(SkiaRendererTest, RenderSimpleDisplayList) {
canvas.clear(SK_ColorWHITE); canvas.clear(SK_ColorWHITE);
DisplayList dl; DisplayList dl;
// Push state and set fill color to red
dl.saveState(); 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(); dl.restoreState();
SkiaRenderer renderer(&canvas); SkiaRenderer renderer(&canvas);
renderer.render(dl); 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); SkColor c = bitmap.getColor(20, 20);
EXPECT_EQ(SkColorGetR(c), 0); EXPECT_EQ(SkColorGetR(c), 0);
EXPECT_EQ(SkColorGetG(c), 0); EXPECT_EQ(SkColorGetG(c), 0);
EXPECT_EQ(SkColorGetB(c), 0); EXPECT_EQ(SkColorGetB(c), 0);
// Check outside the rect
SkColor bg = bitmap.getColor(5, 5); SkColor bg = bitmap.getColor(5, 5);
EXPECT_EQ(SkColorGetR(bg), 255); EXPECT_EQ(SkColorGetR(bg), 255);
EXPECT_EQ(SkColorGetG(bg), 255); EXPECT_EQ(SkColorGetG(bg), 255);
-4
View File
@@ -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 <gtest/gtest.h> #include <gtest/gtest.h>
#include <pdfengine/pdf_engine.hpp> #include <pdfengine/pdf_engine.hpp>
#include <string_view> #include <string_view>
@@ -28,6 +25,5 @@ TEST(EngineSmoke, BuildInfoConsistentWithSkiaLinkage) {
} }
TEST(EngineSmoke, LogBuildInfoDoesNotThrow) { TEST(EngineSmoke, LogBuildInfoDoesNotThrow) {
// Exercises the spdlog dependency end to end (compile + link + call).
EXPECT_NO_THROW(pdfengine::engineLogBuildInfo()); EXPECT_NO_THROW(pdfengine::engineLogBuildInfo());
} }
-2
View File
@@ -3,13 +3,11 @@
extern "C" { extern "C" {
// Exported function: add two numbers
EMSCRIPTEN_KEEPALIVE EMSCRIPTEN_KEEPALIVE
int add(int a, int b) { int add(int a, int b) {
return a + b; return a + b;
} }
// Exported function: print hello message
EMSCRIPTEN_KEEPALIVE EMSCRIPTEN_KEEPALIVE
void hello() { void hello() {
std::cout << "Hello from C++ WASM!" << std::endl; std::cout << "Hello from C++ WASM!" << std::endl;
+1 -36
View File
@@ -30,20 +30,16 @@ function App() {
const [documents, setDocuments] = useState<DocumentInfo[]>([]); const [documents, setDocuments] = useState<DocumentInfo[]>([]);
const [activeDoc, setActiveDoc] = useState<DocumentInfo | null>(null); const [activeDoc, setActiveDoc] = useState<DocumentInfo | null>(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 [hist, setHist] = useState<{ stack: string[]; index: number }>({ stack: [], index: -1 });
const selectedDocId = hist.index >= 0 ? hist.stack[hist.index] : ''; const selectedDocId = hist.index >= 0 ? hist.stack[hist.index] : '';
const canUndo = hist.index > 0; const canUndo = hist.index > 0;
const canRedo = hist.index < hist.stack.length - 1; const canRedo = hist.index < hist.stack.length - 1;
const preservePageRef = useRef(false); 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 permissions = activeDoc?.permissions ?? null;
const can = (flag: keyof PDFPermissions) => !permissions || permissions[flag] !== false; const can = (flag: keyof PDFPermissions) => !permissions || permissions[flag] !== false;
const denyToast = (label: string) => const denyToast = (label: string) =>
toast(`${label} is not permitted by this document's restrictions`, 'error'); 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<ToolId>(); const disabledTools = new Set<ToolId>();
if (!can('canAnnotate')) if (!can('canAnnotate'))
(['highlight', 'underline', 'strikeout', 'squiggly', 'draw', 'comment', 'textbox', 'stamp', 'signature'] as ToolId[]).forEach((t) => disabledTools.add(t)); (['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); const disabledToolsRef = useRef(disabledTools);
disabledToolsRef.current = disabledTools; disabledToolsRef.current = disabledTools;
// View / tools
const [zoom, setZoom] = useState(1.0); const [zoom, setZoom] = useState(1.0);
const [activeTool, setActiveTool] = useState<ToolId>('select'); const [activeTool, setActiveTool] = useState<ToolId>('select');
const [toolSettings, setToolSettings] = useState<ToolSettings>(DEFAULT_TOOL_SETTINGS); const [toolSettings, setToolSettings] = useState<ToolSettings>(DEFAULT_TOOL_SETTINGS);
const [currentPage, setCurrentPage] = useState(0); const [currentPage, setCurrentPage] = useState(0);
const [isInspectorOpen, setIsInspectorOpen] = useState(true); const [isInspectorOpen, setIsInspectorOpen] = useState(true);
// Data
const [annotations, setAnnotations] = useState<Annotation[]>([]); const [annotations, setAnnotations] = useState<Annotation[]>([]);
const [metadata, setMetadata] = useState<DocumentMetadata | null>(null); const [metadata, setMetadata] = useState<DocumentMetadata | null>(null);
const [fonts, setFonts] = useState<FontInfo[]>([]); const [fonts, setFonts] = useState<FontInfo[]>([]);
@@ -69,7 +63,6 @@ function App() {
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
// Tool aux state
const [pendingSignature, setPendingSignature] = useState<{ url: string; aspect: number } | null>(null); const [pendingSignature, setPendingSignature] = useState<{ url: string; aspect: number } | null>(null);
const [signatureModalOpen, setSignatureModalOpen] = useState(false); const [signatureModalOpen, setSignatureModalOpen] = useState(false);
const [aboutModalOpen, setAboutModalOpen] = useState(false); const [aboutModalOpen, setAboutModalOpen] = useState(false);
@@ -77,14 +70,12 @@ function App() {
const [activeStamp, setActiveStamp] = useState<{ label: string; color: string } | null>(null); const [activeStamp, setActiveStamp] = useState<{ label: string; color: string } | null>(null);
const [confirmState, setConfirmState] = useState<(CustomConfirmationOptions & { onConfirm: () => void }) | null>(null); const [confirmState, setConfirmState] = useState<(CustomConfirmationOptions & { onConfirm: () => void }) | null>(null);
// Search
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [searchCaseSensitive, setSearchCaseSensitive] = useState(false); const [searchCaseSensitive, setSearchCaseSensitive] = useState(false);
const [searchWholeWords, setSearchWholeWords] = useState(false); const [searchWholeWords, setSearchWholeWords] = useState(false);
const [searchResults, setSearchResults] = useState<SearchResult[]>([]); const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
const [searchCurrentMatch, setSearchCurrentMatch] = useState(0); const [searchCurrentMatch, setSearchCurrentMatch] = useState(0);
/* ----------------------------------------------------- history helpers */
const openDocument = useCallback((id: string) => setHist({ stack: [id], index: 0 }), []); const openDocument = useCallback((id: string) => setHist({ stack: [id], index: 0 }), []);
const pushHistory = (id: string) => const pushHistory = (id: string) =>
setHist((h) => ({ stack: [...h.stack.slice(0, h.index + 1), id], index: h.index + 1 })); 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); toast('Redo', 'info', 1200);
}; };
/* ------------------------------------------------------ initial loads */
useEffect(() => { 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() gatewayService.getHealth()
.then((h) => { setBackendHealthy(true); setEngineReady(!!h.engine_available); }) .then((h) => { setBackendHealthy(true); setEngineReady(!!h.engine_available); })
.catch(() => setBackendHealthy(false)); .catch(() => setBackendHealthy(false));
@@ -125,7 +113,6 @@ function App() {
})(); })();
}, [openDocument]); }, [openDocument]);
// Load metadata, annotations, fonts when the active document version changes.
useEffect(() => { useEffect(() => {
if (!selectedDocId) return; if (!selectedDocId) return;
let active = true; let active = true;
@@ -153,7 +140,6 @@ function App() {
content: a.content, content: a.content,
timestamp: a.timestamp, timestamp: a.timestamp,
pageIndex: a.pageIndex, 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, paths: Array.isArray(a.paths) && a.paths.length > 0 ? a.paths : undefined,
fieldName: a.fieldName, fieldName: a.fieldName,
fieldValue: a.fieldValue, fieldValue: a.fieldValue,
@@ -172,7 +158,6 @@ function App() {
return () => { active = false; }; return () => { active = false; };
}, [selectedDocId]); }, [selectedDocId]);
/* ---------------------------------------------------------- live search */
useEffect(() => { useEffect(() => {
const t = setTimeout(async () => { const t = setTimeout(async () => {
if (!searchQuery || !selectedDocId) { if (!searchQuery || !selectedDocId) {
@@ -198,7 +183,6 @@ function App() {
viewerRef.current?.scrollToPage(searchResults[i].pageIndex); viewerRef.current?.scrollToPage(searchResults[i].pageIndex);
}; };
/* ------------------------------------------------- keyboard shortcuts */
useEffect(() => { useEffect(() => {
const onKey = (e: KeyboardEvent) => { const onKey = (e: KeyboardEvent) => {
const target = e.target as HTMLElement; const target = e.target as HTMLElement;
@@ -223,9 +207,6 @@ function App() {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [canUndo, canRedo, pendingSignature]); }, [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) => { const adoptNewDocument = (newDocumentId: string) => {
preservePageRef.current = true; preservePageRef.current = true;
pushHistory(newDocumentId); pushHistory(newDocumentId);
@@ -251,7 +232,6 @@ function App() {
const pageHeightPts = (pageIndex: number) => activeDoc?.pages?.[pageIndex]?.height ?? activeDoc?.pageHeight ?? 792; const pageHeightPts = (pageIndex: number) => activeDoc?.pages?.[pageIndex]?.height ?? activeDoc?.pageHeight ?? 792;
/* -------------------------------------- annotation creation (optimistic) */
const handleAnnotationAdded = (a: Annotation) => { const handleAnnotationAdded = (a: Annotation) => {
if (!can('canAnnotate')) { denyToast('Annotations'); return; } if (!can('canAnnotate')) { denyToast('Annotations'); return; }
setAnnotations((prev) => [...prev, a]); 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) => { const handleDecorateText = (pageIndex: number, lines: Rect[], type: 'underline' | 'strikeout' | 'squiggly', color: string) => {
if (!can('canAnnotate')) { denyToast('Text decorations'); return; } 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 quadPoints = lines.map((line) => {
const lx = line.x / zoom, ly = line.y / zoom, lw = line.width / zoom, lh = line.height / zoom; 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 }; 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 => ({ const newAnnos = lines.map(line => ({
id: rid('locdec'), id: rid('locdec'),
type, type,
@@ -319,8 +295,6 @@ function App() {
setActiveTool('select'); 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) => { const handleEditText = (pageIndex: number, run: EditableRun, newText: string) => {
if (!can('canModify')) { denyToast('Editing text'); setActiveTool('select'); return; } if (!can('canModify')) { denyToast('Editing text'); setActiveTool('select'); return; }
applyOps([{ applyOps([{
@@ -335,7 +309,6 @@ function App() {
setActiveTool('select'); setActiveTool('select');
}; };
// Whole-paragraph reflow (Word-style wrap + push-down) for multi-line paragraph edits.
const handleReflowParagraph = (pageIndex: number, payload: ReflowParagraphPayload) => { const handleReflowParagraph = (pageIndex: number, payload: ReflowParagraphPayload) => {
if (!can('canModify')) { denyToast('Editing text'); setActiveTool('select'); return; } if (!can('canModify')) { denyToast('Editing text'); setActiveTool('select'); return; }
applyOps([{ id: rid('reflow'), type: 'reflow_paragraph', pageIndex, data: payload }], 'Text reflowed'); applyOps([{ id: rid('reflow'), type: 'reflow_paragraph', pageIndex, data: payload }], 'Text reflowed');
@@ -368,7 +341,6 @@ function App() {
setActiveTool('select'); setActiveTool('select');
}; };
/* ---------------------------------------------------------- page ops */
const handleRotate = () => { const handleRotate = () => {
if (!activeDoc) return; if (!activeDoc) return;
if (!can('canAssemble')) { denyToast('Rotating pages'); return; } if (!can('canAssemble')) { denyToast('Rotating pages'); return; }
@@ -415,7 +387,6 @@ function App() {
}); });
}; };
/* ----------------------------------------------------------- doc-level */
const handleUpload = async (file: File, password = '') => { const handleUpload = async (file: File, password = '') => {
try { try {
setIsLoading(true); setIsLoading(true);
@@ -426,7 +397,6 @@ function App() {
setPasswordPrompt(null); setPasswordPrompt(null);
} catch (e) { } catch (e) {
if (e instanceof PasswordError) { 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 }); setPasswordPrompt({ file, filename: file.name, error: password ? 'Incorrect password — please try again.' : undefined });
} else { } else {
console.error('Upload failed', e); console.error('Upload failed', e);
@@ -466,8 +436,6 @@ function App() {
setTimeout(() => { setTimeout(() => {
iframe.contentWindow?.focus(); iframe.contentWindow?.focus();
iframe.contentWindow?.print(); iframe.contentWindow?.print();
// Optional: cleanup after a delay
// setTimeout(() => document.body.removeChild(iframe), 10000);
}, 100); }, 100);
}; };
document.body.appendChild(iframe); document.body.appendChild(iframe);
@@ -493,7 +461,7 @@ function App() {
const fitWidth = () => { const fitWidth = () => {
const w = activeDoc?.pageWidth || 612; const w = activeDoc?.pageWidth || 612;
const inspectorWidth = isInspectorOpen ? 322 : 0; 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))); setZoom(Math.max(0.25, Math.min(3, avail / w)));
}; };
@@ -502,7 +470,6 @@ function App() {
}; };
const handleDeleteAnnotation = (a: Annotation) => { 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)); setAnnotations((prev) => prev.filter((x) => x.id !== a.id));
applyOps([{ applyOps([{
id: rid('delanno'), type: 'delete_annotation', pageIndex: a.pageIndex ?? currentPage, id: rid('delanno'), type: 'delete_annotation', pageIndex: a.pageIndex ?? currentPage,
@@ -511,7 +478,6 @@ function App() {
}; };
const handleUpdateAnnotation = (a: Annotation) => { const handleUpdateAnnotation = (a: Annotation) => {
// Optimistically update
setAnnotations((prev) => prev.map((x) => x.id === a.id ? a : x)); setAnnotations((prev) => prev.map((x) => x.id === a.id ? a : x));
applyOps([{ applyOps([{
id: rid('updanno'), type: 'update_annotation', pageIndex: a.pageIndex ?? currentPage, id: rid('updanno'), type: 'update_annotation', pageIndex: a.pageIndex ?? currentPage,
@@ -528,7 +494,6 @@ function App() {
}], 'Annotation updated'); }], 'Annotation updated');
}; };
/* -------------------------------------------------------------- render */
return ( return (
<div className="flex h-full w-full flex-col overflow-hidden bg-[#f1f2f4] text-[#18212e] antialiased"> <div className="flex h-full w-full flex-col overflow-hidden bg-[#f1f2f4] text-[#18212e] antialiased">
<TopBar <TopBar
@@ -73,9 +73,7 @@ export const InspectorPanel: React.FC<InspectorPanelProps> = (p) => {
return ( return (
<aside className="flex h-full shrink-0 border-l border-[#ebedf0] bg-[#ffffff]" style={{ width: '322px' }}> <aside className="flex h-full shrink-0 border-l border-[#ebedf0] bg-[#ffffff]" style={{ width: '322px' }}>
{/* Content */}
<div className="flex min-w-0 flex-1 flex-col"> <div className="flex min-w-0 flex-1 flex-col">
{/* Header: active section title + compact document switcher */}
<div className="flex h-[48px] shrink-0 items-center justify-between gap-2 border-b border-[#ebedf0]" style={{ paddingLeft: '14px', paddingRight: '12px' }}> <div className="flex h-[48px] shrink-0 items-center justify-between gap-2 border-b border-[#ebedf0]" style={{ paddingLeft: '14px', paddingRight: '12px' }}>
<span className="shrink-0 text-[13px] font-bold text-[#18212e]">{activeDef.label}</span> <span className="shrink-0 text-[13px] font-bold text-[#18212e]">{activeDef.label}</span>
{p.documents.length > 0 ? ( {p.documents.length > 0 ? (
@@ -108,7 +106,6 @@ export const InspectorPanel: React.FC<InspectorPanelProps> = (p) => {
)} )}
</div> </div>
{/* Icon-only tab switcher — evenly spaced so all 7 fit with breathing room */}
<div className="flex shrink-0 items-center justify-between border-b border-[#ebedf0] bg-[#f6f7f9]" style={{ paddingLeft: '10px', paddingRight: '10px', paddingTop: '7px', paddingBottom: '7px' }}> <div className="flex shrink-0 items-center justify-between border-b border-[#ebedf0] bg-[#f6f7f9]" style={{ paddingLeft: '10px', paddingRight: '10px', paddingTop: '7px', paddingBottom: '7px' }}>
{TABS.map((t) => { {TABS.map((t) => {
const active = p.activeTab === t.id; const active = p.activeTab === t.id;
@@ -136,7 +133,6 @@ export const InspectorPanel: React.FC<InspectorPanelProps> = (p) => {
})} })}
</div> </div>
{/* Body */}
<div className="custom-scrollbar min-h-0 flex-1 overflow-y-auto"> <div className="custom-scrollbar min-h-0 flex-1 overflow-y-auto">
{p.activeTab === 'pages' && <PagesTab {...p} />} {p.activeTab === 'pages' && <PagesTab {...p} />}
{p.activeTab === 'notes' && <NotesTab annotations={p.annotations.filter((a) => a.type !== 'widget')} onNavigate={p.onNavigateAnnotation} onDelete={p.onDeleteAnnotation} onUpdate={p.onUpdateAnnotation} />} {p.activeTab === 'notes' && <NotesTab annotations={p.annotations.filter((a) => a.type !== 'widget')} onNavigate={p.onNavigateAnnotation} onDelete={p.onDeleteAnnotation} onUpdate={p.onUpdateAnnotation} />}
@@ -396,7 +392,6 @@ const PropertiesTab: React.FC<{ metadata: DocumentMetadata | null; permissions?:
</div> </div>
))} ))}
{/* Security / permissions */}
<div className="mt-3 border-t border-[#ebedf0] pt-3"> <div className="mt-3 border-t border-[#ebedf0] pt-3">
<div className="mb-2 flex items-center gap-1.5"> <div className="mb-2 flex items-center gap-1.5">
<span className="text-[11px] font-semibold uppercase tracking-wide text-[#98a1ad]">Security</span> <span className="text-[11px] font-semibold uppercase tracking-wide text-[#98a1ad]">Security</span>
+1 -2
View File
@@ -3,7 +3,7 @@ import { CustomButton } from './custom/CustomButton';
export interface PasswordPromptState { export interface PasswordPromptState {
filename: string; filename: string;
error?: string; // shown on a failed attempt ("Incorrect password…") error?: string;
} }
interface PasswordModalProps { interface PasswordModalProps {
@@ -16,7 +16,6 @@ export const PasswordModal: React.FC<PasswordModalProps> = ({ state, onSubmit, o
const [value, setValue] = useState(''); const [value, setValue] = useState('');
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
// Reset the field and focus whenever the prompt (re)opens.
useEffect(() => { useEffect(() => {
if (!state) return; if (!state) return;
setValue(''); setValue('');
+1 -1
View File
@@ -6,7 +6,7 @@ import { toast } from '../lib/toast';
interface SignatureModalProps { interface SignatureModalProps {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
onConfirm: (dataUrl: string, aspect: number) => void; // aspect = width/height onConfirm: (dataUrl: string, aspect: number) => void;
} }
type Mode = 'draw' | 'type' | 'upload'; type Mode = 'draw' | 'type' | 'upload';
+1 -8
View File
@@ -23,9 +23,6 @@ export const Thumbnail: React.FC<ThumbnailProps> = ({
}) => { }) => {
const [imageUrl, setImageUrl] = useState<string | null>(null); const [imageUrl, setImageUrl] = useState<string | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
// Whether we already have a rendered thumbnail. Used to KEEP the current image visible during a
// re-fetch (e.g. after an edit mints a new documentId) instead of flashing back to "Loading…".
// A ref (not state) so the effect can read it without re-running when the image swaps.
const hasImageRef = useRef(false); const hasImageRef = useRef(false);
useEffect(() => { useEffect(() => {
@@ -33,14 +30,11 @@ export const Thumbnail: React.FC<ThumbnailProps> = ({
const fetchThumbnail = async () => { const fetchThumbnail = async () => {
try { try {
// Only show the spinner on the very first load. On a re-fetch after an edit, keep the
// previous thumbnail on screen and swap it for the new one when it arrives (no flash).
if (!hasImageRef.current) setLoading(true); if (!hasImageRef.current) setLoading(true);
// Request a lower resolution/zoom image for the thumbnail
const url = await gatewayService.renderPage({ const url = await gatewayService.renderPage({
documentId, documentId,
pageIndex, pageIndex,
zoom: 0.2, // Small zoom for thumbnail size zoom: 0.2,
rotation: 0, rotation: 0,
}); });
@@ -77,7 +71,6 @@ export const Thumbnail: React.FC<ThumbnailProps> = ({
alt={`Page ${pageIndex + 1}`} alt={`Page ${pageIndex + 1}`}
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />
{/* Group Hover Overlay Controls */}
<div className="absolute inset-0 bg-slate-950/60 opacity-0 group-hover:opacity-100 transition-opacity flex flex-col justify-between p-2 pointer-events-auto"> <div className="absolute inset-0 bg-slate-950/60 opacity-0 group-hover:opacity-100 transition-opacity flex flex-col justify-between p-2 pointer-events-auto">
<div className="flex justify-end"> <div className="flex justify-end">
{onDelete && totalPages && totalPages > 1 && ( {onDelete && totalPages && totalPages > 1 && (
-2
View File
@@ -62,7 +62,6 @@ export const Toolbar: React.FC<ToolbarProps> = ({
className="flex h-[48px] shrink-0 items-center gap-3 border-b border-[#ebedf0] bg-[#ffffff]" className="flex h-[48px] shrink-0 items-center gap-3 border-b border-[#ebedf0] bg-[#ffffff]"
style={{ paddingLeft: '16px', paddingRight: '16px' }} style={{ paddingLeft: '16px', paddingRight: '16px' }}
> >
{/* Active tool identity */}
<div className="flex shrink-0 items-center gap-2"> <div className="flex shrink-0 items-center gap-2">
<span <span
className="flex h-7 w-7 items-center justify-center rounded-[8px]" className="flex h-7 w-7 items-center justify-center rounded-[8px]"
@@ -77,7 +76,6 @@ export const Toolbar: React.FC<ToolbarProps> = ({
</div> </div>
<Divider /> <Divider />
{/* Per-tool controls (no overflow clip — would cut off the active-swatch ring) */}
<div className="flex min-w-0 flex-1 items-center gap-3"> <div className="flex min-w-0 flex-1 items-center gap-3">
{activeTool === 'select' && <Hint>Drag to select text release to copy, or press <Kbd>Ctrl/ + C</Kbd></Hint>} {activeTool === 'select' && <Hint>Drag to select text release to copy, or press <Kbd>Ctrl/ + C</Kbd></Hint>}
{activeTool === 'pan' && <Hint>Drag anywhere to move the page.</Hint>} {activeTool === 'pan' && <Hint>Drag anywhere to move the page.</Hint>}
-4
View File
@@ -54,7 +54,6 @@ export const TopBar: React.FC<TopBarProps> = ({
className="flex shrink-0 items-center justify-between gap-3 border-b border-[#ebedf0] bg-[#ffffff]" className="flex shrink-0 items-center justify-between gap-3 border-b border-[#ebedf0] bg-[#ffffff]"
style={{ height: '56px', paddingLeft: '24px', paddingRight: '24px' }} style={{ height: '56px', paddingLeft: '24px', paddingRight: '24px' }}
> >
{/* Left: brand + file menu + doc name */}
<div className="flex shrink-0 items-center gap-2"> <div className="flex shrink-0 items-center gap-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded-[9px] bg-[#2563eb] text-white shadow-sm"> <div className="flex h-8 w-8 items-center justify-center rounded-[9px] bg-[#2563eb] text-white shadow-sm">
@@ -92,7 +91,6 @@ export const TopBar: React.FC<TopBarProps> = ({
)} )}
</div> </div>
{/* Center: history · zoom · rotate · page nav */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="flex items-center gap-0.5"> <div className="flex items-center gap-0.5">
<CustomButton variant="icon" label="Undo (Ctrl+Z)" size={34} onClick={onUndo} disabled={!canUndo}><UndoIcon size={18} /></CustomButton> <CustomButton variant="icon" label="Undo (Ctrl+Z)" size={34} onClick={onUndo} disabled={!canUndo}><UndoIcon size={18} /></CustomButton>
@@ -143,7 +141,6 @@ export const TopBar: React.FC<TopBarProps> = ({
)} )}
</div> </div>
{/* Right: save-state + health + export + inspector toggle */}
<div className="flex shrink-0 items-center gap-2"> <div className="flex shrink-0 items-center gap-2">
<SaveState isSaving={isSaving} saved={isDirtySaved} /> <SaveState isSaving={isSaving} saved={isDirtySaved} />
<HealthChip healthy={backendHealthy} engineReady={!!engineReady} /> <HealthChip healthy={backendHealthy} engineReady={!!engineReady} />
@@ -181,7 +178,6 @@ const SaveState: React.FC<{ isSaving: boolean; saved: boolean }> = ({ isSaving,
}; };
const HealthChip: React.FC<{ healthy: boolean | null; engineReady: boolean }> = ({ healthy, engineReady }) => { const HealthChip: React.FC<{ healthy: boolean | null; engineReady: boolean }> = ({ healthy, engineReady }) => {
// Fully connected (gateway up + engine on) → no noise.
if (healthy && engineReady) return null; if (healthy && engineReady) return null;
let color = '#98a1ad'; let color = '#98a1ad';
-13
View File
@@ -72,7 +72,6 @@ export const WasmInspector: React.FC<WasmInspectorProps> = ({
} }
addLog(`Document loaded successfully. Allocated handle ID: ${docHandle} in ${loadTime.toFixed(1)}ms`); addLog(`Document loaded successfully. Allocated handle ID: ${docHandle} in ${loadTime.toFixed(1)}ms`);
// Update stats
setStats((prev) => ({ setStats((prev) => ({
...prev, ...prev,
docHandle, docHandle,
@@ -80,7 +79,6 @@ export const WasmInspector: React.FC<WasmInspectorProps> = ({
loadTimeMs: loadTime, loadTimeMs: loadTime,
})); }));
// Render Page
setStatus('Rendering page client-side...'); setStatus('Rendering page client-side...');
addLog(`Calling renderPage(handle: ${docHandle}, pageIndex: ${currentPage}, scale: 1.0) ...`); addLog(`Calling renderPage(handle: ${docHandle}, pageIndex: ${currentPage}, scale: 1.0) ...`);
@@ -92,7 +90,6 @@ export const WasmInspector: React.FC<WasmInspectorProps> = ({
addLog(`Page rendered to raw pixel RGBA buffer in ${renderTime.toFixed(1)}ms`); addLog(`Page rendered to raw pixel RGBA buffer in ${renderTime.toFixed(1)}ms`);
// Draw onto Canvas
if (canvasRef.current) { if (canvasRef.current) {
const canvas = canvasRef.current; const canvas = canvasRef.current;
canvas.width = imageData.width; canvas.width = imageData.width;
@@ -104,7 +101,6 @@ export const WasmInspector: React.FC<WasmInspectorProps> = ({
} }
} }
// Get Text JSON
setStatus('Extracting page text...'); setStatus('Extracting page text...');
addLog(`Calling getTextJson(handle: ${docHandle}, pageIndex: ${currentPage}) ...`); addLog(`Calling getTextJson(handle: ${docHandle}, pageIndex: ${currentPage}) ...`);
@@ -115,7 +111,6 @@ export const WasmInspector: React.FC<WasmInspectorProps> = ({
addLog(`Text query complete in ${textTime.toFixed(1)}ms`); addLog(`Text query complete in ${textTime.toFixed(1)}ms`);
// Pretty print JSON
try { try {
const parsed = JSON.parse(textData); const parsed = JSON.parse(textData);
setTextJson(JSON.stringify(parsed, null, 2)); setTextJson(JSON.stringify(parsed, null, 2));
@@ -154,7 +149,6 @@ export const WasmInspector: React.FC<WasmInspectorProps> = ({
return ( return (
<div className="flex flex-col w-[400px] border-l border-slate-800 bg-slate-900/90 backdrop-blur-md h-full text-slate-200 overflow-hidden shadow-2xl z-40 animate-in slide-in-from-right duration-300"> <div className="flex flex-col w-[400px] border-l border-slate-800 bg-slate-900/90 backdrop-blur-md h-full text-slate-200 overflow-hidden shadow-2xl z-40 animate-in slide-in-from-right duration-300">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-slate-800 bg-slate-950/40"> <div className="flex items-center justify-between p-4 border-b border-slate-800 bg-slate-950/40">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="w-2.5 h-2.5 rounded-full bg-indigo-500 animate-pulse shadow-[0_0_8px_#6366f1]" /> <span className="w-2.5 h-2.5 rounded-full bg-indigo-500 animate-pulse shadow-[0_0_8px_#6366f1]" />
@@ -171,9 +165,7 @@ export const WasmInspector: React.FC<WasmInspectorProps> = ({
</CustomButton> </CustomButton>
</div> </div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-4 space-y-4"> <div className="flex-1 overflow-y-auto p-4 space-y-4">
{/* Status Card */}
<div className="p-3.5 rounded-xl border border-slate-800 bg-slate-950/20"> <div className="p-3.5 rounded-xl border border-slate-800 bg-slate-950/20">
<div className="text-xs text-slate-400 font-medium">Pipeline Status</div> <div className="text-xs text-slate-400 font-medium">Pipeline Status</div>
<div className="text-sm font-bold text-slate-100 flex items-center gap-2 mt-1"> <div className="text-sm font-bold text-slate-100 flex items-center gap-2 mt-1">
@@ -182,7 +174,6 @@ export const WasmInspector: React.FC<WasmInspectorProps> = ({
</div> </div>
</div> </div>
{/* Metrics Grid */}
<div className="grid grid-cols-2 gap-2.5"> <div className="grid grid-cols-2 gap-2.5">
<div className="p-3 rounded-lg border border-slate-800 bg-slate-950/10"> <div className="p-3 rounded-lg border border-slate-800 bg-slate-950/10">
<div className="text-[10px] text-slate-400 font-semibold uppercase tracking-wider">Doc Handle ID</div> <div className="text-[10px] text-slate-400 font-semibold uppercase tracking-wider">Doc Handle ID</div>
@@ -208,7 +199,6 @@ export const WasmInspector: React.FC<WasmInspectorProps> = ({
</div> </div>
</div> </div>
{/* Client-side Canvas Preview */}
<div className="space-y-1.5"> <div className="space-y-1.5">
<div className="text-[11px] font-bold tracking-wider uppercase text-slate-400">Client Canvas Output</div> <div className="text-[11px] font-bold tracking-wider uppercase text-slate-400">Client Canvas Output</div>
<div className="border border-slate-800 rounded-xl bg-slate-950 flex items-center justify-center p-4 overflow-hidden relative min-h-[160px]"> <div className="border border-slate-800 rounded-xl bg-slate-950 flex items-center justify-center p-4 overflow-hidden relative min-h-[160px]">
@@ -219,7 +209,6 @@ export const WasmInspector: React.FC<WasmInspectorProps> = ({
</div> </div>
</div> </div>
{/* Text Bounds JSON Output */}
<div className="flex-1 flex flex-col space-y-1.5 min-h-[220px]"> <div className="flex-1 flex flex-col space-y-1.5 min-h-[220px]">
<div className="text-[11px] font-bold tracking-wider uppercase text-slate-400">extractTextWithBounds() JSON</div> <div className="text-[11px] font-bold tracking-wider uppercase text-slate-400">extractTextWithBounds() JSON</div>
<div className="flex-1 min-h-[150px] max-h-[300px] border border-slate-800 rounded-xl bg-slate-950 p-3 overflow-y-auto font-mono text-xs text-indigo-300"> <div className="flex-1 min-h-[150px] max-h-[300px] border border-slate-800 rounded-xl bg-slate-950 p-3 overflow-y-auto font-mono text-xs text-indigo-300">
@@ -231,7 +220,6 @@ export const WasmInspector: React.FC<WasmInspectorProps> = ({
</div> </div>
</div> </div>
{/* Console Logs */}
<div className="space-y-1.5"> <div className="space-y-1.5">
<div className="text-[11px] font-bold tracking-wider uppercase text-slate-400">Execution Console Logs</div> <div className="text-[11px] font-bold tracking-wider uppercase text-slate-400">Execution Console Logs</div>
<div className="border border-slate-800 rounded-xl bg-slate-950 p-3 max-h-[200px] overflow-y-auto font-mono text-[10px] space-y-1 text-slate-400"> <div className="border border-slate-800 rounded-xl bg-slate-950 p-3 max-h-[200px] overflow-y-auto font-mono text-[10px] space-y-1 text-slate-400">
@@ -244,7 +232,6 @@ export const WasmInspector: React.FC<WasmInspectorProps> = ({
</div> </div>
</div> </div>
{/* Footer / Engine Tag */}
<div className="p-3 border-t border-slate-800 bg-slate-950/60 text-[10px] text-center text-slate-500 font-bold uppercase tracking-widest"> <div className="p-3 border-t border-slate-800 bg-slate-950/60 text-[10px] text-center text-slate-500 font-bold uppercase tracking-widest">
{engineInfo || 'WASM Engine Offline'} {engineInfo || 'WASM Engine Offline'}
</div> </div>
@@ -25,13 +25,11 @@ export const CustomConfirmationModal: React.FC<CustomConfirmationModalProps> = (
return ( return (
<div className="fixed inset-0 z-[200] flex items-center justify-center p-4" onMouseDown={onClose}> <div className="fixed inset-0 z-[200] flex items-center justify-center p-4" onMouseDown={onClose}>
{/* Backdrop */}
<div <div
className="absolute inset-0 bg-[#0f172a]/30 backdrop-blur-[2px]" className="absolute inset-0 bg-[#0f172a]/30 backdrop-blur-[2px]"
style={{ animation: 'toastIn 0.2s ease-out' }} style={{ animation: 'toastIn 0.2s ease-out' }}
/> />
{/* Modal Content */}
<div <div
style={{ width: 440, animation: 'slideUp 0.25s cubic-bezier(0.16, 1, 0.3, 1)' }} style={{ width: 440, animation: 'slideUp 0.25s cubic-bezier(0.16, 1, 0.3, 1)' }}
className="relative flex max-h-[90vh] flex-col overflow-hidden rounded-[16px] bg-[#ffffff] shadow-[0_24px_48px_rgba(16,24,40,0.18)] ring-1 ring-[#ebedf0]" className="relative flex max-h-[90vh] flex-col overflow-hidden rounded-[16px] bg-[#ffffff] shadow-[0_24px_48px_rgba(16,24,40,0.18)] ring-1 ring-[#ebedf0]"
-3
View File
@@ -33,7 +33,6 @@ type IC = React.FC<IconProps>;
const mk = (children: React.ReactNode, defStroke = 1.6): IC => const mk = (children: React.ReactNode, defStroke = 1.6): IC =>
({ size = 18, className, strokeWidth }) => base(size, className, strokeWidth ?? defStroke, children); ({ size = 18, className, strokeWidth }) => base(size, className, strokeWidth ?? defStroke, children);
// Tools
export const SelectIcon: IC = mk(<path d="M5 3l14 7-6 1.5L10 18 5 3z" />); export const SelectIcon: IC = mk(<path d="M5 3l14 7-6 1.5L10 18 5 3z" />);
export const PanIcon: IC = mk(<path d="M7 11.5V6a1.5 1.5 0 013 0m0 0v-.5a1.5 1.5 0 013 0V8m0-1a1.5 1.5 0 013 0v4.5a6.5 6.5 0 01-13 0V10a1.5 1.5 0 013 0" />); export const PanIcon: IC = mk(<path d="M7 11.5V6a1.5 1.5 0 013 0m0 0v-.5a1.5 1.5 0 013 0V8m0-1a1.5 1.5 0 013 0v4.5a6.5 6.5 0 01-13 0V10a1.5 1.5 0 013 0" />);
export const HighlightIcon: IC = mk(<><path d="M4 20h4l9.5-9.5-3.5-3.5L4.5 16.5 4 20z" /><path d="M14 6.5l3.5 3.5" /></>); export const HighlightIcon: IC = mk(<><path d="M4 20h4l9.5-9.5-3.5-3.5L4.5 16.5 4 20z" /><path d="M14 6.5l3.5 3.5" /></>);
@@ -48,7 +47,6 @@ export const StampIcon: IC = mk(<><path d="M9 12a3 3 0 113 0c-.7.6-1 1.3-1 2v1h-
export const RedactIcon: IC = mk(<rect x="4" y="4" width="16" height="16" rx="1.5" fill="currentColor" stroke="none" />); export const RedactIcon: IC = mk(<rect x="4" y="4" width="16" height="16" rx="1.5" fill="currentColor" stroke="none" />);
export const ImageIcon: IC = mk(<><rect x="3" y="4" width="18" height="16" rx="2" /><circle cx="8.5" cy="9.5" r="1.5" /><path d="M21 17l-5-5L5 21" /></>); export const ImageIcon: IC = mk(<><rect x="3" y="4" width="18" height="16" rx="2" /><circle cx="8.5" cy="9.5" r="1.5" /><path d="M21 17l-5-5L5 21" /></>);
// Top bar
export const UndoIcon: IC = mk(<path d="M9 14L4 9l5-5M4 9h11a5 5 0 010 10h-3" />); export const UndoIcon: IC = mk(<path d="M9 14L4 9l5-5M4 9h11a5 5 0 010 10h-3" />);
export const RedoIcon: IC = mk(<path d="M15 14l5-5-5-5M20 9H9a5 5 0 000 10h3" />); export const RedoIcon: IC = mk(<path d="M15 14l5-5-5-5M20 9H9a5 5 0 000 10h3" />);
export const ZoomInIcon: IC = mk(<><circle cx="11" cy="11" r="7" /><path d="M11 8v6M8 11h6M20 20l-3.5-3.5" /></>); export const ZoomInIcon: IC = mk(<><circle cx="11" cy="11" r="7" /><path d="M11 8v6M8 11h6M20 20l-3.5-3.5" /></>);
@@ -63,7 +61,6 @@ export const XIcon: IC = mk(<path d="M6 6l12 12M18 6L6 18" />);
export const ShareIcon: IC = mk(<><circle cx="18" cy="5" r="3" /><circle cx="6" cy="12" r="3" /><circle cx="18" cy="19" r="3" /><path d="M8.6 13.5l6.8 4M15.4 6.5l-6.8 4" /></>); export const ShareIcon: IC = mk(<><circle cx="18" cy="5" r="3" /><circle cx="6" cy="12" r="3" /><circle cx="18" cy="19" r="3" /><path d="M8.6 13.5l6.8 4M15.4 6.5l-6.8 4" /></>);
export const FitIcon: IC = mk(<path d="M8 3H5a2 2 0 00-2 2v3m0 8v3a2 2 0 002 2h3m8 0h3a2 2 0 002-2v-3m0-8V5a2 2 0 00-2-2h-3" />); export const FitIcon: IC = mk(<path d="M8 3H5a2 2 0 00-2 2v3m0 8v3a2 2 0 002 2h3m8 0h3a2 2 0 002-2v-3m0-8V5a2 2 0 00-2-2h-3" />);
// Inspector
export const PagesIcon: IC = mk(<><rect x="4" y="4" width="6" height="6" rx="1" /><rect x="14" y="4" width="6" height="6" rx="1" /><rect x="4" y="14" width="6" height="6" rx="1" /><rect x="14" y="14" width="6" height="6" rx="1" /></>); export const PagesIcon: IC = mk(<><rect x="4" y="4" width="6" height="6" rx="1" /><rect x="14" y="4" width="6" height="6" rx="1" /><rect x="4" y="14" width="6" height="6" rx="1" /><rect x="14" y="14" width="6" height="6" rx="1" /></>);
export const NotesIcon: IC = mk(<><path d="M5 4h14a1 1 0 011 1v11a1 1 0 01-1 1H9l-4 4V5a1 1 0 011-1z" /><path d="M8 9h8M8 12h5" /></>); export const NotesIcon: IC = mk(<><path d="M5 4h14a1 1 0 011 1v11a1 1 0 01-1 1H9l-4 4V5a1 1 0 011-1z" /><path d="M8 9h8M8 12h5" /></>);
export const PropertiesIcon: IC = mk(<><circle cx="12" cy="12" r="9" /><path d="M12 11v5M12 8h.01" /></>); export const PropertiesIcon: IC = mk(<><circle cx="12" cy="12" r="9" /><path d="M12 11v5M12 8h.01" /></>);
+3 -3
View File
@@ -11,9 +11,9 @@ export interface Rect {
} }
export interface MappingContext { export interface MappingContext {
zoom: number; // e.g. 1.0 (100%), 1.5 (150%) zoom: number;
dpr: number; // Device Pixel Ratio (window.devicePixelRatio) dpr: number;
rotation: number; // e.g. 0, 90, 180, 270 degrees rotation: number;
scrollX: number; scrollX: number;
scrollY: number; scrollY: number;
} }
+1 -15
View File
@@ -1,23 +1,12 @@
import { gatewayService } from './gatewayService'; import { gatewayService } from './gatewayService';
// Loads a PDF run's real (embedded or PDFium-substitute) font into the browser via the
// FontFace API so the in-place text editor can render in the document's actual font.
// Falls back to null (→ caller uses a CSS font) when the font isn't browser-loadable.
//
// - One fetch + one document.fonts.add per (documentId, internalFontId), deduped.
// - releaseDocumentFonts(docId) unregisters a document's faces on document change
// (NOT on page change — the same fonts recur across pages).
// key -> promise resolving to the registered CSS family name, or null (use fallback).
const fontPromises = new Map<string, Promise<string | null>>(); const fontPromises = new Map<string, Promise<string | null>>();
// Registered FontFace objects, for cleanup on document change.
const fontFaces = new Map<string, FontFace>(); const fontFaces = new Map<string, FontFace>();
function keyOf(documentId: string, internalFontId: string): string { function keyOf(documentId: string, internalFontId: string): string {
return `${documentId}::${internalFontId}`; return `${documentId}::${internalFontId}`;
} }
// Small stable FNV-1a hash → CSS-safe family token (internalFontId has +/_/spaces).
function familyName(key: string): string { function familyName(key: string): string {
let h = 2166136261; let h = 2166136261;
for (let i = 0; i < key.length; i++) { for (let i = 0; i < key.length; i++) {
@@ -27,7 +16,6 @@ function familyName(key: string): string {
return `pdf-${(h >>> 0).toString(16)}`; return `pdf-${(h >>> 0).toString(16)}`;
} }
// Resolves to the CSS family name to use, or null if no real font is available.
export function loadPdfFont(documentId: string, internalFontId: string): Promise<string | null> { export function loadPdfFont(documentId: string, internalFontId: string): Promise<string | null> {
if (!internalFontId) return Promise.resolve(null); if (!internalFontId) return Promise.resolve(null);
const key = keyOf(documentId, internalFontId); const key = keyOf(documentId, internalFontId);
@@ -45,7 +33,7 @@ export function loadPdfFont(documentId: string, internalFontId: string): Promise
fontFaces.set(key, face); fontFaces.set(key, face);
return family; return family;
} catch { } catch {
return null; // rejected load (e.g. unsupported format) — keep it out of the registry return null;
} }
})(); })();
@@ -53,7 +41,6 @@ export function loadPdfFont(documentId: string, internalFontId: string): Promise
return p; return p;
} }
// Unregister all FontFaces loaded for a document (call when the active document changes).
export function releaseDocumentFonts(documentId: string): void { export function releaseDocumentFonts(documentId: string): void {
const prefix = `${documentId}::`; const prefix = `${documentId}::`;
for (const [key, face] of Array.from(fontFaces.entries())) { for (const [key, face] of Array.from(fontFaces.entries())) {
@@ -61,7 +48,6 @@ export function releaseDocumentFonts(documentId: string): void {
try { try {
document.fonts.delete(face); document.fonts.delete(face);
} catch { } catch {
/* ignore */
} }
fontFaces.delete(key); fontFaces.delete(key);
} }
+8 -38
View File
@@ -1,23 +1,12 @@
// Adobe-grade text-selection model.
//
// Operates purely in **page-point space** (unzoomed, top-left origin) — the same
// space the engine emits glyph bounds in. Callers convert mouse coordinates into
// this space (divide by zoom) before querying, and scale the returned rects back
// up (multiply by zoom) for rendering.
//
// A *caret* is a position between glyphs, indexed 0..N (N = glyph count). A
// selection is an ordered pair of carets {start, end}; start may be greater than
// end for a backward (right-to-left) drag. All range helpers normalise
// internally, so the component is free to keep anchor/focus unnormalised.
import type { Glyph } from './gatewayService'; import type { Glyph } from './gatewayService';
export interface OrderedGlyph extends Glyph { export interface OrderedGlyph extends Glyph {
index: number; // position in reading order index: number;
line: number; // line band this glyph belongs to line: number;
right: number; // x + w (cached) right: number;
bottom: number; // y + h (cached) bottom: number;
mid: number; // x + w/2 (cached) mid: number;
} }
export interface CaretRange { export interface CaretRange {
@@ -36,8 +25,8 @@ interface LineBand {
top: number; top: number;
bottom: number; bottom: number;
mid: number; mid: number;
start: number; // first glyph index (inclusive) start: number;
end: number; // last glyph index + 1 (exclusive) end: number;
} }
const isSpace = (t: string) => t.length === 0 || /^\s+$/.test(t); const isSpace = (t: string) => t.length === 0 || /^\s+$/.test(t);
@@ -50,19 +39,14 @@ export class TextSelectionModel {
const clean = raw.filter((g) => g.w >= 0 && g.h > 0); const clean = raw.filter((g) => g.w >= 0 && g.h > 0);
if (clean.length === 0) return; if (clean.length === 0) return;
// Cluster glyphs into lines by vertical overlap. Sort by vertical centre so
// rows arrive top-to-bottom even when the source order is arbitrary.
const byY = [...clean].sort((a, b) => a.y + a.h / 2 - (b.y + b.h / 2)); const byY = [...clean].sort((a, b) => a.y + a.h / 2 - (b.y + b.h / 2));
const rows: Glyph[][] = []; const rows: Glyph[][] = [];
for (const g of byY) { for (const g of byY) {
const gMid = g.y + g.h / 2; const gMid = g.y + g.h / 2;
const row = rows[rows.length - 1]; const row = rows[rows.length - 1];
if (row) { if (row) {
// Band of the current row so far.
const top = Math.min(...row.map((r) => r.y)); const top = Math.min(...row.map((r) => r.y));
const bottom = Math.max(...row.map((r) => r.y + r.h)); const bottom = Math.max(...row.map((r) => r.y + r.h));
// Same line if the glyph centre falls inside the row band (with a small
// tolerance for sub/superscript jitter).
const tol = g.h * 0.25; const tol = g.h * 0.25;
if (gMid >= top - tol && gMid <= bottom + tol) { if (gMid >= top - tol && gMid <= bottom + tol) {
row.push(g); row.push(g);
@@ -72,8 +56,6 @@ export class TextSelectionModel {
rows.push([g]); rows.push([g]);
} }
// Flatten rows (each sorted left→right) into a single reading-order array and
// record the line bands for spatial queries.
let idx = 0; let idx = 0;
rows.forEach((row, lineNo) => { rows.forEach((row, lineNo) => {
row.sort((a, b) => a.x - b.x); row.sort((a, b) => a.x - b.x);
@@ -103,13 +85,11 @@ export class TextSelectionModel {
return this.glyphs.length; return this.glyphs.length;
} }
/** Index of the line whose band contains y, else the nearest line by centre. */
private lineAt(y: number): number { private lineAt(y: number): number {
if (this.lines.length === 0) return -1; if (this.lines.length === 0) return -1;
for (let i = 0; i < this.lines.length; i++) { for (let i = 0; i < this.lines.length; i++) {
if (y >= this.lines[i].top && y <= this.lines[i].bottom) return i; if (y >= this.lines[i].top && y <= this.lines[i].bottom) return i;
} }
// Above the first / below the last / in an inter-line gap → nearest centre.
let best = 0; let best = 0;
let bestDist = Infinity; let bestDist = Infinity;
for (let i = 0; i < this.lines.length; i++) { for (let i = 0; i < this.lines.length; i++) {
@@ -122,7 +102,6 @@ export class TextSelectionModel {
return best; return best;
} }
/** Nearest caret (0..N) to a point. */
caretAt(x: number, y: number): number { caretAt(x: number, y: number): number {
const li = this.lineAt(y); const li = this.lineAt(y);
if (li < 0) return 0; if (li < 0) return 0;
@@ -132,13 +111,11 @@ export class TextSelectionModel {
for (let i = line.start; i < line.end; i++) { for (let i = line.start; i < line.end; i++) {
const g = this.glyphs[i]; const g = this.glyphs[i];
if (x >= g.x && x <= g.right) return x < g.mid ? g.index : g.index + 1; if (x >= g.x && x <= g.right) return x < g.mid ? g.index : g.index + 1;
// In the gap before this glyph.
if (x < g.x) return g.index; if (x < g.x) return g.index;
} }
return line.end; return line.end;
} }
/** Glyph index directly under a point, or -1. */
glyphAt(x: number, y: number): number { glyphAt(x: number, y: number): number {
const li = this.lineAt(y); const li = this.lineAt(y);
if (li < 0) return -1; if (li < 0) return -1;
@@ -151,11 +128,9 @@ export class TextSelectionModel {
return -1; return -1;
} }
/** Word caret-range around a point (run of non-space within one line). */
wordRangeAt(x: number, y: number): CaretRange | null { wordRangeAt(x: number, y: number): CaretRange | null {
let gi = this.glyphAt(x, y); let gi = this.glyphAt(x, y);
if (gi < 0) { if (gi < 0) {
// Snap to the caret, then take the glyph just right of it (or left at EOL).
const c = this.caretAt(x, y); const c = this.caretAt(x, y);
gi = c < this.length ? c : c - 1; gi = c < this.length ? c : c - 1;
if (gi < 0 || gi >= this.length) return null; if (gi < 0 || gi >= this.length) return null;
@@ -169,7 +144,6 @@ export class TextSelectionModel {
return { start: s, end: e + 1 }; return { start: s, end: e + 1 };
} }
/** Whole-line caret-range around a point. */
lineRangeAt(_x: number, y: number): CaretRange | null { lineRangeAt(_x: number, y: number): CaretRange | null {
const li = this.lineAt(y); const li = this.lineAt(y);
if (li < 0) return null; if (li < 0) return null;
@@ -189,7 +163,6 @@ export class TextSelectionModel {
return this.glyphs.slice(start, end); return this.glyphs.slice(start, end);
} }
/** Reconstructed text with intra-line spaces and inter-line newlines. */
textOfRange(r: CaretRange): string { textOfRange(r: CaretRange): string {
const sel = this.glyphsInRange(r); const sel = this.glyphsInRange(r);
if (sel.length === 0) return ''; if (sel.length === 0) return '';
@@ -210,7 +183,6 @@ export class TextSelectionModel {
return out.replace(/[ \t]+\n/g, '\n').trimEnd(); return out.replace(/[ \t]+\n/g, '\n').trimEnd();
} }
/** Per-line union rects covering the selection (page-point space). */
rectsOfRange(r: CaretRange): SelRect[] { rectsOfRange(r: CaretRange): SelRect[] {
const sel = this.glyphsInRange(r); const sel = this.glyphsInRange(r);
if (sel.length === 0) return []; if (sel.length === 0) return [];
@@ -224,14 +196,12 @@ export class TextSelectionModel {
for (const arr of byLine.values()) { for (const arr of byLine.values()) {
const x = Math.min(...arr.map((g) => g.x)); const x = Math.min(...arr.map((g) => g.x));
const right = Math.max(...arr.map((g) => g.right)); const right = Math.max(...arr.map((g) => g.right));
// Use the full line band height for a continuous highlight, not glyph height.
const band = this.lines[arr[0].line]; const band = this.lines[arr[0].line];
rects.push({ x, y: band.top, w: right - x, h: band.bottom - band.top }); rects.push({ x, y: band.top, w: right - x, h: band.bottom - band.top });
} }
return rects; return rects;
} }
/** Tight union of all selection rects (page-point space) — for highlight bbox. */
unionRect(r: CaretRange): SelRect | null { unionRect(r: CaretRange): SelRect | null {
const rects = this.rectsOfRange(r); const rects = this.rectsOfRange(r);
if (rects.length === 0) return null; if (rects.length === 0) return null;
@@ -241,4 +211,4 @@ export class TextSelectionModel {
const bottom = Math.max(...rects.map((q) => q.y + q.h)); const bottom = Math.max(...rects.map((q) => q.y + q.h));
return { x, y, w: right - x, h: bottom - y }; return { x, y, w: right - x, h: bottom - y };
} }
} }

Some files were not shown because too many files have changed in this diff Show More