Tekninen artikkeli

PDF-fonttien ominaisuuksien (Properties with PDFium Component in Delphi) lukeminen (Reading PDF Font Properties with PDFium Component in Delphi) PDFium Component -komponentilla (PDFium Component in Delphi) Delphissä

Jokainen näkyvä merkki PDF-tiedostossa sisältää viittauksen fonttiin, joka sen piirsi, ja PDFium Component antaa sinun seurata tätä viittausta takaisin fonttiobjektiin ja lukea, mitä se tietää. Pääsyn yksikkö on merkki, ei asiakirja: valitset merkin sen indeksillä sivun tekstissä ja pyydät perheen nimeä, perusnimeä, painoa, kursivointikulmaa ja sitä, onko taustalla oleva kirjasin todella mukana tiedoston sisällä. Tuo viimeinen ominaisuus on se, jota useimmat analyysit todella tavoittelevat, koska upotettu fontti kulkee asiakirjan mukana ja upottamaton fontti on vain lupaus siitä, että lukijan koneella sattuu olemaan sama kirjasin asennettuna

Komponentti paljastaa nämä samojen TPdf- ja TPdfView-objektien kautta, joita käytät hahmontamiseen ja tekstin purkamiseen. Avattavana ei ole erillistä "fonttitaulukko"-objektia. Kun sivun teksti on jäsennetty, fontin ominaisuudet riippuvat merkki-indeksistä, ja luet niitä yksi glyyfi kerrallaan. Tuo suunnittelu sopii siihen, miten PDF alun perinkin tallentaa tiedot: yksittäinen sivu voi vaihtaa fonttia kymmeniä kertoja, ja ainoa rehellinen vastaus kysymykseen "millä fontilla tämä asiakirja on" on "se riippuu siitä, mitä merkkiä tarkoitat"

Yhden merkin takana olevan fontin lukeminen

Pienin hyödyllinen toimenpide on ottaa merkki-indeksi ja tulostaa kaikki, mitä PDFium voi kertoa sen fontista. Jokainen fonttiominaisuus TPdf- ja TPdfView-objekteissa on indeksoitu merkkisijainnin mukaan, joten indeksi pujottelee niiden kaikkien läpi. Sivun on myös oltava nykyinen sivu, jotta indeksi ratkeaisi oikeaa tekstiä vasten, millä on merkitystä, kun siirryt sivun yksi ohi

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;

Pari allekirjoitusta yllättää ihmisiä, jotka tulevat muista kirjastoista. FontAscent ja FontDescent ottavat kaksi argumenttia, merkki-indeksin ja pistekoon, koska PDFium raportoi nuo metriikat glyyfitila-yksiköissä, joista tulee pikseleitä vasta, kun skaalaat ne koolla, johon teksti oli asetettu. Anna arvo, jonka olet jo lukenut kohteesta FontSize[CharIndex], niin saat nousun (ascent) ja laskun (descent) samoissa pisteissä kuin muunkin asettelun. Lasku tulee takaisin negatiivisena, koska se mitataan peruslinjan alapuolelta. Perheen nimi ja perusnimi ovat tarkoituksella erillisiä merkkijonoja: perusnimi on raaka /BaseFont-merkintä PDF:stä, ja se kantaa usein osajoukon (subset) etuliitettä, kuten ABCDEF+, kun taas perheen nimi on siivottu nimi, johon hahmontaja (renderer) sen ratkaisee

Napsautuksen (click) muuttaminen merkki-indeksiksi

Katseluohjelmassa tiedät harvoin indeksiä etukäteen. Käyttäjä napsauttaa glyyfiä ja sinun on käännettävä pikselikoordinaatti sen alla olevaksi merkiksi. CharacterIndexAtPos tekee juuri sen, ottaen hiiren sijainnin ja toleranssin ja palauttaen lähimmän merkin indeksin, tai negatiivisen arvon, kun napsautus osui tyhjään tilaan (whitespace) tai tyhjälle sivulle

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;

Toleranssia kannattaa hienosäätää. Liian tiukka ja käyttäjät tuntevat, että heidän on osuttava tarkasti kirjaimen varteen; liian löysä ja marginaaliin tehty napsautus napsahtaa johonkin kaukaiseen merkkiin, jolla ei ole mitään tekemistä sen kanssa, mitä he tarkoittivat. Kolmesta viiteen laitepikseliä on kohtuullinen lähtökohta näytön katseluun. Palautettu indeksi on nykyisen sivun jäsennettyyn tekstiin, samaan indeksiavaruuteen jota jokainen fonttiominaisuus odottaa, joten voit antaa sen suoraan yllä olevalle rutiinille. Sen tallentaminen kohteeseen CurrentCharIndex on valinnaista mutta kätevää: näkymä (view) pitää sen käsityksenään kohdistetusta glyyfistä, mikä on kätevää, jos käyttöliittymän (UI) muut osat haluavat lukea valinnan johtamatta sitä uudelleen

Upottaminen (Embedding) on ominaisuus, jolla on merkitystä

Useimmissa todellisissa töissä ainoa vastaamisen arvoinen kysymys on, onko jokainen fontti upotettu. Asiakirja, jonka kaikki fontit kulkevat sen sisällä, hahmontuu samalla tavalla painotalon (print bureau) RIP:ssä, kollegan kannettavassa tietokoneessa ja palvelimella, jossa ei ole GUI:ta ollenkaan. Asiakirja, joka nojaa upottamattomaan (non-embedded) Helvetica-fonttiin, pelaa uhkapeliä sen suhteen, että jokaisella noista koneista on vastaava kirjasin, ja kun uhkapeli epäonnistuu, lukija korvaa sen jollakin lähellä olevalla, metriikat siirtyvät ja huolellisesti aseteltu lomake juoksuttuu (reflows) juuri sen verran, että se hajoaa. Sivun tekstin läpikäynti ja fonttien lokeroiminen upottamisen tilan mukaan antaa sinulle tuon vastauksen halvalla

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;

Kaksi yksityiskohtaa pitää tämän rehellisenä. Ensinnäkin, CharacterCount on sivukohtainen, joten koko asiakirjan (whole-document) tarkastus tarkoittaa Pdf.PageNumber-ominaisuuden asettamista jokaiselle sivulle vuorotellen ja silmukan suorittamista uudelleen, yhdistäen tulokset. Toiseksi, tekstikerros sisältää generoituja merkkejä, kuten välilyöntejä, joita lukija päättelee sanojen väliin, ja niillä ei ole fonttiobjektia takanaan; tyhjän perusnimen tarkistus ohittaa ne sen sijaan, että kirjaisi haamun. Perusnimi on oikea avain deduplikointiin tässä, koska sen kantama osajoukon etuliite erottaa saman perheen kaksi eri osajoukkoa, mikä on yleensä se, mitä haluat tietää

Upotetun (embedded) kirjasimen vetäminen ulos

Kun fontti on upotettu, voit lukea sen tavut suoraan. FontData palauttaa raa'an fonttiohjelman, saman TrueType- tai CFF-tiedon jota PDF kantaa, mikä riittää kirjoittamaan erillisen (standalone) fonttitiedoston tai sormenjäljentämään (fingerprint) kirjasimen tunnettua kirjastoa vasten. Se palauttaa tyhjän taulukon (empty array), kun fonttia ei ole upotettu, joten upottamisen tarkistus ja pituuden tarkistus yhdessä suojaavat kirjoitusta

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;

Tavut ovat upotettu osajoukko, eivät alkuperäinen vähittäismyyntifontti (retail font), joten se mitä saat takaisin kattaa yleensä vain ne glyyfit, joita asiakirja todella käytti. Se on aivan oikein forensiikkaan (forensics) ja varmentamiseen (verification) ja sopii huonosti uudelleenkäyttöön; Times New Roman -fontin osajoukko, joka sisältää kolmekymmentä glyyfiä, ei ole fontti, jonka voit asentaa ja jolla voit kirjoittaa. Käsittele purkamista (extraction) tapana tarkastaa mitä toimitettiin, ei fontin palautustyökaluna (font-recovery tool). Jos tarvitset vastaavan perusnimen tulosteen merkitsemiseen, lue FontBaseName[CharIndex] tiedon ohella, ja riisu (strip) johtava (leading) osajoukko-tunniste (subset tag), jos haluat paljaan (bare) perheen

Painonumeron (weight number) ymmärtäminen

FontWeight palauttaa numeerisen painoluokan, saman 100-900 -asteikon jota CSS käyttää, jossa 400 on normaali (regular) ja 700 on lihavoitu (bold). PDFium raportoi mitä tahansa fontti ilmoittaakin, mikä ei aina ole tasainen sataluku; kirjasin voi ilmoittaa 350 tai 650, ja minkä tahansa 600 tai sitä suuremman käsitteleminen "tarpeeksi lihavoituna ollakseen merkityksellinen" kestää paremmin kuin tasan 700:n testaaminen. Kursivointikulma (italic angle) on kumppanisignaali: nollasta poikkeava arvo, yleensä negatiivinen, tarkoittaa, että kirjasin on vino (oblique) tai aito kursivoitu (true italic) muotoilu, ja nolla tarkoittaa pystyä (upright). Yhdessä ne antavat sinun erottaa lihavoidun-kursivoidun jakson normaalista ilman, että hahmonnat mitään, mikä on sellainen tarkistus, jonka esitarkastusajo (preflight pass) tai saavutettavuuden auditointi (accessibility audit) haluaa tehdä massana (in bulk)

Mikään näistä luvuista ei vaadi hahmonnettua bittikarttaa. Ne tulevat jäsennetystä tekstikerroksesta, joten avoin asiakirja oikealla sivulla on kaikki tarvitsemasi asetus, mikä tekee fonttien tarkastamisesta halpaa suorittaa koko arkiston yli. Jos yhdistät tämän tekstin purkamiseen, samat merkki-indeksit asettuvat riviin ulos vetämäsi tekstin kanssa, joten glyyfin fontti ja sen Unicode-arvo ovat kaksi lukua yhtä indeksiä vasten. Kumppaniartikkeli (companion article on) tekstin purkamisesta (extracting text from PDF documents with PDFium Component) PDF-asiakirjoista PDFium Component -komponentilla käsittelee (covers) tekstikerroksen tuota puolta perusteellisemmin

Tässä esitetyt fonttiominaisuudet ovat osa PDFium Delphi VCL Component -komponenttia