기술 문서

델파이에서 PDFium 컴포넌트를 사용하여 처음부터 PDF 문서 생성하기

PDFium은 Chrome의 PDF 탭 뒤에 있는 렌더러인 뷰어 엔진으로 명성을 얻고 있지만, 가장 먼저 명확히 해야 할 것은 PDFium 컴포넌트가 이전에 존재하지 않았던 문서를 빌드할 수도 있다는 것입니다. 작성 측면은 PDFium의 페이지 객체 API를 래핑합니다. 빈 문서를 만들고, 명시적인 크기의 페이지를 추가하고, 텍스트, 벡터 경로 및 이미지를 선택한 좌표에 놓습니다. 배워야 할 페이지 설명 언어가 없으며 루프에 프린터 드라이버도 없습니다. 메서드를 호출하면 라이브러리가 PDF 객체를 어셈블링하고, SaveAs가 그 결과를 직렬화합니다

여러분이 얻지 못하는 것은 레이아웃 엔진입니다. 이것은 미리 말해둘 만큼 중요하며, 아래의 모든 예제를 결정짓기 때문입니다. PDFium 컴포넌트는 절대 좌표로 지시한 위치에만 콘텐츠를 배치합니다. 단락을 래핑하거나, 페이지 나누기에 텍스트를 흐르게 하거나, 행과 열에서 표를 계산하지 않습니다. 그것은 여러분의 몫입니다. 워드 프로세서가 하는 방식으로 산문을 리플로우하는 무언가를 기대하고 왔다면, 지금 바로 잡으세요. 이것은 문서의 조판보다는 캔버스에 그리는 것에 더 가까운 정밀하고 저수준의 배치 API입니다. 생성된 청구서, 인증서, 레이블 및 모든 요소가 속할 위치를 이미 알고 있는 보고서 페이지의 경우, 이러한 정밀도는 여러분이 정확히 원하는 것입니다

파일을 생성하는 최소한의 조건

TPdf와 저장된 PDF 사이에는 세 번의 호출이 있습니다. 문서를 만들고, 페이지를 추가하고, 그것을 씁니다. 다른 모든 것은 그 사이에 레이어링하는 콘텐츠입니다

uses
  Vcl.Graphics,   // for clBlack and TColor
  PDFium;         // TPdf lives here

procedure CreateBlankPdf(const FileName: string);
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;                 // empty in-memory document
    Pdf.AddPage(0, 595, 842);           // A4 portrait, in points
    Pdf.AddText('First page', 'Arial', 18, 50, 780);
    Pdf.SaveAs(FileName);               // serialize to disk
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

오래된 스니펫을 본 적이 있는 사람들을 혼란스럽게 하는 디테일 중 하나는 CreateDocument 이후에 Pdf.Active := True를 할당하지 않는다는 것입니다. Active 속성은 문서 핸들이 존재하는지 여부를 보고하며, CreateDocument는 이미 생성했으므로 해당 호출이 반환되는 순간 속성은 True가 됩니다. 그것을 다시 설정하는 것은 기껏해야 아무 작업도 하지 않는 것(no-op)이며, 최악의 경우 다음 독자를 오도할 수 있습니다. Active는 종료 시 제 역할을 합니다. Free 이전에 기본 문서를 해제하기 위해 False를 할당하는 것이 깔끔한 분해(teardown) 순서입니다. CreateDocument와 파일을 로드하는 열기(open)를 상호 배타적으로 취급하세요. 라이브러리는 이미 문서를 열어둔 TPdf에서 새 문서를 생성하는 것을 거부하므로, 재사용을 의미하려면 현재 문서를 먼저 닫아야 합니다

좌표는 왼쪽 하단에서 시작합니다

AddText 및 모든 배치 호출의 두 번째 인수 쌍은 PDF 사용자 공간의 한 점입니다. 원점은 페이지의 왼쪽 하단 모서리에 있고, X는 오른쪽으로 향하며, Y는 위로 향합니다. 1단위는 1포인트, 즉 1/72인치이므로 A4 페이지는 595 x 842 단위이고 US Letter는 612 x 792입니다. 위로 향하는 Y가 "내 텍스트가 페이지를 벗어났어요"라는 혼란의 가장 일반적인 원인인데, 화면과 비트맵 좌표는 원점을 상단에 두고 Y가 아래로 증가하기 때문입니다. 높이가 842포인트인 페이지에서 상단 근처의 제목은 Y 60이 아니라 대략 Y 780 부근에 놓입니다. 실행(run)이 예상치 못한 어딘가에 도달했을 때, 페이지 높이에서 여러분의 Y를 뺀 값이 거의 항상 여러분이 실제로 의도했던 수치입니다

AddPage는 삽입 위치를 첫 번째 인수로 받으며, 1 기반(one-based)으로 표현되고 0은 편리한 "문서의 시작" 단축어입니다. 첫 번째 페이지의 경우 0 또는 1을 전달하면 페이지가 맨 앞에 삽입됩니다. 맨 끝에 추가하려면 추가하려는 횟수와 일치하는 값을 전달하세요. 새로 추가된 페이지는 후속 그리기 호출이 대상으로 하는 현재 페이지가 되므로, 추가한 후 별도의 "이 페이지 선택" 단계가 필요하지 않습니다. 여러 페이지를 추가하고 나중에 이전 페이지에 다시 그려야 하는 경우, PageNumber를 설정하여 커서를 이동하세요. 생성하는 순서대로 페이지를 채우는 동안에는 그대로 두셔도 됩니다

텍스트 쓰기, 그리고 소리 없이 물어뜯는 폰트 규칙

AddText 시그니처에는 단일 실행에 필요한 모든 것이 들어 있습니다: 문자열, 폰트 이름, 포인트 단위의 크기, X 및 Y 앵커, 그리고 선택적인 색상, 투명도를 위한 알파 바이트, 각도 단위의 회전 각도

procedure WriteHeader(Pdf: TPdf; const Title, Author: string);
begin
  // Title in black, default opacity, no rotation
  Pdf.AddText(Title, 'Arial', 20, 50, 780);
  // A lighter byline 24 points below it
  Pdf.AddText('By ' + Author, 'Arial', 11, 50, 756, clGray);
  // A faint diagonal draft stamp across the page
  Pdf.AddText('DRAFT', 'Arial', 64, 180, 380, clGray, $30, 45.0);
end;

알파 바이트는 $00(보이지 않음)부터 $FF(불투명)까지 실행되며, 이는 초안 스탬프를 단색 블록이 아닌 워터마크로 만드는 요소입니다. $30은 대략 19%의 불투명도로 통과해서 읽기에 충분합니다. 각도는 앵커를 중심으로 실행을 시계 반대 방향으로 회전하므로 45도는 전형적인 코너 투 코너 스탬프를 제공합니다. 이 중 어느 것도 별도의 워터마크 기능이 필요하지 않습니다. 워터마크는 단지 크고 반투명하며 회전된 AddText 호출일 뿐이며, 본문 앞이나 뒤에 그리는지에 따라 콘텐츠의 뒤에 놓일지 위에 놓일지가 결정됩니다

폰트는 실패 모드가 조용하기 때문에 주의 깊게 살펴볼 가치가 있습니다. 폰트 이름을 전달하면 PDFium 컴포넌트는 운영 체제에 해당 폰트의 TrueType 데이터를 요청하여 문서에 임베딩합니다. 이것이 바로 기기에서 빌드한 파일이 폰트를 설치한 적 없는 곳에서도 동일하게 렌더링되는 이유입니다. 함정은 오타가 났거나 빌드 기기에 단순히 서체가 존재하지 않아서 이름이 확인(resolve)되지 않을 때 발생합니다. 예외(exception)는 없습니다. 라이브러리는 아무것도 임베딩하지 않은 채 라벨로만 이름을 가지고 텍스트 객체를 생성하는 것으로 폴백(fallback)하고, 뷰어가 가깝다고 판단하는 것으로 대체하도록 둡니다. 텍스트는 테스트에서는 나타나고, 그럴듯해 보이지만 다른 폰트가 설치된 곳에서 파일이 열리는 순간 메트릭이나 글리프를 변환시킵니다. 생성하는 컴퓨터에 존재한다고 확신하는 이름을 사용하고, 폰트 목록을 배포 종속성으로 취급하고, 출력을 신뢰하기 전에 정리된 시스템의 뷰어에서 샘플을 여세요

벡터 도형: 경로를 만들고, 그런 다음 커밋하기

선, 직사각형 및 채워진 영역은 경로(path)를 통과합니다. CreatePath로 경로를 열면, 시작점과 모든 스타일(채우기 모드, 고유한 알파 바이트를 가진 채우기 및 선 색상, 선 두께, 선 끝 및 결합)을 한 번에 설정합니다. 그런 다음 LineTo, BezierToClosePath로 이를 확장하고, 마지막으로 AddPath가 완성된 경로를 페이지에 커밋합니다. 커밋 단계를 잊기 쉬우며 건너뛰면 아무것도 생성되지 않습니다

procedure DrawDivider(Pdf: TPdf; X, Y, Width: Single);
begin
  // A thin horizontal rule. The rectangle overload sets a box directly:
  // X, Y, Width, Height, then fill mode and colors.
  Pdf.CreatePath(X, Y, Width, 0.5, fmNone, clBlack, $FF,
    True, clBlack, $FF, 1.0);
  Pdf.AddPath;
end;

procedure DrawTriangle(Pdf: TPdf);
begin
  // Point overload: start at the first vertex, line to the rest, close.
  Pdf.CreatePath(200, 300, fmWinding, clBlue, $80, True, clNavy, $FF, 2.0);
  Pdf.LineTo(300, 300);
  Pdf.LineTo(250, 400);
  Pdf.ClosePath;
  Pdf.AddPath;          // nothing is drawn until this runs
end;

두 개의 오버로드가 일반적인 사례를 다룹니다. 4좌표 형식은 X, Y, 너비 및 높이를 사용하고 한 번의 호출로 축에 정렬된 직사각형을 제공하며, 이는 규칙, 셀 테두리 또는 채워진 배경 패널을 그릴 때 손이 가는 것입니다. 2좌표 형식은 시작점만 설정하며 나머지 윤곽선은 LineToBezierTo를 사용하여 직접 추적합니다. 채우기 모드는 겹치는 영역이 칠해지는 방식을 제어합니다. fmWinding(nonzero winding)은 대부분의 단색 도형에 적합하고, fmAlternate(even-odd)는 컷아웃 및 자기 교차 윤곽선을 처리하며, fmNone은 채우기 없이 획으로 쳐진 경로만 남기며, 이는 위에서 분할기(divider)가 사용하는 방식입니다

표는 수작업으로 조립된 경로와 텍스트입니다

표 프리미티브(table primitive)가 없기 때문에 표는 루프(loop)입니다. 열의 X 오프셋과 행의 높이를 결정하고, AddText로 각 셀을 쓰고, 사각형 경로로 규칙(rules)을 그립니다. 산술은 여러분의 몫이지만 간단하며, 일단 작성하면 필요한 어떤 그리드에도 일반화할 수 있습니다

procedure DrawTable(Pdf: TPdf; Left, Top: Double);
const
  ColX: array[0..2] of Double = (0, 110, 210);  // column offsets
  RowH = 20;
var
  Y: Double;
  Row: Integer;
begin
  // Header row
  Pdf.AddText('Item', 'Arial', 10, Left + ColX[0], Top);
  Pdf.AddText('Qty', 'Arial', 10, Left + ColX[1], Top);
  Pdf.AddText('Price', 'Arial', 10, Left + ColX[2], Top);

  // Rule under the header
  Pdf.CreatePath(Left, Top - 5, 260, 0.5, fmNone, clBlack, $FF);
  Pdf.AddPath;

  // Data rows, stepping Y downward each iteration
  Y := Top;
  for Row := 1 to 3 do
  begin
    Y := Y - RowH;
    Pdf.AddText('Item ' + IntToStr(Row), 'Arial', 9, Left + ColX[0], Y);
    Pdf.AddText(IntToStr(Row * 2), 'Arial', 9, Left + ColX[1], Y);
    Pdf.AddText('$' + IntToStr(Row * 10) + '.00', 'Arial', 9, Left + ColX[2], Y);
  end;
end;

위쪽이 양수이기 때문에 패스할 때마다 Y가 행의 높이만큼 아래로 이동하는 점에 유의하세요. 텍스트 측정 기능의 부재가 나타나는 곳이기도 한데, 라이브러리는 문자열 렌더링이 얼마나 넓은지 알 수 없기 때문에 항목 이름이 길어서 다음 열로 오버런되는 것을 막을 방법이 없습니다. 여러분이 데이터를 제어하는 고정 형식 출력의 경우 열 크기를 넉넉하게 지정하고 넘어갑니다. 진정한 변수 콘텐츠의 경우, 배치를 하기 전에 직접 입력을 제한하거나 글리프 너비를 측정하는데, 이는 전용 구성 라이브러리가 비용에 맞는 가치를 창출하기 시작하는 시점입니다

이미지 및 다중 페이지

래스터 콘텐츠는 이미지 도우미를 통해 들어옵니다. AddPicture는 로드된 TPicture를 가져와 지점에 배치하고, 크기를 조절하기 위한 너비와 높이를 선택적으로 사용합니다. AddImage는 파일 경로 또는 TBitmap을 직접 수락하고, AddJpegImage는 비트맵을 왕복하지 않고 JPEG 바이트를 스트리밍합니다. 다른 모든 것과 마찬가지로 배치 좌표는 사용자 공간에 있는 이미지의 왼쪽 하단 모서리이며, 너비와 높이는 원본의 픽셀 치수가 아니라 페이지 위 포인트 단위의 크기입니다

procedure CreateMultiPageReport(const FileName: string; PageCount: Integer);
var
  Pdf: TPdf;
  P: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;
    for P := 1 to PageCount do
    begin
      Pdf.AddPage(P, 595, 842);     // append; the new page becomes current
      Pdf.AddText('Page ' + IntToStr(P) + ' of ' + IntToStr(PageCount),
        'Arial', 10, 50, 30);       // footer near the bottom edge
      // ... draw this page's body here ...
    end;
    Pdf.SaveAs(FileName);
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

다중 페이지 문서는 단일 페이지 패턴이 루프 안에 있는 것입니다. 각 AddPage는 페이지를 추가하고 현재 페이지로 만들므로 다음에 그리는 본문과 푸터가 방금 추가한 페이지에 떨어집니다. 페이지를 추가하면 이미 해당 페이지로 커서가 이동했기 때문에 이 루프 내에서 PageNumber를 재할당하지 않습니다. 생성 순서를 벗어나 페이지로 돌아갈 때만 PageNumber가 필요합니다. 마지막 페이지가 채워진 후 마지막에 SaveAs를 한 번 호출합니다. 일반 파일이 아닌 보관(archival) 프로필이 필요한 경우, 동일한 문서 객체가 SaveAsPdfA 및 기타 적합성 변형을 노출하므로 출력 표준의 선택은 다른 빌드 경로가 아니라 다른 저장 호출입니다

이것이 적합한 곳

솔직히 말하면 PDFium 컴포넌트의 작성 API는 PDFium의 페이지-객체 모델에 대한 충실하고 얇은 레이어입니다. 실제 문서 생성, 실제 임베디드 폰트, 실제 벡터 및 래스터 콘텐츠가 표준을 준수하는 파일로 직렬화됩니다. 그것은 리플로우 문서 엔진이 아니며 그런 척하지도 않습니다. 구분선은 텍스트 레이아웃입니다. 출력이 템플릿화되어 고정 그리드에 렌더링된 인보이스, 인증서, 레이블, 대시보드인 경우 절대 좌표 모델은 직접적이고 빠르며 코드를 읽을 수 있습니다. 출력이 자체적으로 래핑되고 페이지가 매겨져야 하는 긴 형식의 산문인 경우 이러한 호출 위에 레이아웃 엔진을 다시 빌드해야 하므로 작업에 적합하지 않은 도구입니다. 자신이 그 선의 어느 쪽에 있는지 아는 것이 결정의 대부분을 차지합니다

여기에 설명된 생성 메서드는 델파이용 PDFium 컴포넌트의 일부이며, 이 작성 경로를 PDFium이 더 잘 알려진 렌더링 및 텍스트 추출 기능과 쌍으로 연결합니다