기술 문서

Delphi에서 PDFium Component를 사용하여 PDF 글꼴 속성 읽기

PDF의 모든 표시되는 문자는 해당 문자를 그린 글꼴에 대한 참조를 전달하며, PDFium Component를 사용하면 이 참조를 따라 글꼴 개체로 돌아가 글꼴이 알고 있는 정보를 읽을 수 있습니다. 액세스 단위는 문서가 아니라 문자입니다. 페이지 텍스트의 인덱스로 문자를 선택하고 글꼴군 이름, 기본 이름, 두께, 기울임꼴 각도, 그리고 기본 서체가 파일 내에 실제로 포함되어 있는지 여부를 묻습니다. 마지막 속성은 대부분의 분석에서 진정으로 추구하는 속성입니다. 왜냐하면 포함된 글꼴은 문서와 함께 이동하지만 포함되지 않은 글꼴은 독자의 기기에도 동일한 서체가 설치되어 있다는 막연한 기대에 불과하기 때문입니다

이 구성 요소는 렌더링 및 텍스트 추출에 사용하는 것과 동일한 TPdfTPdfView 개체를 통해 이를 노출합니다. 열어야 할 별도의 "글꼴 테이블" 개체는 없습니다. 페이지의 텍스트가 파싱되고 나면 글꼴 속성은 문자 인덱스에 매달리게 되며, 이를 한 번에 한 글리프씩 읽습니다. 이 설계는 PDF가 애초에 정보를 저장하는 방식과 잘 맞습니다. 단일 페이지에서 글꼴이 수십 번 전환될 수 있으며, "이 문서의 글꼴이 무엇인가"에 대한 유일하게 정직한 답은 "어떤 문자를 말하는지에 따라 다르다"는 것입니다

문자 하나의 배경에 있는 글꼴 읽기

가장 유용한 최소 작업은 문자 인덱스를 가져와 PDFium이 해당 글꼴에 대해 알려줄 수 있는 모든 것을 덤프하는 것입니다. TPdfTPdfView의 모든 글꼴 속성은 문자 위치에 의해 인덱스되므로 인덱스가 모든 속성을 관통합니다. 인덱스가 올바른 텍스트를 상대로 해석되려면 해당 페이지가 현재 페이지여야 하며, 이는 첫 페이지를 지나 이동할 때 중요합니다

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;

다른 라이브러에서 온 사람들에게는 서명 중 몇 가지가 놀라울 수 있습니다. FontAscentFontDescent는 문자 인덱스와 포인트 크기라는 두 개의 인수를 받는데, 이는 PDFium이 텍스트가 설정된 크기로 크기를 조정해야만 픽셀이 되는 글리프 공간 단위로 해당 메트릭을 보고하기 때문입니다. 이미 FontSize[CharIndex]에서 읽은 값을 전달하면 나머지 레이아웃과 동일한 포인트의 어센트 및 디센트를 얻습니다. 디센트는 기준선 아래를 측정하므로 음수로 반환됩니다. 글꼴군 이름과 기본 이름은 의도적으로 별도의 문자열입니다. 기본 이름은 PDF의 원시 /BaseFont 항목으로 종종 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;

허용 오차는 조정할 가치가 있습니다. 너무 좁으면 사용자들은 문자의 정확한 줄기를 클릭해야 한다고 느끼게 되고, 너무 느슨하면 여백의 클릭이 원래 의도와는 아무 관련 없는 멀리 떨어진 문자로 스냅됩니다. 3에서 5 디바이스 픽셀 정도가 화면 보기의 합리적인 시작점입니다. 반환된 인덱스는 현재 페이지의 파싱된 텍스트로 들어가는 인덱스로, 모든 글꼴 속성이 예상하는 것과 동일한 인덱스 공간이므로 위 루틴에 바로 넘겨줄 수 있습니다. CurrentCharIndex에 저장하는 것은 선택 사항이지만 편리합니다. 뷰는 이를 초점이 맞춰진 글리프에 대한 자체 개념으로 유지하며, 이는 UI의 다른 부분에서 선택 사항을 다시 파생하지 않고 읽고자 할 때 유용합니다

포함 여부는 가장 중요한 속성입니다

대부분의 실제 작업에서 답변할 가치가 있는 유일한 질문은 각 글꼴이 포함되어 있는지 여부입니다. 모든 글꼴이 내부에 포함된 문서는 인쇄소의 RIP, 동료의 랩톱, GUI가 전혀 없는 서버에서 모두 동일하게 렌더링됩니다. 포함되지 않은 Helvetica에 의존하는 문서는 그러한 기기들마다 일치하는 서체가 있다고 도박하는 것과 같으며, 그 도박이 실패하면 독자의 기기는 비슷한 서체로 대체하고, 메트릭스가 변경되며, 세심하게 레이아웃된 양식이 리플로우(reflow)되어 깨질 만큼 충분히 틀어집니다. 페이지 텍스트를 살펴보고 포함 상태에 따라 글꼴을 버킷으로 묶으면 이 답을 비용 효율적으로 얻을 수 있습니다

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는 원시 글꼴 프로그램, 즉 PDF가 전달하는 것과 동일한 TrueType 또는 CFF 데이터를 반환하며, 이는 독립형 글꼴 파일을 쓰거나 알려진 라이브러리를 상대로 서체를 핑거프린팅하기에 충분합니다. 글꼴이 포함되지 않은 경우에는 빈 배열을 반환하므로, 포함 확인과 길이 확인이 함께 쓰기 작업을 보호합니다

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;

이 바이트들은 원본 리테일 글꼴이 아니라 포함된 하위 집합이므로, 반환되는 내용은 보통 문서가 실제로 사용한 글리프만 포괄합니다. 이는 포렌식 및 검증에는 정확히 맞지만 재사용에는 적합하지 않습니다. 30개의 글리프가 포함된 Times New Roman 하위 집합은 설치하고 타이핑할 수 있는 글꼴이 아닙니다. 추출은 제공된 것을 검사하는 방법으로 취급해야 하며 글꼴 복구 도구로 다루어서는 안 됩니다. 출력에 레이블을 지정하기 위해 일치하는 기본 이름이 필요한 경우, 데이터와 함께 FontBaseName[CharIndex]를 읽고 순수 글꼴군만 원한다면 선행 하위 집합 태그를 제거하세요

두께 숫자 이해하기

FontWeight는 숫자 가중치 클래스를 반환하는데, 이는 CSS가 사용하는 100에서 900 척도와 동일하며, 여기서 400은 일반(regular)이고 700은 굵게(bold)입니다. PDFium은 글꼴이 선언한 것을 그대로 보고하며, 이것이 항상 딱 떨어지는 백 단위는 아닙니다. 서체는 350 또는 650을 발표할 수 있으며, 600 이상인 것을 "문제가 될 만큼 충분히 굵다"고 취급하는 것이 정확히 700을 테스트하는 것보다 더 잘 유지됩니다. 기울임꼴 각도는 동반 신호입니다. 보통 음수인 0이 아닌 값은 서체가 오블리크(oblique) 또는 진정한 기울임꼴(italic) 디자인임을 의미하며, 0은 똑바로 서 있음을 의미합니다. 이 두 가지를 함께 사용하면 렌더링을 하지 않고도 일반 런에서 굵은 기울임꼴 런을 구별할 수 있으며, 이는 프리플라이트 통과 또는 접근성 감사가 대량으로 수행하고자 하는 종류의 검사입니다

이러한 읽기 작업 중 어느 것도 렌더링된 비트맵을 요구하지 않습니다. 이들은 파싱된 텍스트 계층에서 오기 때문에 올바른 페이지에 열려 있는 문서만으로도 필요한 모든 설정이 완료되며, 이는 전체 아카이브에 걸쳐 글꼴 검사를 저렴하게 실행할 수 있게 합니다. 이를 텍스트 추출과 연결하는 경우, 동일한 문자 인덱스가 추출하는 텍스트와 일치하므로 글리프의 글꼴과 그 유니코드 값은 하나의 인덱스에 대해 두 번 읽는 것이 됩니다. 관련 기사인 Delphi에서 PDFium Component를 사용하여 PDF 문서에서 텍스트 추출하기에서 텍스트 계층의 해당 측면을 좀 더 깊이 다룹니다

여기 표시된 글꼴 속성은 PDFium Delphi VCL Component의 일부입니다