feat: implemented multi page reflow

This commit is contained in:
Furqan-14
2026-06-18 16:48:29 +05:30
parent ff0962b111
commit a124a43877
23 changed files with 1559 additions and 890 deletions
+35 -46
View File
@@ -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
+3 -30
View File
@@ -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);