PDFium Component 3.117.0 versija nustoja pranešti lygiuotas pastraipas kaip tarpais sulygiuotas lenteles: ji reikalauja, kad kiekviena stulpelio riba būtų vertikalus koridorius be teksto kiekvienoje eilutėje, kurią ji skiria, praleidžia žodžius, kuriuos jau pasiėmė brūkšniuotas tinklelis, ir ląstelės tekstą surenka pagal vertikalų persidengimą, o ne pagal glifų stačiakampių centrų atstumą. Visi trys pakeitimai gyvena ExtractTables ir ExtractDocumentTables viduje ir jokių nustatymų nereikalauja
Pranešimas, nuo kurio visa tai prasidėjo, nebuvo gražus. Puslapis su spaudai skirtu pranešimu, kuriame nėra jokios lentelės, iš ExtractTables grįžo su 5x4 tarpų lentele, kurios pasitikėjimas patogiai viršijo numatytąjį MinConfidence 0,5, o ląstelėse buvo įprasto pagrindinio teksto fragmentai. Priėmimo anketa tą patį padarė su savo esė pastraipomis ir pagamino 3x4 bei 5x3. Abu dokumentai buvo lygiuoti. Akivaizdus atsakymas yra derinti slenksčius, o naudingoji šio leidimo pamoka yra ta, kad derinimas to nesutvarkys, nes derinama taisyklė uždavė netinkamą klausimą
uses
PDFium;
// Regresijos patikra: išvardyti visas tarpų lenteles dokumente, kad puslapį,
// apie kurį žinote, jog jame tik proza, būtų galima patvirtinti švarų
procedure ReportWhitespaceTables(Pdf: TPdf);
var
Options: TPdfTableExtractionOptions;
Tables: TPdfTables;
I: Integer;
begin
Options := TPdfTableExtractionOptions.Default; // MinColumnGap 12pt
Tables := Pdf.ExtractDocumentTables(Options);
for I := 0 to High(Tables) do
if Tables[I].DetectionMode = ptdmWhitespace then
Writeln(Format('page %d: %dx%d whitespace table, confidence %.2f, ' +
'first cell "%s"',
[Tables[I].PageNumber, Tables[I].RowCount, Tables[I].ColumnCount,
Tables[I].Confidence, Tables[I].Cells[0].Text]));
end;
Kodėl lygiuotas tekstas atrodo kaip lentelė?
Lygiuota pastraipa atrodo kaip lentelė todėl, kad lygiuota eilutė yra žodžių eilė, atskirta tarpais, kuriuos dėliotuvas ištempė, ir kai tik ištemptas tarpas pasiekia MinColumnGap, detektorius neturi eilutės viduje jokio būdo atskirti jo nuo stulpelio skyriklio. Tarpų strategija PDFium Component viduje žodžių stačiakampius grupuoja į vaizdines eilutes, kiekvieną eilutę skaldo į žodžių grupes visur, kur horizontalus atstumas iki ankstesnio žodžio yra bent MinColumnGap (12 taškų pagal nutylėjimą), ir lentelę priima tada, kai bent dvi iš eilės einančios eilutės pakartoja bent MinColumns kairėn lygiuotų grupių tvirtinimo taškų AlignmentTolerance ribose, kuri yra 3 taškai. Tai taisyklė, aprašyta lentelių aptikimo apžvalgoje, ir tikrai sulygiuotai lentelei ji yra visiškai teisinga
Dabar pritaikykite ją dvidešimčiai lygiuoto 10 taškų prozos eilučių. Kiekviena eilutė ištempiama iki tos pačios dešiniosios paraštės, tad eilutė, pasibaigianti ilgu žodžiu, išskleidžia savo vidinius tarpus, o pastraipoje su keliomis trumpomis eilutėmis kai kurie tie tarpai peržengia 12 taškų. Dviem iš eilės einančioms eilutėms tereikia po vieno ištempto tarpo, patenkančio į 3 taškų atstumą nuo tos pačios X padėties, kad susidarytų dviejų eilučių ir dviejų stulpelių kandidatas. Per pakankamai daug eilučių tai jau ne nepasisekimas; tai tikimybė, artėjanti prie tikrumo, o tie 5x4 spaudos pranešime tebuvo tas atvejis, kai keturi tokie tarpai išsirikiavo penkiose eilutėse
Kiekvienas slenkstis išmaino vieną dokumentų klasę į kitą. Pakėlus MinColumnGap iki 20 taškų prarandami tankių finansinių ataskaitų kompaktiški stulpeliai — būtent tas atvejis, dėl kurio numatytoji reikšmė jau buvo sumažinta. Pakėlus MinRows iki 3 atmetamos tikros dviejų eilučių lentelės ir tik sumažinama ilgų pastraipų tikimybė. Sugriežtinus AlignmentTolerance žemiau 3 taškų sulaužomi iš OCR gauti žodžių stačiakampiai, kurių kairieji kraštai svyruoja daugiau. Eilučių lygmens signalas tikrai dviprasmis, tad pataisa turi ateiti iš signalo, kurio eilutės pačios savaime neduoda
Kas stulpelio ribą padaro tikrą?
Tikra stulpelio riba yra vertikali puslapio juosta, kuri lieka tuščia kiekvienoje eilutėje, kurią ji skiria. Lentelė tokią turi tarp kiekvienos stulpelių poros jau pačia konstrukcija, nes ląstelės buvo dėliojamos pagal bendras X padėtis. Lygiuota pastraipa savo žodžių tarpus ištempia skirtingose horizontaliose padėtyse kiekvienoje eilutėje, tad jokia juosta neišlaiko daugiau nei vienos ar dviejų eilučių sankirtos. PDFium Component dabar tikrina būtent tai: kai kandidato žodžių grupės jau priskirtos tvirtinimo stulpeliams, kiekvienai gretimų stulpelių porai ji ima kiekvienoje eilutėje, kurioje abiejose ląstelėse yra turinio, intervalą nuo dešiniausio kairiosios ląstelės žodžių krašto iki kairiausio dešiniosios ląstelės žodžių krašto, susikerta tuos intervalus per eilutes ir atmeta visą kandidatą, jei sankirta siauresnė už MinColumnGap kartų 0,5, kuri su numatytąja reikšme yra 6 taškai
Svarbios dvi detalės. Eilutės, kuriose bent viena ląstelė tuščia, nebalsuoja, tad lentelė su tuščia ląstele arba su antrašte, apimančia mažiau stulpelių nei turinys, vis tiek praeina. O koridoriaus plotis išvedamas iš MinColumnGap, o ne atidengiamas kaip atskiras nustatymas, nes abu aprašo tą patį fizinį dalyką: tarpą, kurį dizaineris palieka tarp stulpelių. Logika pakankamai maža, kad ją atkartotumėte, jei statote ant neapdorotų žodžių stačiakampių, o ne ant lentelių API, ir žemiau esantis pavyzdys atkartoja patikrinimą komponento viduje:
uses
Math, PDFium;
type
TIndexList = array of Integer;
TCellIndexes = array of TIndexList; // Row * ColumnCount + Column
// Grąžina False, kai bet kuriai gretimų stulpelių porai trūksta be teksto
// vertikalaus koridoriaus, bent MinColumnGap / 2 pločio eilutėse, kurios jį naudoja
function HasTextFreeCorridors(const Words: TPdfWordBoxes;
const Cells: TCellIndexes; RowCount, ColumnCount: Integer;
MinColumnGap: Double): Boolean;
var
Col, Row, I, LeftCell, RightCell, Supported: Integer;
CorridorLeft, CorridorRight, RowLeft, RowRight: Double;
begin
for Col := 0 to ColumnCount - 2 do
begin
CorridorLeft := -MaxDouble;
CorridorRight := MaxDouble;
Supported := 0;
for Row := 0 to RowCount - 1 do
begin
LeftCell := Row * ColumnCount + Col;
RightCell := LeftCell + 1;
if (Length(Cells[LeftCell]) = 0) or (Length(Cells[RightCell]) = 0) then
Continue; // tuščios ląstelės nebalsuoja
RowLeft := -MaxDouble;
RowRight := MaxDouble;
for I in Cells[LeftCell] do
RowLeft := Max(RowLeft, Words[I].Rect.Right);
for I in Cells[RightCell] do
RowRight := Min(RowRight, Words[I].Rect.Left);
CorridorLeft := Max(CorridorLeft, RowLeft);
CorridorRight := Min(CorridorRight, RowRight);
Inc(Supported);
end;
if (Supported > 0) and
(CorridorRight - CorridorLeft < MinColumnGap * 0.5) then
Exit(False);
end;
Result := True;
end;
Kodėl brūkšniuotos lentelės buvo ištraukiamos du kartus?
Brūkšniuotos lentelės būdavo ištraukiamos du kartus, nes tarpų ėjimas matydavo kiekvieną puslapio žodį, įskaitant tuos, kuriuos brūkšnių ėjimas jau buvo sudėjęs į tinklelį, o tvarkinga brūkšniuota lentelė jau pačia konstrukcija yra ir nepriekaištingai sulygiuota tarpų lentelė. Persidengimo patikra jau atmesdavo tarpų kandidatą, kurio ribos apimdavo daugiau nei pusę esamos lentelės, bet kandidatas, jungiantis apatines lentelės eilutes su keliomis sulygiuotomis teksto eilutėmis po ja, galėjo nepatekti į tą santykį ir išlikti kaip antra, šiek tiek didesnė lentelė, įsiveržianti į savo kaimynę. ExtractTables dabar tuos žodžius pašalina prieš paleisdamas tarpų ėjimą. Žodis išmetamas, kai jo centro taškas patenka į bet kurios brūkšnių ėjimo sukurtos lentelės ribas; naudojamas centras, o ne visiškas apėmimas, kad žodis, keliomis taško dalimis persiritęs per rėmelį, sektų tą lentelę, kuriai vaizdiškai priklauso. Tarpų strategija tada dirba tik su laisvaisiais žodžiais, o tai reiškia ir tai, kad nedidelė nebrūkšniuota lentelė, esanti tiesiai po brūkšniuota, aptinkama savo nuopelnais, o ne suliejama su tinkleliu virš jos
Kodėl „Purpose of Request:" išėjo kaip „of Purpose Request:"?
Žodžiai išėjo pertvarkyti todėl, kad žodžių stačiakampiai, kuriuos kuria PDFium Component, yra glifų ribinių stačiakampių sąjungos, o „of" neturi nusileidėjų, kai „Purpose" ir „Request:" jų turi. FPDFText_GetCharBox grąžina glifo rašalo glaudųjį stačiakampį puslapio erdvėje, o ne stačiakampį, praplėstą iki šrifto viršutinės ir apatinės linijos, o žodžio stačiakampis yra jo simbolių stačiakampių sąjunga. Todėl žodis be nusileidėjų yra trumpesnis ir jo vertikalus centras sėdi aukščiau — toje anketoje 2–3 taškais. Senoji ląstelės teksto rutina žodžius rikiavo pirmiausia pagal centrų Y su 1 taško tolerancija „tai pačiai eilutei", o paskui pagal kairįjį kraštą; „of" viršijo toleranciją, išsirikiavo kaip savo atskira eilutė virš kitų ir buvo išvestas pirmas
Tai ne tiek PDFium keistenybė, kiek pasekmė to, kaip PDF išdėsto tekstą. ISO 32000-1 §9.2.2 ir §9.4.4 glifų padėjimą apibrėžia kaip horizontalų poslinkį išilgai bazinės linijos teksto erdvėje, o vienintelės vertikalios metrikos, kurias failas neša, yra priskirtos šriftui: šrifto deskriptoriaus Ascent, Descent ir FontBBox įrašai §9.8.1. Niekas faile nesako, kad du glifai dalijasi eilute; tai turi būti išvedama iš geometrijos, o glaudieji glifų stačiakampiai, dėl kurių žymėjimo paryškinimas atrodo teisingai, kaip aprašyta straipsnyje apie teksto eilučių žymėjimą su PDFium char box, yra netinkama įvestis centrų atstumo palyginimui
3.117.0 versijos pataisa klausimą „kokiu atstumu yra centrai" pakeičia klausimu „kiek stačiakampiai persidengia vertikaliai". Ląstelės tekstas surenkamas pirmiausia sugrupuojant ląstelės žodžius į vaizdines eilutes, kur žodis prisijungia prie eilutės, kai jo vertikalus persidengimas su eilutės einamosiomis ribomis yra bent 25 procentai mažesniojo iš abiejų aukščių, paskui įterpimo rikiavimu surikiuojant kiekvieną eilutę pagal kairįjį kraštą ir galiausiai sujungiant eilutes eilutės lūžiu. „Purpose" ir „of" persidengia per visą x aukštį, o tai kur kas daugiau nei 25 procentai trumpesniojo stačiakampio, tad jie atsiduria toje pačioje eilutėje ir išsirikiuoja pagal X taip, kaip ir turėjo
Teksto eilutes grupuoti pagal persidengimą, o ne pagal centrų atstumą
Iš šios klaidos verta išsinešti bendrą taisyklę: bet kuris PDF teksto dėstymo kodas, kuris „ta pati eilutė" nustato lygindamas vertikalius centrus su fiksuota tolerancija, tikruose šriftuose suges, ir gedimas bus tylus: niekas neišmes klaidos, žodžiai tiesiog išeis neteisinga tvarka. Mišrūs nusileidėjai yra švelniausias paleidiklis. Storas 12 taškų užrašas šalia 10 taškų reikšmių, viršutinio indekso išnašos žymuo, valiutos simbolis, paimtas iš atsarginio šrifto, ir OCR žodžių stačiakampiai su kiekvieno žodžio aukščio triukšmu — visi jie pastumia centrus labiau nei bet kuri tolerancija, kuri dar atskiria gretimas 10 taškų teksto eilutes su 12 taškų tarpueiliu. Persidengimo santykis nepriklauso nuo dydžio: du stačiakampiai ant vienos bazinės linijos persidengia per savo bendrą x aukštį, kad ir ką daro jų iškėlėjai bei nusileidėjai, o du stačiakampiai gretimose eilutėse nepersidengia visai
Tą pačią taisyklę lengva pritaikyti ir už lentelių ištraukimo ribų. TPdf.PageWordBoxes grąžina kiekvieną aktyvaus puslapio žodį su jo stačiakampiu puslapio erdvėje, tad puslapio sugrupavimas į vaizdines eilutes yra trumpas ciklas:
uses
Math, PDFium;
function SameVisualLine(const A, B: TPdfRectangle): Boolean;
var
Overlap, MinHeight: Double;
begin
Overlap := Min(A.Top, B.Top) - Max(A.Bottom, B.Bottom);
MinHeight := Min(A.Top - A.Bottom, B.Top - B.Bottom);
Result := (MinHeight > 0) and (Overlap >= MinHeight * 0.25);
end;
procedure GroupPageIntoLines(Pdf: TPdf; out Lines: TArray<TPdfWordBoxes>);
var
Words: TPdfWordBoxes;
Bounds: TArray<TPdfRectangle>; // einamoji sąjunga kiekvienai eilutei
I, J, Found: Integer;
begin
Words := Pdf.PageWordBoxes;
Lines := nil;
Bounds := nil;
for I := 0 to High(Words) do
begin
Found := -1;
for J := High(Lines) downto 0 do
if SameVisualLine(Bounds[J], Words[I].Rect) then
begin
Found := J;
Break;
end;
if Found < 0 then
begin
SetLength(Lines, Length(Lines) + 1);
SetLength(Bounds, Length(Bounds) + 1);
Found := High(Lines);
Bounds[Found] := Words[I].Rect;
end;
SetLength(Lines[Found], Length(Lines[Found]) + 1);
Lines[Found][High(Lines[Found])] := Words[I];
Bounds[Found].Left := Min(Bounds[Found].Left, Words[I].Rect.Left);
Bounds[Found].Right := Max(Bounds[Found].Right, Words[I].Rect.Right);
Bounds[Found].Top := Max(Bounds[Found].Top, Words[I].Rect.Top);
Bounds[Found].Bottom := Min(Bounds[Found].Bottom, Words[I].Rect.Bottom);
end;
// prieš skaitant kiekvieną eilutę surikiuoti ją pagal Rect.Left; PageWordBoxes
// grąžina žodžius turinio srauto tvarka, kuri nebūtinai yra vaizdinė
end;
Kas keičiasi esamiems kvietėjams ir kur yra ribos
To kodo gabaliuko esmė yra predikatas, o ne ciklas; viskam, kas viršija greitą išklotinę, pradėkite nuo struktūrizuoto teksto modelio, kuris jau neša blokus, eilutes ir skaitymo tvarkos šaltinį, kaip aptarta straipsnyje apie struktūrizuoto PDF teksto ištraukimą su skaitymo tvarka. Esami lentelių kvietėjai gauna visas tris pataisas neliesdami savo nustatymų. Koridoriaus slenkstis fiksuotas per pusę MinColumnGap, tarpų strategija išlaiko dviejų eilučių ribą net tada, kai MinRows nustatytas į 1 (ką brūkšnių strategija dabar priima), o brūkšnių-pirmiausia žodžių filtravimas yra besąlyginis, kai tik abi strategijos įjungtos. 13 dokumentų pavyzdžių rinkinyje, naudotame šiam leidimui, tarpų ėjimas anksčiau grąžindavo 34 fragmentus ir klaidingus teigiamus šalia 9 brūkšniuotų lentelių; po leidimo jis negrąžina nė vieno, o brūkšniuotų lentelių skaičius pakilo iki 41, nors didžioji to kilimo dalis ateina iš to paties leidimo, kuris išmokė brūkšnių detektorių skaityti rėmelius, nubrėžtus kaip užpildyti stačiakampiai — o tai jau atskira istorija
Sąžiningos ribos: koridoriaus patikrai reikia bent vienos eilutės su turiniu abiejose ribos pusėse, kad apskritai ką nors atmestų, tad dviejų eilučių kandidatas, kurio du ištempti tarpai atsitiktinai patenka į 6 taškų atstumą vienas nuo kito, vis tiek praeina. Tai siauras sutapimas, o ne beveik tikrumas, kaip buvo anksčiau, bet dokumentai su gausia proza ir be tikrų dviejų eilučių lentelių gali jį užverti nustatę MinRows į 3. Kairėn lygiuotas nelygus tekstas niekada nebuvo problema ir nėra paliečiamas. O PDF vis dar neturi lentelės objekto; ISO 32000-1 §14.8.4.3 apibrėžia Table struktūros elementą, bet jį neša tik žymėtas PDF, tad viskam kitam tinklelis lieka išvestis iš geometrijos, ir pasitikėjimo reikšmė kiekviename TPdfTable yra todėl, kad išvedimas nusipelno balo
Lentelių ištraukimas, struktūrizuotas tekstas ir žodžių stačiakampiai visi skaito iš to paties puslapio modelio Delphi, C++Builder ir Lazarus; visa API, įskaitant TPdfTableExtractionOptions ir kartu platinamą TableExtractionLab demonstracinį pavyzdį, aprašyta PDFium Component for Delphi puslapyje