Műszaki cikk

Deklaratív PDF-elrendezés Delphiben, jelölt kimenettel

A HotPDF koordináták helyett deklaratív fából is fel tud építeni egy lapozott dokumentumot. Egy THPDFDOMDocument-et állítasz össze szekciókból, veremekből, szövegből, listákból és táblázatokból, átadod a THPDFDOMRenderer-nek, a renderelő pedig megméri, lapokra bontja, kirajzolja az oldalkereteket, és kérésre előállítja a PDF/UA struktúrafát, amely hozzáférhetővé teszi az eredményt. Az elrendezési kód soha nem számol y koordinátát

Bárki, aki karbantartott már koordináta-vezérelt jelentéskészítőt, tudja, miért számít ez. Az első verzió működik. Aztán egy ügyfélcím háromsorosra nő, egy táblázat sorokat kap, egy lokalizált cím tördelődik, és minden azt követő y-pozíció hibás lesz. A javítások kézi oldaltörés-ellenőrzésekként halmozódnak fel az üzleti logikába szórva, a két évvel később érkező jelölt-PDF követelmény pedig nem építhető utólag olyan kódba, amely fogalmával sincs annak, mi az a bekezdés

Mit birtokol a fa, és miért szigorú a tulajdonlás?

A DOM minden szinten egyszeres tulajdonlást kényszerít ki: a dokumentum birtokolja a szekcióit, egy szekció birtokolja a törzsét, fejlécét és lábjegyzetét, a veremek, konténerek és táblázatok pedig a gyermekeiket. Az újrafelhasználás Clone-on vagy egy regisztrált gyáron keresztül történik, soha nem úgy, hogy ugyanazt az objektumot két szülőhöz csatolják. Ez a szabály nem formalitás. Egy olyan komponens, amely kétszer szerepel a fában, két különböző korlátozással kétszer mérődne, és a lebontáskor kétszer szabadulna fel

A hívó kód számára a gyakorlati következmény az, hogy a segédfüggvények új példányokat adnak vissza. A RegisterComponent-tel regisztrált gyár és a CreateComponent hívása egy elnevezett receptet ad, amely minden alkalommal friss komponenst állít elő, és így kerülhet a fába egy ismétlődő elem, például egy aláírásblokk vagy egy jogi lábjegyzet

uses
  HPDFDoc, HPDFLayoutDOM;

var
  Doc: THPDFDOMDocument;
  Section: THPDFDOMSection;
  Table: THPDFDOMTable;
  Row: THPDFDOMTableRow;
  I: Integer;
begin
  Doc := THPDFDOMDocument.Create;
  Doc.GenerateStructure := True;        // előállítja a PDF/UA struktúrafát
  Doc.Language := 'en-US';

  Section := Doc.AddSection;
  Section.PageWidth := 595;           // A4, pontban
  Section.PageHeight := 842;
  Section.MarginLeft := 56;
  Section.MarginTop := 56;
  Section.MarginRight := 56;
  Section.MarginBottom := 56;
  Section.Style.FontName := 'Helvetica';
  Section.Style.FontSize := 10;

  Section.Body.AddHeading('Annual maintenance report', 1);
  Section.Body.AddText('Every asset inspected during the reporting ' +
    'period is listed below, grouped by site.');
  Section.Body.AddSpacer(12);

  Table := THPDFDOMTable.Create('assets');
  Table.AddColumn(3);                 // súlyok, nem abszolút szélességek
  Table.AddColumn(1);
  Table.AddColumn(1);
  Table.RepeatHeaders := True;
  Row := Table.AddRow(18, True);      // fejlécsor
  Row[0].Text := 'Asset';
  Row[1].Text := 'Last service';
  Row[2].Text := 'Status';
  for I := 0 to High(Assets) do
  begin
    Row := Table.AddRow(16);
    Row[0].Text := Assets[I].Name;
    Row[1].Text := Assets[I].ServiceDate;
    Row[2].Text := Assets[I].Status;
  end;
  Section.Body.Add(Table);
end;

Hogyan kerüli el a lapozás a négyzetes költséget?

Egy fa lapokra bontásának naiv módja az, hogy lemásoljuk mindazt, ami nem fért ki, és átvisszük a következő oldalra. Egy tízezer soros táblázat esetében ez oldalanként egyszer másolja le a hátralévő sorokat, és egy lineáris dokumentumot négyzetessé alakít

A HotPDF ehelyett szűken bont. A felső szintű renderelő index szerint járja be a törzs gyermekeit, és soha nem másolja le a teljes szekciót vagy törzset. Csak azok a beágyazott veremek és konténerek kapják meg az érintett részfa másolatát, amelyek valóban átnyúlnak egy oldalhatáron, a két nehéz levéltípus pedig kurzort visel másolat helyett: egy szövegfolytatás tárolja a forrás azon karaktertartományát, amellyel még tartozik, egy táblázatfolytatás pedig azt a sorszeletet, amelyet még el kell helyeznie. A hosszú dokumentumok lineárisak maradnak, a hosszú bekezdések pedig ugyanannyiba kerülnek, akár egyszer, akár ötször törnek meg

A mérés őszinte marad a mellékhatásokat illetően. A THPDFLayoutElement.Measure-nek rajzolási mellékhatásoktól mentesnek kell lennie, a tényleges elhelyezés pedig mindig a THotPDF.PlaceLayoutElement-en keresztül fut, ugyanazon a központi rutinon, amely újramér minden elhelyezett fragmentumot, beállítja a túlcsordulás-tulajdonlást és rögzíti a diagnosztikát. A DOM-renderelő csak az új oldal szabályát, az oldalkereteket, a térközöket és a folytatások élettartamát dönti el

A táblázatfejléc-szabályok, amelyek megakadályozzák a végtelen dokumentumot

A táblázatfejlécek oldalankénti ismétlése egyszerűnek hangzik, mégis két hibamódot rejt. A HotPDF megköveteli, hogy a fejlécsorok csak az egymást követő sorok első futásában jelenjenek meg, és hogy az első törés minden fejlécsort és legalább egy törzssort is elférjen. A második szabály nélkül egy a fennmaradó helynél magasabb fejléc olyan oldalt eredményez, amely kizárólag a fejlécet tartalmazza, amit egy másik, azonos oldal követ, örökké

A folytatólagos oldalak újrarajzolják a fejlécet, és ez az újrarajzolt másolat artifactként van megjelölve, nem tartalomként, ami a helyes válasz mind a hozzáférhetőség, mind a szövegkinyerés szempontjából. Az eredeti fejlécsor pontosan egyszer marad benne a logikai táblázatszerkezetben. Ha ezt kihagyjuk, egy képernyőolvasó az adatok közepén ismét bemondja az oszlopcímeket, egy szövegkinyerő pedig duplikált fejlécsort szúr be a törzssorok közé

Emellett van egy védelmi felső határ a folytatási mélységre is, mert egy egyéni komponens szabadon implementálhatja a Split-et úgy, hogy az mindig egy vele egyenértékű farkot ad vissza. A renderelő a farok leválasztása után és a következő oldal megkezdése előtt ellenőrzi a korlátot, az aktuális iteráció pedig a saját finally blokkjában szabadítja fel a farkat, így egy hibásan viselkedő harmadik féltől származó komponens diagnosztizálható hibával bukik el, nem pedig teleírja a lemezt

Egy logikai elem, sok oldaltöredék

Az automatikus jelölésnél kell egyeznie a lapozási modellnek és a struktúramodellnek. Egy két oldalra bontott bekezdés egyetlen logikai bekezdés, ezért egyetlen struktúraelemnek kell maradnia. A jelölt tartalom azonosítói viszont oldalanként érvényesek, így minden látható töredéknek saját MCID-re van szüksége azon az oldalon, amelyen megjelenik

A HotPDF ezt úgy oldja meg, hogy egyetlen struktúraelemet tart fenn, és minden töredékhez hozzáfűz egy jelölttartalom-hivatkozást annak /K tömbjéhez, ahol a /Pg és /MCID pár azonosítja az oldalt és az azonosítót. Az adott MCID-hez tartozó ParentTree-hely ugyanarra az elemre mutat vissza. Pontosan ezt várja el az ISO 14289, és emiatt különböznek a folytatás-másolatok a szokásos másolatoktól: egy szokásos Clone új logikai tartalmat jelent, és új szemantikai azonosítót kap, míg a belső folytatás-másolat örökli annak a komponensnek az azonosságát, amelyet folytat

Az elem-újrafelhasználást egy, a komponensmutató szerint rendezett szemantikaiazonosság-indexen keresztül keresi meg, bináris összehasonlítással, ami logaritmikusan tartja a keresést nagy fákon is. Az index csak nem-tulajdonló hivatkozásokat tárol; magának a struktúraobjektumnak az élettartama a PDF-objektumgráfnál marad

Struktúraszabályok, amelyeket a renderelő előre kikényszerít

A GenerateStructure bekapcsolásával több PDF/UA-szabályt már a fa renderelése közben ellenőriz a rendszer, nem a fájl elkészülte után. A címsorok az 1. szinten kezdődnek, és nem hagyhatnak ki szinteket. Az LI csak L-en belül jelenhet meg, az Lbl és az LBody pedig csak LI-n belül. A TR egy táblázathoz tartozik, a TH és a TD egy sorhoz. Egy alternatív szöveg nélküli ábrát PDF/UA módban elutasít a rendszer

A korai elutasítás itt szándékos választás. Egy validátor, amely a dokumentum megírása után jelenti a hiányzó alternatív szöveget, azt mondja, hogy tízezer kimutatásból álló köteget kell újragenerálni; egy renderelő, amely elutasítja a komponenst, megmondja, melyiket, miközben az azt előállító adat még hatókörben van. A megfelelőség-ellenőrzésnek továbbra is külön lépésként kell szerepelnie a folyamatban, ennek mechanizmusát a PDF/A, PDF/X és PDF/UA validáció ismerteti

var
  Pdf: THotPDF;
  Renderer: THPDFDOMRenderer;
  Stats: THPDFDOMRenderStatistics;
begin
  Pdf := THotPDF.Create(nil);
  Renderer := THPDFDOMRenderer.Create;
  try
    Pdf.FileName := 'maintenance-report.pdf';
    Pdf.BeginDoc;
    Stats := Renderer.Render(Doc, Pdf);
    Pdf.EndDoc;

    Writeln(Format('%d page(s), %d placement(s), %d split(s)',
      [Stats.PageCount, Stats.PlacementCount, Stats.SplitCount]));
    Writeln(Format('structure elements=%d marked content=%d artifacts=%d',
      [Stats.StructureElementCount, Stats.MarkedContentCount,
       Stats.ArtifactCount]));
    Writeln(Format('deepest continuation chain: %d',
      [Stats.MaximumContinuationDepth]));
  finally
    Renderer.Free;
    Doc.Free;
    Pdf.Free;
  end;
end;

A statisztikai rekord hasznosabb, mint amilyennek elsőre tűnik. A SplitCount meredek emelkedése egy sablonváltoztatás után általában azt jelenti, hogy egy komponens a konténerénél magasabbnak kezdett mérődni. A MaximumContinuationDepth fokozatos növekedése korai figyelmeztetés egy olyan komponensre, amelynek Split-je túl kevés előrehaladást tesz oldalanként. Az ArtifactCount és a folytatólagos oldalak számának összevetése pedig megerősíti, hogy az ismétlődő fejlécek valóban artifactként lettek megjelölve

Hol illeszkedik a DOM a közvetlen API mellé?

A DOM nem váltja fel a közvetlen rajzolást; ugyanazokra az oldalobjektumokra épül rá. Bármi, amit a renderelő elhelyez, keverhető a THotPDF-en végzett közvetlen hívásokkal, ami akkor számít, ha egy jelentésnek egyetlen kézzel pozicionált elemre van szüksége, például egy aláírásképre, egy pontos helyen. Az oldalak lezárása továbbra is az AddPage és az EndDoc irányítása alatt marad, így az azonnali kiürítési mód nem tart memóriában befejezett oldalakat, a rezidens memóriát pedig az aktuális folytatások, betűtípus-erőforrások és a szokásos dokumentum-objektumgráf szabályozza

Válaszd a DOM-ot, ha a tartalom adatvezérelt, az elrendezés pedig szabályvezérelt, és tartsd meg a közvetlen rajzolást a rögzített grafikákhoz. Ha a jelenlegi problémád kifejezetten a táblázatlapozás, először érdemes elolvasni a táblázatok generálása PDF-ben szűkebb megközelítését, a szövegszintű viselkedést, például a sorkizárást pedig a szövegsorkizárás ismerteti

A deklaratív elrendezés, az automatikus jelölés és a közvetlen rajzolási API ugyanabban a komponensben érkezik Delphihez és C++Builderhez; a teljes funkciólista a HotPDF Delphi PDF-komponens oldalán található