cursor and fontissue

This commit is contained in:
momorew
2026-08-06 14:58:44 +05:30
parent 9134b5e205
commit 4ff16adab3
16 changed files with 385 additions and 68 deletions
+2
View File
@@ -29,6 +29,8 @@ add_library(pdfengine OBJECT
src/fonts/pdf_fonts/font_fallback.cpp src/fonts/pdf_fonts/font_fallback.cpp
src/fonts/pdf_fonts/font_subset.cpp src/fonts/pdf_fonts/font_subset.cpp
src/fonts/pdf_fonts/font_cmap_builder.cpp src/fonts/pdf_fonts/font_cmap_builder.cpp
src/fonts/pdf_fonts/font_validator.cpp
src/fonts/pdf_fonts/font_extraction_service.cpp
src/fonts/pdf_fonts/embedded_font_reconstructor.cpp src/fonts/pdf_fonts/embedded_font_reconstructor.cpp
src/fonts/pdf_fonts/encoding/encoding.cpp src/fonts/pdf_fonts/encoding/encoding.cpp
src/fonts/pdf_fonts/encoding/tounicode_parser.cpp src/fonts/pdf_fonts/encoding/tounicode_parser.cpp
@@ -0,0 +1,49 @@
#include "font_extraction_service.hpp"
#include "font_validator.hpp"
#include "../../qpdf/qpdf_font_extractor.hpp"
#include <spdlog/spdlog.h>
namespace pdfengine::fonts::pdf_fonts {
std::expected<std::shared_ptr<EmbeddedFontProgram>, EngineError>
FontExtractionService::getFontProgram(const std::string& internalFontId,
const std::string& baseFontName,
const std::vector<uint8_t>& documentBuffer,
const std::vector<uint8_t>& pdfiumRawBytes) {
std::lock_guard<std::mutex> lock(cacheMutex_);
if (cache_.count(internalFontId)) {
return cache_[internalFontId];
}
// 1. Try PDFium Bytes (Standard fonts)
if (!pdfiumRawBytes.empty() && FontValidator::isValidSFNT(pdfiumRawBytes)) {
auto prog = std::make_shared<EmbeddedFontProgram>();
prog->fontName = baseFontName;
prog->bytes = pdfiumRawBytes;
prog->source = FontExtractionSource::PDFium;
cache_[internalFontId] = prog;
spdlog::info("[FontExtraction] internalId='{}' source=PDFium size={}", internalFontId, prog->bytes.size());
return prog;
}
// 2. Try QPDF Fallback (Type0 CIDFonts)
if (!documentBuffer.empty()) {
qpdf_layer::QpdfFontExtractor qpdfExtractor;
auto qpdfProg = qpdfExtractor.extractFontProgram(documentBuffer, baseFontName);
if (qpdfProg && FontValidator::isValidSFNT(qpdfProg->bytes)) {
auto prog = std::make_shared<EmbeddedFontProgram>(*qpdfProg);
prog->source = FontExtractionSource::QPDF_Raw;
cache_[internalFontId] = prog;
spdlog::info("[FontExtraction] internalId='{}' source=QPDF_Raw size={}", internalFontId, prog->bytes.size());
return prog;
}
}
// Tier 2 Reconstruction would happen here if needed, but for now we return not found
spdlog::warn("[FontExtraction] Failed to extract valid SFNT for '{}'", internalFontId);
return std::unexpected(EngineError::FileNotFound);
}
} // namespace pdfengine::fonts::pdf_fonts
@@ -0,0 +1,37 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#include <unordered_map>
#include <memory>
#include <mutex>
#include <optional>
#include <expected>
#include "font_extraction_types.hpp"
#include <pdfengine/pdf_document.hpp>
namespace pdfengine::fonts::pdf_fonts {
class FontExtractionService {
public:
static FontExtractionService& getInstance() {
static FontExtractionService instance;
return instance;
}
std::expected<std::shared_ptr<EmbeddedFontProgram>, EngineError>
getFontProgram(const std::string& internalFontId,
const std::string& baseFontName,
const std::vector<uint8_t>& documentBuffer,
const std::vector<uint8_t>& pdfiumRawBytes);
private:
FontExtractionService() = default;
std::mutex cacheMutex_;
std::unordered_map<std::string, std::shared_ptr<EmbeddedFontProgram>> cache_;
};
} // namespace pdfengine::fonts::pdf_fonts
@@ -0,0 +1,20 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace pdfengine::fonts::pdf_fonts {
enum class FontProgramType { Unknown, TrueType, CFF, OpenType, Type1 };
enum class FontExtractionSource { PDFium, QPDF_Raw, QPDF_Reconstructed, Cache };
struct EmbeddedFontProgram {
std::string fontName;
std::vector<uint8_t> bytes;
FontProgramType type = FontProgramType::Unknown;
FontExtractionSource source = FontExtractionSource::PDFium;
};
} // namespace pdfengine::fonts::pdf_fonts
@@ -0,0 +1,28 @@
#include "font_validator.hpp"
namespace pdfengine::fonts::pdf_fonts {
bool FontValidator::isValidSFNT(const std::vector<uint8_t>& bytes) {
if (bytes.size() < 12) { // Minimum SFNT header size
return false;
}
// Check SFNT version / magic bytes
// 0x00010000 for TrueType
// 0x4F54544F ("OTTO") for OpenType CFF
// 0x74727565 ("true") for Apple TrueType
// 0x74797031 ("typ1") for Mac PostScript Type 1
const uint8_t* b = bytes.data();
uint32_t magic = (static_cast<uint32_t>(b[0]) << 24) |
(static_cast<uint32_t>(b[1]) << 16) |
(static_cast<uint32_t>(b[2]) << 8) |
static_cast<uint32_t>(b[3]);
if (magic == 0x00010000 || magic == 0x4F54544F || magic == 0x74727565 || magic == 0x74797031) {
return true;
}
return false;
}
} // namespace pdfengine::fonts::pdf_fonts
@@ -0,0 +1,13 @@
#pragma once
#include <cstdint>
#include <vector>
namespace pdfengine::fonts::pdf_fonts {
class FontValidator {
public:
static bool isValidSFNT(const std::vector<uint8_t>& bytes);
};
} // namespace pdfengine::fonts::pdf_fonts
+31 -27
View File
@@ -1,5 +1,5 @@
#include "parser/pdfium_internal.hpp" #include "parser/pdfium_internal.hpp"
#include "fonts/pdf_fonts/font_extraction_service.hpp"
namespace pdfengine::parser { namespace pdfengine::parser {
void PdfiumDocument::registerAuxFont(const std::string& internalFontId, const std::vector<uint8_t>& sfnt) { void PdfiumDocument::registerAuxFont(const std::string& internalFontId, const std::vector<uint8_t>& sfnt) {
@@ -607,6 +607,13 @@ std::expected<std::vector<uint8_t>, EngineError> PdfiumDocument::getFontData(con
std::string expectedFontName = baseNameFromInternalFontId(internalFontId); std::string expectedFontName = baseNameFromInternalFontId(internalFontId);
// 1. Try cache via service
auto cachedProg = fonts::pdf_fonts::FontExtractionService::getInstance().getFontProgram(
internalFontId, expectedFontName, memoryBuffer_, {});
if (cachedProg) {
return (*cachedProg)->bytes;
}
int numPages = FPDF_GetPageCount(doc_); int numPages = FPDF_GetPageCount(doc_);
int startPage = 0; int startPage = 0;
@@ -639,37 +646,31 @@ std::expected<std::vector<uint8_t>, EngineError> PdfiumDocument::getFontData(con
FPDF_FONT font = FPDFTextObj_GetFont(obj); FPDF_FONT font = FPDFTextObj_GetFont(obj);
if (!font) continue; if (!font) continue;
size_t nameLen = FPDFFont_GetBaseFontName(font, nullptr, 0); size_t nameLen = FPDFFont_GetBaseFontName(font, nullptr, 0);
if (nameLen > 0) { if (nameLen > 0) {
std::vector<char> nameBuf(nameLen); std::vector<char> nameBuf(nameLen);
if (FPDFFont_GetBaseFontName(font, nameBuf.data(), nameLen) > 0) { if (FPDFFont_GetBaseFontName(font, nameBuf.data(), nameLen) > 0) {
std::string fontName(nameBuf.data()); std::string fontName(nameBuf.data());
if (fontName == expectedFontName) {
std::lock_guard<std::mutex> lock(fontsMutex_); std::vector<uint8_t> pdfiumBytes;
if (fontDataCache_.find(fontName) == fontDataCache_.end()) {
size_t buflen = 0; size_t buflen = 0;
FPDFFont_GetFontData(font, nullptr, 0, &buflen); FPDFFont_GetFontData(font, nullptr, 0, &buflen);
if (buflen > 0) { if (buflen > 0) {
std::vector<uint8_t> buffer(buflen); pdfiumBytes.resize(buflen);
size_t actual_len = 0; size_t actual_len = 0;
if (FPDFFont_GetFontData(font, buffer.data(), buflen, &actual_len)) { FPDFFont_GetFontData(font, pdfiumBytes.data(), buflen, &actual_len);
fontDataCache_[fontName] = buffer;
} else {
fontDataCache_[fontName] = std::vector<uint8_t>();
}
} else {
fontDataCache_[fontName] = std::vector<uint8_t>();
} }
}
FPDF_ClosePage(page);
if (fontName == expectedFontName) { fontDataScannedPages_ = i;
const auto& cachedBuf = fontDataCache_[fontName];
if (!cachedBuf.empty()) { auto prog = fonts::pdf_fonts::FontExtractionService::getInstance().getFontProgram(
FPDF_ClosePage(page); internalFontId, expectedFontName, memoryBuffer_, pdfiumBytes);
fontDataScannedPages_ = i;
return cachedBuf; if (prog) {
return (*prog)->bytes;
} else {
return std::unexpected(prog.error());
} }
} }
} }
@@ -681,12 +682,15 @@ std::expected<std::vector<uint8_t>, EngineError> PdfiumDocument::getFontData(con
fontDataScannedPages_ = i + 1; fontDataScannedPages_ = i + 1;
} }
{ // If we searched all pages and didn't find the expected font name in FPDF objects,
std::lock_guard<std::mutex> lock(fontsMutex_); // let the FontExtractionService try QPDF fallback on the whole document as a last resort.
if (fontDataCache_.find(expectedFontName) == fontDataCache_.end()) { auto finalProg = fonts::pdf_fonts::FontExtractionService::getInstance().getFontProgram(
fontDataCache_[expectedFontName] = std::vector<uint8_t>(); internalFontId, expectedFontName, memoryBuffer_, {});
}
if (finalProg) {
return (*finalProg)->bytes;
} }
return std::unexpected(EngineError::FileNotFound); return std::unexpected(EngineError::FileNotFound);
#else #else
(void)internalFontId; (void)internalFontId;
+114 -28
View File
@@ -11,6 +11,7 @@
#endif #endif
#include <cstdio> #include <cstdio>
#include <spdlog/spdlog.h>
namespace pdfengine::qpdf_layer { namespace pdfengine::qpdf_layer {
@@ -46,6 +47,7 @@ QpdfFontExtractor::extractMapping(const std::vector<uint8_t>& pdf, const std::st
::QPDF qpdf; ::QPDF qpdf;
qpdf.processMemoryFile("memory", reinterpret_cast<const char*>(pdf.data()), pdf.size()); qpdf.processMemoryFile("memory", reinterpret_cast<const char*>(pdf.data()), pdf.size());
std::optional<QPDFObjectHandle> bestObj;
for (QPDFObjectHandle obj : qpdf.getAllObjects()) { for (QPDFObjectHandle obj : qpdf.getAllObjects()) {
if (!obj.isDictionary()) continue; if (!obj.isDictionary()) continue;
if (!obj.hasKey("/Type") || !obj.getKey("/Type").isName() || if (!obj.hasKey("/Type") || !obj.getKey("/Type").isName() ||
@@ -53,43 +55,60 @@ QpdfFontExtractor::extractMapping(const std::vector<uint8_t>& pdf, const std::st
continue; continue;
if (!baseFontMatches(obj, baseFontName)) continue; if (!baseFontMatches(obj, baseFontName)) continue;
EmbeddedFontMapping m; bestObj = obj;
m.subtype = obj.hasKey("/Subtype") && obj.getKey("/Subtype").isName() if (obj.hasKey("/ToUnicode")) {
? obj.getKey("/Subtype").getName() : ""; break; // Found the parent Type0 font!
if (obj.hasKey("/ToUnicode") && obj.getKey("/ToUnicode").isStream()) {
std::string s = decodeStream(obj.getKey("/ToUnicode"));
if (!s.empty() && fonts::pdf_fonts::ToUnicodeParser::parse(s, m.codeToUnicode))
m.hasToUnicode = !m.codeToUnicode.empty();
} }
}
if (m.subtype == "/Type0" && obj.hasKey("/DescendantFonts")) { if (!bestObj) return std::unexpected(QpdfError::Unknown);
QPDFObjectHandle df = obj.getKey("/DescendantFonts"); QPDFObjectHandle obj = *bestObj;
QPDFObjectHandle cid = df.isArray() && df.getArrayNItems() > 0 ? df.getArrayItem(0)
: QPDFObjectHandle(); spdlog::info("[DEBUG] Found best matching font dictionary for {}", baseFontName);
if (cid.isDictionary() && cid.hasKey("/CIDToGIDMap")) {
QPDFObjectHandle c2g = cid.getKey("/CIDToGIDMap"); EmbeddedFontMapping m;
if (c2g.isStream()) { m.subtype = obj.hasKey("/Subtype") && obj.getKey("/Subtype").isName()
std::string s = decodeStream(c2g); ? obj.getKey("/Subtype").getName() : "";
m.identityCidToGid = false;
for (size_t i = 0; i + 1 < s.size(); i += 2) { if (obj.hasKey("/ToUnicode") && obj.getKey("/ToUnicode").isStream()) {
uint16_t gid = static_cast<uint16_t>((static_cast<uint8_t>(s[i]) << 8) | std::string s = decodeStream(obj.getKey("/ToUnicode"));
static_cast<uint8_t>(s[i + 1])); if (s.empty()) {
if (gid != 0) m.codeToGid[static_cast<uint32_t>(i / 2)] = gid; spdlog::info("[DEBUG] ToUnicode stream is empty after decodeStream!");
} } else {
} else { bool parseOk = fonts::pdf_fonts::ToUnicodeParser::parse(s, m.codeToUnicode);
m.identityCidToGid = true; if (!parseOk) {
spdlog::info("[DEBUG] ToUnicodeParser::parse returned false!");
}
m.hasToUnicode = !m.codeToUnicode.empty();
}
}
if (m.subtype == "/Type0" && obj.hasKey("/DescendantFonts")) {
QPDFObjectHandle df = obj.getKey("/DescendantFonts");
QPDFObjectHandle cid = df.isArray() && df.getArrayNItems() > 0 ? df.getArrayItem(0)
: QPDFObjectHandle();
if (cid.isDictionary() && cid.hasKey("/CIDToGIDMap")) {
QPDFObjectHandle c2g = cid.getKey("/CIDToGIDMap");
if (c2g.isStream()) {
std::string s = decodeStream(c2g);
m.identityCidToGid = false;
for (size_t i = 0; i + 1 < s.size(); i += 2) {
uint16_t gid = static_cast<uint16_t>((static_cast<uint8_t>(s[i]) << 8) |
static_cast<uint8_t>(s[i + 1]));
if (gid != 0) m.codeToGid[static_cast<uint32_t>(i / 2)] = gid;
} }
} else { } else {
m.identityCidToGid = true; m.identityCidToGid = true;
} }
} else { } else {
m.identityCidToGid = true; m.identityCidToGid = true;
} }
} else {
m.ok = m.hasToUnicode; m.identityCidToGid = true;
return m;
} }
m.ok = m.hasToUnicode;
return m;
return std::unexpected(QpdfError::Unknown); return std::unexpected(QpdfError::Unknown);
} catch (const QPDFExc& e) { } catch (const QPDFExc& e) {
fprintf(stderr, "QpdfFontExtractor QPDFExc: %s\n", e.what()); fprintf(stderr, "QpdfFontExtractor QPDFExc: %s\n", e.what());
@@ -103,5 +122,72 @@ QpdfFontExtractor::extractMapping(const std::vector<uint8_t>& pdf, const std::st
return std::unexpected(QpdfError::NotSupported); return std::unexpected(QpdfError::NotSupported);
#endif #endif
} }
std::optional<fonts::pdf_fonts::EmbeddedFontProgram>
QpdfFontExtractor::extractFontProgram(const std::vector<uint8_t>& pdf, const std::string& baseFontName) const {
#ifdef PDFENGINE_WITH_QPDF
try {
::QPDF qpdf;
qpdf.processMemoryFile("memory", reinterpret_cast<const char*>(pdf.data()), pdf.size());
for (QPDFObjectHandle obj : qpdf.getAllObjects()) {
if (!obj.isDictionary()) continue;
if (!obj.hasKey("/Type") || !obj.getKey("/Type").isName() ||
obj.getKey("/Type").getName() != "/Font")
continue;
if (!baseFontMatches(obj, baseFontName)) continue;
std::string subtype = obj.hasKey("/Subtype") && obj.getKey("/Subtype").isName()
? obj.getKey("/Subtype").getName() : "";
QPDFObjectHandle fontDesc;
if (subtype == "/Type0" && obj.hasKey("/DescendantFonts")) {
QPDFObjectHandle df = obj.getKey("/DescendantFonts");
if (df.isArray() && df.getArrayNItems() > 0) {
QPDFObjectHandle cid = df.getArrayItem(0);
if (cid.isDictionary() && cid.hasKey("/FontDescriptor")) {
fontDesc = cid.getKey("/FontDescriptor");
}
}
} else if (obj.hasKey("/FontDescriptor")) {
fontDesc = obj.getKey("/FontDescriptor");
}
if (!fontDesc.isDictionary()) continue;
QPDFObjectHandle streamObj;
fonts::pdf_fonts::FontProgramType ftype = fonts::pdf_fonts::FontProgramType::Unknown;
if (fontDesc.hasKey("/FontFile2") && fontDesc.getKey("/FontFile2").isStream()) {
streamObj = fontDesc.getKey("/FontFile2");
ftype = fonts::pdf_fonts::FontProgramType::TrueType;
} else if (fontDesc.hasKey("/FontFile3") && fontDesc.getKey("/FontFile3").isStream()) {
streamObj = fontDesc.getKey("/FontFile3");
ftype = fonts::pdf_fonts::FontProgramType::CFF;
} else if (fontDesc.hasKey("/FontFile") && fontDesc.getKey("/FontFile").isStream()) {
streamObj = fontDesc.getKey("/FontFile");
ftype = fonts::pdf_fonts::FontProgramType::Type1;
}
if (streamObj.isStream()) {
std::string s = decodeStream(streamObj);
if (!s.empty()) {
fonts::pdf_fonts::EmbeddedFontProgram fp;
fp.fontName = baseFontName;
fp.bytes = std::vector<uint8_t>(s.begin(), s.end());
fp.type = ftype;
fp.source = fonts::pdf_fonts::FontExtractionSource::QPDF_Raw;
return fp;
}
}
}
} catch (const std::exception& e) {
fprintf(stderr, "QpdfFontExtractor extractFontProgram exception: %s\n", e.what());
}
#else
(void)pdf; (void)baseFontName;
#endif
return std::nullopt;
}
} }
+5
View File
@@ -7,6 +7,8 @@
#include <vector> #include <vector>
#include "qpdf_extractor.hpp" #include "qpdf_extractor.hpp"
#include <optional>
#include "fonts/pdf_fonts/font_extraction_types.hpp"
namespace pdfengine::qpdf_layer { namespace pdfengine::qpdf_layer {
@@ -23,6 +25,9 @@ class QpdfFontExtractor {
public: public:
std::expected<EmbeddedFontMapping, QpdfError> std::expected<EmbeddedFontMapping, QpdfError>
extractMapping(const std::vector<uint8_t>& pdf, const std::string& baseFontName) const; extractMapping(const std::vector<uint8_t>& pdf, const std::string& baseFontName) const;
std::optional<fonts::pdf_fonts::EmbeddedFontProgram>
extractFontProgram(const std::vector<uint8_t>& pdf, const std::string& baseFontName) const;
}; };
} }
+1
View File
@@ -19,6 +19,7 @@ add_executable(pdfengine_smoke
ast_serializer_test.cpp ast_serializer_test.cpp
content_serializer_test.cpp content_serializer_test.cpp
qpdf_writer_test.cpp qpdf_writer_test.cpp
font_extraction_test.cpp
) )
if(PDFENGINE_WITH_QPDF) if(PDFENGINE_WITH_QPDF)
+32
View File
@@ -0,0 +1,32 @@
#include <gtest/gtest.h>
#include "../src/fonts/pdf_fonts/font_extraction_service.hpp"
#include "../src/fonts/pdf_fonts/font_validator.hpp"
using namespace pdfengine::fonts::pdf_fonts;
TEST(FontValidatorTest, ValidatesSFNT) {
// TrueType Magic: 0x00010000
std::vector<uint8_t> ttf = {0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
EXPECT_TRUE(FontValidator::isValidSFNT(ttf));
// OTTO Magic
std::vector<uint8_t> otto = {'O', 'T', 'T', 'O', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
EXPECT_TRUE(FontValidator::isValidSFNT(otto));
// Invalid Magic
std::vector<uint8_t> invalid = {0x12, 0x34, 0x56, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
EXPECT_FALSE(FontValidator::isValidSFNT(invalid));
// Too small
std::vector<uint8_t> small = {0x00, 0x01, 0x00, 0x00};
EXPECT_FALSE(FontValidator::isValidSFNT(small));
}
TEST(FontExtractionServiceTest, FallbacksToQPDFIfPdfiumFails) {
auto& service = FontExtractionService::getInstance();
// We expect it to return FileNotFound because we provided empty buffers
// but this ensures the code compiles and the service instance is accessible.
auto res = service.getFontProgram("F1", "TestFont", {}, {});
EXPECT_FALSE(res.has_value());
}
+3
View File
@@ -5,6 +5,9 @@
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PDF Editor</title> <title>PDF Editor</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Arimo:wght@400;500;600;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
</head> </head>
<body class="h-full w-full overflow-hidden bg-[#f1f2f4] text-[#18212e] antialiased"> <body class="h-full w-full overflow-hidden bg-[#f1f2f4] text-[#18212e] antialiased">
<div id="root" class="flex h-full w-full flex-col"></div> <div id="root" class="flex h-full w-full flex-col"></div>
+4 -1
View File
@@ -30,7 +30,10 @@ export function loadPdfFont(
if (existing) return existing; if (existing) return existing;
const p = (async (): Promise<string | null> => { const p = (async (): Promise<string | null> => {
const bytes = await gatewayService.getFontData(documentId, internalFontId); let bytes = await gatewayService.getReconstructedFontData(documentId, internalFontId);
if (!bytes || bytes.byteLength === 0) {
bytes = await gatewayService.getFontData(documentId, internalFontId);
}
if (!bytes || bytes.byteLength === 0) return null; if (!bytes || bytes.byteLength === 0) return null;
const hint = (cssFamilyHint || '').replace(/^[A-Z]{6}\+/, '').trim(); const hint = (cssFamilyHint || '').replace(/^[A-Z]{6}\+/, '').trim();
const family = hint const family = hint
+23 -6
View File
@@ -74,7 +74,7 @@ function median(xs: number[]): number {
function paraEffSize(lines: any[]): number { function paraEffSize(lines: any[]): number {
let nominal = 0; let nominal = 0;
for (const line of lines) for (const r of (line.runs ?? [])) { for (const line of lines) for (const r of (line.runs ?? [])) {
const sz = Math.max(r.font_size ?? 0, r.h ?? 0); const sz = r.font_size && r.font_size > 0 ? r.font_size : (r.h ?? 0) * 0.8;
if (sz > nominal) nominal = sz; if (sz > nominal) nominal = sz;
} }
return nominal || 12; return nominal || 12;
@@ -105,7 +105,7 @@ function computeLayout(para: any): ParagraphLayout {
} }
const adv = perRun[ri]; const adv = perRun[ri];
const safeColor = sanitizeTextColor(r.color); const safeColor = sanitizeTextColor(r.color);
const rSize = Math.max(r.font_size ?? 0, r.h ?? 0) || effSize; const rSize = (r.font_size && r.font_size > 0 ? r.font_size : (r.h ?? 0) * 0.8) || effSize;
seedRuns.push({ text, fid: r.internal_font_id ?? '', size: rSize, color: safeColor, fontName: r.font_name ?? '', advances: adv }); seedRuns.push({ text, fid: r.internal_font_id ?? '', size: rSize, color: safeColor, fontName: r.font_name ?? '', advances: adv });
if (orig) lineFrags.push({ text: orig, fid: r.internal_font_id ?? '', size: rSize, color: safeColor, advances: adv }); if (orig) lineFrags.push({ text: orig, fid: r.internal_font_id ?? '', size: rSize, color: safeColor, advances: adv });
} }
@@ -631,9 +631,26 @@ export const ParagraphEditor: React.FC<ParagraphEditorProps> = ({
const colWidthPx = (columnRight - columnLeft) * zoom; const colWidthPx = (columnRight - columnLeft) * zoom;
// Fix 1: use extracted PDF font name (loaded via @font-face), not a generic Arial/Times/Courier map. // Fix 1: use extracted PDF font name (loaded via @font-face), not a generic Arial/Times/Courier map.
const extractedFamily = (domFontName || '').replace(/^[A-Z]{6}\+/, '').trim() || 'sans-serif'; let extractedFamily = (domFontName || '').replace(/^[A-Z]{6}\+/, '').trim() || 'sans-serif';
const fallbackFamily = /times|serif/i.test(extractedFamily) ? 'Times New Roman, serif' // FIX: DO NOT split on hyphens here. If the font was downloaded via @font-face (e.g. "Arial-BoldMT"),
: /courier|mono/i.test(extractedFamily) ? 'Courier New, monospace' : 'Arial, sans-serif'; // we must use the exact string "Arial-BoldMT" so the browser maps to the downloaded font, not the OS font.
// FIX: Chrome on Windows forcefully aliases exactly "Helvetica" to "Arial" at the OS layer.
// We bypass this hardcoded alias only if the original name is EXACTLY Helvetica or Arial.
const rawFamily = extractedFamily.toLowerCase();
if (rawFamily === 'helvetica' || rawFamily === 'arial') {
extractedFamily = `"Inter", ${extractedFamily}`;
}
let fallbackFamily = '"Arimo", Arial, sans-serif';
if (/times|serif/i.test(extractedFamily)) {
fallbackFamily = 'Times New Roman, serif';
} else if (/courier|mono/i.test(extractedFamily)) {
fallbackFamily = 'Courier New, monospace';
} else {
fallbackFamily = '"Inter", "Arimo", Arial, sans-serif';
}
const [measureFamily, setMeasureFamily] = useState( const [measureFamily, setMeasureFamily] = useState(
extractedFamily !== 'sans-serif' ? `'${extractedFamily}', ${fallbackFamily}` : fallbackFamily, extractedFamily !== 'sans-serif' ? `'${extractedFamily}', ${fallbackFamily}` : fallbackFamily,
); );
@@ -1011,7 +1028,7 @@ export const ParagraphEditor: React.FC<ParagraphEditorProps> = ({
for (const ln of lines) { for (const ln of lines) {
for (const r of (ln.runs ?? [])) { for (const r of (ln.runs ?? [])) {
pdfFontName = r.font_name || pdfFontName; pdfFontName = r.font_name || pdfFontName;
pdfFontSize = Math.max(pdfFontSize, r.font_size ?? 0, r.h ?? 0); pdfFontSize = Math.max(pdfFontSize, r.font_size && r.font_size > 0 ? r.font_size : (r.h ?? 0) * 0.8);
for (const g of (r.glyphs ?? [])) { for (const g of (r.glyphs ?? [])) {
const x0 = g.bbox_x, y0 = g.bbox_y, x1 = g.bbox_x + g.bbox_w, y1 = g.bbox_y + g.bbox_h; const x0 = g.bbox_x, y0 = g.bbox_y, x1 = g.bbox_x + g.bbox_w, y1 = g.bbox_y + g.bbox_h;
pdfMinX = Math.min(pdfMinX, x0); pdfMinY = Math.min(pdfMinY, y0); pdfMinX = Math.min(pdfMinX, x0); pdfMinY = Math.min(pdfMinY, y0);
+23 -6
View File
@@ -233,13 +233,29 @@ interface TextEditLayerProps {
function fallbackFamily(fontName: string): string { function fallbackFamily(fontName: string): string {
const n = (fontName || '').toLowerCase(); const n = (fontName || '').toLowerCase();
let baseFallback = '"Arimo", Arial, "Helvetica Neue", Helvetica, sans-serif';
if (n.includes('times') || (n.includes('serif') && !n.includes('sans'))) { if (n.includes('times') || (n.includes('serif') && !n.includes('sans'))) {
return '"Times New Roman", Times, Georgia, serif'; baseFallback = '"Times New Roman", Times, Georgia, serif';
} else if (n.includes('courier') || n.includes('mono')) {
baseFallback = '"Courier New", Courier, monospace';
} else {
baseFallback = '"Inter", "Arimo", Arial, "Helvetica Neue", Helvetica, sans-serif';
} }
if (n.includes('courier') || n.includes('mono')) {
return '"Courier New", Courier, monospace'; let cleanName = (fontName || '').replace(/^[A-Z]{6}\+/, '').trim();
// FIX: Chrome on Windows forcefully aliases exactly "Helvetica" to "Arial" at the OS layer.
// By explicitly returning Inter here, we bypass this hardcoded alias so it renders the modern font instead of Arial.
const rawName = cleanName.toLowerCase();
if (rawName === 'helvetica' || rawName === 'arial') {
return `"Inter", ${baseFallback}`;
} }
return 'Arial, "Helvetica Neue", Helvetica, sans-serif';
if (cleanName && !['arial', 'helvetica', 'times', 'courier', 'arimo', 'inter'].includes(rawName)) {
return `"${cleanName}", ${baseFallback}`;
}
return baseFallback;
} }
function flattenRuns(model: any): EditableRun[] { function flattenRuns(model: any): EditableRun[] {
@@ -259,7 +275,7 @@ function flattenRuns(model: any): EditableRun[] {
text: r.text, text: r.text,
x: r.x, y: r.y, w: r.w, h: r.h, x: r.x, y: r.y, w: r.w, h: r.h,
baselineY, baselineY,
fontSize: Math.max(r.font_size ?? 0, r.h ?? 0), fontSize: r.font_size && r.font_size > 0 ? r.font_size : (r.h ?? 0) * 0.8,
objectIndices, objectIndices,
internalFontId: r.internal_font_id ?? '', internalFontId: r.internal_font_id ?? '',
fontName: r.font_name ?? '', fontName: r.font_name ?? '',
@@ -281,7 +297,8 @@ function median(xs: number[]): number {
} }
function displayFontSize(r: EditableRun): number { function displayFontSize(r: EditableRun): number {
return Math.max(r.fontSize ?? 0, r.h ?? 0); if (r.fontSize && r.fontSize > 0) return r.fontSize;
return (r.h ?? 0) * 0.8;
} }
function buildReflowPayload(model: any, run: EditableRun, newText: string): ReflowParagraphPayload | null { function buildReflowPayload(model: any, run: EditableRun, newText: string): ReflowParagraphPayload | null {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 173 KiB