Technisch artikel

PDF-tekstextractie in Delphi: spaties en regeleinden

HotPDF Delphi Component herbouwt woordspaties en regeleinden in THotPDF.ExtractLoadedPageText uit glyphgeometrie, niet uit spatietekens. Een spatie gaat erin als de afstand na de eigen breedte van een glyph meer dan 0,15 van de teksthoogte bedraagt, en een nieuwe regel begint alleen als de tekstoorsprong over de schrijfrichting heen met meer dan de helft van de teksthoogte beweegt. Sinds v2.768.3 omvat de paginatekst ook tekst die via Form XObjects getekend is, en laat hij glyphs buiten het zichtbare cropgebied weg. De rest van dit stuk legt uit waarom elke regel eruitziet zoals hij eruitziet, want elk van hen verving een eenvoudigere regel die op echte documenten geloofwaardige maar verkeerde uitvoer produceerde

De symptomen zijn iedereen bekend die PDF-tekst ooit in een zoekindex heeft gevoerd. Een omslagpagina extraheert als PDFReferenceManualNovember4,1998, een belastingformulier splitst in 156 regels, een diagonaal watermerk komt binnen met één teken per regel, en een bijgesneden proef opent met de slug line van de drukker die geen enkele viewer ooit toont. Geen van deze bestanden is kapot. Elk gebruikt een volstrekt legale manier om tekst te plaatsen die een naieve extractor verkeerd leest

Waarom verliest geëxtraheerde PDF-tekst zijn woordspaties?

Geëxtraheerde tekst verliest woordspaties omdat een PDF ze nooit hoeft te bevatten. Een producent kan woorden scheiden door een spatieteken te tonen, maar hij kan de pen net zo goed verplaatsen met een getal in een TJ-array (ISO 32000-1 §9.4.3) of met een verse Td (§9.4.2), en TeX-uitvoer, veel Distiller-bestanden en de meeste uitgevulde lay-outs doen precies dat. Vóór v2.766.76 keek HPDFAssemblePageText alleen naar verticale beweging, dus een woordgrens door positionering verdween simpelweg. De assembler meet nu, langs de schrijfrichting van de vorige glyph, de afstand van het einde van de eigen breedte van die glyph tot de oorsprong van de huidige glyph, en voegt één spatie in als de afstand meer dan 0,15 van de boxhoogte van de huidige glyph bedraagt, gemeten van ascent tot descent in user space. Er wordt geen spatie toegevoegd als een van beide kanten al leeg is, en ook niet tussen twee CJK-tekens, want uitvulling trekt ideografen uit elkaar zonder dat die rek een woordgrens betekent. De glyph-records leggen dezelfde geometrie bloot, dus u kunt de beslissing reproduceren wanneer een bepaald bestand u voor het raam zet

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-to-descent-hoogte van de glyphbox, in user space
    Height := Sqrt(Sqr(Glyphs[I].QuadX[3] - Glyphs[I].QuadX[0]) +
      Sqr(Glyphs[I].QuadY[3] - Glyphs[I].QuadY[0]));
    // horizontale tekst: afstand vanaf het einde van de eigen breedte van de vorige glyph
    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;

Waarom meten vanaf de eigen breedte van de glyph in plaats van de penpositie?

HotPDF meet woordafstanden vanaf GlyphEndX / GlyphEndY, want de penpositie na een glyph bevat al spatiëring die geen afstand is. ISO 32000-1 §9.4.4 definieert de horizontale verplaatsing als de glyphbreedte keer de fontgrootte, plus character spacing Tc, plus word spacing Tw, allemaal geschaald met Tz. BaselineEndX / BaselineEndY bevatten die volledige verplaatsing, terwijl GlyphEndX / GlyphEndY alleen de font advance en Tz bevatten. Het verschil doet ertoe voor producenten die tracking aanscherpen met een negatieve Tc en daarna de ruimte per glyph teruggeven via een TJ-aanpassing: gemeten vanaf de penpositie lijkt die teruggifte een afstand, en het Chinese begrip “95后” extraheerde als “9 5 后”. De drempel hangt om dezelfde reden aan de glyphboxhoogte in plaats van aan de Tf-grootte. Word-exports schrijven vaak 1 Tf en dragen de echte grootte in een geschaalde Tm, dus Tfs zegt 1 terwijl de tekst 10 punten hoog is, en een regel die aan Tfs hangt zou de twee spellingen van dezelfde pagina anders behandelen

De HotPDF-woordspatieregel voor ExtractLoadedPageText in Delphi: een spatie wordt alleen ingevoegd als de afstand van de GlyphEndX van de vorige glyph naar de BaselineStartX van de volgende glyph meer dan 0,15 van de ascent-to-descent-boxhoogte bedraagt, want de penpositie in BaselineEndX bevat al Tc, Tw en Tz en maakt teruggegeven uitgevulde tracking tot valse afstanden zoals 9 5 后
Geometrie, geen spatietekens, bepaalt waar woorden breken — de glyph-records leggen dezelfde metingen bloot, dus u kunt de beslissing voor elk raadselachtig bestand opnieuw afspelen

De regel heeft eerlijke randen. Een kop gezet met zeer losse tracking, waar Tc alleen al meer dan 0,15 van de teksthoogte tussen letters opent, extraheert met een spatie tussen elke letter, wat is hoe de pagina eruitziet maar waarschijnlijk niet wat u wilde indexeren. Delen die buiten volgorde op één baseline getekend worden leveren een negatieve afstand op en voegen zich zonder spatie samen. Geen van beide gevallen is gebruikelijk in lopende tekst, en op een testcorpus verhoogde de wijziging de woordmatches tegen een referentie-extractor op 28 pagina's zonder er één te verlagen

Wanneer begint HotPDF een nieuwe regel in geëxtraheerde tekst?

Sinds v2.766.79 begint een nieuwe regel als de beweging van de oorsprong van de vorige glyph naar de huidige, geprojecteerd op de normaal van de vorige schrijfrichting, meer dan de helft van de grotere boxhoogte van de twee glyphs bedraagt. De eerdere regel vergeleek de rauwe Y-beweging met de helft van Tfs, wat in twee richtingen faalde. Met 1 Tf en een geschaalde Tm kromp de drempel tot een halve eenheid, dus een superscript verhoogd met een text rise van 0,4 of gewone baseline-jitter brak de regel af. De regel negeerde ook X volledig, dus tekst onder een gedraaide Tm stapte met elke glyph de pagina af en kwam eruit als één glyph per regel. Projecteren op de normaal van de richting zorgt dat gedraaide runs zich gedragen als horizontale, en de grotere van de twee hoogten nemen houdt een groot voorbeeldwoord en zijn kleine bijschrift op één regel zolang ze een baseline delen. Op het hierboven genoemde belastingformulier zakte het regelantal van 156 naar 97. Verticale tekst in writing mode 1 (§9.7.4.3) volgt een apart pad: die glyphs worden in kolommen gegroepeerd, van rechts naar links en van boven naar beneden gelezen, met een regeleinde bij elke kolomwissel

Hoe ExtractLoadedPageText van HotPDF regeleinden in Delphi beslist: de beweging tussen glyphoorsprongen wordt geprojecteerd op de normaal van de schrijfrichting en vergeleken met de helft van de grotere boxhoogte, zodat een superscript verhoogd door een kleine text rise onder een 1 Tf-font en tekst die onder een gedraaide Tm een pagina afstapt niet langer uiteenvallen in één glyph per regel
Projectie laat gedraaide runs zich gedragen als horizontale, en de grotere van de twee boxhoogten nemen houdt een groot voorbeeldwoord en zijn kleine bijschrift op één regel

Welke tekst laat ExtractLoadedPageText in en welke weg?

ExtractLoadedPageText geeft de tekst terug die een viewer toont. Sinds v2.766.80 werkt hij alleen vanuit de zichtbare glyphs, en laat elke glyph vallen waarvan het boxmidden buiten GetLoadedPageVisibleBox valt, de CropBox bijgesneden tot de MediaBox (§14.11.2). Daarmee verdwijnen slug lines en andere drukkersmarkeringen die als tekst buiten het snijvlak gezet zijn. ExtractLoadedPageGlyphs blijft bewust elke glyph van de page content stream teruggeven, dus u kunt dat materiaal nog steeds vinden als u het nodig heeft. Het filter is een boxtest, geen zichtbaarheidstest: tekst die door een clipping path verborgen is, in het wit getekend of door een afbeelding bedekt, wordt nog steeds geëxtraheerd

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]));
    // elke glyph van de page content stream, slug line inbegrepen
    if Pdf.ExtractLoadedPageGlyphs(0, Glyphs) then
      Writeln(Length(Glyphs), ' glyphs in the page content stream');
    // alleen wat de pagina toont, met Form XObject-tekst ingevoegd
    if Pdf.ExtractLoadedPageText(0, PageText) then
      Writeln(PageText);
  finally
    Pdf.Free;
  end;
end;

Tekst die via Form XObjects getekend is, hoort sinds v2.768.3 bij de paginatekst. Headers, stempels en watermerken leven heel vaak in forms, en sommige normdocumenten verloren vóór de wijziging 30 tot 35 procent van hun tekens. THotPDF.InterpretContentWithForms registreert elke Do samen met de geldende CTM, interpreteert de form op zijn /Matrix keer die CTM (§8.10.1), en voegt de glyphs van de form in op de positie van de Do, met recursie in geneste forms. Een form zonder eigen /Resources leent die van de stream die hem tekent, zoals §7.8.3 toestaat. Form-glyphs dragen TokenIndex = -1, en ExtractLoadedPageGlyphs geeft nog steeds alleen page-stream-glyphs terug, omdat zoeken, vervangen en redaction wijzigingen via TokenIndex terugschrijven en de verkeerde bytes zouden bewerken als er een form-glyph tussen zou glippen. Twee vereenvoudigingen zijn de moeite van weten waard: formtekst wordt niet bijgesneden op de /BBox van de form, en de recursie stopt bij 12 niveaus in plaats van via cyclusdetectie, dus een misvormde form die zichzelf tekent herhaalt zijn tekst tot hij die cap bereikt

Welke glyphs HotPDF meeneemt bij het extraheren van PDF-paginatekst in Delphi: ExtractLoadedPageText houdt alleen glyphs waarvan het boxmidden binnen GetLoadedPageVisibleBox valt, de CropBox bijgesneden tot de MediaBox, zodat printer-slug lines verdwijnen, terwijl InterpretContentWithForms Form XObject-glyphs invoegt op elke Do-positie met TokenIndex op -1 en de glyph-level API nog steeds alles teruggeeft
Een boxtest op het glyphmidden is geen zichtbaarheidstest — witte tekst, bijgesneden tekst en bedekte tekst komen nog steeds uit de bus, en formtekst telt mee sinds v2.768.3

Waarom decodeerde tekst na een Q-operator als wartaal?

Tekst na Q kon vóór v2.766.73 verkeerd decoderen omdat de extractor alleen de CTM bewaarde bij q. De text state-parameters, te weten font, grootte, Tc, Tw, Tz, TL, rendering mode en rise, horen bij de graphics state (§9.3.1), dus Q moet ze herstellen samen met al het andere op de stack (§8.4.2). Eén industrierapport koos een two-byte Identity-H-font binnen q … Q en toonde daarna one-byte WinAnsi-tekst zonder eigen Tf. De extractor hield het binnenste font aan, las de leaders en het woord “Adobe” op de inhoudsopgave als two-byte codes, en verloor 15% van de tekens van de pagina. De q/Q-stack van de interpreter bevat nu de volledige text state. De extractieregels die hier beschreven worden gelden voor elke pagina, dus een heel document kan in één aanroep naar een bestand

var
  Output: TFileStream;
  Pages: Integer;
begin
  Output := TFileStream.Create('report.txt', fmCreate);
  try
    // leeg bereik = elke pagina; form feed tussen pagina's; UTF-8 BOM
    Pages := Pdf.ExtractLoadedPagesTextToStream(Output, '', #12, True);
    Writeln(Pages, ' pages extracted');
  finally
    Output.Free;
  end;
end;

Welke HotPDF-tekst-API moet u gebruiken?

ExtractLoadedPageText blijft in content-stream-volgorde, wat de juiste standaard is voor zoeken en indexeren; de decoderingsketen eronder wordt behandeld in tekst extraheren uit geladen PDF's met HotPDF. Voor getagde documenten waarvan de authoring-volgorde telt, loopt structure-order tekstextractie de structure tree af in plaats van uit geometrie te gokken, en voor data die in tabellen opgesloten zitten, geeft typed table extraction over paginagrenzen cellen terug in plaats van regels. De volledige API-referentie en een trial-download staan op de productpagina van HotPDF Delphi PDF Component