Kutsu, joka asettaa tekstin PDF-sivulle, on suoraviivainen. Annetaan AddText-funktiolle merkkijono, fontti, koko ja sijainti, ja glyfit ilmestyvät näkyviin. Se ei kuitenkaan kerro, kuinka leveä tuo merkkijono on sen jälkeen kun se on piirretty, eikä se jaa pitkää merkkijonoa usealle riville. Yksi kutsu maalaa yhden tekstijuoksun yhteen sijaintiin. Jos juoksu on leveämpi kuin sarake, johon sen oli tarkoitus mahtua, se vain jatkuu reunan yli, eikä mikään piirtokutsu varoita siitä. Heti kun haluat kappaleen etkä yksittäistä otsaketta, puuttuva osa on valitulla fontilla ja koolla mitattu merkkijonon leveys ennen kuin sitoudut piirtämään sen sivulle
Tämä on klassinen asetteluongelma. Jotta kappale voidaan rivittää sarakkeeseen, on tiedettävä sana sanalta kuinka paljon vaakasuoraa tilaa kukin ehdokasrivi vie, ja se on tiedettävä ennen kuin mitään piirretään. Rivinvaihto on mittaussilmukka, joka ympäröi piirtokutsun, ja sidonta joka osaa vain piirtää antaa sinulle sen jälkimmäisen puoliskon. PDFium Componentin tekstin mittaustuki sulkee tämän aukon kahdella funktiolla, MeasureText ja MeasureTextWidth, jotka raportoivat merkkijonon renderöidyn laajuuden jättämättä jälkeä yhdellekään sivulle
Miksi mittaus on class helper eikä uusi metodi TPdf-luokassa
Mittaustuki saapuu Delphin class helperinä tyypille TPdf, omassa yksikössään, sen sijaan että siihen olisi pultattu uusia metodeja itse TPdf-luokkaan. Class helper on kieliominaisuus, jonka avulla olemassa olevaan tyyppiin voidaan liittää metodeja sen oman julistuksen ulkopuolelta. Kun yksikkö on näkyvyysalueella, uusia metodeja kutsutaan täsmälleen kuin ne kuuluisivat luokkaan, joten helper-metodi näyttää muodolta Pdf.MeasureTextWidth(...) ilman erillistä oliota, jota pitäisi luoda tai välittää mukana
Syy tähän kerrostukseen on erottelu. Ydintyypin TPdf annetaan pysyä ennallaan ilman uusia kenttiä ja ilman olemassa olevien allekirjoitusten muuttamista, joten projekti joka ei koskaan tarvitse asettelua ei kanna mukanaan mittauskoodia. Projekti joka sitä tarvitsee lisää yhden yksikön uses-lauseeseen ja metodit syttyvät käyttöön. Kyvykkyys muuttuu opt-in-ominaisuudeksi yksittäisen yksikön tarkkuudella, mikä on siistein tapa laajentaa tyyppiä, jota et omista tai jota et halua häiritä
uses
PDFium, FPdfView, FPdfEdit,
FPdfMeasure; // the helper unit; brings MeasureText into scope on TPdf
// With the unit in scope the methods read as members of TPdf:
var
W, H: Double;
begin
Pdf.MeasureText('Subtotal', 'Helvetica', 11, W, H);
// W and H are now the rendered width and height in PDF user units
end;
Mittaaminen koskematta sivuun
Mittaamisen on oltava sivuvaikutuksetonta. Sen täytyy raportoida leveys jättämättä mitään jälkeensä, koska sitä kutsutaan monta kertaa asettelua päätettäessä ja sivun on näytettävä täsmälleen samalta kuin jos mittausta ei olisi koskaan tehty. Tämän mahdollistaa tekniikka, jossa rakennetaan tekstiolio, kysytään siltä koko ja heitetään se pois ennen kuin sitä koskaan liitetään sivuun
Sarja koostuu neljästä PDFium-kutsusta. FPDFPageObj_NewTextObj luo tekstiolion asiakirjaa vasten, kun sille annetaan fontin nimi ja koko. FPDFText_SetText asettaa merkkijonon, jota olio kantaa. FPDFPageObj_GetBounds lukee takaisin olion rajauslaatikon. FPDFPageObj_Destroy vapauttaa olion. Olennaista on, ettei mikään tässä sarjassa kutsu sivulle lisäävää APIa. Olio luodaan, kysellään ja tuhotaan eristettynä, joten asiakirja on muuttumaton funktion palatessa. Se on kertakäyttöinen koetin, jonka ainoa tuotos on rajauslaatikon neljä lukua
Tämä on kestävin tapa tehdä asia, koska PDFium ei tarjoa kätevää glyfikohtaista etenemisleveyttä, jonka voisit summata itse. Glyfimittarit riippuvat fonttiohjelmasta, koodauksesta ja siitä miten PDFium lataa fontin, eikä julkista kutsua ole, joka antaisi jokaisen merkin advance-leveyden merkkijonossa. Oikean tekstiolion rajauslaatikko taas lasketaan samalla koneistolla, joka asettelisi glyfit varsinaista piirtämistä varten, joten se heijastaa todellista renderöityä laajuutta eikä likiarvoa. Yhden kertakäyttöisen olion rakentaminen ja sen rajojen lukeminen antaa luotettavimman mittauksen, jonka kirjasto voi tarjota
// The shape of MeasureText, expressed against the verified PDFium calls.
// A text object is built, measured, and destroyed; no page is involved.
procedure TPdfMeasureHelper.MeasureText(const Text, Font: WString;
FontSize: Single; out Width, Height: Double);
var
TextObject: FPDF_PAGEOBJECT;
L, B, R, T: Single;
begin
Width := 0;
Height := 0;
if Self.Document = nil then
Exit;
TextObject := FPDFPageObj_NewTextObj(Self.Document,
FPDF_BYTESTRING(AnsiString(Font)), FontSize);
if TextObject = nil then
Exit;
try
if FPDFText_SetText(TextObject, FPDF_WIDESTRING(WideString(Text))) = 0 then
Exit;
if FPDFPageObj_GetBounds(TextObject, L, B, R, T) <> 0 then
begin
Width := R - L;
Height := T - B;
end;
finally
FPDFPageObj_Destroy(TextObject); // probe discarded, page untouched
end;
end;
Tuloksen koordinaatit ja yksiköt
Rajauslaatikko palautetaan neljänä reunana, vasen, ala, oikea ja ylä, ja kaksi mittaa saadaan vähennyslaskulla. Leveys on oikea miinus vasen ja korkeus on ylä miinus ala. Molemmat ilmaistaan PDF:n käyttäjäyksiköissä, joissa yksi yksikkö on yksi seitsemäskymmenes toinenosa tuumasta, eli samassa koordinaatistossa jossa teksti sijoitetaan sivulle. Tässä vaiheessa mukana ei ole piilotettua laiteyksikköä eikä pikseleitä. Leveys 36 tarkoittaa puolta tuumaa sivulla, riippumatta myöhemmästä renderöintiresoluutiosta
Pystyakseli kulkee PDF:n määrittelemällä tavalla niin, että Y kasvaa ylöspäin, minkä vuoksi korkeus lasketaan ylä miinus ala eikä toisin päin. Tällä yksityiskohdalla on väliä, kun kohdistinta viedään saraketta alaspäin. Mittaat rivin korkeuden ja vähennät sen nykyisestä perusviivasta löytääksesi seuraavan, koska sivulla alaspäin liikkuminen tarkoittaa siirtymistä pienempiin Y-arvoihin. Jos kohde on näyttö eikä paperi, käyttäjäyksiköt muunnetaan laitepikseleiksi näytön tarkkuuden avulla: käyttäjäyksiköissä oleva arvo kerrotaan DPI:llä ja jaetaan 72:lla, jolloin saadaan pikselit, joten pisteinä määritettyä sarakeleveyttä voidaan verrata mitattuun tekstijuoksuun ennen kuin päätetään missä katkaisu tehdään
Mitä tapahtuu rappeutuneella syötteellä
Funktiot on kirjoitettu epäonnistumaan hiljaa. Jos asiakirjaa ei ole auki tai tekstioliota ei voi luoda, tuloksena on nollalaajuus eikä poikkeus. Leveys ja korkeus alustetaan nollaan heti alussa ja ne korvataan vasta kun rajauslaatikko on saatu luettua onnistuneesti takaisin. Tyhjä merkkijono, puuttuva asiakirja tai fontti, jota kirjasto ei pysty ratkaisemaan olioksi, palauttaa kaikki nollan eikä heitä poikkeusta
Tämä valinta pitää mittaussilmukan yksinkertaisena, koska tuhansien sanojen yli kulkeva silmukka ei ole oikea paikka poikkeuksenkäsittelylle jokaisella iteraatiolla. Hinta on se, että kutsuja kantaa tarkistuksen vastuulleen. Nollaleveys on tunniste eikä fakta tekstistä, joten koodi joka jakaa mitatulla leveydellä tai olettaa positiivisen arvon joutuu suojaamaan nollan ennen kuin siihen voi luottaa. Kun nolla tulkitaan merkitykseen "mittausta ei voitu tehdä", sopimus on selvä; jos se sivuutetaan, rappeutunut syöte muuttuu hiljaa asetteluksi, jossa glyfit menevät päällekkäin
Ahne rivinvaihto mittauksen päälle rakennettuna
Kun käytössä on leveysfunktio, rivinvaihto on lyhyt ahne silmukka. Kappale pilkotaan sanoihin, pidetään nykyinen rivi ja jokaiselle sanalle mitataan, miltä rivi näyttäisi jos sana liitettäisiin siihen. Niin kauan kuin koekappale mahtuu sarakeleveyteen, sanoja lisätään; kun se olisi ylivuotamassa, nykyinen rivi kirjoitetaan AddText-kutsulla ja uusi rivi aloitetaan sanalla, joka ei mahtunut. Kertymä tehdään kokonaan MeasureTextWidth-funktion avulla, ja ainoa sivulle asti päätyvä asia on rivi, jonka mahtuminen on jo varmistettu
procedure WrapParagraph(Pdf: TPdf; const Para, Font: WString;
FontSize: Single; X, TopY, ColumnWidth, LineHeight: Double);
var
Words: TArray<string>;
Line, Trial: WideString;
I: Integer;
Y: Double;
begin
Words := string(Para).Split([' ']);
Line := '';
Y := TopY;
for I := 0 to High(Words) do
begin
if Line = '' then
Trial := Words[I]
else
Trial := Line + ' ' + Words[I];
// Measure the candidate line before drawing anything.
if (Line <> '') and (Pdf.MeasureTextWidth(Trial, Font, FontSize) > ColumnWidth) then
begin
Pdf.AddText(Line, Font, FontSize, X, Y); // flush the line that fit
Y := Y - LineHeight; // Y decreases going down
Line := Words[I]; // overflowing word starts next line
end
else
Line := Trial;
end;
if Line <> '' then
Pdf.AddText(Line, Font, FontSize, X, Y); // flush the final line
end;
Silmukka mittaa koerivin eikä yksittäisiä sanoja summattuna, koska rivin leveys ei ole sen sanojen leveyksien summa. Välilyönnit sanojen välissä vaikuttavat myös, ja mitattu tekstijuoksu ottaa tämän suoraan huomioon. Ahne sääntö, sovita niin monta sanaa kuin sarake sallii ja katkaise viimeiseen mahtuvaan sanaan, on sama sääntö joka täyttää kuilun raakaa AddText-kutsua ja oikeaa kappaletta vastaavan asettelun välillä. Piirtokutsu ei koskaan ollut vaikea osa. Mittaus, joka täytyy tehdä ennen sitä, on, ja juuri sen helper tarjoaa
Mihin tämä sopii
Mittaus on kerros sisällön tuottamisen ja renderöinnin välissä, joten se sopii luontevasti yhteen muun alusta asti rakennetun asiakirjatyönkulun kanssa. Jos kokoat sivuja ja sijoitat tekstiä alun perin itse, perusta löytyy artikkelista PDF-asiakirjojen luominen alusta alkaen PDFium Componentilla Delphissä, jossa AddText ja sivun asetukset käydään läpi kokonaan. Kun mitattava fontti on yhtä tärkeä kuin itse merkkijono, koska mittarit riippuvat kirjasinleikkauksesta, artikkeli PDF-fonttien ominaisuuksien analysointi PDFium Componentilla Delphissä näyttää, miten kirjasto raportoi rajauslaatikoita ohjaavat fonttitiedot. Molemmat rakentuvat saman sidonnan päälle, eli Delphiä ja Lazarusta varten tarjottavan PDFium Component -tuotteen, jossa mittaus-helper toimitetaan yhdessä asiakirja-, sivu- ja teksti-APIen kanssa, joista puhutaan muualla tässä blogissa