Tekninen artikkeli

Rowspan-ruudukot ja toistuvat taulukko-otsikot HotPDF:ssä

HotPDF renderöi HTML-taulukot HTML5 paged media -profiilinsa kautta käyttäen aitoa varausruudukkoa rowspanille ja colspanille, mitattuja rivikorkeuksia merkkimääräarvioiden sijaan ja otsikkorivejä toistettuina jokaisella jatkosivulla. Kaksi tilannetta saa sen kieltäytymään otsikon toistamisesta, ja niiden tunteminen etukäteen on halvempaa kuin monistuneen solun debuggaaminen jälkikäteen

Dokumenttiluokka, joka pakottaa tähän, on se, jonka jokainen raportointitiimi lopulta toimittaa: lasku tai vaatimustenmukaisuusraportti, jossa totuuden lähde on HTML, taulukko jatkuu neljän sivun yli ja otsikon on oltava luettava jokaisella niistä. Mikä tahansa vähemmän kuin oikea taulukkoasettelu tuottaa ne kaksi vikaa, jotka lukijat huomaavat heti: otsikon, joka ilmestyy kerran ensimmäiselle sivulle, ja rivit, joiden korkeudet arvattiin merkkimääristä

Miksi taulukkokyky siirtyi HTML-renderöijään?

Koska vaihtoehto menettää rikastetun tekstin, ja rikastettu teksti on syy, miksi sisältö on HTML:ää alun pitäen. Ilmeinen suunnitelma näyttää uudelleenkäytöltä: HotPDF:llä on jo layout-DOM-taulukko-objekti kunnollisella ruudukolla, joten sillaa HTML-jäsentäjä siihen ja saa spanauksen ilmaiseksi. Ongelma on siinä, mitä tuo taulukko-objekti piirtää. Sen solut kantavat tekstiä ja tyyliä, ja sen piirtopolku emittoi pelkkää tekstiä, joten kaikki mitä HTML oikeasti sisälsi fontin ja värin ulkopuolella, linkit, yläindeksit, inline-kokomuutokset ja per-run-värit, on poissa siihen mennessä, kun se saavuttaa sivun

Suunta, joka selviää kosketuksesta todellisiin dokumentteihin, on käänteinen. Siirrä taulukon moottorin kyvykkyydet, varausruudukko, aito mittaus, otsikkotoisto ja sarakkeiden painotus, HTML-renderöijään ja jätä rikastetun tekstin renderöinti sinne missä se jo toimii. Tuo on suurempi muutos kuin silta, ja se on muutos, joka pitää hyperlinkin taulukon solun sisällä hyperlinkkinä

Rowspan ilman union-findia

Spanaavat solut luovat atomisia riviryhmiä, mutta näiden ryhmien yli tehtävä sulkeuma ei tarvitse yleistä disjoint-set-rakennetta, koska varaus on aina yhtenäinen väli. Solu, jolla on rowspan="3" ja joka alkaa riviltä K, varaa rivit K:sta K+2:een eikä mitään muuta, joten ryhmätieto pelkistyy rivikohtaiseen loppumerkkiin

Algoritmi on kaksi riviä aietta. Kun sijoitat spanaavan solun, joka alkaa K:sta ja päättyy E:hen, merkitse GroupEnd[K] := Max(GroupEnd[K], E). Kävele sitten rivit kerran takaperin ja sovella G[R] := G[G[R]], mikä propagoi jokaisen rivin lopun taaksepäin päällekkäisten spanien läpi ja tuottaa transitiivisen sulkeuman yhdellä kierroksella. Tulos on jokaiselle riville sen viimeinen rivi, jonka on pysyttävä samalla sivulla sen kanssa, eli täsmälleen se, mitä sivutusvaihe tarvitsee päättääkseen, mihin tauko voi osua

Korkeuden jakaminen on toinen puolikas. Kun spanaava solu tarvitsee enemmän pystytilaa kuin sen kattamat rivit tällä hetkellä tarjoavat, ylijäämä menee spanin viimeiselle riville, ei jaettuna tasaisesti niiden yli. Käsittele spanaavat solut sen jälkeen, kun tavalliset rivikorkeudet on sovittu, ja täydennä sitten jokaisen spanin viimeistä riviä. Ylijäämän tasainen jakaminen näyttää reilummalta ja tuottaa näkyvästi väärää tulosta: rivit, jotka sisältävät vain lyhyitä yksirivisiä soluja, paisuvat, koska jokin kolme riviä ylempänä oleva asiaaton solu sattui olemaan pitkä

HotPDF:n HTML-taulukon ruudukko, jossa yksi rowspan 3 -solu alkaen rivistä 2 varaa rivit 2–4 yhtenäisenä atomisena suorakulmiona, rinnalla rivikohtaiset ryhmänloppuarvot G(R), jotka yksi takaperin kävely tuottaa osoittaen rivit 2, 3 ja 4 sidottuina samalle sivulle
Spanaava varaus on aina yhtenäinen väli, joten rivikohtaiset loppumerkit ja yksi takaperin kävely korvaavat union-findin ja kertovat sivutukselle täsmälleen, mihin tauko voi osua
var
  Pdf: THotPDF;
  Importer: THPDFHTMLImporter;
  Stats: THPDFHTMLImportStatistics;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'audit-report.pdf';
    Pdf.BeginDoc;
    Importer := THPDFHTMLImporter.Create(Pdf);
    try
      Importer.Margin := 48;
      Importer.BaseFontName := 'Arial';
      Importer.BaseFontSize := 10;
      Importer.MaxDOMNodes := 200000;
      Importer.MaxLayoutOperations := 2000000;
      if Importer.RenderHTML5(SourceHtml, PrintStyleSheet) then
      begin
        Stats := Importer.Statistics;
        Writeln('tables ', Stats.TableCount,
                '  page breaks ', Stats.PageBreakCount);
      end;
    finally
      Importer.Free;
    end;
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

RenderHTML5 ottaa valinnaisen tekijän tyylitaulun toisena argumenttinaan, ja juuri sinne tulostussäännöt kuuluvat. Pidä ruudun tyylitaulu poissa siitä. Profiili on versioitu, ja HTML5ProfileMilestones raportoi, mitkä ominaisuusryhmät nykyinen käännös toteuttaa, himParserCascade, himPagedLayout, himTablesForms ja himBoundedResources, joten sovellus voi heiketä hallitusti tuotannossa aukon löytämisen sijaan

Mitauksen on vastattava piirtoa, täsmälleen

Rivikorkeus on oikein vain silloin, kun rivitystä mittaava koodi rivittää samalla säännöllä kuin piirtävä koodi. Tämä kuulostaa itsestäänselvyydeltä ja on yleisin yksittäinen syy taulukoille, joiden reunat eivät asetu sisältönsä kanssa kohdalleen. HotPDF mittaa ahneella rivilaskurilla, ja tuon laskurin on vastattava rikastetun tekstin tulostuspolun rivityssemantiikkaa kolmessa tarkassa kohdassa: se katkaisee vain välilyönnistä, se ei koskaan jaa sanaa, ja saraketta leveämpi sana saa oman rivin

Toinen vaatimus on fontti. Mittauksen on ajettava solun omalla fontilla, asetettuna SetFontilla oikealla nimellä, tyylillä ja koolla ennen leveysfunktion kutsumista, ei sillä fontilla, joka sattui olemaan aktiivisena. Lihavoitu teksti on säännöllisesti yli kymmenen prosenttia leveämpää kuin tavallinen samalla koolla, mikä riittää muuttamaan kolmen rivin solun neljän rivin soluksi. Taulukko, jonka otsikkosolut ovat lihavoituja ja sisältösolut eivät, mitattuna yhdellä fontilla, on väärin täsmälleen niissä riveissä, joita lukijat katsovat ensin

Tämän oikein saaminen muuttaa sitä, mitä testissä voi väittää. Tarkan mittauksen havaittava vaikutus on riviväli, ei glyfien määrät: yksirivinen rivi on noin 20 pisteen korkuinen, kun taas saman sisällön merkkimääräarvio ennustaa kaksi riviä ja noin 35. Väitä pystysuorasta etäisyydestä rivien välillä. Ja muista, että PDF:n käyttäjäavaruudessa Y kasvaa ylöspäin, joten sisältörivin yläpuolella istuva otsikko tarkoittaa, että otsikon Y-arvo on suurempi, mikä on päinvastaista kuin mitä ruutukoordinaattivaisto kirjoittaisi

Milloin HotPDF kieltäytyy toistamasta otsikkoa?

Kahdessa tapauksessa, joissa kummassakin jatkaminen tuottaisi näkyvästi väärää tulosta. Ensimmäinen on otsikkolohko, joka sisältää spanaavan solun, joka ulottuu otsikon ohi sisältöriveihin. Otsikon toistaminen piirtäisi tuon solun sisällön toisen kerran paikkaan, johon se ei enää kuulu, joten otsikko piirretään kerran ja taulukko jatkuu ilman sitä. Toinen on otsikko, joka on korkeampi kuin 90 prosenttia käytettävissä olevasta sivukorkeudesta, missä toistaminen jättäisi datalle tuskin lainkaan tilaa ja taulukko ei etenisi lainkaan

HotPDF:n päätösvirta HTML-taulukoiden otsikoiden toistamisesta sivunvaihtojen yli: rowspan, joka ylittää sisältöriveihin, otsikko piirretään kerran, yli 90 prosenttia käytettävissä olevasta sivukorkeudesta otsikko piirretään kerran, ja jokainen muu otsikko toistuu joka jatkosivulla
Kaksi kieltäytymistä ovat tahallisia: otsikon toistaminen, joka omistaa spanaavan sisältösolun tai täyttää suurimman osan sivusta, piirtäisi sisällön paikkaan, johon se ei enää kuulu, tai jättäisi datalle tuskin lainkaan tilaa

Molemmat kieltäytymiset ovat tahallisia ja hiljaisia suunnittelusta johtuen, koska vaihtoehto on pahempi. Jos otsikkosi ei toistu ja odotit sen tekevän, tarkista merkinnästä rowspan, joka ylittää thead-rajan, ennen kuin epäilet moottoria. Tuo yksittäinen merkintäkaava selittää suurimman osan yllätyksestä

// Sarakepainot tulevat merkinnästä, joten tulostustyylitaulu on paikka,
// jolla niitä hallitaan. Leveyksiä käsitellään painoina, ei pikseleinä
const
  PrintStyleSheet =
    'table { width: 100%; }' +
    'thead th { font-weight: bold; background: #eee; }' +
    'td.amount { text-align: right; }';

// Otsikkorivi, joka kantaa rowspania ylittäen sisältöön, tukahduttaa
// otsikkotoiston. Pidä spanit yhden osion sisällä:
//   <thead><tr><th rowspan="2">Item</th>...</tr></thead>  ok
//   <tr><th rowspan="3">Item</th>...  ulottuu tbodyyn, ei toistoa

Sarakeleveydet käyttäytyvät painoina eikä absoluettisina mittoina, mikä on käytös, joka pitää taulukon käytettävänä silloin, kun sisältö ei vastaa tekijän arviota. 30 prosentiksi määritelty sarake saa suunnilleen 30 prosenttia käytettävissä olevasta leveydestä, mutta jakelu kunnioittaa minimileveyttä, jonka kukin sarake todella tarvitsee, joten kapea sarake, joka pitää pitkää katkeamatonta sanaa, ei ylivuoda taulukon laatikosta hiljaa

Missä tämä istuu dokumenttiputkessa

Taulukkotyö istuu laajemman paged media -profiilin sisällä, ja HTML5 paged media -tuontipolun kuvaamat sivutussäännöt, resurssibudjetit ja CSS-käsittely pätevät muuttumattomina dokumentteihin, jotka sisältävät taulukoita. Jos datasi ei ala HTML:stä, taulukoiden suora rakentaminen PDF:ään välttää jäsennyskerroksen kokonaan ja antaa saman ruudukkokäytöksen API:n kautta. Ja koska rivikorkeus riippuu lopulta siitä, mihin rivit katkeavat, artikkelin tekstin tasaus ja rivien katkaisu mittaustarkastelu on kumppaniteos jokaiselle, joka virittää tiheää taulukkotulostetta

Uudelleenkäytettävä oppi tässä ei koske lainkaan taulukoita. Kun uusi alijärjestelmä tarvitsee kyvykkyyden, jonka vanha alijärjestelmä jo omistaa, kysy, kumpi kahdesta omistaa sen asian, jota on vaikeinta toteuttaa uudelleen. Ruudukkoaritmetiikka on kourallinen kymmeniä rivejä ja siirtyy helposti. Rikastetun tekstin renderöinti inline-linkkeineen, yläindekseineen ja per-run-tyylityksineen ei, joten ruudukko siirtyi ja teksti jäi. HotPDF toimittaa molemmat polut osana HotPDF Delphi PDF -komponenttia, joten valinta HTML-syötteen ja suoran rakentamisen välillä on projektipäätös eikä kirjastopäätös