Technical Article

Måling af PDF-tekst til layout og ordombrydning i Delphi

Kaldet, der sætter tekst på en PDF-side, er ligetil. Du giver AddText en streng, en skrifttype, en størrelse og en position, og glyferne dukker op. Hvad det ikke gør, er at fortælle dig, hvor bred den streng vil være, når den først er tegnet, og det bryder ikke en lang streng på tværs af flere linjer. Et enkelt kald maler ét tekstforløb på én position. Hvis forløbet er bredere end den kolonne, du mente det skulle passe i, løber det simpelthen ud over kanten, og intet i tegnekaldet advarer dig. I det øjeblik du ønsker et afsnit frem for en enkelt etiket, er den manglende brik bredden af en streng i den valgte skrifttype og størrelse, målt før du forpligter den til siden

Dette er det klassiske layoutproblem. For at ombryde et afsnit ind i en kolonne er du nødt til at vide, ord for ord, hvor meget vandret plads hver kandidatlinje vil optage, og du skal vide det, forud for tegning af noget som helst. Ordombrydning er en målingssløjfe viklet omkring et tegnekald, og en binding, der kun tegner, giver dig den anden halvdel. Understøttelsen af tekstmåling i PDFium-komponenten lukker det hul med to funktioner, MeasureText og MeasureTextWidth, der rapporterer en strengs gengivne udstrækning uden at sætte et mærke på nogen side

Hvorfor måling er en class helper, ikke en ny metode på TPdf

Målingsunderstøttelsen ankommer som en Delphi class helper til TPdf, der lever i sin egen enhed, frem for som nye metoder boltet ind i TPdf-klassen. En class helper er en sprogfunktion, der lader dig vedhæfte metoder til en eksisterende type udefra dens deklaration. Når enheden først er i scope, kaldes de nye metoder præcist som om de tilhørte klassen, så en helper-metode læses som Pdf.MeasureTextWidth(...) uden noget separat objekt at konstruere eller sende rundt

Grunden til at lægge det i lag på denne måde er adskillelse. Kerne-TPdf-typen forbliver, som den er, uden noget felt tilføjet og uden at røre ved nogen eksisterende signatur, så et projekt, der aldrig har brug for layout, aldrig bærer målingskoden. Et projekt, der har brug for det, tilføjer én enhed til en uses-klausul, og metoderne lyser op. Kapabilitet bliver opt-in på granulariteten af en enkelt enhed, hvilket er den reneste måde at udvide en type, du ikke ejer eller ikke ønsker at forstyrre

Måling uden at røre siden

Målingen skal være fri for bivirkninger. Den skal rapportere en bredde uden at efterlade noget, fordi du kalder den mange gange, mens du beslutter et layout, og siden skal se præcis ud, som den ville have gjort, hvis du aldrig havde målt overhovedet. Teknikken, der gør dette muligt, er at bygge et tekstobjekt, spørge det om dets størrelse, og smide det væk, før det nogensinde er vedhæftet til en side

Sekvensen er fire PDFium-kald. FPDFPageObj_NewTextObj opretter et tekstobjekt mod dokumentet, givet skrifttypenavn og størrelse. FPDFText_SetText sætter den streng, objektet bærer. FPDFPageObj_GetBounds læser objektets bounding box tilbage. FPDFPageObj_Destroy frigiver objektet. Helt afgørende er det, at intet i den sekvens kalder API'en for sideindsættelse. Objektet oprettes, forespørges og ødelægges i isolation, så dokumentet er uændret, når funktionen returnerer. Det er en kast-væk-sonde, hvis eneste output er de fire tal for dens bounding box

Dette er den robuste måde at gøre det på, fordi PDFium ikke eksponerer en praktisk per-glyf fremrykningsbredde, som du selv kunne summere. Glyf-metrikker afhænger af skrifttypeprogrammet, af kodningen, og af hvordan PDFium indlæser snittet, og der er intet offentligt kald, der overdrager dig fremrykningen for hvert tegn i en streng. Bounding boxen for et rigtigt tekstobjekt derimod, beregnes af det samme maskineri, som ville lægge glyferne ud til tegning, så det afspejler den faktiske gengivne udstrækning frem for en tilnærmelse. At bygge ét engangsobjekt og læse dets grænser er den mest pålidelige måling, biblioteket kan give

Koordinater og enheder for resultatet

Bounding boxen kommer tilbage som fire kanter, venstre, bund, højre og top, og de to dimensioner falder ud ved subtraktion. Bredde er højre minus venstre, og højde er top minus bund. Begge er udtrykt i PDF-brugerenheder, hvor én enhed er én tooghalvfjerdsindstyvende af en tomme, det samme koordinatrum, hvori du positionerer tekst på siden. Der er ingen skjult enhedsenhed og ingen pixel involveret på dette stadie. En bredde på 36 betyder en halv tomme af siden, uanset hvad den endelige gengivelsesopløsning er

Den lodrette akse løber den vej, PDF definerer det, med Y stigende opad, hvilket er grunden til at højden er top minus bund frem for det omvendte. Den detalje betyder noget, når du fører en markør ned ad en kolonne. Du måler en linjes højde, trækker den derefter fra den aktuelle baseline for at finde den næste, fordi at bevæge sig ned ad siden betyder at bevæge sig mod mindre Y. Hvis din destination er en skærm frem for papir, konverterer du brugerenheder til enhedspixels med skærmopløsningen: en værdi i brugerenheder multipliceret med DPI'en og divideret med 72 giver pixels, så en kolonnebredde, du sætter i punkter, kan matches mod et målt forløb, før du beslutter, hvor bruddet går

Hvad der sker ved degenereret input

Funktionerne er skrevet til at fejle stille. Hvis der ikke er noget åbent dokument, eller hvis tekstobjektet ikke kan oprettes, er resultatet en nuludstrækning frem for en rejst undtagelse. Bredden og højden initialiseres til nul i toppen og overskrives kun, når en bounding box er blevet læst tilbage med succes. En tom streng, et manglende dokument, en skrifttype, biblioteket ikke kan opløse til et objekt, hver af disse returnerer nul frem for at kaste en fejl

Det valg holder en målingssløjfe simpel, fordi en sløjfe, der løber over tusindvis af ord, ikke er stedet for undtagelseshåndtering på hver iteration. Omkostningen er, at kalderen bærer tjekket. En nulbredde er en sentinel, ikke et faktum om teksten, så kode, der dividerer med en målt bredde eller antager en positiv værdi, skal gardere sig mod nul før den stoler på den. Behandl nul som "kunne ikke måle" og kontrakten er klar; ignorer det, og et degenereret input bliver stille og roligt til et layout med en kolonne af overlappende glyfer

En grådig ordombrydning bygget på målingen

Med en breddefunktion i hånden er ordombrydning en kort grådig sløjfe. Du opdeler afsnittet i ord, bevarer en aktuel linje, og for hvert ord måler du, hvad linjen ville være, hvis du tilføjede det ord. Mens forsøgslinjen stadig passer i kolonnebredden, fortsætter du med at tilføje; når den ville løbe over, flusher du den aktuelle linje med AddText og starter en ny med det ord, der ikke passede. Akkumuleringen gøres udelukkende med MeasureTextWidth, og det eneste, der nogensinde når siden, er en linje, du allerede har bekræftet passer

Sløjfen måler forsøgslinjen frem for at måle hvert ord og summere, fordi bredden af en linje ikke er summen af dens ords bredder. Mellemrum mellem ord bidrager, og et målt forløb fanger det direkte. Den grådige regel, tilpas så mange ord som kolonnen tillader, og bryd ved det sidste, der passer, er den samme regel, der udfylder kløften mellem en rå AddText og et rigtigt afsnit. Tegnekaldet var aldrig den svære del. Målingen, der skal gå forud for det, er, og det er præcis, hvad helperen leverer

Hvor dette passer ind

Måling er laget mellem generering af indhold og dets gengivelse, så det parres naturligt med resten af en fra-bunden dokument-arbejdsgang. Hvis du samler sider og placerer tekst i første omgang, er fundamentet i oprettelse af PDF-dokumenter fra bunden med PDFium-komponenten i Delphi, hvor AddText og sideopsætning er dækket i fuldt omfang. Når skrifttypen, du måler, betyder lige så meget som strengen, fordi metrikker afhænger af snittet, viser analyse af PDF-skrifttypeegenskaber med PDFium-komponenten i Delphi, hvordan biblioteket rapporterer den skrifttypeinformation, der driver disse bounding boxes. Begge bygger på den samme binding, PDFium Component til Delphi og Lazarus, hvor målingshelperen leveres ved siden af de dokument-, side- og tekst-API'er, der er beskrevet på tværs af denne blog