move fonts and auto bold

This commit is contained in:
Anthony Stirling 2025-11-10 00:47:26 +00:00
parent f645eaff18
commit e2ba4cf693
29 changed files with 260 additions and 23 deletions

View File

@ -49,7 +49,16 @@ public class PdfJsonFallbackFontService {
Map.entry("tinos", "fallback-liberation-serif"),
Map.entry("courier", "fallback-liberation-mono"),
Map.entry("couriernew", "fallback-liberation-mono"),
Map.entry("cousine", "fallback-liberation-mono"));
Map.entry("cousine", "fallback-liberation-mono"),
// DejaVu fonts - widely used open source fonts
Map.entry("dejavu", "fallback-dejavu-sans"),
Map.entry("dejavusans", "fallback-dejavu-sans"),
Map.entry("dejavuserif", "fallback-dejavu-serif"),
Map.entry("dejavumono", "fallback-dejavu-mono"),
Map.entry("dejavusansmono", "fallback-dejavu-mono"),
// Noto Sans - Google's universal font (use as last resort generic fallback)
Map.entry("noto", "fallback-noto-sans"),
Map.entry("notosans", "fallback-noto-sans"));
private static final Map<String, FallbackFontSpec> BUILT_IN_FALLBACK_FONTS =
Map.ofEntries(
@ -85,40 +94,172 @@ public class PdfJsonFallbackFontService {
Map.entry(
"fallback-liberation-sans",
new FallbackFontSpec(
"classpath:/static/pdfjs-legacy/standard_fonts/LiberationSans-Regular.ttf",
"classpath:/static/fonts/LiberationSans-Regular.ttf",
"LiberationSans-Regular",
"ttf")),
Map.entry(
"fallback-liberation-sans-bold",
new FallbackFontSpec(
"classpath:/static/pdfjs-legacy/standard_fonts/LiberationSans-Bold.ttf",
"classpath:/static/fonts/LiberationSans-Bold.ttf",
"LiberationSans-Bold",
"ttf")),
Map.entry(
"fallback-liberation-sans-italic",
new FallbackFontSpec(
"classpath:/static/pdfjs-legacy/standard_fonts/LiberationSans-Italic.ttf",
"classpath:/static/fonts/LiberationSans-Italic.ttf",
"LiberationSans-Italic",
"ttf")),
Map.entry(
"fallback-liberation-sans-bolditalic",
new FallbackFontSpec(
"classpath:/static/pdfjs-legacy/standard_fonts/LiberationSans-BoldItalic.ttf",
"classpath:/static/fonts/LiberationSans-BoldItalic.ttf",
"LiberationSans-BoldItalic",
"ttf")),
// Liberation Serif family
Map.entry(
"fallback-liberation-serif",
new FallbackFontSpec(
"classpath:/static/pdfjs-legacy/standard_fonts/LiberationSerif-Regular.ttf",
"classpath:/static/fonts/LiberationSerif-Regular.ttf",
"LiberationSerif-Regular",
"ttf")),
Map.entry(
"fallback-liberation-serif-bold",
new FallbackFontSpec(
"classpath:/static/fonts/LiberationSerif-Bold.ttf",
"LiberationSerif-Bold",
"ttf")),
Map.entry(
"fallback-liberation-serif-italic",
new FallbackFontSpec(
"classpath:/static/fonts/LiberationSerif-Italic.ttf",
"LiberationSerif-Italic",
"ttf")),
Map.entry(
"fallback-liberation-serif-bolditalic",
new FallbackFontSpec(
"classpath:/static/fonts/LiberationSerif-BoldItalic.ttf",
"LiberationSerif-BoldItalic",
"ttf")),
// Liberation Mono family
Map.entry(
"fallback-liberation-mono",
new FallbackFontSpec(
"classpath:/static/pdfjs-legacy/standard_fonts/LiberationMono-Regular.ttf",
"classpath:/static/fonts/LiberationMono-Regular.ttf",
"LiberationMono-Regular",
"ttf")),
Map.entry(
"fallback-liberation-mono-bold",
new FallbackFontSpec(
"classpath:/static/fonts/LiberationMono-Bold.ttf",
"LiberationMono-Bold",
"ttf")),
Map.entry(
"fallback-liberation-mono-italic",
new FallbackFontSpec(
"classpath:/static/fonts/LiberationMono-Italic.ttf",
"LiberationMono-Italic",
"ttf")),
Map.entry(
"fallback-liberation-mono-bolditalic",
new FallbackFontSpec(
"classpath:/static/fonts/LiberationMono-BoldItalic.ttf",
"LiberationMono-BoldItalic",
"ttf")),
// Noto Sans family (enhanced with weight variants)
Map.entry(
FALLBACK_FONT_ID,
new FallbackFontSpec(
DEFAULT_FALLBACK_FONT_LOCATION, "NotoSans-Regular", "ttf")),
Map.entry(
"fallback-noto-sans-bold",
new FallbackFontSpec(
"classpath:/static/fonts/NotoSans-Bold.ttf",
"NotoSans-Bold",
"ttf")),
Map.entry(
"fallback-noto-sans-italic",
new FallbackFontSpec(
"classpath:/static/fonts/NotoSans-Italic.ttf",
"NotoSans-Italic",
"ttf")),
Map.entry(
"fallback-noto-sans-bolditalic",
new FallbackFontSpec(
"classpath:/static/fonts/NotoSans-BoldItalic.ttf",
"NotoSans-BoldItalic",
"ttf")),
// DejaVu Sans family
Map.entry(
"fallback-dejavu-sans",
new FallbackFontSpec(
"classpath:/static/fonts/DejaVuSans.ttf", "DejaVuSans", "ttf")),
Map.entry(
"fallback-dejavu-sans-bold",
new FallbackFontSpec(
"classpath:/static/fonts/DejaVuSans-Bold.ttf",
"DejaVuSans-Bold",
"ttf")),
Map.entry(
"fallback-dejavu-sans-oblique",
new FallbackFontSpec(
"classpath:/static/fonts/DejaVuSans-Oblique.ttf",
"DejaVuSans-Oblique",
"ttf")),
Map.entry(
"fallback-dejavu-sans-boldoblique",
new FallbackFontSpec(
"classpath:/static/fonts/DejaVuSans-BoldOblique.ttf",
"DejaVuSans-BoldOblique",
"ttf")),
// DejaVu Serif family
Map.entry(
"fallback-dejavu-serif",
new FallbackFontSpec(
"classpath:/static/fonts/DejaVuSerif.ttf",
"DejaVuSerif",
"ttf")),
Map.entry(
"fallback-dejavu-serif-bold",
new FallbackFontSpec(
"classpath:/static/fonts/DejaVuSerif-Bold.ttf",
"DejaVuSerif-Bold",
"ttf")),
Map.entry(
"fallback-dejavu-serif-italic",
new FallbackFontSpec(
"classpath:/static/fonts/DejaVuSerif-Italic.ttf",
"DejaVuSerif-Italic",
"ttf")),
Map.entry(
"fallback-dejavu-serif-bolditalic",
new FallbackFontSpec(
"classpath:/static/fonts/DejaVuSerif-BoldItalic.ttf",
"DejaVuSerif-BoldItalic",
"ttf")),
// DejaVu Mono family
Map.entry(
"fallback-dejavu-mono",
new FallbackFontSpec(
"classpath:/static/fonts/DejaVuSansMono.ttf",
"DejaVuSansMono",
"ttf")),
Map.entry(
"fallback-dejavu-mono-bold",
new FallbackFontSpec(
"classpath:/static/fonts/DejaVuSansMono-Bold.ttf",
"DejaVuSansMono-Bold",
"ttf")),
Map.entry(
"fallback-dejavu-mono-oblique",
new FallbackFontSpec(
"classpath:/static/fonts/DejaVuSansMono-Oblique.ttf",
"DejaVuSansMono-Oblique",
"ttf")),
Map.entry(
"fallback-dejavu-mono-boldoblique",
new FallbackFontSpec(
"classpath:/static/fonts/DejaVuSansMono-BoldOblique.ttf",
"DejaVuSansMono-BoldOblique",
"ttf")));
private final ResourceLoader resourceLoader;
@ -198,7 +339,7 @@ public class PdfJsonFallbackFontService {
/**
* Resolve fallback font ID based on the original font name and code point. Attempts to match
* font family for visual consistency.
* font family and weight/style for visual consistency.
*
* @param originalFontName the name of the original font (may be null)
* @param codePoint the Unicode code point that needs to be rendered
@ -223,13 +364,22 @@ public class PdfJsonFallbackFontService {
String aliasedFontId = FONT_NAME_ALIASES.get(baseName);
if (aliasedFontId != null) {
// Detect weight and style from the normalized font name
boolean isBold = detectBold(normalized);
boolean isItalic = detectItalic(normalized);
// Apply weight/style suffix to fallback font ID
String styledFontId = applyWeightStyle(aliasedFontId, isBold, isItalic);
log.debug(
"Matched font '{}' (normalized: '{}', base: '{}') to fallback '{}'",
"Matched font '{}' (normalized: '{}', base: '{}', bold: {}, italic: {}) to fallback '{}'",
originalFontName,
normalized,
baseName,
aliasedFontId);
return aliasedFontId;
isBold,
isItalic,
styledFontId);
return styledFontId;
}
}
@ -237,6 +387,89 @@ public class PdfJsonFallbackFontService {
return resolveFallbackFontId(codePoint);
}
/**
* Detect if font name indicates bold weight.
*
* @param normalizedFontName lowercase font name without subset prefix or spaces
* @return true if bold weight is detected
*/
private boolean detectBold(String normalizedFontName) {
// Check for explicit bold indicators
if (normalizedFontName.contains("bold")
|| normalizedFontName.contains("heavy")
|| normalizedFontName.contains("black")) {
return true;
}
// Check for numeric weight indicators (600-900 = bold)
// Handles: "Arimo_700wght", "Arial-700", "Font-w700"
if (normalizedFontName.matches(".*[_-]?[6-9]00(wght)?.*")) {
return true;
}
return false;
}
/**
* Detect if font name indicates italic/oblique style.
*
* @param normalizedFontName lowercase font name without subset prefix or spaces
* @return true if italic style is detected
*/
private boolean detectItalic(String normalizedFontName) {
return normalizedFontName.contains("italic") || normalizedFontName.contains("oblique");
}
/**
* Apply weight/style suffix to fallback font ID.
*
* <p>Weight/style variants are only applied to font families where we have the actual font
* files available. Currently supported: - Liberation Sans: Regular, Bold, Italic, BoldItalic
* (full support) - Liberation Serif: Regular, Bold, Italic, BoldItalic (full support) -
* Liberation Mono: Regular, Bold, Italic, BoldItalic (full support) - Noto Sans: Regular, Bold,
* Italic, BoldItalic (full support) - DejaVu Sans: Regular, Bold, Oblique, BoldOblique (full
* support) - DejaVu Serif: Regular, Bold, Italic, BoldItalic (full support) - DejaVu Mono:
* Regular, Bold, Oblique, BoldOblique (full support)
*
* <p>To add weight/style support for additional font families: 1. Download the font files
* (Bold, Italic, BoldItalic) to: app/core/src/main/resources/static/fonts/ 2. Register the
* variants in BUILT_IN_FALLBACK_FONTS map (see lines 63-267) 3. Update the check below to
* include the font family prefix
*
* @param baseFontId base fallback font ID (e.g., "fallback-liberation-sans")
* @param isBold true if bold weight needed
* @param isItalic true if italic style needed
* @return styled font ID (e.g., "fallback-liberation-sans-bold"), or base ID if variants not
* available
*/
private String applyWeightStyle(String baseFontId, boolean isBold, boolean isItalic) {
// Only apply weight/style to font families where we have the font files available
// Supported: Liberation (Sans/Serif/Mono), Noto Sans, DejaVu (Sans/Serif/Mono)
boolean isSupported =
baseFontId.startsWith("fallback-liberation-")
|| baseFontId.equals("fallback-noto-sans")
|| baseFontId.startsWith("fallback-dejavu-");
if (!isSupported) {
return baseFontId;
}
// DejaVu Sans and Mono use "oblique" instead of "italic"
boolean useOblique =
baseFontId.equals("fallback-dejavu-sans")
|| baseFontId.equals("fallback-dejavu-mono");
if (isBold && isItalic) {
return baseFontId + (useOblique ? "-boldoblique" : "-bolditalic");
} else if (isBold) {
return baseFontId + "-bold";
} else if (isItalic) {
return baseFontId + (useOblique ? "-oblique" : "-italic");
}
return baseFontId;
}
/**
* Resolve fallback font ID based on Unicode code point properties.
*

View File

@ -23,15 +23,19 @@
- Update `buildFontMap` to resolve aliases when recreating PDFBox fonts, and adjust the front end to load programs via the canonical UID.
- Optional: expose a lazy endpoint for the original COS dictionary if the canonical record strips it, so export still reconstructs untouched fonts.
- **Font Weight Matching for Fallback Fonts**
- Font family matching is now implemented (Arial→LiberationSans, Times→LiberationSerif, Courier→LiberationMono).
- However, fallback fonts still use Regular weight for all missing glyphs, regardless of the original font weight (e.g., bold text falls back to regular weight).
- TODO: Parse weight from font names (e.g., `Arimo_700wght`, `Arial-Bold`, `TimesNewRoman,SemiBold`) and map to corresponding Liberation font variants:
- Regular/Normal → LiberationSans-Regular, LiberationSerif-Regular, LiberationMono-Regular
- Bold/700 → LiberationSans-Bold, LiberationSerif-Bold, LiberationMono-Bold
- Italic/Oblique → LiberationSans-Italic, LiberationSerif-Italic, LiberationMono-Italic
- BoldItalic → LiberationSans-BoldItalic, LiberationSerif-BoldItalic, LiberationMono-BoldItalic
- Add all Liberation font variants to `BUILT_IN_FALLBACK_FONTS` map with appropriate IDs (e.g., `fallback-liberation-sans-bold`).
- Update `resolveFallbackFontId(String originalFontName, int codePoint)` in `PdfJsonFallbackFontService.java` to detect weight/style and return the matching variant ID.
- Benefits: Better visual consistency when editing text in bold/italic fonts, as missing characters will match the original weight.
- Implementation reference: `app/proprietary/src/main/java/stirling/software/SPDF/service/PdfJsonFallbackFontService.java:186-213`
- **Font Weight Matching for Fallback Fonts** ✓ COMPLETED (January 2025)
- Font family matching is now implemented:
- Liberation fonts (metric-compatible with Microsoft core): Arial/Helvetica→LiberationSans, Times→LiberationSerif, Courier→LiberationMono
- DejaVu fonts (widely used open source): DejaVu→DejaVuSans, DejaVuSerif, DejaVuMono
- Noto fonts (Google universal font): Noto→NotoSans
- Font weight/style matching is now implemented for multiple font families:
- Liberation Sans/Serif/Mono: Regular, Bold, Italic, BoldItalic (full support)
- Noto Sans: Regular, Bold, Italic, BoldItalic (full support)
- DejaVu Sans/Serif/Mono: Regular, Bold, Italic/Oblique, BoldItalic/BoldOblique (full support)
- All font variants registered in `BUILT_IN_FALLBACK_FONTS` map (`PdfJsonFallbackFontService.java:63-267`)
- Weight/style detection implemented in `resolveFallbackFontId()`:
- `detectBold()`: Detects "bold", "heavy", "black", or numeric weights 600-900 (e.g., "700wght")
- `detectItalic()`: Detects "italic" or "oblique"
- `applyWeightStyle()`: Applies appropriate suffix (handles both "italic" and "oblique" naming)
- All fonts consolidated from Type3 library into main fonts directory for unified fallback support
- Benefits: Comprehensive visual consistency when editing text in bold/italic fonts across many font families