HotPDF Delphi Component genopbygger ordmellemrum og linjeskift i THotPDF.ExtractLoadedPageText ud fra glyfgeometri, ikke ud fra mellemrumstegn. Et mellemrum kommer ind, når afstanden efter en glyfs egen bredde overstiger 0,15 af teksthøjden, og en ny linje starter først, når tekst-origen flytter sig på tværs af skriveretningen med mere end halvdelen af teksthøjden. Siden v2.768.3 omfatter sideteksten også tekst tegnet gennem Form XObjects og udelader glyffer uden for det synlige crop-område. Resten af denne artikel forklarer, hvorfor hver regel ser ud, som den gør, for hver eneste af dem erstattede en enklere regel, der gav plausibelt men forkert output på rigtige dokumenter
Symptomerne er kendte for alle, der har fodret PDF-tekst ind i et søgeindeks. Et forsiderekstrakt giver PDFReferenceManualNovember4,1998, en selvangivelse splittes i 156 linjer, et diagonalt vandmærke ankommer ét tegn pr. linje, og en beskåret korrektur åbner med printervens slug-linje, som ingen viewer nogensinde viser. Ingen af disse filer er ødelagt. Hver af dem bruger en fuldt lovlig måde at placere tekst på, som en naiv extractor mislæser
Hvorfor mister udtrukket PDF-tekst sine ordmellemrum?
Udtrukket tekst mister sine ordmellemrum, fordi en PDF aldrig er forpligtet til at indeholde dem. En producent kan adskille ord ved at vise et mellemrumstegn, men den kan lige så vel flytte pennen med et tal inde i et TJ-array (ISO 32000-1 §9.4.3) eller med en frisk Td (§9.4.2), og TeX-output, mange Distiller-filer og de fleste justification-layouts gør netop det. Før v2.766.76 kiggede HPDFAssemblePageText kun på lodret bevægelse, så et ordskift lavet med positionering forsvandt bare. Assembleren måler nu, langs den forrige glyfs skriveretning, afstanden fra enden af den glyfs egen bredde til den aktuelle glyfs origen og indsætter ét mellemrum, når afstanden overstiger 0,15 af den aktuelle glyfs boks-højde, målt fra ascent til descent i user space. Ingen mellemrum tilføjes, når en af siderne allerede er blank, og ingen mellem to CJK-tegn, for justification strækker ideografer fra hinanden uden at den strækning betyder en ordgrænse. Glyf-recordne eksponerer samme geometri, så du kan genskabe beslutningen, når en bestemt fil driller
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-til-descent-højde af glyph-boksen, i user space
Height := Sqrt(Sqr(Glyphs[I].QuadX[3] - Glyphs[I].QuadX[0]) +
Sqr(Glyphs[I].QuadY[3] - Glyphs[I].QuadY[0]));
// vandret tekst: gab fra den forrige glyfs egen bredde-slut
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;
Hvorfor måle fra glyfens egen bredde i stedet for pen-positionen?
HotPDF måler ordgab fra GlyphEndX / GlyphEndY, fordi pen-positionen efter en glyf allerede indeholder mellemrum, der ikke er et gab. ISO 32000-1 §9.4.4 definerer den vandrette forskydning som glyfbredden ganget med fontstørrelsen plus tegnafstand Tc plus ordmellemrum Tw, alt sammen skaleret med Tz. BaselineEndX / BaselineEndY holder den fulde forskydning, mens GlyphEndX / GlyphEndY kun holder font-fremskridtet og Tz. Forskellen betyder noget for producenter, der strammer tracking med en negativ Tc og derefter giver pladsen tilbage gennem en TJ-justering efter hver glyf: målt fra pen-positionen ligner tilbøjegivelsen et gab, og det kinesiske udtryk "95后" blev udtrukket som "9 5 后". Tærsklen er knyttet til glyph-boksens højde frem for Tf-størrelsen af en lignende grund. Word-eksporter skriver ofte 1 Tf og bærer den rigtige størrelse i en skaleret Tm, så Tfs siger 1, mens teksten er 10 points høj, og en regel nøglet til Tfs ville behandle sidens to stavemåder forskelligt
Reglen har ærlige kanter. En overskrift sat med meget løs tracking, hvor Tc alene åbner mere end 0,15 af teksthøjden mellem bogstaver, udtrækkes med et mellemrum mellem hvert bogstav, hvilket er, hvad siden ser ud som, men nok ikke det, du ville indeksere. Stykker tegnet i forkert rækkefølge på samme baseline giver et negativt gab og sammenføjes uden mellemrum. Ingen af tilfældene er almindelige i brødtekst, og på et testkorpus øgede ændringen ord-match mod en reference-extractor på 28 sider uden at sænke nogen
Hvornår starter HotPDF en ny linje i udtrukket tekst?
Siden v2.766.79 starter en ny linje, når bevægelsen fra den forrige glyfs origen til den aktuelle, projiceret på normalen til den forrige skriveretning, overstiger halvdelen af den største af de to glyffers boks-højder. Den tidligere regel sammenlignede den rå Y-bevægelse med halvdelen af Tfs, hvilket fejlede i to retninger. Med 1 Tf og en skaleret Tm krympede tærsklen til en halv enhed, så hævet skrift løftet af en text rise på 0,4 eller almindelig baseline-jitter brød linjen. Reglen ignorerede også X helt, så tekst under en roteret Tm gik ned ad siden med hver glyf og kom ud som én glyf pr. linje. At projicere på retningens normal får roterede løb til at opføre sig som vandrette, og at tage den største af de to højder holder et stort eksempelord og dets lille billedtekst på én linje, når de deler baseline. På selvangivelsen oven faldt linjeantallet fra 156 til 97. Lodret tekst i writing mode 1 (§9.7.4.3) følger en separat sti: de glyffer grupperes i kolonner, læses fra højre til venstre og fra top til bund, med linjeskift ved hvert kolonneskift
Hvilken tekst inkluderer ExtractLoadedPageText, og hvad udelader den?
ExtractLoadedPageText returnerer den tekst, en viewer viser. Siden v2.766.80 arbejder den ud fra de synlige glyffer alene og dropper hver glyf, hvis boks-center falder uden for GetLoadedPageVisibleBox, som er CropBoxen beskåret til MediaBoxen (§14.11.2). Det fjerner slug-linjer og andre printermærker sat som tekst uden for beskæringsområdet. ExtractLoadedPageGlyphs beholder bevidst at returnere hver glyf i sidens content stream, så du stadig kan finde det materiale, når du får brug for det. Filtret er en boks-test, ikke en synlighedstest: tekst skjult af en klippesti, tegnet i hvidt eller dækket af et billede udtrækkes stadig
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]));
// hver glyf i sidens content stream, slug-linje inkluderet
if Pdf.ExtractLoadedPageGlyphs(0, Glyphs) then
Writeln(Length(Glyphs), ' glyphs in the page content stream');
// kun hvad siden viser, med Form XObject-tekst indsat
if Pdf.ExtractLoadedPageText(0, PageText) then
Writeln(PageText);
finally
Pdf.Free;
end;
end;
Tekst tegnet gennem Form XObjects er en del af sideteksten siden v2.768.3. Sidehoveder, stempler og vandmærker lever meget ofte i forms, og nogle standarddokumenter mistede 30 til 35 procent af deres tegn før ændringen. THotPDF.InterpretContentWithForms noterer hver Do sammen med den gældende CTM, fortolker formen ved dens /Matrix ganget med den CTM (§8.10.1) og indsætter formens glyffer ved Do'ens position med rekursion ind i indlejrede forms. En form uden egen /Resources låner dem fra den stream, der tegner den, som §7.8.3 tillader. Form-glyffer bærer TokenIndex = -1, og ExtractLoadedPageGlyphs returnerer stadig kun side-stream-glyffer, fordi søgning, erstatning og redaktion skriver ændringer tilbage gennem TokenIndex og ville redigere de forkerte bytes, hvis en form-glyf snek sig ind. To forenklinger er værd at kende: form-tekst klippes ikke til formens /BBox, og rekursionen stopper ved 12 niveauer frem for gennem cyklusdetektion, så en misdannet form, der tegner sig selv, gentager sin tekst, indtil den når den grænse
Hvorfor afkodede tekst efter en Q-operator som vrøvl?
Tekst efter Q kunne afkodes forkert før v2.766.73, fordi extractoren kun gemte CTM'en ved q. Text state-parametrene, nemlig font, størrelse, Tc, Tw, Tz, TL, rendering mode og rise, hører til graphics state (§9.3.1), så Q skal gendanne dem sammen med alt andet på stakken (§8.4.2). Én brancherapport valgte en to-byte Identity-H-font inde i q … Q og viste derefter én-byte WinAnsi-tekst uden egen Tf. Extractoren beholdt den indre font, læste prikkerne og ordet "Adobe" i indholdsfortegnelsen som to-byte-koder og mistede 15 % af sidens tegn. Tolkens q/Q-stak holder nu den fulde text state. Udtræksreglerne beskrevet her gælder hver side, så et helt dokument kan gå til en fil i ét kald
var
Output: TFileStream;
Pages: Integer;
begin
Output := TFileStream.Create('report.txt', fmCreate);
try
// tomt interval = alle sider; form feed mellem sider; UTF-8 BOM
Pages := Pdf.ExtractLoadedPagesTextToStream(Output, '', #12, True);
Writeln(Pages, ' pages extracted');
finally
Output.Free;
end;
end;
Hvilken HotPDF tekst-API bør du bruge?
ExtractLoadedPageText forbliver i content stream-rækkefølge, hvilket er det rigtige default til søgning og indeksering; dekodningskæden under den er dækket i at udtrække tekst fra indlæste PDF'er med HotPDF. For taggede dokumenter, hvor forfatter-rækkefølgen betyder noget, går structure-order tekstudtræk strukturtræet igennem i stedet for at gætte ud fra geometrien, og for data låst i tabeller returnerer typed tabeludtræk hen over sideskift celler frem for linjer. Fuld API-reference og en trial-download ligger på HotPDF Delphi PDF Component-produktsiden