Tehnični članak

Lažni pozitivi tabel iz obojestransko poravnanega besedila

PDFium Component v različici 3.117.0 preneha poročati obojestransko poravnane odstavke kot po belini poravnane tabele: zahteva, da je vsaka meja stolpca navpičen koridor brez besedila v vsaki vrstici, ki jo ločuje, preskoči besede, ki jih je mreža z obrobami že zahtevala, in sestavi besedilo celic po navpičnem prekrivanju in ne po razdalji med središči okvirov glifov. Vse tri spremembe živijo znotraj ExtractTables in ExtractDocumentTables in ne potrebujejo nobene možnosti

Prijava, ki je to pognala, ni bila nič bleščeča. Stran s sporočilom za javnost, na kateri ni bilo nobene tabele, se je iz ExtractTables vrnila s tabelo po belini 5x4, z zanesljivostjo udobno nad privzetim MinConfidence 0,5, celice pa so držale drobce običajnega besedila telesa. Pristopni obrazec je naredil isto s svojimi eseji in ustvaril 3x4 in 5x3. Oba dokumenta sta bila obojestransko poravnana. Očiten odziv je nastaviti meje, koristna lekcija te različice pa je, da nastavljanje tega ne more popraviti, ker je pravilo, ki se ga je nastavljalo, zastavljalo napačno vprašanje

uses
  PDFium;

// Regresijski pregled: izpiši vsako tabelo po belini v dokumentu, da
// lahko preverimo, da je stran s samo prozo čista
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;

Zakaj je obojestransko poravnano besedilo videti kot tabela?

Obojestransko poravnan odstavek je videti kot tabela, ker je poravnana vrstica vrsta besed, ločenih z razmiki, ki jih je motor za postavitev raztegnil, in ko raztegnjen razmik doseže MinColumnGap, zaznavanje na ravni vrstice nima načina, da bi ga ločilo od ločila stolpca. Strategija po belini v PDFium Component okvire besed združi v vidne vrstice, vsako vrstico razdeli v skupine besed povsod, kjer je vodoravna razdalja do prejšnje besede vsaj MinColumnGap (privzeto 12 točk), in tabelo sprejme, kadar vsaj dve zaporedni vrstici ponovita vsaj MinColumns levih sidrišč skupin znotraj AlignmentTolerance, kar je 3 točke. To je pravilo, opisano v pregledu zaznavanja tabel, in za resnično poravnano tabelo je natanko pravilno

Uporabite to na dvajsetih vrsticah obojestransko poravnane proze velikosti 10 točk. Vsaka vrstica je raztegnjena do istega desnega roba, zato vrstica, ki se konča z dolgo besedo, razpre svoje notranje razmike, v odstavku z nekaj kratkimi vrsticami pa nekateri od teh razmikov presežejo 12 točk. Dve zaporedni vrstici potrebujeta le po en raztegnjen razmik, ki pristane znotraj 3 točk istega položaja X, da nastane kandidat z dvema vrsticama in dvema stolpcema. Čez dovolj vrstic to ni slaba sreča; je verjetnost, ki se približuje gotovosti, 5x4 v sporočilu za javnost pa je bil preprosto tisti odsek, kjer so se štirje takšni razmiki poravnali v petih vrsticah

Diagram PDFium Component, zakaj se je obojestransko poravnana proza štela kot tabela: vsaka vrstica je raztegnjena do istega roba, zato posamezni razmiki presežejo MinColumnGap na drugem X v vsaki vrstici, dva zaporedna razmika znotraj AlignmentTolerance pa sta zgradila lažne kandidate, ki jih preskus koridorja zdaj zavrne
Resnična tabela svoja sidrišča stolpcev ponovi v vsaki vrstici, obojestransko poravnan odstavek pa v vsaki vrstici raztegne drug razmik, zato nastavljanje na ravni vrstice samo po sebi teh dveh ni moglo ločiti

Vsaka meja trguje en razred dokumentov za drugega. Dvig MinColumnGap na 20 točk izgubi kompaktne stolpce gostih finančnih poročil, kar je natanko primer, zaradi katerega je bila privzeta vrednost že znižana. Dvig MinRows na 3 zavrže resnične dvovrstične tabele in le zmanjša verjetnost pri dolgih odstavkih. Zaostritev AlignmentTolerance pod 3 točke zlomi okvire besed iz OCR, katerih levi robovi nihajo za več od tega. Signal na ravni vrstice je resnično dvoumen, zato mora popravek priti iz signala, ki ga vrstice same ne nosijo

Kaj naredi mejo stolpca resnično?

Resnična meja stolpca je navpičen pas strani, ki ostane prazen v vsaki vrstici, ki jo ločuje. Tabela ga ima med vsakim parom stolpcev po zgradbi, ker so bile celice postavljene glede na skupne položaje X. Obojestransko poravnan odstavek raztegne svoje razmike med besedami na različnih vodoravnih položajih v vsaki vrstici, zato noben pas ne preživi preseka več kot ene ali dveh vrstic. PDFium Component zdaj preverja natanko to: ko so skupine besed kandidata dodeljene sidriščem stolpcev, za vsak par sosednjih stolpcev v vsaki vrstici, ki ima vsebino v obeh celicah, vzame interval od najbolj desnega roba besed leve celice do najbolj levega roba besed desne celice, te intervale preseka čez vrstice in celotnega kandidata zavrne, če je presek ožji od MinColumnGap krat 0,5, kar je pri privzeti vrednosti 6 točk

Diagram PDFium Component preskusa koridorja brez besedila za ExtractTables: vsaka vrstica prispeva interval od desnega roba svoje leve celice do levega roba svoje desne celice, presek pa ostane širši od polovice MinColumnGap pri resnični tabeli in se pri obojestransko poravnanem besedilu zloži na nič
Resnična meja stolpca je prazna v vsaki vrstici, ki jo ločuje, zato presek razmikov po vrsticah za tabelo pusti skupen pas, za raztegnjeno prozo pa nobenega

Pomembni sta dve podrobnosti. Vrstice, v katerih je ena od celic prazna, ne glasujejo, zato tabela s prazno celico ali glava, ki sega čez manj stolpcev kot telo, še vedno gre skozi. In širina koridorja je izpeljana iz MinColumnGap in ni izpostavljena kot ločena možnost, ker oba opisujeta isto fizično stvar: razmik, ki ga oblikovalec pusti med stolpci. Logika je dovolj majhna, da jo ponovite, če gradite na surovih okvirih besed in ne na API-ju za tabele, spodnji vzorec pa zrcali preverjanje znotraj komponente:

uses
  Math, PDFium;

type
  TIndexList = array of Integer;
  TCellIndexes = array of TIndexList;   // Row * ColumnCount + Column

// Vrne False, kadar kateremu paru sosednjih stolpcev manjka navpičen
// koridor brez besedila, širok vsaj MinColumnGap / 2, čez vrstice, ki ga uporabljajo
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;                             // prazne celice ne glasujejo
      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;

Zakaj so bile tabele z obrobami izluščene dvakrat?

Tabele z obrobami so bile izluščene dvakrat, ker je prehod po belini nekoč videl vsako besedo na strani, vključno z besedami, ki jih je prehod po obrobah že postavil v mrežo, čista tabela z obrobami pa je po zgradbi tudi popolnoma poravnana tabela po belini. Preverjanje prekrivanja je že zavračalo kandidata po belini, katerega meje so pokrivale več kot polovico obstoječe tabele, kandidat, ki je združil spodnje vrstice tabele z nekaj poravnanimi vrsticami besedila pod njo, pa je lahko padel pod ta delež in preživel kot druga, nekoliko večja tabela, ki je segala v sosedo. ExtractTables zdaj te besede odstrani, preden se zažene prehod po belini. Beseda se spusti, kadar njeno središče leži znotraj meja katere koli tabele, ki jo je ustvaril prehod po obrobah; uporabljeno je središče in ne popolna vsebnost zato, da beseda, ki za delček točke sega čez obrobo, sledi tabeli, ki ji vidno pripada. Strategija po belini nato dela le na prostih besedah, kar pomeni tudi, da se majhna neobrobljena tabela, ki sedi neposredno pod obrobljeno, zazna po svojih lastnih zaslugah, namesto da bi se zlila z mrežo nad njo

Zakaj se je "Purpose of Request:" izpisalo kot "of Purpose Request:"?

Besede so se izpisale prerazvrščene, ker so okviri besed, ki jih zgradi PDFium Component, unije omejevalnih okvirov glifov, "of" pa nima spusta, medtem ko ga "Purpose" in "Request:" imata. FPDFText_GetCharBox vrne tesen okvir črnila glifa v prostoru strani in ne okvira, oblazinjenega do vzpona in spusta pisave, okvir besede pa je unija okvirov njenih znakov. Beseda brez spustov je zato krajša in njeno navpično središče sedi višje, pri obrazcu v vprašanju za 2 do 3 točke. Stara rutina za besedilo celic je besede najprej razvrstila po središču Y, s 1-točkovno toleranco za "isto vrstico", in nato po levem robu; "of" je toleranco preseglo, razvrstilo se je kot svoja vrstica nad ostalimi in bilo oddano prvo

To ni toliko posebnost PDFiuma kot posledica tega, kako PDF postavlja besedilo. ISO 32000-1 §9.2.2 in §9.4.4 določata postavitev glifov kot vodoravni premik po osnovnici v prostoru besedila, edine navpične metrike, ki jih datoteka nosi, pa so na ravni pisave: vnosi Ascent, Descent in FontBBox v opisu pisave v §9.8.1. Nič v datoteki ne pove, da si dva glifa delita vrstico; to je treba izpeljati iz geometrije, in tesni okviri glifov, zaradi katerih je označevanje z izborom videti pravilno, kot opisuje izbiranje vrstic besedila z okviri znakov PDFium, so napačen vhod za primerjavo razdalje med središči

Popravek v različici 3.117.0 vprašanje spremeni iz "kako daleč narazen so središča" v "koliko se okvira navpično prekrivata". Besedilo celice se sestavi tako, da se besede celice najprej združijo v vidne vrstice, pri čemer se beseda pridruži vrstici, kadar je njeno navpično prekrivanje s tekočimi mejami vrstice vsaj 25 odstotkov manjše od obeh višin, nato se vsaka vrstica razvrsti z vstavljanjem po levem robu in nazadnje se vrstice združijo s prelomom vrstice. "Purpose" in "of" se prekrivata čez celotno višino male črke x, kar je veliko več kot 25 odstotkov krajšega okvira, zato pristaneta v isti vrstici in se razvrstita po X, kot je bilo mišljeno

Diagram PDFium Component popravka prerazvrstitve pri Purpose of Request: tesni okviri glifov iz FPDFText_GetCharBox dajo besedi of brez spustov višje središče, ki ga je stara 1-točkovna toleranca središča Y razvrstila kot svojo vrstico, pravilo o 25 odstotkih navpičnega prekrivanja pa jo obdrži na osnovnici in obnovi vrstni red besed
Središče Y se premika s tem, koliko vzponov in spustov črnilo slučajno nosi, medtem ko se okvira na isti osnovnici prekrivata čez skupno višino male črke x ne glede na svoji višini

Vrstice besedila združujte po prekrivanju in ne po razdalji med središči

Pravilo, ki ga velja odnesti iz tega hrošča, je splošno: vsaka koda za postavitev besedila v PDF, ki "ista vrstica" odloči s primerjanjem navpičnih središč proti fiksni toleranci, bo odpovedala pri resničnih pisavah, odpoved pa je tiha: nič ne javi napake, besede preprosto pridejo v napačnem vrstnem redu. Mešani spusti so najmilejši sprožilec. Krepka 12-točkovna oznaka poleg 10-točkovnih vrednosti, nadpisana oznaka opombe, simbol valute, narisan iz nadomestne pisave, in okviri besed iz OCR s šumom v višini vsake besede — vsi premaknejo središča za več, kot znaša vsaka toleranca, ki še loči sosednji vrstici 10-točkovnega besedila pri 12-točkovnem presledku. Delež prekrivanja je neodvisen od velikosti: okvira na isti osnovnici se prekrivata čez skupno višino male črke x ne glede na svoje vzpone in spuste, okvira v sosednjih vrsticah pa se ne prekrivata nič

Isto pravilo je zlahka uporabiti tudi zunaj izluščevanja tabel. TPdf.PageWordBoxes vrne vsako besedo na aktivni strani z njenim pravokotnikom v prostoru strani, zato je združevanje strani v vidne vrstice kratka zanka:

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>;   // tekoča unija na vrstico
  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;
  // vsako vrstico pred branjem razvrsti po Rect.Left; PageWordBoxes vrne
  // besede v vrstnem redu toka vsebine, ki ni nujno viden
end;

Kaj se spremeni za obstoječe klicatelje in kje so meje

Bistvo tega izrezka je predikat in ne zanka; za kar koli več od hitrega izpisa izhajajte iz modela strukturiranega besedila, ki že nosi bloke, vrstice in vir vrstnega reda branja, kot pokriva strukturirano izluščevanje besedila PDF z vrstnim redom branja. Obstoječi klicatelji tabel dobijo vse tri popravke, ne da bi se dotaknili svojih možnosti. Meja koridorja je fiksna pri polovici MinColumnGap, strategija po belini obdrži svojo dvovrstično mejo tudi kadar je MinRows nastavljen na 1 (kar strategija po obrobah zdaj sprejme), filtriranje besed po obrobah najprej pa je brezpogojno, kadar koli sta omogočeni obe strategiji. Na vzorčnem nizu 13 dokumentov, uporabljenem za to različico, je prehod po belini prej vrnil 34 fragmentov in lažnih pozitivov ob 9 tabelah z obrobami; po izdaji ne vrne nobenega, število tabel z obrobami pa je naraslo na 41, čeprav večina tega dviga pripada isti različici, ki je zaznavanje mrež z obrobami naučila brati obrobe, narisane kot zapolnjene pravokotnike, kar je ločena zgodba

Poštene meje: preskus koridorja potrebuje vsaj eno vrstico z vsebino na obeh straneh meje, da sploh kaj zavrne, zato kandidat z dvema vrsticama, katerega dva raztegnjena razmika slučajno padeta znotraj 6 točk eden od drugega, še vedno gre skozi. To je ozko naključje in ne skorajšnja gotovost, kot je bila prej, a dokumenti z veliko proze, ki nimajo resničnih dvovrstičnih tabel, ga lahko zaprejo z nastavitvijo MinRows na 3. Levo poravnano neravno besedilo ni bilo nikoli težava in ni prizadeto. In PDF še vedno nima objekta tabele; ISO 32000-1 §14.8.4.3 določa strukturni element Table, a ga nosi le označeni PDF, zato za vse drugo mreža ostaja sklep iz geometrije, vrednost zanesljivosti pri vsakem TPdfTable pa je tam zato, ker si sklepanje zasluži oceno

Izluščevanje tabel, strukturirano besedilo in okviri besed vsi berejo iz istega modela strani v Delphiju, C++Builderju in Lazarusu; celoten API, vključno s TPdfTableExtractionOptions in predstavitvijo TableExtractionLab, ki izhaja z njim, je opisan na strani izdelka PDFium Component za Delphi