Techninis straipsnis

PDF lentelių tęsinys per puslapius pagal turinį Delphi

PDFium Component 3.117.0 versija susieja lentelę, kuri lūžta per puslapio ribą, kai arba abu fragmentai liečia puslapio kraštus, arba po pirmuoju fragmentu ir virš antrojo nėra pagrindinio teksto, neatsižvelgiant į bėgančias antraštes ir poraštes. ExtractDocumentTables tą turiniu pagrįstą patikrą taiko kaip alternatyvą senesnei paraščių patikrai, atmeta kito puslapio fragmentą, kurio pirmoji eilutė yra viena per visą plotį einanti antraštės ląstelė, ir išlaiko vieną eilutę, persiliejusią į kitą puslapį, kaip savo tęsinio grandinės dalį

Straipsnis apie lentelių aptikimą ir ištraukimą tęsinį pateikė kaip keturis griežtus vartus ir vienus iš jų laikė „liečia puslapio kraštą". Tas aprašymas buvo tikslus tam leidimui, kurį apėmė, ir kartu neteisingas daugumai lentelių, kurias žmonės iš tikrųjų paduoda komponentui. Šis straipsnis yra pataisa: kokių dokumentų paraščių patikra neįveikia, kas ją pakeitė ir du šalutiniai atvejai, kuriuos pataisa atsitempė kartu

Kodėl paraščių patikra nepavyksta su Word eksportais?

Paraščių patikra nepavyksta, nes teksto procesorius eilutes dėlioti nustoja ties apatine parašte, o ne ties popieriaus kraštu. Su numatytuoju ContinuationMargin 36 taškais pradinė taisyklė reikalavo, kad ankstesnio fragmento apatinis kraštas būtų ne toliau kaip 36 taškai nuo puslapio apačios, o vėlesnio fragmento viršutinis kraštas — ne toliau kaip 36 taškai nuo puslapio viršaus. Iš Word su numatytosiomis vieno colio paraštėmis eksportuotame dokumente paskutinė eilutė atsiduria bent 72 taškais virš puslapio apačios, o jei yra poraštė — dar aukščiau, tad sąlyga niekada neišsipildydavo. Kiekviena ilga lentelė tokiame dokumente grįždavo kaip atskiri fragmentai su nuliniu ContinuationGroup, ir kvietėjas vėl turėjo siūti rankomis. Patikra vis tiek prasminga tam, kam buvo skirta: ataskaitoms, generuojamoms dėliotuvų, kurie užpildo puslapį iki fiksuoto turinio stačiakampio, o kitą puslapį pradeda tiesiai nuo viršaus. Tai nėra bloga taisyklė, ji yra nepilna, ir todėl 3.117.0 versija ją paliko, o vietoj jos pakeitimo pridėjo antrą kelią

Ką vietoj to tikrina turiniu pagrįsta patikra?

Turiniu pagrįsta patikra tikrina, ar tarpą tarp dviejų fragmentų užima kas nors kitas, o ne lentelė, naudodama kiekvieno puslapio žodžių stačiakampius, o ne puslapio geometriją. ExtractDocumentTables eidamas per dokumentą fiksuoja kiekvienam puslapiui žemiausią apatinį kraštą to žodžio, kurio viršus yra virš poraštės juostos, ir aukščiausią viršutinį kraštą to žodžio, kurio apačia yra žemiau antraštės juostos. Abi juostos yra ContinuationMargin taškų gylio, tad tas pats nustatymas dabar atlieka dvigubą pareigą: ir puslapio krašto atsargą, ir bėgančių antraščių bei poraščių zonų aukštį. Fragmentų pora praeina, kai ankstesniojo apatinis kraštas yra ties jo puslapio žemiausiu pagrindiniu tekstu arba žemiau jo, o vėlesniojo viršutinis kraštas yra ties kito puslapio aukščiausiu pagrindiniu tekstu arba aukščiau jo, kiekvienas AlignmentTolerance ribose. Paprastais žodžiais: lentelė buvo paskutinis dalykas N puslapyje ir pirmas dalykas N+1 puslapyje, o puslapio numeris ar dokumento pavadinimas paraščių juostoje nesiskaito. Tas išskyrimas nėra savavališkas. ISO 32000-1 §14.8.2.2 bėgančias antraštes ir poraštes priskiria puslapių skaidymo artefaktams — turiniui, kuris egzistuoja dėl puslapio lūžio, o ne jam nepaisant — ir ta pati idėja, leidžianti žymėtam skaitytuvui jas praleisti, leidžia lentelei jų nesustoti. Straipsnis apie pažymėtą turinį aptaria, kaip žymėti failai tuos artefaktus deklaruoja aiškiai; čia priskyrimas nustatomas pagal padėtį, nes dauguma eksportuotų lentelių jokių žymų neturi

Kodėl PDFium Component lentelių tęsiniui reikia dviejų patikrų: su Word vieno colio paraštėmis paraščių patikra reikalauja fragmentų kraštų 36 taškų languose, kurių išdėstymas niekada nepasiekia, o turiniu pagrįsta patikra lygina žodžių stačiakampius ir susieja, kai lentelė yra paskutinis pagrindinis turinys N puslapyje ir pirmas N+1 puslapyje, ignoruodama bėgančių antraščių ir poraščių juostas
Bet kuri patikra atveria vartus, ir tik tada paleidžiamos likusios: gretimi puslapiai, jokios per visą plotį einančios antraštės eilutės vėlesniame fragmente ir stulpelių ribos, sutampančios dvigubos AlignmentTolerance ribose

Abi patikros jungiamos su OR. Dėliotuvo ataskaita, kurios lentelės eina iki popieriaus krašto, praeina pirmąją; Word eksportas, kurio lentelės sustoja ties parašte, praeina antrąją; dokumentas, kuriame yra abu atvejai, praeina du kartus. Tik vienai iš jų pasisekus paleidžiami likę vartai, ir jie eina fiksuota tvarka: puslapių numeriai turi būti gretimi, vėlesnis fragmentas neturi prasidėti antraštės eilute, o stulpelių ribos turi sutapti dvigubos AlignmentTolerance ribose, kuri su numatytosiomis reikšmėmis yra 6 taškai. Sąrašas yra TPdfTableContinuation su reikšmėmis ptcNone, ptcStart, ptcMiddle ir ptcEnd. Fragmentas, pažymėtas ptcEnd ir paskui susietas su dar vienu puslapiu, paaukštinamas į ptcMiddle, tad trijų puslapių lentelė puslapių tvarka skaitosi kaip pradžia, vidurys, pabaiga. Grupių numeriai prasideda nuo 1, o 0 reiškia nesusietą, ir ToJson išveda tą pačią informaciją kaip continuation ir continuationGroup nariai — tai forma, kurios geriau laikytis, jei siuvimą atlieka tolesnė paslauga

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;     // numatytoji; parodyta aiškumui
    Options.ContinuationMargin := 54;        // dviejų eilučių poraštė, ~50 taškų

    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;

Kaip antraštės eilutė neleidžia dviem lentelėms suaugti?

Kito puslapio fragmentas, kurio pirmoji eilutė yra viena per visus stulpelius einanti ląstelė, laikomas nauja lentele, niekada ne ankstesnės tęsiniu. Ši taisyklė egzistuoja todėl, kad turiniu pagrįsta patikra pati viena susieja per godžiai. Atvejis, kuris ją atskleidė, buvo protokolo tipo forma: lentelė baigiasi prie 1 puslapio apačios, antra lentelė su identiškais stulpelių pločiais prasideda prie 2 puslapio viršaus, tarp jų yra vien poraštė, o stulpeliai sutampa iki taško. Pagal paraščių patikrą abi niekada nesusitikdavo, nes nė viena nelietė krašto; pagal turinio patikrą jos susijungdavo iš karto, ir forma su skyriais virsdavo vienu nerišliu tinkleliu. Kas jas išskiria, matyti ląstelių struktūroje. Antroji lentelė prasideda skyriaus antrašte, tokia kaip „RECIPIENT INFORMATION", išdėstyta kaip viena sulieta ląstelė per visą plotį, o tikras tęsinys taip niekada neprasideda, nes antraštė priklauso lentelei, kuri jau prasidėjo ankstesniame puslapyje. TableStartsWithCaptionRow koduoja būtent tai: fragmentas turi bent du stulpelius ir turi ląstelę su RowIndex = 0, ColumnIndex = 0 ir ColumnSpan = ColumnCount. Patikra vykdoma tik vėlesniam fragmentui, tad lentelė, kurios pačios antraštės eilutė yra pirmame jos puslapyje, nepaveikiama; antraštė yra N puslapyje, o tikrinamas vien N+1 puslapio fragmentas

Antraštės eilutės vartai PDFium Component: tikras tęsinys prasideda duomenų ląstelėmis ir prisijungia prie tos pačios ContinuationGroup, o vėlesnis fragmentas, kurio nulinėje eilutėje yra viena sulieta ląstelė su RowIndex 0, ColumnIndex 0 ir ColumnSpan lygiu ColumnCount, atmetamas kaip tęsinys ir paskelbiamas nauja lentele
Patikra liečia tik vėlesnį fragmentą, tad lentelė, kurios pačios antraštės eilutė yra pirmame jos puslapyje, nepaveikiama, o vartai suveikia po to, kai viena iš dviejų krašto patikrų jau susiejo porą

Po to einantis stulpelių palyginimas TablesHaveMatchingColumns yra griežtesnis nei „tiek pat stulpelių". Jis iš naujo surenka kiekvieno fragmento ribų pozicijas iš ląstelių stačiakampių, interpoliuoja ribas, kurias paslepia sulietos ląstelės, ir atmeta porą, kai bet kuri riba nuslenka daugiau nei tolerancija. Tad dvi keturių stulpelių lentelės su skirtingomis proporcijomis lieka atskiros, net kai visa kita sutampa

Kas nutinka vienai eilutei, persiliejusiai į kitą puslapį?

Tinklelis su brūkšniais, kuris vieną eilutę nuneša į kitą puslapį, dabar aptinkamas ir susiejamas, jei tik patenka į tęsinio grandinę; atskirai jis atmetamas. Numatytasis MinRows 2 egzistuoja tam, kad atsitiktinė pora eilučių nebūtų paskelbta lentele, bet paskutinė eilutė, nustumta per lūžį, yra tikra eilutė, kurią kieta dviejų riba tyliai numesdavo, ir likusi lentelė atrodydavo užbaigta, nors nebuvo. Dokumento lygmens nuskaitymas tai sprendžia trimis žingsniais. Kai DetectContinuations ir DetectRuledTables abu įjungti, kiekvieno puslapio ėjimas paleidžia brūkšnių detektorių su laikinai iki 1 nuleista eilučių riba, ir todėl ExtractTables dabar priima MinRows 1 brūkšniuotiems tinkleliams, kai tarpų aptikimas viduje vis tiek laikosi 2 ribos. Tęsiniai pažymimi visame rezultate. Tada pašalinama kiekviena lentelė, trumpesnė už kvietėjo MinRows ir nepriklausanti jokiai grandinei. Vienos eilutės fragmentas išlieka tik todėl, kad buvo susietas, o vienos eilutės tinklelis kitokio įprasto puslapio viduryje atmetamas lygiai kaip anksčiau

Kaip PDFium Component išlaiko brūkšniuotą eilutę, persiliejusią per puslapio lūžį: kiekvieno puslapio brūkšnių ėjimas vyksta su eilučių riba 1, kai DetectContinuations ir DetectRuledTables įjungti, tęsiniai pažymimi visame rezultate, ir pašalinami tik fragmentai, trumpesni už MinRows ir esantys už kiekvienos grandinės
Persiliejusi eilutė išlieka todėl, kad grandinė ją susieja, o atskiras vienos eilutės tinklelis įprastame puslapyje atmetamas lygiai kaip anksčiau, ir tarpų aptiktuose tinkleliuose dviejų eilučių riba išlieka be jokios tokios lengvatos
// Perstatyti kiekvieną grandinę į vieną CSV, išmetant pasikartojančias
// antraščių eilutes tęsinio fragmentuose
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);            // teksto procesoriaus pakartota antraštė
      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;

Dvi tos rutinos detalės yra sąmoningos. Vienos eilutės persiliejimas niekada nepašalinamas, nes apsauga pagal RowCount jį išsaugo, o teksto procesorius, kartojantis antraštės eilutę kiekviename puslapyje, sukuria fragmentą, kurio pirma eilutė vėl yra antraštė, tad nulinės eilutės atmetimas vidurio ir pabaigos fragmentuose tam atvejui yra teisingas, o generatoriui, antraščių nekartojančiam, neteisingas. Prieš paleisdami šią rutiną ant viso katalogo, patikrinkite vieną dokumentą

Kur taisyklės vis dar sustoja

Turiniu pagrįsta patikra yra verta tiek, kiek vertas tekstinis sluoksnis, kurį ji skaito. Nuskaitytame puslapyje visiškai be teksto užfiksuoti pagrindinio teksto kraštutinumai nusileidžia į puslapio ribas, sąlyga „tarp jų nieko" patenkinama tuščiai, ir lieka tik antraštės eilutės bei stulpelių vartai; brūkšniuotas tinklelis tokiame puslapyje vis tiek randamas kaip tuščias skeletas, tad grandinė gali susijungti teisingai, bet apie supantį tekstą nieko nebuvo tikrai patikrinta. Jei tai svarbu, pirmiau pridėkite tekstinį sluoksnį. Poraštės, atvaizduotos kaip vaizdai, o ne tekstas, juostų logikai yra nematomos ir dėl tos pačios priežasties nekenkia

Juostos yra vienas skaičius. Poraštė, gilesnė už ContinuationMargin, palieka savo apatines eilutes pagrindinio teksto zonoje, ir tada ankstesnis fragmentas atrodo turįs tekstą po savimi, o sąsaja užblokuojama; padidinkite nustatymą iki tikrojo juostos gylio, kaip daro pirmasis pavyzdys. Padidinkite per daug — ir trumpa baigiamoji pastraipa prie puslapio apačios įslysta į juostą bei tampa ignoruojama, o tai susieja lentelę su tuo, kas po jos eina. Antraštės taisyklė turi veidrodinę nesėkmę: generatorius, rašantis sulietą „continued" juostą kaip pirmą kiekvieno tęsinio fragmento eilutę, sulauks, kad tie fragmentai bus atmesti kaip naujos lentelės, ir vienintelis šiandieninis vaistas yra pačiam susiūti pagal ContinuationGroup, nieko neatlaisvinus, nes taisyklė neturi jungiklio

Tarpų aptiktoms lentelėms jokios vienos eilutės lengvatos netenka. Tarpų strategijai reikia dviejų sulygiuotų eilučių, kad apskritai pamatytų lentelę, tad nebrūkšniuota lentelė, kurios viena eilutė persilieja, vis tiek paskelbiama trumpesnė ta viena eilute. Kai su tuo susidursite, žodžių stačiakampiai, slypintys už struktūrizuotų teksto blokų ir skaitymo tvarkos, duos neapdorotas pozicijas jai atgauti. Pavyzdžių rinkinyje, kuris ir paskatino šį darbą — trylika teksto procesorių ir naršyklių eksportų — penki dokumentai su tikromis daugiapuslapėmis lentelėmis visi susijungė į vientisas grandines, o protokolo forma, kuri anksčiau suaugdavo, liko atskira; tai yra kartelė, pagal kurią leidimas buvo matuojamas, o ne pažadas apie bet kokį išdėstymą

Tęsinio žymėjimas, antraštės taisyklė ir vienos eilutės ėjimas visi gyvena dokumento lygmens kelyje, bendrame Delphi, C++Builder ir Lazarus versijoms; visa lentelių ištraukimo API aprašyta PDFium Component for Delphi puslapyje