Skip to content

Commit ec939ad

Browse files
authored
Detect bold for fonts named "*heavy" (#131) (#247)
Font weight names heavier than bold (e.g. FranklinGothic-Heavy) were not flagged as bold because the detection only matched "bold"/"_bd". Add "heavy" to both font-name checks, matching the fix reported in #131. Black is a 900 weight, heavier than Bold, and sits alongside Heavy at the top of the weight scale (... Semibold, Bold, ExtraBold, Heavy, Black, ExtraBlack, Ultra). The existing "bold" substring already covers Bold, ExtraBold, UltraBold and Semi/Demibold, and the previous commit added Heavy, which left Black as the only common weight above bold still reported as regular. Measured over 400 documents of the GROBID end-to-end corpus (4.15M tokens, raw font names via -fullFontName): bold tokens, master 81,153 bold tokens, with "heavy" and "black" 149,861 (+68,708) Of that gain "black" accounts for roughly 63,000 tokens and "heavy" for 5,700, so Black is by a wide margin the larger of the two omissions. 67 of the 400 documents are affected; token counts are unchanged in all of them, i.e. the change is style-only and does not alter segmentation or extracted text. All 135 distinct font names containing "black" in that corpus are genuine 900-weight faces (Avenir LT Std Black and its oblique). No blackletter or decorative faces (Blackadder, Blackoak, Fraktur) appear, so the substring does not produce false positives here. Deliberately not matched: - "ext": ambiguous. Extended is a width designation, not a weight, and the corpus hits are all false positives (tex_cm_maths_extension, stixmathextensions, mt-extra, and charterbt-roman whose subset prefix happens to end in "ext"). - "blk", "ultra", "-bd": real but negligible here (15, 8 and 7 tokens). Bare abbreviations such as "bl" or "x" would match far too much; if they are ever wanted they should be delimiter-anchored the way "_bd" already is. * Don't match font style keywords inside the subset tag A subsetted font's name begins with six letters and a plus sign, and per PDF 32000-1 9.6.4 "the choice of letters is arbitrary" -- the tag identifies a subset, not a style. The style checks ran strstr() over the whole name, tag included, so the tag could spell a keyword: ABCDEF+Helvetica -> (no style) BOLDXY+Helvetica -> bold same font, same text ITALIC+Helvetica -> italics The style of a word therefore depended on a tag the producer picked arbitrarily rather than on the font. Re-saving a document with another tool changes the tag, so the same document could come out bold or not. It also works against font deduplication (#233): two subsets of one face can get different tags and so split into different TextStyle entries. This applies to every keyword, including the "heavy" and "black" added in the preceding commits, and to the italic checks. Only a conformant tag is skipped. Anything else before a '+' belongs to the real name and is kept -- notably the synthetic "cidfont+fN" names used for unnamed CID fonts, which account for ~14,500 tokens in the GROBID end-to-end corpus and would be mangled by cutting at the first '+'. 36 names in that corpus contain more than one '+', so the tag is matched by position rather than by searching for a separator. Anchoring the keywords to the end of the name instead was considered and rejected: it would lose 52% of genuine matches, because the weight is routinely followed by another modifier (arial-boldmt, avenirltstd- blackoblique, arial-bolditalicmt). Only the tag region is ambiguous, so only it is excluded. Verified over 400 documents of the GROBID end-to-end corpus: output is byte-identical to master in all 400. No tag in that corpus (8,586 subsetted fonts) spells a keyword, which is expected -- the odds are about 1 in 152,000 for "bold" and far lower for the others. This is a correctness fix for a rare case, not a behaviour change. The same guard is applied to the disabled TextWord constructor so the two stay in step if that block is ever re-enabled.
1 parent c28fb9e commit ec939ad

1 file changed

Lines changed: 39 additions & 10 deletions

File tree

src/XmlAltoOutputDev.cc

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -553,11 +553,33 @@ int TextChar::cmpY(const void *p1, const void *p2) {
553553
}
554554

555555

556+
// Skip a PDF subset tag when matching a font name for style keywords.
557+
//
558+
// A subsetted font's name begins with exactly six letters and a plus sign, and
559+
// "the choice of letters is arbitrary" (PDF 32000-1 9.6.4) -- the tag carries no
560+
// style information. Searching it means "BOLDXY+Helvetica" is reported bold while
561+
// the identical "ABCDEF+Helvetica" is not, so the style of a word would depend on
562+
// a tag the producer picked at random rather than on the font.
563+
//
564+
// Only a conformant tag is skipped: anything else before a '+' is part of the real
565+
// name and must be kept, e.g. the synthetic "cidfont+f2" used for unnamed CID fonts.
566+
static const char *skipSubsetTag(const char *fontName) {
567+
if (fontName && strlen(fontName) > 7 && fontName[6] == '+' &&
568+
isalpha((unsigned char) fontName[0]) && isalpha((unsigned char) fontName[1]) &&
569+
isalpha((unsigned char) fontName[2]) && isalpha((unsigned char) fontName[3]) &&
570+
isalpha((unsigned char) fontName[4]) && isalpha((unsigned char) fontName[5])) {
571+
return fontName + 7;
572+
}
573+
return fontName;
574+
}
575+
556576
#if 0
577+
557578
//------------------------------------------------------------------------
558579
// TextWord
559580
//------------------------------------------------------------------------
560581

582+
561583
TextWord::TextWord(GList *charsA, int start, int lenA,
562584
int rotA, int dirA, GBool spaceAfterA, GfxState *state,
563585
TextFontInfo *fontA, double fontSizeA, int idCurrentWord,
@@ -678,15 +700,18 @@ TextWord::TextWord(GList *charsA, int start, int lenA,
678700
//Type 1 font. (See implementation note 62 in Appendix H.)
679701
fontName = strdup(fontA->getFontName()->getCString());
680702
char* localLowerFontName = fontA->getFontName()->lowerCase()->getCString();
681-
if (strstr(localLowerFontName, "bold") ||
682-
strstr(localLowerFontName, "_bd")) {
703+
const char* styleName = skipSubsetTag(localLowerFontName);
704+
if (strstr(styleName, "bold") ||
705+
strstr(styleName, "heavy") ||
706+
strstr(styleName, "black") ||
707+
strstr(styleName, "_bd")) {
683708

684709
bold = gTrue;
685710
}
686711

687-
if (strstr(localLowerFontName, "italic") ||
688-
strstr(localLowerFontName, "oblique") ||
689-
strstr(localLowerFontName, "_it")) {
712+
if (strstr(styleName, "italic") ||
713+
strstr(styleName, "oblique") ||
714+
strstr(styleName, "_it")) {
690715

691716
italic = gTrue;
692717
}
@@ -893,6 +918,7 @@ TextWord::~TextWord() {
893918
}
894919
#endif
895920

921+
896922
//------------------------------------------------------------------------
897923
// TextRawWord
898924
//------------------------------------------------------------------------
@@ -1014,12 +1040,15 @@ TextRawWord::TextRawWord(GfxState *state, double x0, double y0,
10141040
//Type 1 font. (See implementation note 62 in Appendix H.)
10151041
fontName = strdup(state->getFont()->getName()->getCString());
10161042
char *localLowerFontName = state->getFont()->getName()->lowerCase()->getCString();
1017-
if (strstr(localLowerFontName, "bold") ||
1018-
strstr(localLowerFontName, "_bd"))
1043+
const char *styleName = skipSubsetTag(localLowerFontName);
1044+
if (strstr(styleName, "bold") ||
1045+
strstr(styleName, "heavy") ||
1046+
strstr(styleName, "black") ||
1047+
strstr(styleName, "_bd"))
10191048
bold = gTrue;
1020-
if (strstr(localLowerFontName, "italic") ||
1021-
strstr(localLowerFontName, "oblique") ||
1022-
strstr(localLowerFontName, "_it"))
1049+
if (strstr(styleName, "italic") ||
1050+
strstr(styleName, "oblique") ||
1051+
strstr(styleName, "_it"))
10231052
italic = gTrue;
10241053
} else {
10251054
fontName = NULL;

0 commit comments

Comments
 (0)