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