Teknisk artikkel

Typet PDF-tabelluthenting i Delphi på tvers av sider

HotPDF gjenoppretter tabeller fra en eksisterende PDF gjennom ExtractLoadedTypedTables, et Delphi-API som slår sammen radfragmentene layout-passet produserer, bygger ett kanonisk kolonnerutenett per tabell, fortsetter tabellen over et sideskift når geometrien støtter det og returnerer hver celle som en typet verdi med sideproveniens, kolonnespenn og grenser. ExportLoadedTypedTables skriver det samme resultatet direkte til CSV eller JSON. Situasjonen som gjør dette verdt å bygge, er kjedelig og svært vanlig. Et førti siders fakturaregister, logisk sett én tabell, skrives ut med overskriften gjentatt øverst på hver side. Kjør et naivt leserekkefølgepass over det, og du får førti tabeller, trettini falske overskriftsrader og en valutakolonne som glir én posisjon til venstre på hver rad der midtcellen tilfeldigvis var tom. Å rydde dette opp nedstrøms, inne i kallerapplikasjonen, er der dokumentimportprosjekter går for å dø

Hvorfor gir en PDF-side deg fragmenter i stedet for en tabell?

Fordi en PDF-side ikke har tabellsemantikk i det hele tatt med mindre dokumentet er tagget. Innholdsstrømmen holder tekstvisningsoperatorer og posisjoneringsmatriser (ISO 32000-1 §9.4.3) og ikke mer; den innrammede boksen du ser på skjermen, er urelatert banemaling som ingen uthenter er forpliktet til å koble til teksten. Struktur-elementtypene Table, TR, TH og TD finnes bare i det logiske strukturhierarkiet til en tagget PDF (ISO 32000-1 §14.8.4), og det overveldende flertallet av forretningsdokumenter i omløp er ikke tagget. Alt som beskrives nedenfor er geometrisk gjenoppretting, ikke parsing, og det er verdt å si høyt før noen bygger en avstemmingsrapport oppå det

HotPDF kjører derfor først en semantisk layoutanalyse over de uthentede glyphene, det samme passet som ligger bak strukturordnet tekstuthenting fra en innlastet PDF og de strukturerte HTML- og XML-eksportene. Dette passet grupperer grunnlinjer i løp der cellene justeres vertikalt, og det fortsetter bare et løp så lenge påfølgende rader har samme antall celler. For en layoutmotor er denne regelen riktig og billig. For en caller har den feil form: Én rad med en tom indre celle deler én visuell tabell i to kildetabeller. Det typede tabellaget ligger over dette passet nettopp for å sette delene sammen igjen

Kanoniske kolonnerutenett og ColumnTolerance-knappen

ExtractLoadedTypedTables slår sammen fragmenter på samme side før den gjør noe annet, og den slår sammen etter kolonnegeometri, ikke etter radtekst. To tilstøtende kildetabeller på én side kobles når begge har minst to kolonner, når det vertikale gapet mellom siste rad i den første og første rad i den andre holder seg innenfor toleransebåndet og når startposisjonene for kolonnene ligger på linje. Kolonnestarter som ligger innenfor ColumnTolerance fra hverandre, kollapser til én kanonisk kolonne og gjennomsnittberegnes når de slås sammen. Standardtoleransen er 12 user-space-enheter, noe som passer vanlig forretningstypografi, men bør heves for bredt sporede eller dypt innrykkede layouter

Det som skjer med en rad som mangler en indre verdi, er delen som betyr noe. HotPDF snapper hver celle til den nærmeste kanoniske kolonnestarten og setter deretter ColumnSpan til avstanden fra den kolonnen til den neste kolonnen som er i bruk, i stedet for å skyve de gjenværende cellene til venstre. En rad med tre celler i et rutenett med fem kolonner beholder verdiene under de riktige overskriftene og registrerer nøyaktig hvor hullene er. Det er forskjellen mellom en tabell du kan avstemme og en som stille tilordner penger feil

var
  Pdf: THotPDF;
  Options: THPDFTypedTableExtractionOptions;
  Tables: THPDFTypedTables;
  Info: THPDFTypedTableExtractionInfo;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('register.pdf', '') <= 0 then
      Exit;
    Options := THPDFTypedTableExtractionOptions.Default;
    Options.ColumnTolerance := 12;           // user-space-enheter
    Options.MinimumTableConfidence := 0.55;  // under dette forkastes tabeller
    Options.DateOrder := ttdoDMY;            // 03/04/2026 er 3. april
    Options.DecimalSeparator := ',';
    Options.ThousandsSeparator := '.';
    if Pdf.ExtractLoadedTypedTables([0, 1, 2, 3], Options, Tables, Info) then
      // Info.TableCount kontra Info.SourceTableCount viser hvor mye som ble slått sammen
      ProcessTables(Tables)
    else if Info.Status = ttesBudgetExceeded then
      Log(string(Info.Diagnostic));
  finally
    Pdf.Free;
  end;
end;

Hva garanterer sammenslåing på tvers av sider egentlig?

Den garanterer konservatisme, med hensikt. HotPDF kobler to tabeller over en sidegrense bare når MergeAcrossPages er aktivert, når den andre tabellen begynner på nøyaktig den sideindeksen som følger etter siden den første slutter på, når begge har minst to kolonner og når minst to kanoniske kolonnestarter justeres innenfor ColumnTolerance. Kravet om påfølgende sider er det bærende. Callers sender PageIndices som en åpen matrise i den rekkefølgen de vil, og uten denne kontrollen kunne en forespørsel på sidene 3, 9 og 14 sveise tre urelaterte tabeller sammen til ett helt plausibelt resultat. Kostnaden er at en ekte fortsettelse som hopper over en side, et innskutt vedlegg eller en tosidig skanning med en blank bakside, kommer tilbake som to tabeller, og ingen option lemper på det. Å sette dem sammen igjen er en policyavgjørelse som bare kallerapplikasjonen kan ta, så API-et eksponerer FirstPageIndex, LastPageIndex, SourceTableCount og en PageIndex per rad, og lar avgjørelsen ligge der den hører hjemme

Gjentatte overskrifter merkes, aldri slettes

ExtractLoadedTypedTables fjerner aldri en gjentatt overskriftsrad fra resultatet. Når en sammenslåing på tvers av sider finner at den innkommende tabellen åpner med overskriftstekst som er identisk med den akkumulerte tabellen, sammenlignet etter trimming og case-folding, merker den disse radene med IsHeader og IsRepeatedHeader og legger dem uansett til i kilde-rekkefølge. Sletting er et informasjonstap som ikke kan reverseres, og forskjellige konsumenter vil ha forskjellige svar: En CSV-import vil ha gjentakelsene bort, et revisjonsspor vil ha dem med sidetall, og et diff-verktøy vil ha kilde-rekkefølgen bevart byte for byte. Biblioteket rapporterer derfor, og caller-en bestemmer

var
  T, R, C: Integer;
  Row: THPDFTypedTableRow;
  Total: Double;
begin
  Total := 0;
  for T := 0 to High(Tables) do
    for R := 0 to High(Tables[T].Rows) do
    begin
      Row := Tables[T].Rows[R];
      if Row.IsRepeatedHeader then
        Continue;                    // behold bare den første overskriftsblokken
      for C := 0 to High(Row.Cells) do
        if Row.Cells[C].ValueKind = ttvkCurrency then
          Total := Total + Row.Cells[C].NumberValue;
    end;
end;

Typede verdier og separatorene du må oppgi

Typeinferens kjører i en fast rekkefølge som løser tvetydighetene i den eneste fornuftige retningen: boolsk først, deretter dato, prosent, valuta og vanlig tall, mens alt som ikke passer, forblir en streng. Rekkefølgen hindrer at 2026 i en datokolonne avgjøres av en tallparser før datoparseren får se det. Valuta gjenkjennes fra en innledende $, £, ¥ eller , eller fra en tresifret ISO 4217-kode fulgt av et mellomrom, og koden bevares i CurrencyCode. Viktig: HotPDF gjetter ikke lokalet ditt. DecimalSeparator, ThousandsSeparator og DateOrder kommer fra options, fordi 1.234 enten er ett tall eller ett tusen to hundre og trettifire, avhengig av en opplysning PDF-en ikke inneholder. Den rå Unicode-Text-verdien beholdes på hver celle ved siden av den typede verdien, så en feil gjetning alltid kan gjenvinnes uten et nytt uthentingspass

var
  Stream: TFileStream;
  Info: THPDFTypedTableExtractionInfo;
begin
  Stream := TFileStream.Create('tables.json', fmCreate);
  try
    if not Pdf.ExportLoadedTypedTables([0, 1, 2], ttefJSON,
      Stream, Options, Info) then
      case Info.Status of
        ttesInvalidOptions:   ReportBadConfiguration;
        ttesBudgetExceeded:   ReportOversizedDocument;
        ttesCancelled:        ReportUserCancelled;
        ttesWriteFailed:      ReportDestinationProblem;
      else
        ReportExtractionFailure;
      end;
  finally
    Stream.Free;
  end;
end;

De to eksportformatene svarer på forskjellige spørsmål og er med vilje ikke ekvivalente. CSV skriver fortsettelseskolonnene i et sammenslått spenn som tomme felt, noe et regneark eller en bulklaster forventer. JSON beholder alt uthentingen visste: den typede verdien under sin egen type, columnSpan, konfidens per celle og rad, cellegrensene og proveniensen for side og kildetabell. Begge formatene legger hele dokumentet i en avgrenset buffer i minnet og publiserer først deretter til målstrømmen, og gjenoppretter de opprinnelige byte-ene, lengden og posisjonen hvis skrivingen feiler underveis, så en mislykket eksport etterlater aldri en halvskrevet fil. Budsjetter for sider, glypher per side, tabeller, rader, celler, tegn og output-byte føres separat, og rader telles før allokering fordi en SetLength per rad degenererer til kvadratisk kopiering lenge før standardtaket på én million rader

Hvor gir geometrisk tabellgjenoppretting opp?

Det er mer nyttig å være tydelig på feilmodusene enn å gi en funksjonsliste, fordi hvert av disse punktene er et sted der caller-en trenger sin egen policy i stedet for en bedre option-verdi

  • Vertikale sammenslåinger gjenopprettes ikke. HotPDF rapporterer ColumnSpan for horisontale spenn og lar RowSpan stå på 1, så en celle som spenner over tre rader i den trykte tabellen, kommer inn som én celle pluss to hull
  • Overskriftsdeteksjon er datadrevet, ikke visuell. Overskriftsblokken er rekken av rader før den første raden som inneholder en ikke-streng-typet verdi, så en tabell hvis kropp bare består av tekst, rapporterer HeaderRowCount som null uansett hvordan den er stylet
  • Tabeller under MinimumTableConfidence forkastes fra resultatet uten feil. Sammenlign Info.TableCount med Info.SourceTableCount når du trenger å vite at noe ble forkastet
  • Et løp trenger minst to rader og minst to kolonner før layout-passet i det hele tatt kaller det en tabell, så en pseudo-tabell på én linje eller en layout med to kolonner og lang prosa er korrekt, om enn lite hjelpsomt, ikke en tabell
  • Skannede sider inneholder ingen tekstoperatorer, så det finnes ingenting å gjenopprette geometrisk før et OCR-tekstlag finnes på siden

Hvis PDF-ene dine kommer fra din egen rapporteringsstakk, er den billigste løsningen på alt dette oppstrøms: Skriv taggede tabeller, eller behold kildedataene, og behandle uthenting som en fallback for dokumenter du ikke produserte. For resten er det verdt å lære pipelinen i denne rekkefølgen, siden hvert lag bygger på laget under: Begynn med vanlig tekstuthenting fra en innlastet PDF, gå opp til det typede tabell-API-et når geometrien må bevares, og se på rendering av en datatabell til en ny PDF når du er på genereringssiden og selv kan bestemme hvor gjenopprettbart resultatet skal bli

ExtractLoadedTypedTables og ExportLoadedTypedTables leveres som del av den native HotPDF Delphi PDF Component for Delphi og C++Builder, uten ekstern DLL og uten runtime-avhengighet. Produktsiden inneholder den fullstendige referansen for options, statuser og records i det typede tabell-API-et