PDFium Component version 3.117.0 kopplar ihop en tabell som bryts över en sidgräns när antingen båda fragmenten når sidkanterna eller ingen brödtext ligger under det första fragmentet och ovanför det andra, med löpande sidhuvud och sidfot ignorerade. ExtractDocumentTables tillämpar det innehållsmedvetna testet som ett alternativ till det äldre sidmarginalstestet, avvisar ett nästa-sida-fragment vars första rad är en enda fullbreddsrubrikcell, och behåller en enstaka rad som spiller över på följande sida som en del av sin fortsättningskedja
Artikeln om tabelldetektering och -utvinning presenterade fortsättning som fyra strikta grindar och behandlade "når sidkanten" som en av dem. Den beskrivningen var korrekt för den release den täckte, och den var också fel för de flesta tabeller folk faktiskt matar komponenten med. Den här artikeln är rättelsen: vilka dokument marginaltestet inte klarar, vad som ersatte det, och de två sidofall som fixen drog med sig
Varför misslyckas sidmarginalstestet på Word-exporter?
Sidmarginalstestet misslyckas för att ett ordbehandlingsprogram slutar lägga ut rader vid nedre marginalen, inte vid papperskanten. Med standardvärdet ContinuationMargin på 36 punkter krävde den ursprungliga regeln att det tidigare fragmentets underkant låg inom 36 punkter från sidans botten och det senare fragmentets överkant inom 36 punkter från sidans topp. Ett dokument exporterat från Word med sina standardmarginaler på en tum placerar sista raden minst 72 punkter ovanför sidans botten, ännu högre om en sidfot finns, så villkoret höll aldrig. Varje lång tabell i ett sådant dokument kom tillbaka som oberoende fragment med ContinuationGroup på noll, och anroparen var tillbaka vid att sy ihop för hand. Testet är fortfarande vettigt för det det designades kring: rapporter genererade av layoutmotorer som fyller en sida till en fast innehållsbox och startar nästa sida kant i kant högst upp. Det är inte en dålig regel, det är en ofullständig regel, vilket är varför version 3.117.0 behöll den och lade till en andra väg i stället för att ersätta den
Vad kontrollerar det innehållsmedvetna testet i stället?
Det innehållsmedvetna testet kontrollerar om något annat än tabellen upptar utrymmet mellan de två fragmenten, och använder ordfyrkanterna på varje sida i stället för sidgeometrin. Medan ExtractDocumentTables går genom dokumentet registrerar den, per sida, den lägsta underkanten av något ord vars överkant ligger ovanför sidfotsbandet och den högsta överkanten av något ord vars underkant ligger nedanför sidhuvudsbandet. Båda banden är ContinuationMargin punkter djupa, så samma inställning gör nu dubbel tjänst som spelrum vid sidkanten och som höjden på zonerna för löpande sidhuvud och sidfot. Ett fragmentpar godkänns när det tidigare fragmentets underkant ligger i nivå med eller under den lägsta brödtexten på sin sida och det senare fragmentets överkant ligger i nivå med eller över den högsta brödtexten på nästa sida, vardera inom AlignmentTolerance. I klartext: tabellen var det sista på sidan N och det första på sidan N+1, och ett sidnummer eller en dokumenttitel i marginalbandet räknas inte. Det undantaget är inte godtyckligt. ISO 32000-1 §14.8.2.2 klassificerar löpande sidhuvuden och sidfötter som pagineringsartefakter, innehåll som finns på grund av sidbrytningen snarare än trots den, och samma tanke som låter en taggad läsare hoppa över dem är vad som låter en tabell fortsätta förbi dem. Artikeln om markerat innehåll tar upp hur taggade filer deklarerar de artefakterna uttryckligen; här härleds klassificeringen ur position, eftersom de flesta exporterade tabeller inte bär några taggar alls
De två testerna kombineras med OR. En layoutmotorrapport vars tabeller går ända till papperskanten klarar det första; en Word-export vars tabeller stannar vid marginalen klarar det andra; ett dokument som gör båda klarar båda. Först efter att ett av dem lyckats kör resten av grindarna, och de kör i fast ordning: sidnumren måste vara intilliggande, det senare fragmentet får inte inledas med en rubrikrad, och kolumngränserna måste stämma inom två gånger AlignmentTolerance, vilket är 6 punkter med standardvärdena. Uppräkningen är TPdfTableContinuation med värdena ptcNone, ptcStart, ptcMiddle och ptcEnd. Ett fragment som märktes ptcEnd och sedan kopplas vidare till ännu en sida befordras till ptcMiddle, så en tabell över tre sidor läses som start, middle, end i sidordning. Gruppnumren börjar på 1 och 0 betyder okopplad, och ToJson ger samma information som medlemmarna continuation och continuationGroup, vilket är formen att föredra om en nedströms tjänst gör ihopsyrningen
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; // standard; visas för tydlighetens skull
Options.ContinuationMargin := 54; // tvåradig sidfot, ca 50 pt djup
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;
Hur hindrar en rubrikrad två tabeller från att smälta samman?
Ett nästa-sida-fragment vars första rad är en enda cell som spänner över varje kolumn behandlas som en ny tabell, aldrig som resten av den föregående. Regeln finns för att det innehållsmedvetna testet på egen hand kopplar ihop alltför villigt. Fallet som avslöjade det var ett formulär av utskriftstyp: en tabell slutar nära botten av sida 1, en andra tabell med identiska kolumnbredder börjar nära toppen av sida 2, inget annat än sidfoten ligger mellan dem, och kolumnerna stämmer på punkten. Under marginaltestet möttes de aldrig eftersom ingen av dem nådde en kant; under innehållstestet kopplades de ihop direkt, och ett formulär med avsnitt blev ett enda osammanhängande rutnät. Det som skiljer dem åt syns i cellstrukturen. Den andra tabellen inleds med en avsnittsrubrik som "RECIPIENT INFORMATION" utlagd som en enda sammanslagen cell över hela bredden, och en äkta fortsättning gör aldrig det, för rubriken tillhör den tabell som redan började på föregående sida. TableStartsWithCaptionRow kodar exakt det: fragmentet har minst två kolumner och innehåller en cell med RowIndex = 0, ColumnIndex = 0 och ColumnSpan = ColumnCount. Kontrollen körs bara på det senare fragmentet, så en tabell vars egen rubrikrad ligger på dess första sida påverkas inte; rubriken ligger på sidan N, och bara fragmentet på sidan N+1 inspekteras
Kolumnjämförelsen som följer, TablesHaveMatchingColumns, är striktare än "samma kolumnantal". Den bygger om gränspositionerna för varje fragment ur cellrektanglarna, interpolerar gränser som sammanslagna celler döljer, och avvisar paret när någon gräns driver mer än toleransen. Två fyrakolumnstabeller med olika proportioner hålls därför åtskilda även när allt annat stämmer
Vad händer med en enstaka rad som spiller över på nästa sida?
Ett linjerat rutnät som för en rad över till följande sida upptäcks och kopplas nu, förutsatt att det hamnar i en fortsättningskedja; på egen hand kastas det. Standardvärdet MinRows på 2 finns för att hindra ett löst par linjer från att rapporteras som en tabell, men en sista rad som knuffats över brytningen är en riktig rad som ett hårt golv på 2 tyst tappade, och resten av tabellen såg komplett ut när den inte var det. Den dokumentövergripande genomsökningen hanterar det i tre steg. När DetectContinuations och DetectRuledTables båda är satta kör sidgenomgången den linjerade detektorn med radgolvet tillfälligt sänkt till 1, vilket är varför ExtractTables nu accepterar MinRows på 1 för linjerade rutnät medan whitespace-detekteringen behåller ett internt golv på 2. Fortsättningar markeras över hela resultatet. Sedan tas varje tabell bort som är kortare än anroparens MinRows och inte ingår i någon kedja. Det enradiga fragmentet överlever bara för att det kopplades, och ett enradigt rutnät mitt på en i övrigt vanlig sida filtreras bort precis som förut
// Bygg om varje kedja till en CSV och släpp upprepade rubrikrader
// på fortsättningsfragmenten
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); // rubrik upprepad av ordbehandlingsprogrammet
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;
Två detaljer i den rutinen är avsiktliga. Den enradiga överflödningen skalas aldrig bort, för vakten på RowCount behåller den, och ett ordbehandlingsprogram som upprepar rubrikraden på varje sida ger ett fragment vars första rad är rubriken igen, så att släppa rad noll på middle- och end-fragment är rätt för det fallet och fel för en generator som inte upprepar rubriker. Kontrollera ett dokument innan du släpper rutinen lös på en mapp
Var reglerna fortfarande tar stopp
Det innehållsmedvetna testet är bara så bra som det textlager det läser. På en skannad sida helt utan text faller de registrerade brödtextextremvärdena tillbaka på sidans gränser, villkoret "inget mellan" uppfylls tomt, och bara grindarna för rubrikrad och kolumner återstår; ett linjerat rutnät på en sådan sida hittas ändå som ett tomt skelett, så kedjan kan kopplas rätt, men inget om den omgivande texten verifierades faktiskt. Lägg till ett textlager först om det spelar roll. Sidfötter som renderas som bilder i stället för text är osynliga för bandlogiken och ofarliga av samma skäl
Banden är en enda siffra. En sidfot djupare än ContinuationMargin lämnar sina nedre rader inne i brödtextzonen, vilket får det tidigare fragmentet att se ut att följas av text och blockerar länken; höj inställningen till det verkliga banddjupet, som det första exemplet gör. Höj den för mycket och ett kort avslutande stycke nära sidans botten glider in i bandet och ignoreras, vilket kopplar en tabell till vad som än följer efter den. Rubrikregeln har ett spegelvänt fel: en generator som skriver en sammanslagen "continued"-banner som första rad i varje fortsättningsfragment får de fragmenten avvisade som nya tabeller, och enda botemedlet i dag är att sy ihop själv efter ContinuationGroup, utan att lätta på något, eftersom regeln inte har någon brytare
Whitespace-detekterade tabeller får ingen av lindringen för enstaka rader. Whitespace-strategin behöver två justerade rader för att alls se en tabell, så en olinjerad tabell som spiller en rad rapporteras fortfarande en rad för kort. När du stöter på det ger ordfyrkanterna bakom strukturerade textblock och läsordning dig råpositionerna för att återställa den. På det urval som drev det här arbetet, tretton exporter från ordbehandlare och webbläsare, kopplades de fem dokumenten med äkta flersidiga tabeller alla till enkla kedjor och utskriftsformuläret som tidigare smälte samman hölls åtskilt, vilket är ribban releasen mättes mot, inte ett löfte om varje layout
Fortsättningsmarkering, rubrikregeln och enradspasset ligger alla i den dokumentövergripande vägen som delas av Delphi-, C++Builder- och Lazarus-byggen; hela API:et för tabellutvinning beskrivs på sidan för PDFium Component for Delphi