Tekninen artikkeli

PDF-sisällön uudelleenvirtautus responsiiviseksi HTML:ksi Delphissä

PDFium Component muuttaa kiinteän asettelun PDF:n semanttiseksi malliksi, jonka voi virtauttaa uudelleen, metodilla BuildReflowDocument, ja vie tuon mallin itsenäisenä HTML:nä metodilla ToHtml. Otsikot pysyvät otsikkoina, listakohteet pysyvät listakohteina, ja sivulta havaitut taulukot tulevat ulos todellisena taulukkomerkintänä, jossa otsikkosolut ja span-arvot säilyvät. Mikään tulosteessa ei viittaa ulkoiseen skriptiin tai tyylitiedostoon

Syy tämän haluamiseen on se, että PDF-sivu on joukko sijoitettuja glyyfejä, mikä on täsmälleen väärin puhelimen näytölle, ruudunlukijalle tai hakuindeksille. Jokainen yritys ratkaista se poimimalla pelkkää tekstiä menettää rakenteen, joka teki dokumentista luettavan, ja jokainen yritys ratkaista se muuntamalla sivut kuviksi menettää tekstin kokonaan. Uudelleenvirtausmalli säilyttää molemmat: sanat ja niiden väliset suhteet

Mistä semanttinen tieto tulee?

Kaikki alkaa metodista GetStructuredText, komponentin ainoasta tekstin ja semantiikan lähteestä. Kun PDF kantaa rakennepuun, merkitty PDF ISO 32000-1:n kohdan 14.7 määrittelemällä tavalla, malli noudattaa tuottajan tallentamaa loogista hierarkiaa. Kun näin ei ole, ja useimmat luonnossa esiintyvät PDF-tiedostot eivät kanna, malli palaa fyysiseen asettelujärjestykseen, joka on jo laskettu lukujärjestystarkoituksia varten

Tämä valinta säilyttää tiukan rajan: mitään toista PDF-jäsentäjää tai toista renderöintimoottoria ei oteta käyttöön vastaamaan kysymyksiin, joihin olemassa oleva jo vastaa. Taustalla oleva lukujärjestyskoneisto kuvataan artikkelissa rakenteelliset tekstilohkot ja lukujärjestys, ja uudelleenvirtausmalli on semanttinen kerros sen päällä, ei sen korvaaja

Jokainen solmu tallentaa, mistä sen tieto tuli, joten kuluttaja voi erottaa otsikon, jonka dokumentti ilmoitti, otsikosta, jonka asetteluheuristiikka päätteli. Luottamusherkkien putkien tulisi lukea tuo kenttä sen sijaan, että ne kohtelisivat kaikkia solmuja yhtä auktoritatiivisina

Litteä puu ja miksi se ei ole objektien puu

Malli on esijärjestyksessä litistetty puu: taulukko solmuja, joista jokainen kantaa arvot ParentIndex ja Depth, ei rekursiivinen tietue tai omistuksellinen objektigraafi. Sivut, otsikot, kappaleet, listat, listakohteet, kuvat, kuvatekstit, taulukot, rivit ja solut asuvat kaikki tuossa yhdessä lineaarisessa taulukossa

Tästä seuraa kaksi etua. Kuluttajat voivat suoratoistaa taulukon järjestyksessä ilman rekursiota, mikä tekee HTML:n, Markdownin tai puunäkymän tuottamisesta yksinkertaisen silmukan. Ja asettelu pysyy siirrettävänä Delphin, C++Builderin ja Free Pascalin välillä, jotka eroavat siinä, miten ne käsittelevät rekursiivisia hallittuja tyyppejä ABI-rajan yli. Dynaamisten taulukoiden rekursiivinen tietue on juuri sellainen rakenne, joka kääntyy kaikkialla ja käyttäytyy hienovaraisesti eri tavalla jokaisessa

uses
  PDFium;

var
  Pdf: TPdf;
  Options: TPdfReflowOptions;
  Doc: TPdfReflowDocument;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'report.pdf';
    Pdf.LoadDocument;

    Options := TPdfReflowOptions.Default;
    Options.FullDocument := True;
    Options.DetectTables := True;
    Options.IncludeCss := True;          // sisäinen tyylilohko, ei ulkoista tiedostoa
    Options.MaxNodes := 200000;          // turvallisesti sulkeutuva budjetti
    Options.MaxCharacters := 4000000;

    Doc := Pdf.BuildReflowDocument(Options);

    for I := 0 to High(Doc.Nodes) do
      case Doc.Nodes[I].Kind of
        prnkHeading:
          Writeln(Format('%sH%d: %s', [StringOfChar(' ', Doc.Nodes[I].Depth),
            Doc.Nodes[I].HeadingLevel, Doc.Nodes[I].Text]));
        prnkParagraph:
          Writeln(Format('%sp: %s', [StringOfChar(' ', Doc.Nodes[I].Depth),
            Copy(Doc.Nodes[I].Text, 1, 60)]));
        prnkTable:
          Writeln(Format('table on page %d', [Doc.Nodes[I].PageNumber]));
      end;

    Writeln(Format('%d node(s), %d table(s), %d character(s)',
      [Length(Doc.Nodes), Doc.TableCount, Doc.CharacterCount]));
  finally
    Pdf.Free;
  end;
end;

Miten taulukot pidetään esiintymästä kahdesti?

Taulukoiden havaitseminen tapahtuu sen jälkeen, kun rakenteellinen teksti on kerätty sivulle, mikä luo ilmeisen vaaran: sama solusisältö on olemassa sekä tekstilohkoissa että havaitussa taulukossa. Molempien tuottaminen tuottaisi HTML:n, jossa jokaista taulukkoa seuraa sen oma sisältö uudelleen irrallisina kappaleina

Sääntö, joka ratkaisee tämän, on geometrinen. Kun havaittu taulukko peittää yli puolet tekstilohkon alueesta, taulukkosolmu korvaa tuon lohkon sen sijaan, että liittyisi siihen. Solujen indeksointi rivin sisällä rakennetaan laskemalla ne koreihin, joten mallin rakentaminen pysyy lineaarisena solujen plus rivien suhteen sen sijaan, että se skannaisi jokaisen solun uudelleen jokaista riviä kohden, millä on merkitystä talousdokumenteissa, joissa yksi sivu voi kantaa satoja soluja

Havaittu rakenne on rehellinen siitä, että se on havaintoa. Taulukko, jossa on viivoitusviivoja, tunnistetaan luotettavammin kuin sellainen, joka on tasattu puhtaasti tyhjätilalla, ja solmun luottamus heijastaa tätä. Sisällölle, jossa väärä taulukko on parempi kuin ei taulukkoa lainkaan, pidä havainnointi päällä; arkistomuunnokselle, jossa väärä taulukko on huonompi, portitse luottamuksen mukaan

Itsenäisenä pysyvän HTML:n vienti

ToHtml käy läpi jo rakennetun mallin eikä koskaan käy uudelleen PDFiumissa, joten kahdesti vieminen ei maksa mitään ylimääräistä eikä voi tuottaa eri tulosta samasta mallista. Teksti- ja attribuuttiarvot koodataan yhtenäisesti, otsikkotasot rajataan h1:sta h6:een alueeseen, jonka HTML todella määrittelee, ja otsikkosolut, RowSpan ja ColumnSpan siirtyvät sellaisenaan

Valinnainen CSS on tavallinen sisäinen tyylilohko. Siinä ei ole skriptiä, verkkofonttia eikä minkäänlaista ulkoista resurssia, mikä tekee tulosteesta turvallisen upottaa sähköpostiin, ohjekatseluohjelmaan tai hiekkalaatikoituun selainohjaimeen:

var
  Html: WideString;
  Stream: TFileStream;
  Bytes: TBytes;
begin
  Options := TPdfReflowOptions.Default;
  Options.FullDocument := True;
  Options.IncludeCss := True;
  Options.IncludePageSections := True;   // pidä sivujen rajat näkyvissä
  Options.PreserveLineBreaks := False;   // anna selaimen rivittää kappaleet

  Html := Pdf.BuildReflowDocument(Options).ToHtml;

  Bytes := TEncoding.UTF8.GetBytes(string(Html));
  Stream := TFileStream.Create('report.html', fmCreate);
  try
    if Length(Bytes) > 0 then
      Stream.WriteBuffer(Bytes[0], Length(Bytes));
  finally
    Stream.Free;
  end;
end;

PreserveLineBreaks on asetus, jota kannattaa eniten pohtia. PDF:n rivinvaihto on ladontapäätös, joka on tehty kiinteälle sivun leveydelle, joten sen säilyttäminen kapealla näytöllä toistaa juuri sen ongelman, jota varten uudelleenvirtaus on olemassa. Säilytä rivinvaihdot runoudelle, koodilistauksille ja osoitteille; pudota ne proosalta

Budjetit, peruutus ja sivun tila

Merkeillä, solmuilla, taulukoilla ja soluilla on kullakin katto, ja jokainen tarkistetaan ennen varausta eikä sen jälkeen, joten virheellinen tai vihamielinen dokumentti epäonnistuu siististi sen sijaan, että se kuluttaisi muistia, kunnes jokin muu tekee niin. Peruutustunniste tarkistetaan sivun, lohkon, taulukon, rivin ja solun rajoilla, mikä pitää tuhatsivuisen dokumentin peruutetun skannauksen responsiivisena

Yksi käyttäytyminen on erityisen tärkeä GUI-sovelluksille: koko dokumentin skannaus toimii laajuudessa, joka palauttaa aktiivisen sivun, joten onnistuminen, budjettivika ja peruutus jättävät kaikki kutsujan nykyisen sivun koskemattomaksi. Katseluohjelma, joka antaa käyttäjän viedä samalla, kun tämä katsoo sivua 340, huomaa olevansa yhä sivulla 340 jälkikäteen

Mihin uudelleenvirtaus sopii ja mihin ei

Uudelleenvirtauksen tuloste on erinomainen syöte hakuindeksoinnille, esteettömille lukunäkymille, mobiilinäytölle ja sisällön migraatiolle. Se ei ole uskollisuutta säilyttävä muunnin: absoluuttiset sijainnit, tarkat fontit, vektorigrafiikka ja tarkka sivugeometria ovat tarkoituksellisesti sen tarkoituksen ulkopuolella. Kun työ vaatii sivun näyttävän samalta, renderöi se; kun se vaatii sivun olevan luettavissa jossain muualla, virtauta se uudelleen

Nimenomaan avustavalle teknologialle uudelleenvirtausmalli yhdistyy artikkelissa esteettömän lukijan rakentaminen kuvattuihin lukuominaisuuksiin, ja dokumentit, jotka kantavat aidon rakennepuun, tuottavat huomattavasti parempia malleja, mikä on hyvä perustelu merkinnän validoimiselle ylävirrassa, kuten kuvataan artikkelissa PDF/UA-rakennepuun validointi

Uudelleenvirtaus, rakenteellinen teksti, merkinnän validointi ja renderöinti jakavat yhden dokumenttiobjektin Delphin, C++Builderin ja Lazaruksen välillä; täydellinen API kuvataan sivulla PDFium Component for Delphi -sivulla