Technical Article

Анализ на свойствата на PDF шрифтове с PDFium VCL в Delphi

Всеки видим символ в PDF документ носи препратка към шрифта, с който е изчертан, и PDFium VCL ви позволява да проследите тази препратка обратно до обекта на шрифта и да прочетете информацията за него. Единицата за достъп е символът, а не документът: вие избирате символ по неговия индекс в текста на страницата и извличате името на фамилията, основното име, теглото, ъгъла на курсива и дали съответният шрифт всъщност е вграден във файла. Последното свойство е това, което вълнува най-много анализи, тъй като вграденият шрифт се пренася заедно с документа, докато невграденият е просто предположение, че на машината на потребителя случайно е инсталиран същият шрифт.

Компонентът предоставя достъп до тях чрез същите обекти 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 приемат два аргумента â€?индекса на символа и размера в точки (points), тъй като PDFium докладва тези метрики в единици от пространството на глифа, които стават пиксели едва след като ги мащабирате според размера, с който е зададен текстът. Предайте стойността, която вече сте прочели от FontSize[CharIndex], и ще получите ascent и descent в същите точки, каквито се използват за останалата част от оформлението. Descent се връща като отрицателна стойност, тъй като се измерва под базовата линия. Името на фамилията (family name) и основното име (base name) са умишлено разделени низове: основното име е суровият запис /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 устройството на печатницата, на лаптопа на колегата и на сървър без никакъв графичен интерфейс. Документ, който разчита на невграден 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 последователно и да изпълнявате цикъла отново, обединявайки резултатите. Второ, текстовият слой съдържа автоматично генерирани символи, като интервалите между думите, и те нямат реален обект на шрифт зад себе си; проверката за празно основно име (base name) ги пропуска, вместо да записва несъществуващи обекти. Основното име е правилният ключ за премахване на дубликати тук, тъй като префиксът на подмножеството (subset prefix), който то съдържа, разграничава две различни подмножества на една и съща фамилия, което обикновено е информацията, която ви е необходима.

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

Когато даден шрифт е вграден, можете да прочетете неговите байтове директно. 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;

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

Разчитане на числото за тегло на шрифта

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

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

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