fix: font handling in revista
This commit is contained in:
@@ -118,6 +118,7 @@ struct Glyph {
|
||||
double bboxX = 0.0, bboxY = 0.0, bboxW = 0.0, bboxH = 0.0;
|
||||
double angle = 0.0;
|
||||
int pageObjectIndex = -1;
|
||||
int srcIndex = 0;
|
||||
};
|
||||
|
||||
struct TextRun {
|
||||
|
||||
@@ -135,6 +135,21 @@ std::string FontFallback::getFallbackFontPath(const std::string& fontName, bool
|
||||
stylePattern += "-italic";
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
{
|
||||
auto has = [&](const char* s) { return lowerName.find(s) != std::string::npos; };
|
||||
const bool serif = has("times") || has("serif") || has("roman") || has("georgia") ||
|
||||
has("garamond") || has("minion") || has("cambria") || has("tinos") ||
|
||||
has("book antiqua") || has("palatino");
|
||||
const std::string sysBase = serif ? "times" : "arial";
|
||||
const std::string sysSfx = (bold && italic) ? "bi" : bold ? "bd" : italic ? "i" : "";
|
||||
const std::string sysPath = "C:\\Windows\\Fonts\\" + sysBase + sysSfx + ".ttf";
|
||||
if (std::filesystem::exists(sysPath)) {
|
||||
return sysPath;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
std::lock_guard<std::mutex> lock(rules_mutex_);
|
||||
|
||||
for (const auto& rule : custom_rules_) {
|
||||
|
||||
@@ -1040,6 +1040,7 @@ std::expected<PageModel, EngineError> PdfiumPage::extractDocumentModel() const {
|
||||
Glyph g;
|
||||
g.text = std::move(utf8_char);
|
||||
g.unicode = cp;
|
||||
g.srcIndex = i;
|
||||
|
||||
double left, right, bottom, top;
|
||||
FPDFText_GetCharBox(textPage_, i, &left, &right, &bottom, &top);
|
||||
@@ -1112,7 +1113,8 @@ std::expected<PageModel, EngineError> PdfiumPage::extractDocumentModel() const {
|
||||
|
||||
for (auto& line : lines) {
|
||||
std::sort(line.glyphs.begin(), line.glyphs.end(), [](const Glyph& a, const Glyph& b) {
|
||||
return a.originX < b.originX;
|
||||
if (a.originX != b.originX) return a.originX < b.originX;
|
||||
return a.srcIndex < b.srcIndex;
|
||||
});
|
||||
|
||||
std::vector<double> gaps;
|
||||
@@ -1128,6 +1130,17 @@ std::expected<PageModel, EngineError> PdfiumPage::extractDocumentModel() const {
|
||||
p25Gap = gaps[gaps.size() / 4];
|
||||
}
|
||||
|
||||
double lineEm = 0.0;
|
||||
{
|
||||
std::vector<double> hs;
|
||||
for (const auto& gg : line.glyphs) if (gg.bboxH > 0.1) hs.push_back(gg.bboxH);
|
||||
if (!hs.empty()) {
|
||||
std::sort(hs.begin(), hs.end());
|
||||
double capH = hs[(hs.size() * 9) / 10];
|
||||
lineEm = capH / 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
TextRun currentRun;
|
||||
if (!line.glyphs.empty()) {
|
||||
const Glyph* firstG = &line.glyphs[0];
|
||||
@@ -1147,9 +1160,10 @@ std::expected<PageModel, EngineError> PdfiumPage::extractDocumentModel() const {
|
||||
const auto& currG = line.glyphs[i];
|
||||
|
||||
double gap = currG.bboxX - (prevG.bboxX + prevG.bboxW);
|
||||
double spaceThreshold = currG.fontSize * 0.2;
|
||||
double em = (std::max)(static_cast<double>(currG.fontSize), lineEm);
|
||||
double spaceThreshold = em * 0.2;
|
||||
if (p25Gap > spaceThreshold) {
|
||||
spaceThreshold = (std::min)(p25Gap * 1.5, currG.fontSize * 0.38);
|
||||
spaceThreshold = (std::min)(p25Gap * 1.5, em * 0.38);
|
||||
}
|
||||
|
||||
bool addSpace = gap > spaceThreshold && prevG.text != " " && currG.text != " ";
|
||||
@@ -1231,6 +1245,17 @@ std::expected<PageModel, EngineError> PdfiumPage::extractDocumentModel() const {
|
||||
}
|
||||
|
||||
std::vector<Paragraph> paragraphs;
|
||||
auto lineStyleKey = [](const TextLine& ln) -> std::pair<bool,bool> {
|
||||
for (const auto& r : ln.runs) {
|
||||
if (r.text.empty()) continue;
|
||||
std::string n = r.fontName + "|" + r.internalFontId;
|
||||
std::transform(n.begin(), n.end(), n.begin(), [](unsigned char c){ return static_cast<char>(std::tolower(c)); });
|
||||
bool bold = n.find("bold") != std::string::npos;
|
||||
bool sans = n.find("arial") != std::string::npos || n.find("helvetica") != std::string::npos;
|
||||
return {bold, sans};
|
||||
}
|
||||
return {false, false};
|
||||
};
|
||||
if (!lines.empty()) {
|
||||
Paragraph currentPara;
|
||||
currentPara.lines.push_back(std::move(lines[0]));
|
||||
@@ -1242,10 +1267,13 @@ std::expected<PageModel, EngineError> PdfiumPage::extractDocumentModel() const {
|
||||
double prevY = prevLine.glyphs.empty() ? 0 : prevLine.glyphs[0].originY;
|
||||
double currY = currLine.glyphs.empty() ? 0 : currLine.glyphs[0].originY;
|
||||
double fontSize = currLine.runs.empty() ? 12.0 : currLine.runs[0].fontSize;
|
||||
|
||||
double capH = 0.0;
|
||||
for (const auto& g : currLine.glyphs) if (g.bboxH > capH) capH = g.bboxH;
|
||||
if (capH > 0.0) fontSize = (std::max)(fontSize, capH / 0.7);
|
||||
double vGap = std::abs(prevY - currY);
|
||||
|
||||
if (vGap > fontSize * 1.5) {
|
||||
bool styleChanged = lineStyleKey(currLine) != lineStyleKey(prevLine);
|
||||
if (vGap > fontSize * 1.5 || styleChanged) {
|
||||
paragraphs.push_back(std::move(currentPara));
|
||||
currentPara = Paragraph();
|
||||
}
|
||||
@@ -1317,17 +1345,18 @@ std::expected<PageModel, EngineError> PdfiumPage::extractDocumentModel() const {
|
||||
};
|
||||
auto computeRunColor = [&](TextRun& r) {
|
||||
if (r.objectIndices.empty()) return;
|
||||
int minIdx = r.objectIndices[0];
|
||||
for (int idx : r.objectIndices) {
|
||||
if (idx < minIdx) minIdx = idx;
|
||||
}
|
||||
FPDF_PAGEOBJECT obj = FPDFPage_GetObject(page_, minIdx);
|
||||
if (!obj) return;
|
||||
unsigned int cr = 0, cg = 0, cb = 0, ca = 0;
|
||||
bool got = (FPDFPageObj_GetFillColor(obj, &cr, &cg, &cb, &ca) && ca != 0) ||
|
||||
(FPDFPageObj_GetStrokeColor(obj, &cr, &cg, &cb, &ca) && ca != 0);
|
||||
if (got) {
|
||||
r.fillColor = "#" + hex2(cr) + hex2(cg) + hex2(cb);
|
||||
std::vector<int> idxs = r.objectIndices;
|
||||
std::sort(idxs.begin(), idxs.end());
|
||||
for (int idx : idxs) {
|
||||
FPDF_PAGEOBJECT obj = FPDFPage_GetObject(page_, idx);
|
||||
if (!obj || FPDFPageObj_GetType(obj) != FPDF_PAGEOBJ_TEXT) continue;
|
||||
unsigned int cr = 0, cg = 0, cb = 0, ca = 0;
|
||||
bool got = (FPDFPageObj_GetFillColor(obj, &cr, &cg, &cb, &ca) && ca != 0) ||
|
||||
(FPDFPageObj_GetStrokeColor(obj, &cr, &cg, &cb, &ca) && ca != 0);
|
||||
if (got) {
|
||||
r.fillColor = "#" + hex2(cr) + hex2(cg) + hex2(cb);
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2143,7 +2172,7 @@ PdfiumDocument::EmissionFont PdfiumDocument::loadEmissionFont(
|
||||
cacheKey += "#" + std::to_string(h);
|
||||
}
|
||||
|
||||
if (useEmbedded && reuseFont) {
|
||||
if (useEmbedded && reuseFont && !isSubsetFont) {
|
||||
out.font = reuseFont;
|
||||
if (auto perObj = getFontDataFromObjects(pageIndex, srcObjects, internalFontId);
|
||||
perObj.has_value() && !perObj->empty()) {
|
||||
@@ -2173,9 +2202,8 @@ PdfiumDocument::EmissionFont PdfiumDocument::loadEmissionFont(
|
||||
sourceBytes = std::move(fontDataRes.value());
|
||||
}
|
||||
if (!sourceBytes.empty()) {
|
||||
auto subset = fonts::pdf_fonts::FontSubset::buildSubsetByUnicode(sourceBytes, codepoints);
|
||||
std::lock_guard<std::mutex> lock(loadedFontsMutex_);
|
||||
loadedFontDataBuffers_[cacheKey] = !subset.empty() ? std::move(subset) : std::move(sourceBytes);
|
||||
loadedFontDataBuffers_[cacheKey] = std::move(sourceBytes);
|
||||
const auto& bytes = loadedFontDataBuffers_[cacheKey];
|
||||
out.font = FPDFText_LoadFont(doc_, bytes.data(), static_cast<uint32_t>(bytes.size()), FPDF_FONT_TRUETYPE, true);
|
||||
}
|
||||
@@ -2186,9 +2214,8 @@ PdfiumDocument::EmissionFont PdfiumDocument::loadEmissionFont(
|
||||
if (fs) {
|
||||
std::vector<uint8_t> fileBytes((std::istreambuf_iterator<char>(fs)), std::istreambuf_iterator<char>());
|
||||
if (!fileBytes.empty()) {
|
||||
std::vector<uint8_t> subsetBytes = fonts::pdf_fonts::FontSubset::buildSubsetByUnicode(fileBytes, codepoints);
|
||||
std::lock_guard<std::mutex> lock(loadedFontsMutex_);
|
||||
loadedFontDataBuffers_[cacheKey] = !subsetBytes.empty() ? std::move(subsetBytes) : std::move(fileBytes);
|
||||
loadedFontDataBuffers_[cacheKey] = std::move(fileBytes);
|
||||
const auto& bytes = loadedFontDataBuffers_[cacheKey];
|
||||
out.font = FPDFText_LoadFont(doc_, bytes.data(), static_cast<uint32_t>(bytes.size()), FPDF_FONT_TRUETYPE, true);
|
||||
}
|
||||
@@ -2500,13 +2527,31 @@ std::expected<void, EngineError> PdfiumDocument::applyEdits(const std::string& e
|
||||
origCenterY = (origBottom + origTop) / 2.0;
|
||||
}
|
||||
|
||||
bool axisAligned = (std::abs(b) < 1e-6 && std::abs(c) < 1e-6 && a > 0.0 && d > 0.0);
|
||||
double colRight = origRight;
|
||||
bool sawSibling = false;
|
||||
if (axisAligned && hasOrigBounds) {
|
||||
int nObjForCol = FPDFPage_CountObjects(page);
|
||||
for (int k = 0; k < nObjForCol; ++k) {
|
||||
if (std::find(objectIndices.begin(), objectIndices.end(), k) != objectIndices.end()) continue;
|
||||
FPDF_PAGEOBJECT o = FPDFPage_GetObject(page, k);
|
||||
if (!o || FPDFPageObj_GetType(o) != FPDF_PAGEOBJ_TEXT) continue;
|
||||
float l = 0, bo = 0, rr = 0, tt = 0;
|
||||
if (!FPDFPageObj_GetBounds(o, &l, &bo, &rr, &tt)) continue;
|
||||
if (std::abs(l - origLeft) <= 3.0) { sawSibling = true; if (rr > colRight) colRight = rr; }
|
||||
}
|
||||
}
|
||||
double justifyTol = (std::max)(4.0, (colRight - origLeft) * 0.02);
|
||||
bool wasJustified = axisAligned && resolvedFont && hasOrigBounds && sawSibling &&
|
||||
(colRight - origLeft) > 20.0 && (origRight >= colRight - justifyTol);
|
||||
|
||||
double deltaX = 0.0;
|
||||
if (hasOrigBounds) {
|
||||
deltaX = totalWidth - origWidth;
|
||||
spdlog::info("Reflow Engine: origWidth = {}, newWidth = {}, deltaX = {}", origWidth, totalWidth, deltaX);
|
||||
}
|
||||
|
||||
if (hasOrigBounds && std::abs(deltaX) > 0.001) {
|
||||
if (!wasJustified && hasOrigBounds && std::abs(deltaX) > 0.001) {
|
||||
int pageObjCount = FPDFPage_CountObjects(page);
|
||||
double tolerance = (std::max)(5.0, fontSize * 0.5);
|
||||
int reflowedCount = 0;
|
||||
@@ -2627,16 +2672,82 @@ std::expected<void, EngineError> PdfiumDocument::applyEdits(const std::string& e
|
||||
}
|
||||
|
||||
if (font) {
|
||||
FPDF_PAGEOBJECT newTextObj = FPDFPageObj_CreateTextObj(doc_, font, static_cast<float>(fontSize));
|
||||
if (newTextObj) {
|
||||
FPDFPageObj_SetFillColor(newTextObj, r, g, b_color, a_color);
|
||||
FPDFTextObj_SetTextRenderMode(newTextObj, renderMode);
|
||||
FPDFText_SetText(newTextObj, reinterpret_cast<FPDF_WIDESTRING>(utf16.data()));
|
||||
FPDFPageObj_Transform(newTextObj, a, b, c, d, e, f);
|
||||
constexpr unsigned int kRef = 1000;
|
||||
double emToPage = (fontSize > 0.0 ? fontSize : 1.0) * a / static_cast<double>(kRef);
|
||||
auto pageWidthOf = [&](const std::string& s) -> double {
|
||||
if (s.empty() || !resolvedFont) return 0.0;
|
||||
double sum = 0.0;
|
||||
try {
|
||||
fonts::HbShaper sh;
|
||||
auto gl = sh.shapeRun(s, resolvedFont->getFontFace(), kRef);
|
||||
for (const auto& gg : gl) sum += gg.advanceX;
|
||||
} catch (...) {
|
||||
for (unsigned char ch : s) sum += resolvedFont->getAdvanceWidth(ch, kRef);
|
||||
}
|
||||
return sum * emToPage;
|
||||
};
|
||||
|
||||
FPDFPage_InsertObjectAtIndex(page, newTextObj, minIndex);
|
||||
std::vector<std::string> words;
|
||||
if (wasJustified) {
|
||||
std::string cur;
|
||||
for (char ch : newText) {
|
||||
if (ch == ' ') { if (!cur.empty()) { words.push_back(cur); cur.clear(); } }
|
||||
else cur.push_back(ch);
|
||||
}
|
||||
if (!cur.empty()) words.push_back(cur);
|
||||
}
|
||||
|
||||
auto measuredWidth = [&](const std::vector<unsigned short>& u16le) -> double {
|
||||
FPDF_PAGEOBJECT m = FPDFPageObj_CreateTextObj(doc_, font, static_cast<float>(fontSize));
|
||||
if (!m) return 0.0;
|
||||
FPDFText_SetText(m, reinterpret_cast<FPDF_WIDESTRING>(u16le.data()));
|
||||
FPDFPageObj_Transform(m, a, b, c, d, 0.0, 0.0);
|
||||
float l = 0, bo = 0, rr = 0, tt = 0;
|
||||
double w = FPDFPageObj_GetBounds(m, &l, &bo, &rr, &tt) ? (rr - l) : 0.0;
|
||||
FPDFPageObj_Destroy(m);
|
||||
return w;
|
||||
};
|
||||
|
||||
if (wasJustified && words.size() > 1) {
|
||||
std::vector<double> wpx;
|
||||
wpx.reserve(words.size());
|
||||
double estWords = 0.0;
|
||||
for (const auto& w : words) { double ww = pageWidthOf(w); wpx.push_back(ww); estWords += ww; }
|
||||
double estSpace = pageWidthOf(" ");
|
||||
int gaps = static_cast<int>(words.size()) - 1;
|
||||
double estTotal = estWords + gaps * estSpace;
|
||||
double actualFull = measuredWidth(utf16);
|
||||
double k = (estTotal > 1e-6 && actualFull > 1e-6) ? actualFull / estTotal : 1.0;
|
||||
double targetW = colRight - e;
|
||||
double slack = targetW - actualFull;
|
||||
double extraPerGap = (slack > 0.0) ? slack / gaps : 0.0;
|
||||
spdlog::info("replace_text: justify {} words targetW={:.1f} actualW={:.1f} extraPerGap={:.2f}",
|
||||
words.size(), targetW, actualFull, extraPerGap);
|
||||
double penX = e;
|
||||
for (size_t wi = 0; wi < words.size(); ++wi) {
|
||||
FPDF_PAGEOBJECT wobj = FPDFPageObj_CreateTextObj(doc_, font, static_cast<float>(fontSize));
|
||||
if (wobj) {
|
||||
FPDFPageObj_SetFillColor(wobj, r, g, b_color, a_color);
|
||||
FPDFTextObj_SetTextRenderMode(wobj, renderMode);
|
||||
auto wu = utf8_to_utf16le(words[wi]); wu.push_back(0);
|
||||
FPDFText_SetText(wobj, reinterpret_cast<FPDF_WIDESTRING>(wu.data()));
|
||||
FPDFPageObj_Transform(wobj, a, b, c, d, penX, f);
|
||||
FPDFPage_InsertObjectAtIndex(page, wobj, minIndex);
|
||||
}
|
||||
penX += (wpx[wi] + estSpace) * k + extraPerGap;
|
||||
}
|
||||
} else {
|
||||
spdlog::error("Failed to create new text object");
|
||||
FPDF_PAGEOBJECT newTextObj = FPDFPageObj_CreateTextObj(doc_, font, static_cast<float>(fontSize));
|
||||
if (newTextObj) {
|
||||
FPDFPageObj_SetFillColor(newTextObj, r, g, b_color, a_color);
|
||||
FPDFTextObj_SetTextRenderMode(newTextObj, renderMode);
|
||||
FPDFText_SetText(newTextObj, reinterpret_cast<FPDF_WIDESTRING>(utf16.data()));
|
||||
FPDFPageObj_Transform(newTextObj, a, b, c, d, e, f);
|
||||
|
||||
FPDFPage_InsertObjectAtIndex(page, newTextObj, minIndex);
|
||||
} else {
|
||||
spdlog::error("Failed to create new text object");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,20 @@
|
||||
const NEAR_WHITE_THRESHOLD = 0xf0;
|
||||
|
||||
function parseHex(color: string): [number, number, number] | null {
|
||||
const m = /^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.exec(color.trim());
|
||||
if (!m) return null;
|
||||
let h = m[1];
|
||||
if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
|
||||
return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
|
||||
}
|
||||
|
||||
export function sanitizeTextColor(color: unknown, fallback = '#000000'): string {
|
||||
if (typeof color !== 'string') return fallback;
|
||||
const rgb = parseHex(color);
|
||||
if (!rgb) return fallback;
|
||||
const [r, g, b] = rgb;
|
||||
if (r >= NEAR_WHITE_THRESHOLD && g >= NEAR_WHITE_THRESHOLD && b >= NEAR_WHITE_THRESHOLD) {
|
||||
return '#000000';
|
||||
}
|
||||
return color;
|
||||
}
|
||||
@@ -16,7 +16,7 @@ function getModule(): Promise<PdfiumModule | null> {
|
||||
if (!modulePromise) {
|
||||
modulePromise = (async () => {
|
||||
try {
|
||||
const V = '20260618f-allfonts';
|
||||
const V = '20260623-tier1b';
|
||||
const resp = await fetch(`/pdfium-engine.mjs?v=${V}`, { cache: 'no-store' });
|
||||
if (!resp.ok) throw new Error(`pdfium-engine.mjs ${resp.status}`);
|
||||
const blobUrl = URL.createObjectURL(new Blob([await resp.text()], { type: 'text/javascript' }));
|
||||
|
||||
@@ -98,6 +98,8 @@ export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(({
|
||||
const [pageTexts, setPageTexts] = useState<Record<number, Glyph[]>>({});
|
||||
const renderedDocIdRef = useRef<string>('');
|
||||
const prevDocumentIdRef = useRef<string>(documentId);
|
||||
const inFlightRenderRef = useRef<Set<number>>(new Set());
|
||||
const inFlightTextRef = useRef<Set<number>>(new Set());
|
||||
useEffect(() => {
|
||||
setPageTexts({});
|
||||
const prev = prevDocumentIdRef.current;
|
||||
@@ -225,63 +227,70 @@ export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(({
|
||||
}, [primaryVisiblePage, onPageVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
const docChanged = renderedDocIdRef.current !== documentId;
|
||||
const fetchPageImages = async () => {
|
||||
const missingPages = docChanged
|
||||
const base = docChanged
|
||||
? visiblePages
|
||||
: visiblePages.filter((page) => !renderedPages[page.index]);
|
||||
const missingPages = base.filter((page) => !inFlightRenderRef.current.has(page.index));
|
||||
if (missingPages.length === 0) return;
|
||||
missingPages.forEach((page) => inFlightRenderRef.current.add(page.index));
|
||||
|
||||
const renders = await Promise.all(
|
||||
missingPages.map(async (page) => {
|
||||
const url = await gatewayService.renderPage({
|
||||
documentId,
|
||||
pageIndex: page.index,
|
||||
zoom,
|
||||
rotation: 0,
|
||||
try {
|
||||
const renders = await Promise.all(
|
||||
missingPages.map(async (page) => {
|
||||
const url = await gatewayService.renderPage({
|
||||
documentId,
|
||||
pageIndex: page.index,
|
||||
zoom,
|
||||
rotation: 0,
|
||||
});
|
||||
return { index: page.index, url };
|
||||
})
|
||||
);
|
||||
|
||||
if (documentIdRef.current !== documentId) return;
|
||||
|
||||
renderedDocIdRef.current = documentId;
|
||||
setRenderedPages((prev) => {
|
||||
const next = docChanged ? [] : [...prev];
|
||||
renders.forEach(({ index, url }) => {
|
||||
next[index] = url;
|
||||
});
|
||||
return { index: page.index, url };
|
||||
})
|
||||
);
|
||||
|
||||
if (!active) return;
|
||||
|
||||
renderedDocIdRef.current = documentId;
|
||||
setRenderedPages((prev) => {
|
||||
const next = docChanged ? [] : [...prev];
|
||||
renders.forEach(({ index, url }) => {
|
||||
next[index] = url;
|
||||
return next;
|
||||
});
|
||||
return next;
|
||||
});
|
||||
} finally {
|
||||
missingPages.forEach((page) => inFlightRenderRef.current.delete(page.index));
|
||||
}
|
||||
};
|
||||
|
||||
fetchPageImages();
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [visiblePages, documentId, zoom, renderedPages]);
|
||||
|
||||
const textToolActive = activeTool === 'select' || activeTool === 'highlight' || activeTool === 'underline' || activeTool === 'strikeout' || activeTool === 'squiggly';
|
||||
useEffect(() => {
|
||||
if (!textToolActive || !documentId) return;
|
||||
let active = true;
|
||||
const fetchTexts = async () => {
|
||||
const missing = visiblePages.filter((p) => !(p.index in pageTexts));
|
||||
const missing = visiblePages
|
||||
.filter((p) => !(p.index in pageTexts))
|
||||
.filter((p) => !inFlightTextRef.current.has(p.index));
|
||||
if (missing.length === 0) return;
|
||||
const results = await Promise.all(
|
||||
missing.map(async (p) => ({ index: p.index, page: await gatewayService.getPageText(documentId, p.index) })),
|
||||
);
|
||||
if (!active) return;
|
||||
setPageTexts((prev) => {
|
||||
const next = { ...prev };
|
||||
results.forEach(({ index, page }) => { next[index] = page.glyphs; });
|
||||
return next;
|
||||
});
|
||||
missing.forEach((p) => inFlightTextRef.current.add(p.index));
|
||||
try {
|
||||
const results = await Promise.all(
|
||||
missing.map(async (p) => ({ index: p.index, page: await gatewayService.getPageText(documentId, p.index) })),
|
||||
);
|
||||
if (documentIdRef.current !== documentId) return;
|
||||
setPageTexts((prev) => {
|
||||
const next = { ...prev };
|
||||
results.forEach(({ index, page }) => { next[index] = page.glyphs; });
|
||||
return next;
|
||||
});
|
||||
} finally {
|
||||
missing.forEach((p) => inFlightTextRef.current.delete(p.index));
|
||||
}
|
||||
};
|
||||
fetchTexts();
|
||||
return () => { active = false; };
|
||||
}, [textToolActive, visiblePages, documentId, pageTexts]);
|
||||
|
||||
const handleTextSelection = (text: string, bbox: Rect, lines: Rect[], pageIndex: number) => {
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface OverflowPreviewRegion { pageIndex: number; yTopPt: number; data
|
||||
export interface OverflowCaret { pageIndex: number; left: number; top: number; height: number; }
|
||||
import type { ReflowParagraphPayload, CommitFrame } from './TextEditLayer';
|
||||
import type { ReflowFragment } from '../lib/gatewayService';
|
||||
import { sanitizeTextColor } from '../lib/colorUtils';
|
||||
|
||||
interface SeedRun { text: string; fid: string; size: number; color: string; fontName: string; advances?: number[]; }
|
||||
interface OrigLine { frags: { text: string; fid: string; size: number; color: string; advances?: number[] }[]; x: number; baselineY: number; }
|
||||
@@ -66,8 +67,21 @@ function median(xs: number[]): number {
|
||||
const s = [...xs].sort((a, b) => a - b);
|
||||
return s[Math.floor(s.length / 2)];
|
||||
}
|
||||
function paraEffSize(lines: any[]): number {
|
||||
const heights: number[] = [];
|
||||
let nominal = 0;
|
||||
for (const line of lines) for (const r of (line.runs ?? [])) {
|
||||
nominal = Math.max(nominal, r.font_size ?? 0);
|
||||
for (const g of (r.glyphs ?? [])) if ((g.bbox_h ?? 0) > 0.1) heights.push(g.bbox_h);
|
||||
}
|
||||
if (!heights.length) return nominal || 12;
|
||||
heights.sort((a, b) => a - b);
|
||||
const p75 = heights[Math.floor(heights.length * 0.75)];
|
||||
return Math.max(nominal, p75 / 0.7);
|
||||
}
|
||||
function computeLayout(para: any): ParagraphLayout {
|
||||
const lines = para?.lines ?? [];
|
||||
const effSize = paraEffSize(lines);
|
||||
let columnLeft = Infinity, columnRight = -Infinity, firstBaselineY = -Infinity;
|
||||
const baselines: number[] = [], rightEdges: number[] = [], objectIndices: number[] = [];
|
||||
const seedRuns: SeedRun[] = [];
|
||||
@@ -91,8 +105,9 @@ function computeLayout(para: any): ParagraphLayout {
|
||||
if (prev && !/\s$/.test(prev) && !/^\s/.test(text)) text = ' ' + text;
|
||||
}
|
||||
const adv = perRun[ri];
|
||||
seedRuns.push({ text, fid: r.internal_font_id ?? '', size: r.font_size ?? 12, color: typeof r.color === 'string' ? r.color : '#000000', fontName: r.font_name ?? '', advances: adv });
|
||||
if (orig) lineFrags.push({ text: orig, fid: r.internal_font_id ?? '', size: r.font_size ?? 12, color: typeof r.color === 'string' ? r.color : '#000000', advances: adv });
|
||||
const safeColor = sanitizeTextColor(r.color);
|
||||
seedRuns.push({ text, fid: r.internal_font_id ?? '', size: effSize, color: safeColor, fontName: r.font_name ?? '', advances: adv });
|
||||
if (orig) lineFrags.push({ text: orig, fid: r.internal_font_id ?? '', size: effSize, color: safeColor, advances: adv });
|
||||
}
|
||||
if (lineFrags.length) origLines.push({ frags: lineFrags, x: anchorX, baselineY: line.baseline_y ?? 0 });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { gatewayService } from '../lib/gatewayService';
|
||||
import { loadPdfFont, releaseDocumentFonts } from '../lib/fontFaceLoader';
|
||||
import { sanitizeTextColor } from '../lib/colorUtils';
|
||||
import { ParagraphEditor } from './ParagraphEditor';
|
||||
import type { ReflowAlign } from './ParagraphEditor';
|
||||
|
||||
@@ -225,7 +226,7 @@ function flattenRuns(model: any): EditableRun[] {
|
||||
objectIndices,
|
||||
internalFontId: r.internal_font_id ?? '',
|
||||
fontName: r.font_name ?? '',
|
||||
color: typeof r.color === 'string' ? r.color : '#000000',
|
||||
color: sanitizeTextColor(r.color),
|
||||
paraIndex: pi, lineIndex: li, runIndex: ri,
|
||||
});
|
||||
}
|
||||
@@ -241,6 +242,10 @@ function median(xs: number[]): number {
|
||||
return s[Math.floor(s.length / 2)];
|
||||
}
|
||||
|
||||
function displayFontSize(r: EditableRun): number {
|
||||
return Math.max(r.fontSize, r.h / 0.92);
|
||||
}
|
||||
|
||||
function buildReflowPayload(model: any, run: EditableRun, newText: string): ReflowParagraphPayload | null {
|
||||
const para = model?.paragraphs?.[run.paraIndex];
|
||||
if (!para || !Array.isArray(para.lines)) return null;
|
||||
@@ -447,7 +452,7 @@ export const TextEditLayer: React.FC<TextEditLayerProps> = ({
|
||||
}
|
||||
committedRef.current = false;
|
||||
const fb = fallbackFamily(run.fontName);
|
||||
caretIdxRef.current = caretIndexFromX(run.text, `${run.fontSize * zoom}px ${fb}`, clickX);
|
||||
caretIdxRef.current = caretIndexFromX(run.text, `${displayFontSize(run) * zoom}px ${fb}`, clickX);
|
||||
setEditing(i);
|
||||
setValue(run.text);
|
||||
setFontFamily(fb);
|
||||
@@ -543,7 +548,7 @@ export const TextEditLayer: React.FC<TextEditLayerProps> = ({
|
||||
})}
|
||||
|
||||
{run && (() => {
|
||||
const fpx = run.fontSize * zoom;
|
||||
const fpx = displayFontSize(run) * zoom;
|
||||
const baselineScreen = (heightPts - run.baselineY) * zoom;
|
||||
const inputTop = baselineScreen - 0.8 * fpx;
|
||||
const box = rectOf(run);
|
||||
|
||||
Reference in New Issue
Block a user