PDFium Component versie 3.117.0 koppelt een tabel die over een paginagrens breekt wanneer beide fragmenten de paginaranden raken of wanneer er geen broodtekst onder het eerste fragment en boven het tweede staat, waarbij doorlopende kop- en voetteksten worden genegeerd. ExtractDocumentTables past die inhoudsbewuste test toe als alternatief voor de oudere paginamargetest, weigert een fragment op de volgende pagina waarvan de eerste rij één bijschriftcel over de volle breedte is, en houdt één rij die naar de volgende pagina doorloopt als deel van zijn vervolgketen
Het artikel over tabledetectie en -extractie presenteerde vervolg als vier strikte gates en behandelde "raakt de paginarand" als één daarvan. Die beschrijving klopte voor de release die het behandelde, en ze was ook onjuist voor de meeste tabellen die mensen de component werkelijk voeren. Dit artikel is de correctie: welke documenten de margetest niet aankan, wat hem verving, en de twee randgevallen die de fix meesleepte
Waarom faalt de paginamargetest bij Word-exports?
De paginamargetest faalt omdat een tekstverwerker rijen afbreekt bij de ondermarge, niet bij de papierrand. Met de standaard ContinuationMargin van 36 punten eiste de oorspronkelijke regel dat de onderrand van het eerdere fragment binnen 36 punten van de paginaonderrand lag en de bovenrand van het latere fragment binnen 36 punten van de paginabovenrand. Een document dat uit Word is geëxporteerd met de standaard marges van één inch zet de laatste rij minstens 72 punten boven de paginaonderrand, verder nog als er een voettekst is, dus de voorwaarde hield nooit stand. Elke lange tabel in zo'n document kwam terug als losse fragmenten met ContinuationGroup op nul, en de aanroeper was weer met de hand aan het aaneenrijgen. De test klopt nog steeds voor waar hij omheen is ontworpen: rapporten uit layout-engines die een pagina tot een vaste contentbox vullen en de volgende pagina bovenaan strak beginnen. Het is geen slechte regel, het is een onvolledige regel, en daarom heeft versie 3.117.0 hem gehouden en een tweede pad toegevoegd in plaats van hem te vervangen
Wat controleert de inhoudsbewuste test in plaats daarvan?
De inhoudsbewuste test controleert of er iets anders dan de tabel de ruimte tussen de twee fragmenten bezet, met de woordboxen van elke pagina in plaats van de paginageometrie. Terwijl ExtractDocumentTables het document doorloopt, legt hij per pagina de laagste onderrand vast van elk woord waarvan de bovenkant boven de voettekstband ligt, en de hoogste bovenrand van elk woord waarvan de onderkant onder de koptekstband ligt. Beide banden zijn ContinuationMargin punten diep, dus dezelfde optie doet nu dubbel dienst als speling aan de paginarand en als hoogte van de zones voor doorlopende kop- en voettekst. Een paar fragmenten slaagt wanneer de onderrand van het eerdere op of onder de laagste broodtekst op zijn pagina ligt en de bovenrand van het latere op of boven de hoogste broodtekst op de volgende pagina, elk binnen AlignmentTolerance. In gewone taal: de tabel was het laatste op pagina N en het eerste op pagina N+1, en een paginanummer of documenttitel in de margeband telt niet mee. Die uitsluiting is niet willekeurig. ISO 32000-1 §14.8.2.2 classificeert doorlopende kop- en voetteksten als pagineringartefacten, inhoud die bestaat vanwege de paginagrens in plaats van ondanks hem, en hetzelfde idee dat een getagde reader ze laat overslaan is wat een tabel er voorbij laat lopen. Het artikel over marked content behandelt hoe getagde bestanden die artefacten expliciet declareren; hier wordt de classificatie uit positie afgeleid, omdat de meeste geëxporteerde tabellen helemaal geen tags dragen
De twee tests combineren met OR. Een rapport van een layout-engine waarvan de tabellen tot de papierrand lopen slaagt voor de eerste; een Word-export waarvan de tabellen bij de marge stoppen slaagt voor de tweede; een document dat beide doet slaagt twee keer. Pas nadat één van de twee is geslaagd lopen de overige gates, en ze lopen in een vaste volgorde: de paginanummers moeten aangrenzend zijn, het latere fragment mag niet met een bijschriftrij openen, en de kolomgrenzen moeten binnen tweemaal AlignmentTolerance overeenkomen, wat met de standaardwaarden 6 punten is. De enumeratie is TPdfTableContinuation met de waarden ptcNone, ptcStart, ptcMiddle en ptcEnd. Een fragment dat als ptcEnd is gemarkeerd en daarna doorlinkt naar nog een pagina wordt gepromoveerd naar ptcMiddle, zodat een tabel van drie pagina's start, midden, eind leest in paginavolgorde. Groepsnummers beginnen bij 1 en 0 betekent ongekoppeld, en ToJson schrijft dezelfde informatie uit als de leden continuation en continuationGroup, wat de vorm is om te verkiezen als een downstream-service het aaneenrijgen doet
uses
PDFium;
var
Pdf: TPdf;
Options: TPdfTableExtractionOptions;
Tables: TPdfTables;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'itinerary-from-word.pdf';
Pdf.LoadDocument;
Options := TPdfTableExtractionOptions.Default;
Options.DetectContinuations := True; // standaard; hier voor de duidelijkheid
Options.ContinuationMargin := 54; // voettekst van twee regels, ~50 pt diep
Tables := Pdf.ExtractDocumentTables(Options);
for I := 0 to High(Tables) do
case Tables[I].Continuation of
ptcStart:
Writeln(Format('group %d starts on page %d (%d rows)',
[Tables[I].ContinuationGroup, Tables[I].PageNumber,
Tables[I].RowCount]));
ptcMiddle, ptcEnd:
Writeln(Format('group %d continues on page %d (%d rows)',
[Tables[I].ContinuationGroup, Tables[I].PageNumber,
Tables[I].RowCount]));
else
Writeln(Format('standalone table on page %d (%d rows)',
[Tables[I].PageNumber, Tables[I].RowCount]));
end;
finally
Pdf.Free;
end;
end;
Hoe voorkomt een bijschriftrij dat twee tabellen aan elkaar smelten?
Een fragment op de volgende pagina waarvan de eerste rij één cel is die alle kolommen omspant, wordt behandeld als een nieuwe tabel, nooit als de rest van de vorige. Die regel bestaat omdat de inhoudsbewuste test op zichzelf te graag koppelt. Het geval dat hem blootlegde was een formulier in transcriptstijl: een tabel eindigt bij de onderkant van pagina 1, een tweede tabel met identieke kolombreedtes begint bij de bovenkant van pagina 2, er staat niets tussen behalve de voettekst, en de kolommen kloppen tot op de punt. Onder de margetest kwamen de twee nooit bij elkaar omdat geen van beide een rand raakte; onder de inhoudstest koppelden ze onmiddellijk, en een formulier met secties werd één onsamenhangend raster. Wat ze scheidt is zichtbaar in de celstructuur. De tweede tabel opent met een sectiebijschrift als "RECIPIENT INFORMATION", opgemaakt als één samengevoegde cel over de volle breedte, en een echt vervolg doet dat nooit, want het bijschrift hoort bij de tabel die al op de vorige pagina begon. TableStartsWithCaptionRow codeert precies dat: het fragment heeft minstens twee kolommen en bevat een cel met RowIndex = 0, ColumnIndex = 0 en ColumnSpan = ColumnCount. De controle loopt alleen op het latere fragment, dus een tabel waarvan de eigen bijschriftrij op zijn eerste pagina staat blijft ongemoeid; het bijschrift staat op pagina N, en alleen het fragment op pagina N+1 wordt bekeken
De kolomvergelijking die volgt, TablesHaveMatchingColumns, is strenger dan "hetzelfde aantal kolommen". Hij bouwt de grensliggingen van elk fragment opnieuw op uit de celrechthoeken, interpoleert grenzen die samengevoegde cellen verbergen, en weigert het paar wanneer een grens meer dan de tolerantie afwijkt. Twee tabellen met vier kolommen in andere verhoudingen blijven dus gescheiden, ook wanneer al het andere klopt
Wat gebeurt er met één rij die naar de volgende pagina doorloopt?
Een gelijnd raster dat één rij naar de volgende pagina draagt wordt nu gevonden en gekoppeld, mits het in een vervolgketen terechtkomt; op zichzelf wordt het weggegooid. De standaard MinRows van 2 bestaat om te voorkomen dat een verdwaald paar lijnen als tabel wordt gemeld, maar een laatste rij die over de breek wordt geduwd is een echte rij die een harde ondergrens van 2 stilzwijgend liet vallen, en de rest van de tabel leek compleet terwijl hij dat niet was. De scan op documentniveau handelt dat in drie stappen af. Wanneer DetectContinuations en DetectRuledTables beide aan staan, laat de ronde per pagina de gelijnde detector lopen met de rijondergrens tijdelijk op 1, en daarom accepteert ExtractTables nu MinRows van 1 voor gelijnde rasters terwijl whitespace-detectie een interne ondergrens van 2 houdt. Vervolgen worden over het volledige resultaat gemarkeerd. Daarna wordt elke tabel verwijderd die korter is dan de MinRows van de aanroeper en in geen enkele keten zit. Het fragment van één rij overleeft alleen doordat het gekoppeld was, en een raster van één rij midden op een verder gewone pagina wordt er precies zoals voorheen uitgefilterd
// Bouw elke keten opnieuw op als één CSV en laat herhaalde kopregels
// op de vervolgfragmenten wegvallen
procedure ExportChains(const Tables: TPdfTables; const Folder: string);
var
I, R: Integer;
Lines: TStringList;
Csv: TStringList;
begin
Csv := TStringList.Create;
Lines := TStringList.Create;
try
for I := 0 to High(Tables) do
begin
if Tables[I].Continuation in [ptcNone, ptcStart] then
Csv.Clear;
Lines.Text := string(Tables[I].ToCsv);
if (Tables[I].Continuation in [ptcMiddle, ptcEnd]) and
(Lines.Count > 1) and (Tables[I].RowCount > 1) then
Lines.Delete(0); // kopregel herhaald door de tekstverwerker
for R := 0 to Lines.Count - 1 do
Csv.Add(Lines[R]);
if Tables[I].Continuation in [ptcNone, ptcEnd] then
Csv.SaveToFile(Format('%s\page%d-group%d.csv',
[Folder, Tables[I].PageNumber, Tables[I].ContinuationGroup]));
end;
finally
Lines.Free;
Csv.Free;
end;
end;
Twee details in die routine zijn bewust. De doorlopende rij van één wordt nooit gestript, want de bewaking op RowCount houdt hem tegen, en een tekstverwerker die de kopregel op elke pagina herhaalt levert een fragment op waarvan de eerste regel weer de kop is, dus regel nul weglaten op midden- en eindfragmenten is juist voor dat geval en fout voor een generator die geen koppen herhaalt. Controleer één document voordat je de routine op een hele map loslaat
Waar de regels nog steeds stoppen
De inhoudsbewuste test is alleen zo goed als de tekstlaag die hij leest. Op een gescande pagina zonder enige tekst vallen de vastgelegde uitersten van de broodtekst terug op de paginagrenzen, is aan de voorwaarde "niets ertussen" leeg voldaan, en blijven alleen de gates voor bijschriftrijen en kolommen over; een gelijnd raster op zo'n pagina wordt nog steeds als leeg skelet gevonden, dus de keten kan correct koppelen, maar er is niets aan de omringende tekst werkelijk geverifieerd. Voeg eerst een tekstlaag toe als dat ertoe doet. Voetteksten die als afbeelding zijn gerenderd in plaats van als tekst zijn onzichtbaar voor de bandlogica en om dezelfde reden onschadelijk
De banden zijn één getal. Een voettekst die dieper is dan ContinuationMargin laat zijn onderste regels binnen de broodtekstzone, waardoor het eruitziet of het eerdere fragment gevolgd wordt door tekst en de koppeling geblokkeerd wordt; verhoog de optie naar de werkelijke banddiepte, zoals het eerste voorbeeld doet. Verhoog hem te ver en een korte afsluitende alinea onderaan de pagina glipt de band in en wordt genegeerd, wat een tabel aan wat er dan ook volgt koppelt. De bijschriftregel heeft een spiegelbeeldige fout: een generator die een samengevoegde "continued"-banner als eerste rij van elk vervolgfragment schrijft, krijgt die fragmenten als nieuwe tabellen geweigerd, en het enige middel dat er vandaag is, is zelf aaneenrijgen op ContinuationGroup zonder iets te versoepelen, want de regel heeft geen schakelaar
Whitespace-gedetecteerde tabellen krijgen niets van de verlichting voor één rij. De whitespace-strategie heeft twee uitgelijnde rijen nodig om überhaupt een tabel te zien, dus een ongelijnde tabel die één rij laat doorlopen wordt nog steeds die ene rij te kort gemeld. Loop je daar tegenaan, dan geven de woordboxen achter gestructureerde tekstblokken en leesorde je de ruwe posities om hem terug te halen. Op de voorbeeldset die dit werk dreef, dertien exports van tekstverwerkers en browsers, koppelden de vijf documenten met echte meerbladige tabellen allemaal tot enkele ketens en bleef het transcriptformulier dat eerder samensmolt gescheiden, en dat is de lat waaraan de release is gemeten, geen belofte over elke lay-out
Vervolgmarkering, de bijschriftregel en de ronde voor één rij zitten allemaal in het documentniveau-pad dat Delphi-, C++Builder- en Lazarus-builds delen; de volledige tabelextractie-API staat beschreven op de PDFium Component for Delphi-pagina