Техническа статия

Четене на свойства на PDF шрифтове с PDFium Component в Delphi

Всеки видим символ в PDF носи препратка към шрифта, който го е нарисувал, и PDFium Component ви позволява да проследите тази препратка обратно до обекта на шрифта и да прочетете какво знае той. Единицата за достъп е символът, а не документът: избирате символ по неговия индекс в текста на страницата и питате за името на семейството, основното име, теглото, ъгъла на курсив (italic angle) и дали основният шрифт е действително пренасян вътре във файла. Това последно свойство е онова, което повечето анализи наистина търсят, защото вграденият шрифт пътува с документа, а невграденият е обещание, че машината на читателя случайно има инсталиран същия шрифт

Компонентът ги излага чрез същите обекти TPdf и TPdfView, които използвате за рендиране и извличане на текст. Няма отделен обект „таблица с шрифтове“, който да отваряте. След като текстът на страницата е бил анализиран, свойствата на шрифта висят от индекса на символа и вие ги четете глиф по глиф. Този дизайн съответства на начина, по който PDF съхранява информацията на първо място: една единствена страница може да превключва шрифтове десетки пъти, и единственият честен отговор на въпроса „на какъв шрифт е този документ“ е „зависи кой символ имате предвид“

Четене на шрифта зад един символ

Най-малката полезна операция е да вземете индекс на символ и да изведете всичко, което PDFium може да ви каже за неговия шрифт. Всяко свойство на шрифта в TPdf и TPdfView се индексира по позиция на символ, така че индексът преминава през всички тях. Страницата също така трябва да бъде текущата страница, за да може индексът да се разреши спрямо правилния текст, което има значение, след като преминете отвъд страница едно

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;

Няколко от сигнатурите изненадват хората, идващи от други библиотеки. FontAscent и FontDescent приемат два аргумента, индекса на символа и размер в точки, защото PDFium отчита тези показатели в единици на пространството на глифа (glyph-space units), които стават пиксели едва след като ги мащабирате по размера, в който е бил зададен текстът. Подайте стойността, която вече сте прочели от FontSize[CharIndex], и получавате възход (ascent) и спускане (descent) в същите точки като останалата част от оформлението. Спускането се връща отрицателно, тъй като се измерва под базовата линия. Името на семейството и основното име са отделни низове нарочно: основното име е суровият запис /BaseFont от PDF-а, често носещ префикс на подмножество като ABCDEF+, докато името на семейството е изчистеното име, до което рендерерът го разрешава

Превръщане на клик в индекс на символ

В програма за преглед рядко знаете индекса предварително. Потребителят кликва върху глиф и вие трябва да преведете координатата в пиксели в символа под него. CharacterIndexAtPos прави точно това, като взема позицията на мишката и толеранс и връща индекса на най-близкия символ, или отрицателна стойност, когато кликът е попаднал на празно пространство или празна страница

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;

Толерансът си струва да се настрои. Твърде стегнат и потребителите имат чувството, че трябва да попаднат на точното стъбло на буква; твърде хлабав и кликване в полето прихваща някакъв далечен символ, който няма нищо общо с това, което са имали предвид. Три до пет пиксела на устройството е разумна отправна точка за гледане на екран. Върнатият индекс е в анализирания текст на текущата страница, същото пространство от индекси, което всяко свойство на шрифта очаква, така че можете да го предадете направо на рутината по-горе. Съхраняването му в CurrentCharIndex не е задължително, но е удобно: изгледът запазва това като своето понятие за фокусиран глиф, което е удобно, ако други части от потребителския интерфейс искат да прочетат селекцията, без да я извличат отново

Вграждането е свойството, което има значение

За по-голямата част от реалната работа единственият въпрос, на който си струва да се отговори, е дали всеки шрифт е вграден. Документ, чиито шрифтове всички се возят вътре в него, се рендира по същия начин на RIP на печатно бюро, на лаптопа на колега и на сървър без GUI изобщо. Документ, който се опира на невградена Helvetica, залага на това, че всяка от тези машини има съвпадащ шрифт, и когато хазартът се провали, четецът замества с нещо близко, показателите се изместват и внимателно оформен формуляр се пренарежда точно толкова, колкото да се счупи. Обхождането на текста на страницата и групирането на шрифтовете по статус на вграждане ви дава този отговор евтино

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;

Две подробности поддържат това честно. Първо, CharacterCount е за страница, така че одит на целия документ означава задаване на Pdf.PageNumber за всяка страница последователно и повторно стартиране на цикъла, обединявайки резултатите. Второ, текстовият слой съдържа генерирани символи, като например интервалите, които четецът извежда между думите, и те нямат обект за шрифт зад себе си; проверката за празно основно име ги пропуска, вместо да записва фантом. Основното име е правилният ключ за дедупликация тук, защото префиксът на подмножеството, който носи, отличава две различни подмножества от едно и също семейство, което обикновено е това, което искате да знаете

Изваждане на вградения шрифт

Когато даден шрифт е вграден, можете да четете неговите байтове директно. FontData връща суровата програма за шрифт, същите TrueType или CFF данни, които PDF пренася, което е достатъчно за записване на самостоятелен файл с шрифт или за снемане на отпечатък от шрифта спрямо известна библиотека. Той връща празен масив, когато шрифтът не е вграден, така че проверката за вграждане и проверката за дължина заедно пазят записа

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;

Байтовете са вграденото подмножество, а не оригиналният шрифт за продажба на дребно, така че това, което получавате обратно, обикновено покрива само глифовете, които документът действително е използвал. Това е точно правилното за форензика и проверка и лошо прилягане за повторна употреба; подмножество на Times New Roman, което съдържа тридесет глифа, не е шрифт, който можете да инсталирате и да пишете с него. Третирайте извличането като начин за проверка на това, което е доставено, а не като инструмент за възстановяване на шрифтове. Ако се нуждаете от съвпадащото основно име, за да етикетирате изхода, прочетете FontBaseName[CharIndex] заедно с данните и премахнете водещия етикет на подмножеството, ако искате голото семейство

Осмисляне на числото за тегло

FontWeight връща цифровия клас на тегло, същата скала от 100 до 900, която използва CSS, където 400 е нормално (regular), а 700 е получер (bold). PDFium отчита каквото и да декларира шрифтът, което не винаги е кръгла стотица; даден шрифт може да обяви 350 или 650, и третирането на всичко на или над 600 като „достатъчно получер, за да има значение“, се държи по-добре от тестването за точно 700. Ъгълът на курсив е съпътстващ сигнал: ненулева стойност, обикновено отрицателна, означава, че шрифтът има наклонен или истински курсивен дизайн, а нула означава изправен. Заедно те ви позволяват да различите bold-italic пасаж от нормален, без да рендирате нищо, което е видът проверка, която пропуск за предварителна проверка (preflight pass) или одит за достъпност иска да направи в насипно състояние

Нито едно от тези четения не изисква рендирана растерна графика (bitmap). Те идват от анализирания текстов слой, така че отворен документ на правилната страница е цялата настройка, от която се нуждаете, което прави проверката на шрифтове евтина за изпълнение в целия архив. Ако сдвоявате това с извличане на текст, същите индекси на символи се подреждат с текста, който изваждате, така че шрифтът на глиф и неговата Unicode стойност са две четения срещу един индекс. Съпътстващата статия за извличане на текст от PDF документи с PDFium Component покрива тази страна на текстовия слой в по-голяма дълбочина

Показаните тук свойства на шрифта са част от PDFium Delphi VCL Component