Műszaki cikk

PDF betűtípus-tulajdonságok olvasása PDFium Component-lel Delphiben

Minden látható karakter egy PDF-ben hivatkozást hordoz arra a betűtípusra, amely megrajzolta, a PDFium Component pedig lehetővé teszi, hogy ezt a hivatkozást követve visszajussunk a betűtípus-objektumhoz, és kiolvassuk, amit az tud. A hozzáférés egysége a karakter, nem pedig a dokumentum: kiválaszt egy karaktert az oldalszövegben lévő indexe alapján, és lekéri a családnevet, az alapnevet, a vastagságot, a dőlt dőlésszögét (italic angle), valamint azt, hogy a mögöttes betűkészlet (face) ténylegesen megtalálható-e a fájlban. A legtöbb elemzés valójában ez utóbbi tulajdonságot keresi, mivel egy beágyazott betűtípus a dokumentummal együtt utazik, a nem beágyazott pedig csak egy ígéret arra, hogy az olvasó gépére történetesen fel van telepítve ugyanaz a betűkészlet

A komponens ezeket ugyanazon a TPdf és TPdfView objektumokon keresztül teszi elérhetővé, amelyeket a rendereléshez és a szövegkinyeréshez is használ. Nincs külön "betűtípus-tábla" objektum, amit meg kellene nyitni. Miután egy oldal szövegét elemezték, a betűtípus-tulajdonságok a karakterindexhez kapcsolódnak, és ön egyszerre egy glyphet (betűjelet) olvas be. Ez a kialakítás illeszkedik ahhoz, ahogyan a PDF eredendően tárolja az információkat: egyetlen oldal tucatszor is válthat betűtípust, és az egyetlen őszinte válasz a "milyen betűtípussal készült ez a dokumentum?" kérdésre az, hogy "attól függ, melyik karakterre gondol"

Egyetlen karakter mögötti betűtípus olvasása

A legkisebb hasznos művelet egy karakterindex fogadása, és mindannak a kiíratása (dump), amit a PDFium el tud mondani a betűtípusáról. A TPdf és TPdfView minden betűtípus-tulajdonsága karakterpozíció alapján van indexelve, így az index mindegyiken végigfut. Az oldalnak az aktuális oldalnak is kell lennie ahhoz, hogy az index a megfelelő szövegre vonatkozzon, ami akkor számít, ha túllép az első oldalon

procedure DescribeFontAt(Pdf: TPdf; CharIndex: Integer);
var
  Report: TStringList;
  PtSize: Single;
begin
  Report := TStringList.Create;
  try
    PtSize := Pdf.FontSize[CharIndex];

    Report.Add('Character : ' + Pdf.Character[CharIndex]);
    Report.Add('Family    : ' + Pdf.FontFamilyName[CharIndex]);
    Report.Add('Base name : ' + Pdf.FontBaseName[CharIndex]);
    Report.Add('Weight    : ' + IntToStr(Pdf.FontWeight[CharIndex]));
    Report.Add('Italic    : ' + IntToStr(Pdf.FontItalicAngle[CharIndex]) + ' deg');
    Report.Add('Size      : ' + FormatFloat('0.0', PtSize) + ' pt');
    Report.Add('Ascent    : ' + FormatFloat('0.0', Pdf.FontAscent[CharIndex, PtSize]));
    Report.Add('Descent   : ' + FormatFloat('0.0', Pdf.FontDescent[CharIndex, PtSize]));
    Report.Add('Embedded  : ' + BoolToStr(Pdf.FontIsEmbedded[CharIndex], True));

    ShowMessage(Report.Text);
  finally
    Report.Free;
  end;
end;

Néhány aláírás meglepi a más könyvtárakból érkező embereket. A FontAscent és a FontDescent két argumentumot vár, a karakterindexet és a pontméretet, mert a PDFium ezeket a metrikákat glyph-tér egységekben (glyph-space units) adja vissza, amelyek csak akkor válnak pixelekké, ha megszorozza őket azzal a mérettel, amellyel a szöveg be lett állítva. Adja át azt az értéket, amelyet már kiolvasott a FontSize[CharIndex]-ből, és megkapja a fel- és lenyúlást ugyanazokban a pontokban, mint az elrendezés többi része. A lenyúlás (descent) negatív értékként tér vissza, mivel az alapvonal alá mér. A családnév és az alapnév szándékosan különálló karakterláncok: az alapnév a nyers /BaseFont bejegyzés a PDF-ből, amely gyakran hordoz egy részhalmaz (subset) előtagot, például ABCDEF+, míg a családnév a megtisztított név, amelyet a megjelenítő felold

Egy kattintás átalakítása karakterindexszé

Egy megjelenítőben ritkán tudja előre az indexet. A felhasználó rákattint egy glyphre, önnek pedig le kell fordítania a pixelkoordinátát az alatta lévő karakterre. A CharacterIndexAtPos pontosan ezt teszi: megkapja az egérpozíciót és egy tűréshatárt, majd visszaadja a legközelebbi karakter indexét, vagy egy negatív értéket, ha a kattintás szóközre (whitespace) vagy üres oldalra esett

procedure TfrmMain.PdfViewMouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
var
  Index: Integer;
begin
  if not PdfView.Active then
    Exit;

  // 4 px of slack in each direction so a near-miss still hits the glyph.
  Index := PdfView.CharacterIndexAtPos(X, Y, 4.0, 4.0);
  if Index < 0 then
    Exit;                      // clicked between glyphs; leave the panel alone

  PdfView.CurrentCharIndex := Index;
  DescribeFontAt(PdfView.Pdf, Index);
end;

A tűréshatárt (tolerance) érdemes finomhangolni. Ha túl szoros, a felhasználók úgy érzik, hogy pontosan a betű szárára kell kattintaniuk; ha túl laza, a margón történő kattintás valami távoli karakterre ugrik (snap), aminek semmi köze ahhoz, amire gondoltak. Képernyős megtekintéshez a három-öt eszközpixel egy ésszerű kiindulópont. A visszaadott index az aktuális oldal elemzett szövegére vonatkozik, ugyanarra az indextérre, amelyet minden betűtípus-tulajdonság elvár, így egyenesen átadhatja a fenti rutinnak. Ennek tárolása a CurrentCharIndex-ben opcionális, de kényelmes: a nézet (view) ezt tartja nyilván a fókuszált glyph fogalmaként, ami hasznos, ha a felhasználói felület más részei újra-származtatás nélkül szeretnék beolvasni a kijelölést

A beágyazás a lényeges tulajdonság

A legtöbb valódi munka esetében az egyetlen megválaszolandó kérdés az, hogy minden betűtípus be van-e ágyazva. Egy olyan dokumentum, amelynek minden betűtípusa benne utazik, ugyanúgy jelenik meg egy nyomda RIP-jén, egy kolléga laptopján és egy olyan szerveren, amelynek egyáltalán nincs grafikus felhasználói felülete. Egy olyan dokumentum, amely egy nem beágyazott Helvetica-ra támaszkodik, azzal kockáztat, hogy mindegyik gép rendelkezik egy egyező betűkészlettel; és amikor a kockázatvállalás kudarcot vall, az olvasó behelyettesít valami hasonlót, a metrikák eltolódnak, és egy gondosan elrendezett űrlap éppen annyira törik újra, hogy szétessen. Az oldalszöveg bejárása és a betűtípusok beágyazási státusz szerinti csoportosítása olcsón megadja önnek ezt a választ

procedure ReportNonEmbeddedFonts(Pdf: TPdf);
var
  Embedded, External: TStringList;
  I: Integer;
  Name: string;
begin
  Embedded := TStringList.Create;
  External := TStringList.Create;
  try
    Embedded.Sorted := True;
    Embedded.Duplicates := dupIgnore;
    External.Sorted := True;
    External.Duplicates := dupIgnore;

    for I := 0 to Pdf.CharacterCount - 1 do
    begin
      Name := Pdf.FontBaseName[I];
      if Name = '' then
        Continue;              // generated spaces and the like have no font
      if Pdf.FontIsEmbedded[I] then
        Embedded.Add(Name)
      else
        External.Add(Name);
    end;

    if External.Count > 0 then
      ShowMessage(IntToStr(External.Count) +
        ' non-embedded font(s):' + sLineBreak + External.Text)
    else
      ShowMessage('All ' + IntToStr(Embedded.Count) +
        ' font(s) on this page are embedded.');
  finally
    Embedded.Free;
    External.Free;
  end;
end;

Két részlet tartja ezt őszintének. Először is, a CharacterCount oldalanként értendő, így egy teljes dokumentumra kiterjedő ellenőrzés (audit) azt jelenti, hogy a Pdf.PageNumber-t sorban minden oldalra be kell állítani, és újra le kell futtatni a ciklust, egyesítve az eredményeket. Másodszor, a szövegréteg tartalmaz generált karaktereket, például azokat a szóközöket, amelyeket egy olvasó a szavak között feltételez, és ezek mögött nincs betűtípus-objektum; az üres alapnév (empty-base-name) ellenőrzés kihagyja őket ahelyett, hogy egy fantomot naplózna. Itt az alapnév a megfelelő kulcs a duplikációk kiszűrésére (de-duplication), mivel a benne lévő részhalmaz (subset) előtag megkülönbözteti ugyanannak a családnak két különböző részhalmazát, és általában pontosan ezt szeretné tudni

A beágyazott betűkészlet kinyerése

Ha egy betűtípus be van ágyazva, a bájtokat közvetlenül is beolvashatja. A FontData a nyers betűtípus-programot adja vissza, ugyanazt a TrueType vagy CFF adatot, amit a PDF hordoz, ami elegendő egy önálló betűtípus-fájl írásához, vagy a betűkészlet ujjlenyomatának (fingerprint) azonosításához egy ismert könyvtárral szemben. Egy üres tömböt ad vissza, amikor a betűtípus nincs beágyazva, így a beágyazás-ellenőrzés és a hossz-ellenőrzés együtt védik az írási műveletet

procedure SaveEmbeddedFont(Pdf: TPdf; CharIndex: Integer;
  const OutputFile: string);
var
  Data: TBytes;
  Stream: TFileStream;
begin
  if not Pdf.FontIsEmbedded[CharIndex] then
  begin
    ShowMessage('That glyph''s font is not embedded; nothing to extract.');
    Exit;
  end;

  Data := Pdf.FontData[CharIndex];
  if Length(Data) = 0 then
    Exit;

  Stream := TFileStream.Create(OutputFile, fmCreate);
  try
    Stream.WriteBuffer(Data[0], Length(Data));
  finally
    Stream.Free;
  end;
  ShowMessage('Wrote ' + IntToStr(Length(Data)) + ' bytes.');
end;

A bájtok a beágyazott részhalmazt képezik, nem az eredeti kiskereskedelmi (retail) betűtípust, így amit visszakap, az általában csak a dokumentum által ténylegesen használt glyphekre terjed ki. Ez pontosan megfelelő törvényszéki elemzésekhez (forensics) és hitelesítéshez, de rosszul illeszkedik az újrahasznosításhoz; a Times New Roman harminc glyphet tartalmazó részhalmaza nem olyan betűtípus, amit telepíthetne és gépelhetne vele. Kezelje a kinyerést úgy, mint egy módot a leszállított tartalom megvizsgálására, és ne úgy, mint egy betűtípus-helyreállító eszközt. Ha a megfelelő alapnévre van szüksége a kimenet felcímkézéséhez, olvassa ki a FontBaseName[CharIndex]-et az adatok mellett, és vágja le (strip) a bevezető részhalmaz-címkét, ha a puszta családot szeretné

A vastagság számának értelmezése

A FontWeight a numerikus vastagsági (weight) osztályt adja vissza, ugyanazt az 100-tól 900-ig terjedő skálát, amit a CSS használ, ahol a 400 a normál (regular) és a 700 a félkövér (bold). A PDFium azt jelenti, amit a betűtípus deklarál, ami nem mindig egy kerek százas; egy betűkészlet hirdethet 350-et vagy 650-et, és minden 600-as vagy annál nagyobb érték "elég félkövér ahhoz, hogy számítson" elv alapján történő kezelése jobban beválik, mint a pontosan 700-as értékre történő tesztelés. A dőlt dőlésszög (italic angle) egy társított jelzés: egy nem nulla, általában negatív érték azt jelenti, hogy a betűkészlet oblique vagy valódi dőlt (italic) dizájn, a nulla pedig azt, hogy álló (upright). Ezek ketten együtt lehetővé teszik a félkövér-dőlt futás megkülönböztetését a normáltól anélkül, hogy bármit renderelne; ez az a fajta ellenőrzés, amit egy preflight menet vagy egy hozzáférhetőségi audit nagy mennyiségben (in bulk) szeretne elvégezni

Ezek az olvasások egyike sem igényel renderelt bittérképet. Az elemzett szövegrétegből (text layer) származnak, így a megfelelő oldalon lévő nyitott dokumentum a teljes szükséges beállítás; ez teszi a betűtípus-vizsgálatot olcsóvá egy egész archívumon keresztül. Ha ezt a szövegkinyeréssel (text extraction) párosítja, ugyanazok a karakterindexek igazodnak ahhoz a szöveghez, amit kihúz, így egy glyph betűtípusa és a Unicode értéke egyetlen indexszel szembeni két olvasás. A kiegészítő (companion) cikk a szöveg PDF dokumentumokból PDFium Component segítségével történő kinyeréséről részletesebben is kitér a szövegréteg ezen oldalára

Az itt bemutatott betűtípus-tulajdonságok a PDFium Delphi VCL Component részei