Technisch artikel

PDF-tekst in structuurvolgorde extraheren met HotPDF

Elke geometrische tekstextractor gokt. Die leest de glyphs die een pagina tekent, sorteert ze op basislijn en horizontale positie, en hoopt dat de visuele schikking overeenkomt met de volgorde waarin een mens zou lezen. Bij een rapport met één kolom is die gok juist. Bij een tijdschriftartikel met twee kolommen, een formulier met een zijbalk of een tabel waarvan de cellen kolom voor kolom zijn uitgezonden, zit die ernaast op manieren die moeilijk op te merken zijn en duur om stroomafwaarts te ontdekken. HotPDF antwoordt daarop met ExtractLoadedPageStructureText, dat geometrie volledig negeert: het loopt de documentstructuurboom af in de auteursvolgorde zoals gedefinieerd in ISO 32000-1 §14.8.4 en bouwt daarna de paginaglyphs opnieuw op aan de hand van hun marked-content identifier. Voor een getagde PDF is dat geen heuristiek, het is de volgorde die de producerende applicatie heeft gedeclareerd

De functie geeft False terug wanneer de pagina geen bruikbare structuurboom heeft, en dat is het signaal om terug te vallen op de geometrische extractor in plaats van te falen. Dat tweewegenontwerp doet er meer toe dan het algoritme: echte documentintake ziet getagde overheidsformulieren en scanneruitvoer in dezelfde map, en een pijplijn die er maar één van aankan is geen pijplijn

Waarom krijgt geometrische extractie de leesvolgorde verkeerd?

Omdat een PDF-contentstream helemaal geen leesvolgorde meedraagt. Het is een reeks tekenoperatoren, en een producent mag die uitzenden in welke volgorde ook bij zijn eigen layout-engine past. Tekstverwerkers zenden meestal uit in flowvolgorde en geometrisch sorteren oogt prima. Layouttools, formulierontwerpers en rapportgeneratoren vaak niet: een paginavoettekst kan vóór de body worden uitgezonden, een tabel kan kolomgewijs worden gevuld, en een pagina met twee kolommen kan regels uit beide kolommen verweven omdat de componist ze samen heeft opgelost

Vergelijking van een PDF-pagina met twee kolommen: op basislijn gesorteerde geometrische extractie die kolommen samenvoegt tegen MCID-extractie in structuurvolgorde in HotPDF
Glyphs op basislijn sorteren verweeft twee kolommen tot onzin, terwijl de structuurboom de volgorde herafspeelt die de producent declareerde

De faalmodus is stil. Een geometrische extractor meldt nooit een fout, die geeft gewoon proza terug waarvan de zinnen uit twee kolommen zijn gespijst. Alles wat die tekst verbruikt, een zoekindex, een veldmapper voor e-facturen, een retrievalpijplijn die een taalmodel voedt, erft de schade zonder waarschuwing. HotPDF levert ook de geometrische extractors voor geladen documenten, en die blijven het juiste gereedschap voor niet-getagde bestanden; het punt van het pad in structuurvolgorde is stoppen met gokken wanneer het document het antwoord al meedraagt

Wat de structuurboom werkelijk opslaat

Een getagde PDF bewaart een tweede, parallelle omschrijving van de pagina. De catalog wijst naar een /StructTreeRoot, waarvan de /K-kinderen een boom van structuurelementen vormen: /Document, /Sect, /P, /Table, /TR, /TD enzovoort. De bladeren van die boom zijn marked-content references, integers die een reikwijdte van de paginacontentstream benoemen. Aan de inhoudskant worden die reikwijdtes geopend met een BDC-operator die een /MCID meedraagt en gesloten met EMC. Elk structuurelement draagt ook een /Pg-ingang die de pagina benoemt waartoe het behoort, en juist daardoor is per-pagina-traversatie mogelijk in een document waarvan de structuurboom honderden pagina's beslaat

Anatomie van de PDF-structuurboom die StructTreeRoot-elementen zoals Sect, Table, TR en TD koppelt aan BDC MCID-reikwijdtes in de HotPDF-paginacontentstream
De bladeren van de boom zijn marked-content references en elk element draagt een Pg-ingang waarmee de wandeling filtert op de huidige pagina

HotPDF doorloopt die boom met een dieptecap van 128 niveaus en filtert op /Pg zodat alleen de huidige pagina bijdraagt. De uitvoer van de traversatie is geen tekst, het is een geordende lijst van MCID-waarden: de auteursvolgorde van de marked-content reikwijdtes op deze pagina. Tekst weer opbouwen is dan een kwestie van de glyphs in die volgorde herafspelen

De MCID wordt vastgelegd tijdens glyph-extractie, niet achteraf opgezocht

Dit is het implementatiedetail dat de functie goedkoop maakt. HotPDF legt de actieve marked-content identifier al vast op elke glyph die die extraheert, in het veld MCID van THPDFGlyphRecord, want de contentstream-interpreter weet welke BDC-scope open staat op het moment dat die elke operator Tj of TJ verwerkt. Extractie in structuurvolgorde heeft dus geen tweede passe over de contentstream nodig. Die verzamelt de MCID-reeks uit de structuurboom, sorteert daarna de al geëxtraheerde glyphs in bakken op MCID en zendt ze in die reeks uit

var
  Pdf: THotPDF;
  PageCount, I, Untagged: Integer;
  PageText, AllText: UnicodeString;
  Report: TStrings;   // diagnostiek-sink in beheer van de aanroeper
begin
  Pdf := THotPDF.Create(nil);
  try
    PageCount := Pdf.LoadFromFile('accessible-form.pdf');
    AllText := '';
    for I := 0 to PageCount - 1 do
    begin
      if Pdf.ExtractLoadedPageStructureText(I, PageText, Untagged) then
      begin
        // Auteursvolgorde rechtstreeks uit de structuurboom
        if Untagged > 0 then
          Report.Add(Format('page %d: %d glyphs outside the structure tree',
            [I, Untagged]));
      end
      else
        // Geen bruikbare structuurboom op deze pagina: geometrische fallback
        Pdf.ExtractLoadedPageText(I, PageText);
      AllText := AllText + PageText + #13#10;
    end;
  finally
    Pdf.Free;
  end;
end;

Niet-getagde glyphs worden geteld, nooit geruisloos weggegooid

Een pagina kan deels getagd zijn. Producenten voegen een decoratieve lijn, een paginanummer of een laat-stadium-watermerk toe buiten elke BDC-scope, en die glyphs behoren tot geen enkele MCID. Ze weglaten zou de nette implementatie zijn en de verkeerde, want hetzelfde gat verschijnt ook wanneer een producent de body tagt maar de tabel vergeet, en dan verliest u de tabel zonder het te merken

HotPDF voegt niet-opgeëiste glyphs toe als een geometrische staart na de tekst in structuurvolgorde en meldt hun aantal via de outputparameter UntaggedGlyphCount. Dat aantal is een kwaliteitssignaal waarop u kunt handelen. Een handjevol glyphs op een pagina van tweeduizend is paginainboel en kan worden genegeerd. Veertig procent van de pagina buiten de structuurboom betekent dat het taggen decoratief is en dat de geometrische extractor het eerlijkere antwoord is voor dat bestand

Beslissingsstroom voor HotPDF structuurtekstextractie met geometrische fallback wanneer een pagina geen bruikbare structuurboom of decoratieve tagging heeft
True betekent structuurvolgorde met de niet-getagde staart toegevoegd en False stuurt de pagina naar de geometrische extractor in plaats van te falen
function ExtractPageBestEffort(Pdf: THotPDF; PageIndex: Integer;
  out AText: UnicodeString; out UsedStructure: Boolean): Boolean;
var
  Untagged, TotalGlyphs: Integer;
  Glyphs: THPDFGlyphArray;
begin
  UsedStructure := False;
  if Pdf.ExtractLoadedPageStructureText(PageIndex, AText, Untagged) then
  begin
    TotalGlyphs := 0;
    if Pdf.ExtractLoadedPageGlyphs(PageIndex, Glyphs) then
      TotalGlyphs := Length(Glyphs);
    // Vertrouw de structuurboom alleen wanneer die het grootste deel van de pagina opeist
    if (TotalGlyphs = 0) or (Untagged * 4 <= TotalGlyphs) then
    begin
      UsedStructure := True;
      Result := True;
      Exit;
    end;
  end;
  Result := Pdf.ExtractLoadedPageText(PageIndex, AText);
end;

Wat de functie False laat teruggeven

Drie gevallen, en ze zijn het onderscheiden waard want slechts één ervan is een defect in het document. De eerste is een gewone niet-getagde PDF: geen /StructTreeRoot, niets om te doorlopen, en False is simpelweg de waarheid. De tweede is een gescande pagina waarvan de tekst komt uit een OCR-laag die nooit is getagd. Het derde is het interessante: inhoud die BDC-operatoren met /MCID-waarden meedraagt maar waarvan de pagina geen /StructParents-ingang heeft en waarvan de structuurboom die identifiers nooit refereert. De marked content bestaat, de structuurkant niet, en er is geen volgorde terug te halen. HotPDF meldt False in plaats van er een te verzinnen

Dat laatste geval verschijnt in met de hand bewerkte bestanden en in uitvoer van tools die marked content uitzenden voor optional-content- of artefactdoelen zonder een structuurboom op te bouwen. Als u zelf getagde PDFs produceert, dan is dezelfde asymmetrie wat PDF/UA-validatie controleert, en de tegenhanger aan de schrijfkant wordt behandeld in de layout-DOM die getagde, gepagineerde uitvoer uitzendt

Waar structuurvolgorde zichzelf terugverdient

Toegankelijkheidsaudit is de voor de hand liggende: certificeert u een document tegen PDF/UA, dan is de leesvolgorde die een schermlezer zal aankondigen precies de structuurvolgorde, dus die extraheren is hoe u die beoordeelt zonder schermlezer. Gegevensvastlegging is het grotere commerciële geval. Getagde overheidsformulieren, gereguleerde openbaarmakingen en e-factuurbijlagen dragen veldlabels en -waarden in gedeclareerde volgorde, en ze in die volgorde lezen verwijdert een hele klasse mapping-bugs die geometrische extractie veroorzaakt op meerkolomlay-outs

De nieuwste verbruiker is retrieval voor taalmodellen. Een document in chunks verdelen voor embedding is slechts zo goed als de tekstvolgorde, en een chunk die twee kolommen samenvoegt produceert zinnen die nooit hebben bestaan. Extractie in structuurvolgorde is de goedkoopste beschikbare oplossing daarvoor, want voor getagde documenten staat de juiste volgorde al in het bestand en hoeft alleen gelezen te worden

HotPDF is een native VCL-component voor Delphi en C++Builder, dus de structuurboom-traversatie en het herafspelen van glyphs draaien beide in-proces tegen een geladen document zonder externe renderer. Volledige API-details voor de extractiefamilie van geladen documenten staan op de productpagina van de HotPDF Delphi PDF component