De HotPDF Delphi Component verwijdert sinds v2.768.15 de dubbele tekens die fake bold en overgedrukte logo's achterlaten in geëxtraheerde PDF-tekst. Wanneer een producent dezelfde glyph twee keer tekent op een fractie van een punt afstand, lezen ExtractLoadedPageText, ExtractLoadedPageTextLayout en ExtractLoadedPageTextColumns dat teken nu één keer, zodat "Bold words" niet meer uitkomt als "Bold wordsBold words"
Dit is zo'n bug die eruitziet als een decoderfout en dat niet is. Elke glyph decodeert correct; de tekst staat echt twee keer op de pagina. De fix moet beslissen welke van twee volstrekt geldige glyphs de kopie is, en dat doen zonder legitieme herhaalde letters op te eten zoals de dubbele l in "fill" of de identieke cellen van een tabel. De rest van dit artikel legt uit welke regel HotPDF gebruikt, welke drempels erachter zitten, waar die bewust afwijkt van andere extractietools, en over een tweede, ongerelateerde extractiebug uit dezelfde release-trein die ook tekst van regeleinden afhakte
Waarom produceert fake bold dubbele tekst bij PDF-extractie?
Fake bold verdubbelt geëxtraheerde tekst omdat de producent de string twee keer tekent en beide kopieën echte tekstshow-operaties zijn. Heeft een fontfamilie geen bold snit, of kan de authoringtool het gewoon niet schelen, dan emuleren veel generatoren bold door dezelfde string te tonen met Tj of TJ, de tekstpositie enkele tienden van een punt naar rechts te schuiven en hem opnieuw te tonen. Op het scherm smelten de twee kopieën samen tot een zwaardere streep. In de content stream staan twee complete reeksen character codes, en ISO 32000-1 §9.4 geeft een extractor geen enkele vlag die zegt "deze is decoratie"
Overgedrukte logo's raken hetzelfde probleem vanaf een andere hoek. Een logotype kan één keer als fill getekend zijn en nog een keer als stroke of als licht verschoven schaduw, met hetzelfde font en dezelfde tekens. Een naïeve extractor die de content stream in tekenvolgorde doorloopt, stuurt de hele eerste run uit en daarna de hele tweede, en daarom leest het resultaat "Bold wordsBold words" in plaats van "BBoolldd". Vóór v2.768.15 hield de tekstassemblage van HotPDF beide kopieën aan, en de duplicaat brak hetgene waar de meeste aanroepers echt om geven: een zoekactie naar "Bold words" over de tekst van de pagina kon falen omdat de woordgrens tussen de twee runs verdwenen was
Echte text rendering mode 2 (fill daarna stroke, ISO 32000-1 §9.3.6) heeft dit probleem niet, want dat is één tekstshow-operatie die één set glyphs produceert. De duplicaten verschijnen alleen wanneer de producent de tekst twee keer uitzendt
Hoe bepaalt HotPDF welke glyph de duplicaat is?
HotPDF behandelt twee glyphs als één getekend teken wanneer ze dezelfde Unicode-waarde voeren, dezelfde schrijfrichting hebben, in grootte binnen 5% van elkaar zitten en bijna op elkaar liggen: niet verder dan 0.1 van de glyphgrootte uit elkaar langs de baseline en niet verder dan 0.2 van de glyphgrootte dwars erop. Van elk zo'n paar blijft de eerst getekende glyph over en valt de latere weg. Dat zijn de waarden in de v2.768.15-implementatie, en elk daarvan staat er met reden
- Zelfde Unicode, zelfde richting. Alleen identieke tekens kunnen duplicaten zijn, en een horizontale glyph wordt nooit vergeleken met een verticale (WMode 1)
- Grootte binnen 5%. Een grote kopletter en een kleine bijschriftletter die toevallig overlappen zijn niet dezelfde tekst, dus een grootteverschil schrapt een paar
- 0.1 van de grootte langs de baseline. Fake-bold-offsets zijn enkele tienden van een punt, enkele procenten van een normale tekstgrootte. Twee echte aangrenzende letters, zelfs strak gekerende "ll", liggen grofweg een volledige advance width uit elkaar, een orde van grootte meer dan deze tolerantie
- 0.2 van de grootte dwars op de baseline. Schaduwen en outline-passes schuiven vaak ook omlaag, niet alleen naar rechts, dus de verticale tolerantie is ruimer, maar nog ver onder de afstand tot de volgende regel
- Eerst getekend wint. De eerdere glyph houden behoudt de leesvolgorde van de eerste run, dus de overlevende tekst stroomt nog steeds zoals de producent hem schreef
De afstanden worden gemeten in het eigen baseline-frame van de glyph in plaats van ruwe pagina-X en -Y, dus gedraaide tekst en tekst op een scheve baseline gebruiken dezelfde toleranties als gewone horizontale regels. De gebruikte grootte is de grootte van de glyph zoals getekend op de pagina, afgeleid uit zijn quad, niet de grootteoperand die aan Tf meegegeven werd; dat telt voor documenten die Tf op 1 zetten en alles via de text matrix schalen, waar een ruwe fontgrootte elke tolerantie nutteloos zou maken
Spaties zijn nooit kandidaat. Code points tot U+0020, de no-break space U+00A0 en de ideografische spatie U+3000 worden overgeslagen, want lege glyphs voeren geen zichtbare inkt om te dupliceren, en er een weglaten zou woorden aan elkaar plakken
De controle snel houden op dichte pagina's
De duplicaatcontrole in HotPDF draait in grofweg n log n-tijd, niet n kwadraat, want hij sorteert eerst en vergelijkt alleen glyphs binnen een klein venster. Elke glyph met elke andere glyph vergelijken zou correct en onbruikbaar zijn: een dichte pagina met kleine lettertjes draagt makkelijk tienduizenden glyphs, en de controle draait bij elke tekstextractie-aanroep
De interne routine sorteert de niet-spatie-glyphs op Unicode-waarde, daarna op origin-X, daarna op tekenvolgorde, met een stabiele merge sort. Na het sorteren zit elke potentiële duplicaat van een glyph direct na hem in de volgorde, dus de scan loopt alleen vooruit zolang de Unicode-waarde matcht en het X-verschil binnen 0.25 van de glyphgrootte blijft. Het venster van 0.25 is bewust ruimer dan de 0.1-tolerantie langs de baseline, zodat een paar op een licht scheve baseline er nog binnen valt voordat de precieze baseline-frame-test draait. Eén subtiel geval: komt de scan vanaf glyph A een matchende glyph tegen die vóór A getekend werd, dan is A de kopie, valt A weg en stopt de scan, terwijl de eerdere glyph de vergelijking met alles verderop voortzet. Die regel maakt dat "eerst getekend wint" ook voor drievoudige stempels geldt, niet alleen voor paren
De vergelijking is op Unicode-waarde, niet op font of character code. Twee glyphs die via verschillende fonts naar dezelfde Unicode mappen — bijvoorbeeld een fill-pass in de ene embedded subset en een stroke-pass in de andere — worden alsnog als één teken herkend. De keerzijde: een glyph waarvan de code naar Unicode 0 resolveert heeft niets betrouwbaars om te vergelijken, wat nog een reden is om het aantal unresolved in de gaten te houden, dat behandeld wordt in tekst extraheren uit geladen PDF's met HotPDF
Welke HotPDF-extractieaanroepen verwijderen dubbele glyphs?
Alle tekstproducerende aanroepen in HotPDF verwijderen duplicaten, en de ruwe glyph-aanroep doet dat niet. De deduplicatie zit in THotPDF.ExtractVisibleLoadedPageGlyphs, de stap die ook glyphs buiten de zichtbare box van de pagina weggooit, en de platte tekst-, kolom-, lay-out- en semantische tekstextractie lezen hun glyphs er allemaal uit. THotPDF.ExtractLoadedPageGlyphs geeft nog steeds elke glyph terug die de content stream tekent, overprints incluis, want aanroepers die highlighting, redaction of forensische tooling bouwen hebben het volledige verslag nodig van wat er getekend werd
Voor gewoon tekstwerk verandert er niets in de aanroepende code. Dezelfde lus die vóór v2.768.15 verdubbelde tekst extraheerde, extraheert elk woord nu één keer
var
Pdf: THotPDF;
PageCount, I: Integer;
PageText: UnicodeString;
begin
Pdf := THotPDF.Create(nil);
try
PageCount := Pdf.LoadFromFile('brochure.pdf');
for I := 0 to PageCount - 1 do
if Pdf.ExtractLoadedPageText(I, PageText) then
if Pos('Bold words', PageText) > 0 then
Writeln('Found on page ', I + 1);
finally
Pdf.Free;
end;
end;
Lay-out- en kolomuitvoer profiteert net zo hard, want een gedupliceerde run duwde vroeger de rest van een regel zijwaarts op het tekengrid. ExtractLoadedPageTextColumns behoudt zijn optionele gap-multiplier; het duplicaatfilter draait vóór het kolomsplitsen, dus het interageert niet met die instelling
var
Grid, Columns: UnicodeString;
begin
if Pdf.ExtractLoadedPageTextLayout(0, Grid) then
TFile.WriteAllText('page1-layout.txt', Grid, TEncoding.UTF8);
// Kolommen splitsen waar een gap boven 3x de mediane glyph-advance uitkomt
if Pdf.ExtractLoadedPageTextColumns(0, Columns, 3.0) then
TFile.WriteAllText('page1-columns.txt', Columns, TEncoding.UTF8);
end;
Wilt u weten of een pagina überhaupt overgedrukte tekst bevat, vergelijk dan de twee glyph-aanroepen. Wees ervan bewust dat het verschil ook glyphs telt die buiten de crop box vallen, dus behandel het als bovengrens voor duplicaten in plaats van als exact cijfer
var
AllGlyphs, Visible: THPDFGlyphArray;
begin
if Pdf.ExtractLoadedPageGlyphs(0, AllGlyphs) and
Pdf.ExtractVisibleLoadedPageGlyphs(0, Visible) then
Writeln(Format('%d painted, %d read as text (%d overprinted or off-page)',
[Length(AllGlyphs), Length(Visible),
Length(AllGlyphs) - Length(Visible)]));
end;
De regels voor woordspatiëring en regeleinde die deze glyphs tot tekst maken, blijven door het filter onaangetast; ze worden in detail behandeld in hoe HotPDF woordspaties en regeleinden plaatst in de tekst van een pagina
Waar de regel bewust afwijkt van andere tools
HotPDF laat exacte duplicaten vallen die sommige andere extractietools aanhouden, en dat verschil is opzettelijk. Toen de v2.768.15-wijziging tegen een referentiecorpus werd getest, extraheerde één voettekst die dubbel op exact dezelfde positie getekend was één keer in HotPDF en twee keer in een referentie-extractor. Een glyph getekend bovenop een identieke glyph is onzichtbaar als apart teken, dus hem één keer lezen is wat de pagina werkelijk toont
De eerlijke beperking loopt de andere kant op. Fake bold die de tweede pass meer dan 0.1 van de glyphgrootte langs de baseline verschuift, wordt niet gevangen, en een schaduw-pass op merkbaar andere grootte ook niet. Die lay-outs zijn in de praktijk zeldzaam, want het visuele effect hoort op te houden als bold zodra de offset zo groot wordt, maar ziet u verdubbelde tekst van een decoratieve kop, vergelijk dan de glyph-X-waarden uit ExtractLoadedPageGlyphs voordat u aanneemt dat het filter stuk is. Het filter doet ook niets voor duplicaten die niet geometrisch op dezelfde plek zitten, zoals een koptekst die in een verborgen laag elders op de pagina herhaald wordt; dat is een inhoudskwestie, geen overprint
Waarom vielen regeleinden weg in CID-fonttekst?
Een aparte bug, gefixt in HotPDF v2.768.12, liet de uiteinden van regels vallen in tekst gezet met CID-keyed (Type 0) fonts waarvan de /W-breedtearray niet op CID gesorteerd was. ISO 32000-1 §9.7.4.3 definieert het formaat van de /W-array maar eist niet dat de entries in oplopende CID-volgorde staan, en sommige generatoren schrijven ze in de volgorde waarin glyphs voor het eerst in het document gebruikt werden
De decoder van de tekstextractie zocht breedtes op met een binaire zoekactie, die stilletjes uitgaat van gesorteerde invoer. Op een ongesorteerde array misten de meeste opzoekingen en viel alles terug op de defaultbreedte /DW, die 1000 units is, een volle em. Latijnse tekst in zo'n font werd dus gezet alsof elke letter full-width was, regels liepen ver voorbij de rechterrand van de pagina, en de glyphs die buiten de zichtbare box belandden werden weggegooid, zodat de uiteinden van regels uit geëxtraheerde tekst en uit zoeken verdwenen. Niets crashte en niets werd gemeld, en dat maakte het moeilijk te vinden
HotPDF sorteert de /W-entries nu met een stabiele merge sort direct na het inlezen, zodat waar twee entries overlappen degene die eerst geschreven werd wint, passend bij hoe de array in bestandsvolgorde leest. De paginarenderder is nooit geraakt, want die houdt een eigen kopie van de breedtedata aan en scant die lineair, en de verticale breedtearray /W2 wordt ook lineair gescand. Die scheiding is het onthouden waard wanneer een pagina er perfect uitziet maar slecht extraheert: rendering en extractie delen in een PDF-bibliotheek vaak veel minder code dan u zou verwachten
FAQ
Verwijdert HotPDF herhaalde letters zoals de dubbele l in "fill"?
Nee. Twee echte aangrenzende letters liggen grofweg één advance width uit elkaar, terwijl HotPDF alleen identieke tekens samenvoegt die binnen 0.1 van de glyphgrootte langs de baseline en 0.2 dwars erop zitten, dus legitieme herhalingen blijven staan
Hoe krijg ik elke glyph, inclusief overgedrukte duplicaten?
Roep THotPDF.ExtractLoadedPageGlyphs aan, die elke glyph geeft die de pagina tekent in tekenvolgorde. THotPDF.ExtractVisibleLoadedPageGlyphs en alle tekstextractie-aanroepen geven de ontdubbelde, zichtbare set terug
Welke HotPDF-versie fixt verdubbelde tekst door fake bold?
HotPDF v2.768.15 voegde duplicaat-glyph-verwijdering toe aan tekst-, lay-out-, kolom- en semantische tekstextractie, en v2.768.12 fixte ontbrekende regeleinden in CID-fonts met een ongesorteerde /W-array
Beide fixes zitten in de huidige HotPDF Delphi PDF component, die tekst uit geladen PDF's extraheert in Delphi en C++Builder naast zijn PDF-creatie-, bewerkings- en renderingfuncties