HotPDF återställer tabeller från en befintlig PDF genom ExtractLoadedTypedTables, ett Delphi-API som sammanfogar radfragmenten från layoutpasset, bygger ett kanoniskt kolumnrutnät per tabell, fortsätter tabellen över en sidbrytning när geometrin stöder det och returnerar varje cell som ett typat värde med sidproveniens, kolumnspann och gränser. ExportLoadedTypedTables skriver samma resultat direkt till CSV eller JSON. Scenariot som gör detta värt att bygga är tråkigt och extremt vanligt. Ett fyrtio sidor långt fakturaregister, en logisk tabell, utskriven med rubriken upprepad överst på varje sida. Kör en naiv läsordningspassage över den och du får fyrtio tabeller, trettionio falska rubrikrader och en valutakolumn som flyttar en position åt vänster på varje rad där mittcellen råkade vara tom. Att städa upp det nedströms i den anropande applikationen är där dokumentimportprojekt dör
Varför ger en PDF-sida dig fragment i stället för en tabell?
För att en PDF-sida inte bär någon tabellsemantik alls om inte dokumentet är taggat. Innehållsströmmen innehåller textvisande operatorer och positioneringsmatriser (ISO 32000-1 §9.4.3) och inget mer; den linjerade ruta du ser på skärmen är orelaterad banmålning som ingen extraherare måste korrelera med texten. Strukturelementtyperna Table, TR, TH och TD finns bara i den logiska strukturhierarkin i en taggad PDF (ISO 32000-1 §14.8.4), och den överväldigande majoriteten av affärsdokument i omlopp är inte taggade. Allt som beskrivs nedan är geometrisk återställning, inte parsing, och det är värt att säga högt innan någon bygger en avstämningsrapport ovanpå den
HotPDF kör därför först en semantisk layoutanalys över de extraherade glyferna, samma pass som driver strukturordnad textextrahering från en inläst PDF och de strukturerade HTML- och XML-exporterna. Det passet grupperar baslinjer i körningar vars celler linjerar vertikalt, och det fortsätter bara en körning så länge på varandra följande rader har samma antal celler. För en layoutmotor är regeln korrekt och billig. För en anropare är den fel form: en enda rad med en tom inre cell delar en visuell tabell i två källtabeller. Det typade tabellagret ligger ovanpå passet just för att sätta ihop delarna igen
Kanoniska kolumnrutnät och reglaget ColumnTolerance
ExtractLoadedTypedTables sammanfogar fragment från samma sida innan den gör något annat, och den sammanfogar efter kolumngeometri snarare än efter radtext. Två intilliggande källtabeller på en sida slås ihop när båda har minst två kolumner, när det vertikala avståndet mellan den första tabellens sista rad och den andras första rad ligger inom toleransbandet och när deras kolumnstarter linjerar. Kolumnstarter inom ColumnTolerance från varandra faller ihop till en kanonisk kolumn och medelvärdesbildas när de slås ihop. Standardtoleransen är 12 enheter i användarutrymme, vilket passar vanlig affärstypografi och bör höjas för bredt spärrade eller djupt indragna layouter
Det som händer med en rad som saknar ett inre värde är den viktiga delen. HotPDF fäster varje cell vid sin närmaste kanoniska kolumnstart och sätter sedan ColumnSpan till avståndet från den kolumnen till nästa upptagna, i stället för att flytta de återstående cellerna åt vänster. En rad med tre celler i ett rutnät med fem kolumner behåller sina värden under rätt rubriker och registrerar exakt var luckorna finns. Det är skillnaden mellan en tabell du kan stämma av och en som i tysthet tilldelar pengar till fel plats
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; // enheter i användarutrymme
Options.MinimumTableConfidence := 0.55; // under detta tas tabeller bort
Options.DateOrder := ttdoDMY; // 03/04/2026 är 3 april
Options.DecimalSeparator := ',';
Options.ThousandsSeparator := '.';
if Pdf.ExtractLoadedTypedTables([0, 1, 2, 3], Options, Tables, Info) then
// Info.TableCount kontra Info.SourceTableCount visar hur mycket som slogs ihop
ProcessTables(Tables)
else if Info.Status = ttesBudgetExceeded then
Log(string(Info.Diagnostic));
finally
Pdf.Free;
end;
end;
Vad garanterar sammanfogning över sidor egentligen?
Den garanterar konservatism, avsiktligt. HotPDF sammanfogar två tabeller över en sidgräns endast när MergeAcrossPages är aktiverat, när den andra tabellen börjar på exakt det sidindex som följer efter den första tabellens slut, när båda har minst två kolumner och när minst två kanoniska kolumnstarter linjerar inom ColumnTolerance. Villkoret med på varandra följande sidor är det bärande. Anropare skickar PageIndices som en öppen array i vilken ordning de vill, och utan den kontrollen skulle en begäran om sidorna 3, 9 och 14 kunna svetsa ihop tre orelaterade tabeller till ett helt trovärdigt resultat. Kostnaden är att en äkta fortsättning som hoppar över en sida, en mellanlagd bilaga eller en duplexskanning med en tom baksida kommer tillbaka som två tabeller och ingen flagga luckrar upp det. Att foga ihop sådana igen är ett policybeslut som bara den anropande applikationen kan fatta, så API:et exponerar FirstPageIndex, LastPageIndex, SourceTableCount och ett PageIndex per rad och lämnar beslutet där det hör hemma
Upprepade rubriker märks, aldrig raderas
ExtractLoadedTypedTables tar aldrig bort en upprepad rubrikrad från resultatet. När en sammanfogning över sidor upptäcker att den inkommande tabellen öppnar med rubriktext som är identisk med den ackumulerade tabellens, jämförd efter trimning och skiftlägesnormalisering, markerar den raderna som IsHeader och IsRepeatedHeader och lägger ändå till dem i källordningen. Borttagning är ett informationsförstörande och oåterkalleligt val, och olika konsumenter vill ha olika svar: en CSV-import vill ta bort upprepningarna, ett revisionsspår vill ha dem kvar med sina sidnummer och ett diffverktyg vill bevara källordningen byte för byte. Därför rapporterar biblioteket och anroparen bestämmer
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; // behåll bara det första rubrikblocket
for C := 0 to High(Row.Cells) do
if Row.Cells[C].ValueKind = ttvkCurrency then
Total := Total + Row.Cells[C].NumberValue;
end;
end;
Typade värden och separatorerna du måste ange
Typinferens körs i en fast ordning som löser tvetydigheterna i den enda rimliga riktningen: booleskt först, sedan datum, procent, valuta och vanligt tal, medan allt som inte matchar förblir en sträng. Ordningen hindrar 2026 i en datumkolumn från att avgöras av en talparser innan datumparsern hinner se det. Valuta känns igen från ett inledande $, £, ¥ eller €, eller från en treställig ISO 4217-kod följd av ett blanksteg, och koden bevaras i CurrencyCode. Avgörande är att HotPDF inte gissar din lokal. DecimalSeparator, ThousandsSeparator och DateOrder kommer från alternativen, eftersom 1.234 är antingen ett tal eller ett tusen tvåhundratrettiofyra beroende på en uppgift som PDF-filen inte innehåller. Den råa Unicode-Text behålls i varje cell bredvid det typade värdet, så en felaktig gissning alltid kan återvinnas utan en andra extraheringspassage
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 två exportformaten svarar på olika frågor och är medvetet inte likvärdiga. CSV skriver fortsättningskolumnerna i ett sammanslaget spann som tomma fält, vilket är vad ett kalkylblad eller en massinläsare förväntar sig. JSON behåller allt som extraheringen visste: det typade värdet under sin egen typ, columnSpan, konfidens per cell och rad, cellgränserna samt sid- och källtabellsproveniens. Båda formaten mellanlagrar hela dokumentet i en begränsad minnesbuffert och publicerar först därefter till destinationsströmmen, och återställer de ursprungliga bytena, längden och positionen om skrivningen misslyckas halvvägs, så en misslyckad export lämnar aldrig efter sig en halvt skriven fil. Budgetar för sidor, glyfer per sida, tabeller, rader, celler, tecken och utdata i byte räknas alla separat, och rader räknas före allokering eftersom ett SetLength per rad degenererar till kvadratisk kopiering långt innan standardtaket på en miljon rader
Var den geometriska tabellåterställningen ger upp
Att vara tydlig med fellägena är mer användbart än en funktionslista, eftersom vart och ett är en plats där en anropare behöver sin egen policy snarare än ett bättre alternativvärde
- Vertikala sammanslagningar återställs inte. HotPDF rapporterar
ColumnSpanför horisontella spann och lämnarRowSpanpå 1, så en cell som spänner över tre utskrivna rader kommer som en cell plus två luckor - Rubrikdetektering är datadriven, inte visuell. Rubrikblocket är körningen av rader före den första raden som innehåller ett typat värde som inte är en sträng, så en tabell vars kropp helt består av text rapporterar
HeaderRowCountsom noll oavsett hur den är formgiven - Tabeller under
MinimumTableConfidencetas bort från resultatet utan ett fel. JämförInfo.TableCountmedInfo.SourceTableCountnär du behöver veta att något kasserades - En körning behöver minst två rader och minst två kolumner innan layoutpasset över huvud taget kallar den en tabell, så en pseudotabell på en rad eller en layout med två kolumner av lång prosa är korrekt, och ohjälpsamt, inte en tabell
- Skannade sidor innehåller inga textoperatorer, så det finns inget att återställa geometriskt förrän ett OCR-textlager finns på sidan
Om dina PDF-filer kommer från din egen rapportstack är den billigaste lösningen på allt detta uppströms: avge taggade tabeller eller behåll källdatan, och behandla extrahering som en reservväg för dokument du inte producerade. För allt annat är pipelinen värd att lära sig i den här ordningen eftersom varje lager bygger på det under: börja med vanlig textextrahering från en inläst PDF, gå upp till API:et för typade tabeller när geometrin måste bevaras och titta på att rendera en datatabell till en ny PDF när du är på genereringssidan och själv kan avgöra hur återställningsbar utdata ska vara
ExtractLoadedTypedTables och ExportLoadedTypedTables levereras som en del av den inbyggda HotPDF Delphi PDF-komponenten för Delphi och C++Builder, utan extern DLL och utan runtime-beroende; produktsidan innehåller hela referensen för alternativ, status och poster i API:et för typade tabeller