HotPDF Delphi Component bygger upp ordmellanrum och radbrytningar i THotPDF.ExtractLoadedPageText från glyfgeometri, inte från blanktecken. Ett mellanslag läggs in när gapet efter en glyfs egen bredd överstiger 0.15 av texthöjden, och en ny rad börjar bara när textorigo flyttas över skrivriktningen med mer än hälften av texthöjden. Sedan v2.768.3 omfattar sidtexten också text som ritas genom Form XObjects och lämnar utanför glyfer utanför det synliga crop-området. Resten av denna text förklarar varför varje regel ser ut som den gör, för var och en av dem ersatte en enklare regel som producerade rimlig men felaktig utdata på riktiga dokument
Symtomen är välbekanta för alla som har matat PDF-text in i ett sökindex. Ett försättsblad extraheras som PDFReferenceManualNovember4,1998, ett skatteformulär delas upp i 156 rader, en diagonal vattenstämpel anländer ett tecken per rad, och en beskuren korrekturfil inleds med skrivarens slug-rad som ingen visare någonsin visar. Ingen av dessa filer är trasig. Var och en använder ett helt lagligt sätt att placera text som en naiv extraktor missläser
Varför tappar extraherad PDF-text sina ordmellanrum?
Extraherad text tappar ordmellanrum för att en PDF aldrig krävs innehålla dem. En producent kan skilja ord åt genom att visa ett blanktecken, men den kan lika gärna flytta pennan med ett tal inuti en TJ-array (ISO 32000-1 §9.4.3) eller med en ny Td (§9.4.2), och TeX-utdata, många Distiller-filer och de flesta marginaljusterade layouter gör exakt det. Före v2.766.76 tittade HPDFAssemblePageText bara på vertikal rörelse, så ett ordbryt framställd genom positionering försvann helt enkelt. Assemblern mäter nu, längs föregående glyfs skrivriktning, avståndet från slutet av den glyfens egen bredd till den aktuella glyfens origo och lägger in ett mellanslag när avståndet överstiger 0.15 av den aktuella glyfens boxhöjd, mätt från ascent till descent i user space. Inget mellanslag läggs till när någon av sidorna redan är tom, och inget mellan två CJK-tecken, eftersom marginaljustering drar isär ideografier utan att det glappet betyder en ordbrytning. Glyfposterna exponerar samma geometri, så du kan reproducera beslutet när en viss fil förbryllar dig
uses
SysUtils, HPDFDoc, HPDFContentStream;
procedure DumpWordGaps(Pdf: THotPDF; PageIndex: Integer);
var
Glyphs: THPDFGlyphArray;
I: Integer;
Height, Gap: Double;
begin
if not Pdf.ExtractLoadedPageGlyphs(PageIndex, Glyphs) then
Exit;
for I := 1 to High(Glyphs) do
begin
// ascent-till-descent-höjd för glyfboxen, i user space
Height := Sqrt(Sqr(Glyphs[I].QuadX[3] - Glyphs[I].QuadX[0]) +
Sqr(Glyphs[I].QuadY[3] - Glyphs[I].QuadY[0]));
// horisontell text: gap från föregående glyfs egen breddslut
Gap := Glyphs[I].BaselineStartX - Glyphs[I - 1].GlyphEndX;
if (Height > 0) and (Gap > 0.15 * Height) then
Writeln(Format('U+%.4x gap %.2f height %.2f: space',
[Glyphs[I].Unicode, Gap, Height]));
end;
end;
Varför mäta från glyfens egen bredd i stället för pennpositionen?
HotPDF mäter ordgap från GlyphEndX / GlyphEndY eftersom pennpositionen efter en glyf redan innehåller mellanrum som inte är ett gap. ISO 32000-1 §9.4.4 definierar den horisontella förskjutningen som glyfbredden gånger fontstorleken, plus teckenmellanrum Tc, plus ordmellanrum Tw, allt skalat med Tz. BaselineEndX / BaselineEndY håller den fullständiga förskjutningen, medan GlyphEndX / GlyphEndY bara håller fontens advance och Tz. Skillnaden spelar roll för producenter som åtstramar spärrningen med ett negativt Tc och sedan räcker tillbaka utrymmet genom en TJ-justering efter varje glyf: mätt från pennpositionen ser tillbakaräckningen ut som ett gap, och det kinesiska begreppet “95后” extraherades som “9 5 后”. Tröskeln är knuten till glyfboxens höjd i stället för till Tf-storleken av liknande skäl. Word-exporter skriver ofta 1 Tf och bär den verkliga storleken i en skalad Tm, så Tfs säger 1 medan texten är 10 punkter hög, och en regel nycklad till Tfs skulle behandla samma sidas två stavningar olika
Regeln har ärliga kanter. En rubrik satt med mycket lös spärrning, där Tc ensam öppnar mer än 0.15 av texthöjden mellan bokstäver, extraheras med ett mellanslag mellan varje bokstav, vilket är vad sidan ser ut som men förmodligen inte vad du ville indexera. Bitar ritade i fel ordning på en baslinje ger ett negativt gap och fogas ihop utan mellanslag. Inget av fallen är vanligt i brödtext, och på en testkorpus höjde ändringen ordmatchningar mot en referensextraktor på 28 sidor utan att sänka någon
När börjar HotPDF en ny rad i extraherad text?
Sedan v2.766.79 börjar en ny rad när flyttningen från föregående glyfs origo till den aktuella, projicerad på normalen av föregående skrivriktning, överstiger hälften av den större boxhöjden hos de två glyferna. Den tidigare regeln jämförde den råa Y-förflyttningen med hälften av Tfs, vilket misslyckades i två riktningar. Med 1 Tf och en skalad Tm krympte tröskeln till en halv enhet, så ett upphöjt tecken lyft av en text rise på 0.4 eller vanlig baslinjebrytning bröt raden. Regeln ignorerade också X helt, så text under en roterad Tm steg nedåt på sidan med varje glyf och kom ut en glyf per rad. Att projicera på riktningsnormalen får roterade körningar att bete sig som horisontella, och att ta det större av de två höjderna håller ett stort exempelord och dess lilla bildtext på en rad när de delar baslinje. På skatteformuläret ovan sjönk radantalet från 156 till 97. Vertikal text i skrivläge 1 (§9.7.4.3) följer en separat väg: de glyferna grupperas i kolumner, läses höger till vänster och uppifrån och ned, med radbrytning vid varje kolumnbyte
Vilken text inkluderar eller lämnar ExtractLoadedPageText?
ExtractLoadedPageText returnerar den text en visare visar. Sedan v2.766.80 arbetar den från de synliga glyferna bara och släpper varje glyf vars boxmitt faller utanför GetLoadedPageVisibleBox, som är CropBox:en klippt mot MediaBox:en (§14.11.2). Det tar bort slug-rader och andra skrivarmärken satta som text utanför beskärningsområdet. ExtractLoadedPageGlyphs fortsätter medvetet att returnera varje glyf i sidans innehållsström, så du kan fortfarande hitta det materialet när du behöver det. Filtret är ett boxtest, inte ett synlighetstest: text dold av en klippväg, ritad i vitt eller täckt av en bild extraheras fortfarande
var
Pdf: THotPDF;
Glyphs: THPDFGlyphArray;
PageText: UnicodeString;
L, B, R, T: Single;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('trimmed-proof.pdf');
if Pdf.GetLoadedPageVisibleBox(0, L, B, R, T) then
Writeln(Format('Visible box: %.1f %.1f %.1f %.1f', [L, B, R, T]));
// varje glyf i sidans innehållsström, slug-raden inkluderad
if Pdf.ExtractLoadedPageGlyphs(0, Glyphs) then
Writeln(Length(Glyphs), ' glyphs in the page content stream');
// bara det sidan visar, med Form XObject-text inskriven
if Pdf.ExtractLoadedPageText(0, PageText) then
Writeln(PageText);
finally
Pdf.Free;
end;
end;
Text ritad genom Form XObjects är en del av sidtexten sedan v2.768.3. Sidhuvuden, stämplar och vattenstämplar bor mycket ofta i formulär, och vissa standarddokument förlorade 30 till 35 procent av sina tecken före ändringen. THotPDF.InterpretContentWithForms registrerar varje Do tillsammans med den CTM som gäller, tolkar formuläret med dess /Matrix gånger den CTM:en (§8.10.1) och skriver in formulärets glyfer på Do:ets position, rekursivt in i nästlade formulär. Ett formulär utan egen /Resources lånar strömmens som ritar det, vilket §7.8.3 tillåter. Formglyfer bär TokenIndex = -1, och ExtractLoadedPageGlyphs returnerar fortfarande bara sidströms-glyfer, eftersom sökning, ersättning och redaktion skriver tillbaka ändringar genom TokenIndex och skulle redigera fel byte om en formglyf smög in. Två förenklingar är värda att känna till: formtext klipps inte till formulärets /BBox, och rekursionen stannar vid 12 nivåer i stället för att upptäcka cykler, så ett felformat formulär som ritar sig självt upprepar sin text tills det når den gränsen
Varför avkods text efter en Q-operator som skräp?
Text efter Q kunde avkodas felaktigt före v2.766.73 för att extraktorn bara sparade CTM:en vid q. Texttillståndsparametrarna, nämligen font, storlek, Tc, Tw, Tz, TL, renderingsläge och rise, hör till grafiktillståndet (§9.3.1), så Q måste återställa dem tillsammans med allt annat på stacken (§8.4.2). En branschrapport valde ett tvåbyte Identity-H-font inuti q … Q och visade sedan enbyte-WinAnsi-text utan egen Tf. Extraktorn behöll det inre fontet, läste punktraderna och ordet “Adobe” på innehållsförteckningen som tvåbyte-koder och tappade 15% av sidans tecken. Tolkens q/Q-stack håller nu hela texttillståndet. Extraktionsreglerna som beskrivs här gäller varje sida, så ett helt dokument kan hamna i en fil med ett anrop
var
Output: TFileStream;
Pages: Integer;
begin
Output := TFileStream.Create('report.txt', fmCreate);
try
// tomt intervall = alla sidor; form feed mellan sidor; UTF-8-BOM
Pages := Pdf.ExtractLoadedPagesTextToStream(Output, '', #12, True);
Writeln(Pages, ' pages extracted');
finally
Output.Free;
end;
end;
Vilket HotPDF-text-API ska du använda?
ExtractLoadedPageText stannar i innehållsströmsordning, vilket är rätt standardval för sökning och indexering; avkodningskedjan under den täcks i att extrahera text ur inlästa PDF:er med HotPDF. För taggade dokument där författningsordningen spelar roll går strukturordnings-textextraktion strukturträdet igenom i stället för att gissa ur geometrin, och för data låsta i tabeller returnerar typad tabellextraktion över sidbrytningar celler i stället för rader. Fullständig API-referens och en testnedladdning finns på produktsidan för HotPDF Delphi PDF Component