fix: font handling in revista

This commit is contained in:
Furqan-14
2026-06-24 10:42:05 +05:30
parent e8620947a5
commit e6e1e0ba7c
9 changed files with 254 additions and 78 deletions
@@ -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_) {
+141 -30
View File
@@ -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.
+20
View File
@@ -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;
}
+1 -1
View File
@@ -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' }));
+46 -37
View File
@@ -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) => {
+17 -2
View File
@@ -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 });
}
+8 -3
View File
@@ -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);