feat: implemented multi page reflow
This commit is contained in:
@@ -28,23 +28,20 @@ struct DocumentMetadata {
|
||||
std::string modificationDate;
|
||||
};
|
||||
|
||||
// Encryption + permission state of a loaded document. Booleans are the EFFECTIVE
|
||||
// permissions for how the doc was opened (an owner-unlocked doc reports all true).
|
||||
// The engine only surfaces these — enforcement happens in the gateway/app layers.
|
||||
struct DocumentPermissions {
|
||||
bool isEncrypted = false;
|
||||
std::string encryption = "None"; // "None", "RC4-40", "RC4-128", "AES-128", "AES-256"
|
||||
int securityRevision = -1; // FPDF_GetSecurityHandlerRevision (-1 if unencrypted)
|
||||
bool ownerUnlocked = false; // encrypted but opened with full (owner) access
|
||||
std::string encryption = "None";
|
||||
int securityRevision = -1;
|
||||
bool ownerUnlocked = false;
|
||||
|
||||
bool canPrint = true;
|
||||
bool canPrintHighRes = true;
|
||||
bool canModify = true;
|
||||
bool canCopy = true; // extract text / graphics
|
||||
bool canAnnotate = true; // add/modify annotations
|
||||
bool canCopy = true;
|
||||
bool canAnnotate = true;
|
||||
bool canFillForms = true;
|
||||
bool canExtractForAccessibility = true;
|
||||
bool canAssemble = true; // insert / rotate / delete pages
|
||||
bool canAssemble = true;
|
||||
};
|
||||
|
||||
struct PageImage {
|
||||
@@ -72,45 +69,40 @@ struct GlyphBounds {
|
||||
double fontSize;
|
||||
};
|
||||
|
||||
// Result of a point hit-test against a page's glyphs (page-point space, top-left
|
||||
// origin — the same space GlyphBounds use).
|
||||
struct HitResult {
|
||||
int glyphIndex = -1; // glyph directly under the point in reading order, -1 if none
|
||||
int caret = 0; // nearest caret position (0..N) for selection anchoring
|
||||
int line = -1; // line band the point resolved to, -1 if the page has no text
|
||||
int glyphIndex = -1;
|
||||
int caret = 0;
|
||||
int line = -1;
|
||||
};
|
||||
|
||||
// A resolved text selection: a half-open glyph range plus its reconstructed text
|
||||
// and per-line union rectangles (for drawing the selection).
|
||||
struct TextSelection {
|
||||
int startGlyph = 0; // inclusive, reading order
|
||||
int endGlyph = 0; // exclusive
|
||||
int startGlyph = 0;
|
||||
int endGlyph = 0;
|
||||
std::string text;
|
||||
std::vector<GlyphBounds> rects; // per-line union rects (text field left empty)
|
||||
std::vector<GlyphBounds> rects;
|
||||
};
|
||||
|
||||
struct FontInfo {
|
||||
std::string fontName;
|
||||
std::string type; // "TrueType", "Type1", "CIDFontType0", "CIDFontType2"
|
||||
std::string type;
|
||||
bool isEmbedded = false;
|
||||
bool isSubset = false;
|
||||
bool isVertical = false;
|
||||
|
||||
// Advanced Introspection & Diagnostics
|
||||
std::string encoding; // "WinAnsiEncoding", "MacRomanEncoding", "Identity-H", "Identity-V", "Symbol", "Custom", "None"
|
||||
bool hasToUnicode = false; // True if font has an active /ToUnicode map
|
||||
std::string cmapName; // e.g. "Identity-H", "Identity-V", "UniJIS-UTF16-H"
|
||||
std::string cidSystemInfo; // e.g. "Adobe-Japan1", "Adobe-GB1", "Adobe-Korea1"
|
||||
std::string subsetTag; // e.g. "ABCDEE" (6-character uppercase tag)
|
||||
std::string sourceType; // "Embedded", "SystemFallback", "Substituted"
|
||||
std::string substitutedFrom; // e.g. "Helvetica" (Original requested font)
|
||||
std::string substitutedTo; // e.g. "Liberation Sans" (Actual fallback font used)
|
||||
std::string normalizedFamily; // e.g. "Arial" (Normalized family grouping name)
|
||||
std::string internalFontId; // Unique stable identifier for internal tracking
|
||||
uint32_t flags = 0; // PDF font descriptor flags
|
||||
double ascent = 0.0; // Font descriptor Ascent metric
|
||||
double descent = 0.0; // Font descriptor Descent metric
|
||||
double capHeight = 0.0; // Font descriptor CapHeight metric
|
||||
std::string encoding;
|
||||
bool hasToUnicode = false;
|
||||
std::string cmapName;
|
||||
std::string cidSystemInfo;
|
||||
std::string subsetTag;
|
||||
std::string sourceType;
|
||||
std::string substitutedFrom;
|
||||
std::string substitutedTo;
|
||||
std::string normalizedFamily;
|
||||
std::string internalFontId;
|
||||
uint32_t flags = 0;
|
||||
double ascent = 0.0;
|
||||
double descent = 0.0;
|
||||
double capHeight = 0.0;
|
||||
};
|
||||
|
||||
struct Glyph {
|
||||
@@ -139,7 +131,8 @@ struct TextRun {
|
||||
std::vector<Glyph> glyphs;
|
||||
std::vector<int> objectIndices;
|
||||
double x = 0.0, y = 0.0, w = 0.0, h = 0.0;
|
||||
std::string fillColor = "#000000"; // hex of the run's fill (or stroke) color, for in-place editing
|
||||
std::string fillColor = "#000000";
|
||||
std::string paraId;
|
||||
};
|
||||
|
||||
struct TextLine {
|
||||
@@ -183,10 +176,8 @@ public:
|
||||
|
||||
[[nodiscard]] std::expected<std::vector<GlyphBounds>, EngineError> orderedGlyphs() const;
|
||||
|
||||
// Nearest glyph/caret to a point.
|
||||
[[nodiscard]] std::expected<HitResult, EngineError> hitGlyph(double x, double y) const;
|
||||
|
||||
// Reading-order selection between two points (e.g. drag anchor → focus).
|
||||
[[nodiscard]] std::expected<TextSelection, EngineError>
|
||||
selectRange(double ax, double ay, double bx, double by) const;
|
||||
|
||||
@@ -197,7 +188,7 @@ public:
|
||||
|
||||
struct AnnotationInfo {
|
||||
std::string id;
|
||||
std::string type; // "highlight", "comment", "ink", "strikeout", "signature", "widget"
|
||||
std::string type;
|
||||
double x = 0.0, y = 0.0, width = 0.0, height = 0.0;
|
||||
std::string color;
|
||||
std::string author;
|
||||
@@ -205,8 +196,7 @@ public:
|
||||
std::string timestamp;
|
||||
int pageIndex = 0;
|
||||
std::vector<std::vector<Point2D>> paths;
|
||||
|
||||
// Form field specific properties
|
||||
|
||||
std::string fieldName;
|
||||
std::string fieldValue;
|
||||
std::string fieldType;
|
||||
@@ -237,11 +227,10 @@ public:
|
||||
[[nodiscard]] virtual DocumentMetadata metadata() const noexcept = 0;
|
||||
[[nodiscard]] virtual DocumentPermissions permissions() const noexcept = 0;
|
||||
|
||||
// A single entry in the document outline (bookmarks), flattened with a depth level.
|
||||
struct OutlineItem {
|
||||
std::string title;
|
||||
int pageIndex = -1; // -1 if the destination page can't be resolved
|
||||
int level = 0; // 0 = top-level
|
||||
int pageIndex = -1;
|
||||
int level = 0;
|
||||
};
|
||||
[[nodiscard]] virtual std::expected<std::vector<OutlineItem>, EngineError> extractOutline() const = 0;
|
||||
|
||||
@@ -259,10 +248,10 @@ public:
|
||||
|
||||
virtual std::expected<void, EngineError> applyEdits(const std::string& editsJson) = 0;
|
||||
|
||||
// Per-character caret layout (JSON, PDF coords) of the most recent reflow_paragraph op, or
|
||||
// empty if none. Lets the WASM live preview align a caret exactly to the rendered glyphs.
|
||||
[[nodiscard]] virtual std::string lastReflowLayout() const { return {}; }
|
||||
|
||||
[[nodiscard]] virtual bool lastReflowOverflowed() const { return false; }
|
||||
|
||||
[[nodiscard]] virtual std::expected<std::vector<uint8_t>, EngineError>
|
||||
saveIncremental() const = 0;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,7 @@ namespace pdfengine::fonts { class FontFace; }
|
||||
|
||||
namespace pdfengine::parser {
|
||||
|
||||
class PdfiumDocument; // a page keeps its owning document alive (see ownerDoc_)
|
||||
class PdfiumDocument;
|
||||
|
||||
#ifdef PDFENGINE_WITH_PDFIUM
|
||||
using NativeDocHandle = FPDF_DOCUMENT;
|
||||
@@ -64,9 +64,6 @@ private:
|
||||
mutable NativeTextHandle textPage_ = nullptr;
|
||||
int pageIndex_ = 0;
|
||||
mutable std::mutex textMutex_;
|
||||
// Keeps the owning document (and thus the native FPDF_DOCUMENT) alive for as
|
||||
// long as this page exists, so page_/textPage_ can never dangle. Declared
|
||||
// here (destroyed last) so it outlives the handle teardown in the destructor.
|
||||
std::shared_ptr<PdfiumDocument> ownerDoc_;
|
||||
#ifdef PDFENGINE_WITH_PDFIUM
|
||||
mutable std::unordered_map<std::string, FPDF_FONT> fontHandleCache_;
|
||||
@@ -102,13 +99,12 @@ public:
|
||||
std::expected<std::vector<uint8_t>, EngineError> saveIncremental() const override;
|
||||
std::expected<std::vector<uint8_t>, EngineError> saveFull() const override;
|
||||
|
||||
// Per-character layout (PDF coords) of the LAST reflow_paragraph op, as JSON. The WASM
|
||||
// preview uses it to align a custom caret exactly to the rendered glyphs. Empty if the
|
||||
// most recent applyEdits contained no reflow_paragraph operation.
|
||||
std::string lastReflowLayout() const { return lastReflowLayout_; }
|
||||
bool lastReflowOverflowed() const { return lastReflowOverflowed_; }
|
||||
|
||||
private:
|
||||
mutable std::string lastReflowLayout_;
|
||||
mutable bool lastReflowOverflowed_ = false;
|
||||
NativeDocHandle doc_ = nullptr;
|
||||
std::vector<uint8_t> memoryBuffer_;
|
||||
|
||||
@@ -116,53 +112,31 @@ private:
|
||||
mutable bool hasCachedFonts_ = false;
|
||||
mutable std::mutex fontsMutex_;
|
||||
|
||||
// Cache to prevent O(N*M) full-document scans for font data extraction
|
||||
mutable std::unordered_map<std::string, std::vector<uint8_t>> fontDataCache_;
|
||||
mutable int fontDataScannedPages_ = 0;
|
||||
|
||||
// Resolve an embedded font program from the SPECIFIC page objects of the paragraph being
|
||||
// edited (its objectIndices), matching each object's /BaseFont to internalFontId. Used by
|
||||
// reflow so a re-subset re-uses the exact font its source text referenced — no document-wide
|
||||
// name collision when several same-named subsets accumulate across sequential edits.
|
||||
// Returns nullopt if no object in the set uses that font.
|
||||
std::optional<std::vector<uint8_t>>
|
||||
getFontDataFromObjects(int pageIndex, const std::vector<int>& objectIndices,
|
||||
const std::string& internalFontId) const;
|
||||
|
||||
// Weak so the document never co-owns its pages: ownership runs page → document
|
||||
// only. A cached entry is reused while a page is still referenced elsewhere,
|
||||
// and lazily rebuilt once it expires.
|
||||
mutable std::unordered_map<int, std::weak_ptr<PdfPage>> pageCache_;
|
||||
mutable std::mutex pageCacheMutex_;
|
||||
|
||||
// Font Engine Bridge
|
||||
std::unique_ptr<pdfengine::fonts::loader::FontResolver> fontResolver_;
|
||||
std::unordered_map<std::string, std::shared_ptr<fonts::pdf_fonts::Font>> resolvedFontsCache_;
|
||||
std::mutex resolvedFontsMutex_;
|
||||
|
||||
// Loaded PDFium Font Cache for Font Reuse
|
||||
#ifdef PDFENGINE_WITH_PDFIUM
|
||||
mutable std::unordered_map<std::string, FPDF_FONT> loadedFontsCache_;
|
||||
mutable std::unordered_map<std::string, std::vector<uint8_t>> loadedFontDataBuffers_;
|
||||
mutable std::unordered_map<std::string, std::shared_ptr<fonts::FontFace>> loadedMeasureFaces_;
|
||||
mutable std::mutex loadedFontsMutex_;
|
||||
|
||||
// Resolve + load a PDFium font for emitting NEW text (mirrors the replace_text tiers:
|
||||
// embedded full/subset reuse, base-14 standard, or system-font subset embed). Returns
|
||||
// the FPDF_FONT (for FPDFPageObj_CreateTextObj) and the resolved font (FontFace for
|
||||
// HarfBuzz width measurement during line-breaking). Either field may be null on failure.
|
||||
struct EmissionFont {
|
||||
FPDF_FONT font = nullptr;
|
||||
std::shared_ptr<fonts::pdf_fonts::Font> resolved;
|
||||
// A FontFace built from the EXACT bytes loaded into PDFium, so width measurement
|
||||
// matches what PDFium renders (the resolver's face can have different advances).
|
||||
std::shared_ptr<fonts::FontFace> measureFace;
|
||||
};
|
||||
// srcObjects = the page objects of the paragraph being reflowed (for per-object font
|
||||
// resolution); pass {} when no source objects are known (falls back to the name scan).
|
||||
// reuseFont = the ORIGINAL embedded FPDF_FONT of the source text (from a still-open page);
|
||||
// when non-null the embedded path reuses it directly instead of re-subsetting + loading a new
|
||||
// same-named font (which PDFium aliases, scrambling multi-edit output). null => subset+load.
|
||||
EmissionFont loadEmissionFont(int pageIndex, const std::string& internalFontId,
|
||||
double fontSize, const std::vector<uint32_t>& codepoints,
|
||||
const std::vector<int>& srcObjects = {},
|
||||
@@ -170,7 +144,6 @@ private:
|
||||
#endif
|
||||
};
|
||||
|
||||
// Exposed for testing
|
||||
std::vector<unsigned short> utf8_to_utf16le(const std::string& utf8);
|
||||
std::string utf16le_to_utf8(const char16_t* utf16, size_t length);
|
||||
std::string code_point_to_utf8(unsigned int cp);
|
||||
|
||||
Reference in New Issue
Block a user