기술 문서

PDFium로 Delphi에서 PDF 축 및 방사형 그라데이션 만들기

PDFium Component는 TPdf.SaveAsShadingCreated를 통해 Delphi에서 PDF 축 및 방사형 그라데이션을 만듭니다. 각 그라데이션을 TPdfShadingCreate 레코드(타입, 색 공간, 좌표, 색 또는 stop, 대상 페이지)로 기술하면, 이 메서드는 모든 채우기가 실제 ISO 32000-1 shading 딕셔너리이고 페이지 /Resources /Shading 딕셔너리에 등록되며 sh 연산자로 칠해지는, 검증된 사본을 써서 PDFium이 네이티브로 렌더하게 합니다

수요는 보고서와 수료증 생성기에서 나옵니다. 디자이너가 부드러운 파랑에서 흰색으로 가는 헤더 밴드나 도장 뒤의 방사형 광채가 있는 목업을 넘기는데, 텍스트와 경로, 이미지를 다루는 페이지 객체 API에는 그럴 만한 것이 없습니다. 조금씩 색이 다른 얇은 사각형 백 개로 그라데이션을 흉내 낼 수도 있지만, 결과는 확대하면 띠가 보이고 콘텐츠 스트림을 부풀리고 인쇄가 나쁩니다. PDF는 1.3부터 매끄러운 shading이 있었습니다. 일은 렌더러가 실제로 그리도록 쓰는 데 있습니다

PDFium Component는 그라데이션을 별도 저장으로 쓰는 이유

TPdf.SaveAsShadingCreated는 페이지 객체 호출이 아니라 저장 연산입니다. PDFium의 공개 ABI에는 shading 딕셔너리를 만들거나 sh 연산자를 내보내는 함수가 없기 때문입니다. PDFium Component로 백지에서 PDF 만들기에서 쓰는 페이지 객체 층은 경로, 텍스트, 이미지를 만들고 거기서 멈춥니다. Shading은 한 층 아래, 객체 그래프와 콘텐츠 스트림 구문에 삽니다

그래서 이 메서드는 단계적으로 동작합니다. PDFium은 먼저 열린 문서를 네이티브로 저장해 여러분이 정상 API로 한 모든 편집을 유지합니다. 그다음 PDFium Component는 그 스테이징된 출력을 다시 씁니다. shading마다 하나의 간접 객체를 할당하고, 각 대상 페이지에 리소스 항목을 더하고, 페이지 /Contents에 새 콘텐츠 스트림을 덧붙이며, 새 크로스 레퍼런스 테이블을 씁니다. 무엇이든 여러분의 스트림이나 파일에 도달하기 전에 결과는 엄격한 크로스 레퍼런스 검사로 다시 적재되고, 그 재적재가 실패하면 메서드는 보고서의 메시지와 함께 False를 반환합니다. 파일 오버로드는 먼저 임시 파일에 쓰므로, 실패한 저장이 기존 대상을 덮어치지 않습니다

두 색 axial 그라데이션 만들기

axial shading(ISO 32000-1 §8.7.4.5.3, Type 2)은 x0 y0에서 x1 y1까지의 선을 따라 색을 섞으므로 Coords는 네 값을 받습니다. radial shading(§8.7.4.5.4, Type 3)은 두 원 사이를 섞으므로 여섯을 받습니다. x0 y0 r0 x1 y1 r1입니다. 단순한 StartColor와 EndColor에서 PDFium Component는 /N 1인 Type 2 지수 함수(§7.10.3)를 내보내는데, 이는 곧은 선형 보간이며, 축 양 끝을 지나 끝 색이 계속되도록 /Extend [true true]를 설정합니다

Delphi용 PDFium Component의 axial 및 radial shading 지오메트리: Type 2 그라데이션은 x0 y0에서 x1 y1까지의 선을 따라 StartColor에서 EndColor로 섞이고 /Extend true true가 양쪽 끝을 지나 계속되며, Type 3 그라데이션은 반지름 r0의 시작 원과 반지름 r1의 끝 원 사이에서 섞임
PDFium은 클립이 허락하는 곳에 shading을 칠합니다. Type 2 Coords는 x0 y0에서 x1 y1까지 혼합 축을 놓고, Type 3 Coords는 두 원을 놓으며, /Extend [true true]가 양 끝 색을 지오메트리 너머까지 운반합니다
uses
  System.SysUtils, PDFium, FPdfCompress;

procedure AddHeaderBand(const InFile, OutFile: string);
var
  Pdf: TPdf;
  Fills: TPdfShadingCreateShadings;
  Report: TPdfShadingCreateReport;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := InFile;
    Pdf.Active := True;

    SetLength(Fills, 1);
    Fills[0] := Default(TPdfShadingCreate);
    Fills[0].ShadingType := pstAxial;
    Fills[0].ColorSpace := pscDeviceRGB;
    Fills[0].Coords := [0, 792, 0, 640];        // x0 y0 x1 y1, 위에서 아래로
    Fills[0].StartColor := [0.12, 0.35, 0.75];  // 위쪽 모서리의 짙은 파랑
    Fills[0].EndColor := [1, 1, 1];             // 아래로 갈수록 흰색
    Fills[0].PageNumber := 1;                   // 1부터 시작
    Fills[0].ResourceName := 'HeaderBand';      // 선택, 기본값은 Sh1, Sh2...

    if not Pdf.SaveAsShadingCreated(OutFile, Fills, Report) then
      raise Exception.Create('Shading creation failed: ' + Report.ErrorMessage);
  finally
    Pdf.Free;
  end;
end;

모든 색의 성분 수는 색 공간과 맞아야 합니다. pscDeviceGray와 pscCalGray에는 하나, pscDeviceRGB, pscCalRGB, pscLab에는 셋, pscDeviceCMYK에는 넷입니다. pscICCBased의 개수는 IccProfile로 넘기는 프로필 바이트에서 나오며, 1, 3 또는 4 성분을 노출해야 합니다. 프로필은 맞는 /N을 단 자체 ICC 스트림으로 임베드됩니다. 보정된 공간은 선택적 CalWhitePoint(세 값, 양의 Y), CalGamma(CalGray에는 하나, CalRGB에는 셋)와, Lab이라면 여섯 값의 LabRange를 받습니다. 모든 좌표와 성분은 바이트 하나 쓰이기 전에 NaN과 무한을 검사합니다

좌표는 원점이 페이지 왼쪽 아래인 기본 사용자 공간에 있음에 유의하십시오. 축은 혼합이 일어나는 위치만 정의한다는 것도 기억하십시오. 칠해지는 영역은 클립이 허락하는 것이고, 다음 절들이 보여 주듯 들리는 것보다 중요합니다

다중 stop 그라데이션은 Type 3 스티칭 함수로 컴파일된다

PDFium Component는 Stops 배열을 PDF 리더가 기대하는 함수로 바꿉니다. stop 둘은 단일 Type 2 함수가 되고, 3에서 256개 stop은 인접 쌍마다 하나의 선형 Type 2 세그먼트로 이루어진 Type 3 스티칭 함수(ISO 32000-1 §7.10.4)가 됩니다. 각 TPdfShadingCreateStop은 Position과 Color를 실립니다. Stops가 비어 있지 않으면 StartColor와 EndColor를 완전히 대체합니다

스티칭 함수는 손으로 만들면 미묘하게 틀리기 쉬운 배열 셋을 필요로 합니다. /Functions가 세그먼트를 담고, /Bounds는 내부 stop 위치만 담으며(k개 세그먼트에는 k-1개 bound), /Encode는 각 하위 도메인을 [0 1]로 되돌려 모든 세그먼트가 전체 입력 범위를 보게 합니다. 컴포넌트는 이 셋을 stop들에서 유도하므로 입력에 엄격합니다. 위치는 유한하고, 엄격히 증가하며, 0에서 시작해 1에서 끝나야 합니다(1e-6 이내). 0.1에서 시작하는 stop 목록은 도메인의 일부를 함수 없이 남겨 두므로, 메서드는 추측하는 대신 거부합니다

function Stop(Position: Double; const Rgb: array of Double): TPdfShadingCreateStop;
var
  I: Integer;
begin
  Result.Position := Position;
  SetLength(Result.Color, Length(Rgb));
  for I := 0 to High(Rgb) do
    Result.Color[I] := Rgb[I];
end;

procedure BuildSealGlow(var Fill: TPdfShadingCreate);
begin
  Fill := Default(TPdfShadingCreate);
  Fill.ShadingType := pstRadial;
  Fill.ColorSpace := pscDeviceRGB;
  // x0 y0 r0 x1 y1 r1: 도장 중심의 점이 90 pt 원으로 자람
  Fill.Coords := [450, 160, 0, 450, 160, 90];
  Fill.Stops := [Stop(0.0, [1.00, 0.95, 0.70]),
                 Stop(0.4, [0.95, 0.75, 0.30]),
                 Stop(1.0, [1.00, 1.00, 1.00])];
  Fill.PageNumber := 1;
end;

세 stop 그라데이션은 두 Type 2 세그먼트의 /Functions, /Bounds [0.4], /Encode [0 1 0 1]을 만들어 냅니다. 세그먼트 경계는 하드 색 이음새이므로 stop 목록이 그 경계를 넘어 매끄러워지지 않습니다. 이음새가 너무 날카로워 보이면 리더가 곡률을 보간해 주길 기대하는 대신 stop을 더하십시오

Delphi에서 PDFium Component의 다중 stop 그라데이션 컴파일: 위치 0.0, 0.4, 1.0의 세 Stops는 인접 stop 쌍마다 하나의 Type 2 세그먼트를 가진 Type 3 스티칭 함수가 되고, 내부 위치에는 /Bounds [0.4], 각 하위 도메인을 전체 범위로 되돌리는 /Encode [0 1 0 1]이 붙음
인접 stop 쌍마다 Type 3 함수 안의 Type 2 세그먼트가 하나씩 됩니다. 유도된 /Bounds와 /Encode 배열이 바로 손으로 쓴 스티칭 함수가 보통 틀리는 부분입니다

PDFium은 왜 조용히 아무것도 그리지 않을까

PDFium은 두 디테일이 맞을 때만 새로 만든 shading을 렌더합니다. /ShadingType이 정수여야 하고, sh 호출이 클리핑 경로 안에 있어야 합니다. 어느 하나라도 틀리면 파일은 열리고, 구조적으로 검증되고, 만지지 않은 페이지를 보여 줍니다. 오류도 경고도 로그 줄도 없습니다

함정 하나: /ShadingType은 이름이 아니라 숫자다

ISO 32000-1 표 78은 /ShadingType을 정수로 정의합니다. axial은 2, radial은 3입니다. 이 라이터의 첫 버전은 헥스 덤프에서 더 잘 읽힌다는 이유로 /ShadingType /Axial을 내보냈습니다. PDFium은 이 키를 정수 게터로 읽어 이름에 0을 받고, shading을 잘못된 것으로 분류해 적재에 실패하며, 불평 없이 sh 연산자를 건너뜁니다. 페이지는 백색으로 렌더됐고 PDFium은 왜인지 말해 주지 않았습니다. 원인은 최소한의 손으로 쓴 PDF를 만들어 별개의 독립 렌더러에서 열어 본 뒤에야 보였고, 그 렌더러는 알 수 없는 shading 타입 0을 보고했습니다. 교훈은 모든 PDF 라이터에 일반화됩니다. 사양이 열거형을 정수로 타입하면 자기 서명적 이름이 아무리 달콤해 보여도 정수를 쓰십시오

함정 둘: sh만 담은 콘텐츠 스트림에는 경계 상자가 없다

sh 연산자(ISO 32000-1 §8.7.4.2)는 현재 클리핑 영역 위에 shading을 칠하므로, 종이 위에서는 페이지 자체의 범위가 경계가 됩니다. 그러나 PDFium은 shading 페이지 객체를 만들 때 그 사각형을 활성 클립 경로에서, 아니면 같은 스트림에서 이전 드로잉 연산들이 쌓은 경계 상자에서 가져오고, 렌더러는 빈 사각형의 객체를 잘라냅니다. /Sh1 sh만 담은 갓 붙은 스트림에는 둘 다 없으므로, 그라데이션은 래스터화 전에 버려집니다. 따라서 TPdf.SaveAsShadingCreated는 각 페이지의 crop 박스(미디어 박스로, 그다음 페이지 크기로 폴백)를 읽고 모든 호출을 명시적 re W n 클립으로 감쌉니다. 원점이 0인 US Letter 페이지에서 덧붙은 스트림은 다음과 같습니다:

q
0 0 612 792 re W n
/HeaderBand sh
Q

q/Q 쌍이 클립과 변환을 기존 페이지 콘텐츠에서 격리하므로, 나중에 페이지에 그려지는 것은 그것들을 물려받지 않습니다. 다른 도구에서 직접 shading 호출을 쓴다면 이 모양을 베끼십시오. 같은 스트림의 명시적 클립은 싸고, 앞서 온 것에 대한 의존을 제거합니다

그럴듯해 보이는 PDF shading이 PDFium에서 아무것도 그리지 않는 이유: 이름 Axial로 쓰인 /ShadingType은 정수 게터가 0으로 읽어 sh 연산자가 조용히 건너뛰어지고, sh만 담은 콘텐츠 스트림은 명시적 re W n 클립이 사각형을 주기 전까지는 경계 상자가 없음
두 보이지 않는 shading 실패는 PDFium에서 조용합니다. 그래서 SaveAsShadingCreated는 정수 ShadingType을 직접 쓰고 모든 sh 호출을 crop 박스 re W n 클립으로 감쌉니다

커스텀 그라데이션 행렬은 클립과 어떻게 상호작용하나

PDFium Component의 그라데이션 행렬은 클립 뒤, sh 앞에 놓이는 cm 연산자로 적용되므로, 클립은 풀 페이지로 남고 그라데이션 지오메트리만 변환됩니다. HasMatrix를 True로 하고 MatrixA에서 MatrixF까지, 임의 PDF 변환 행렬과 같은 여섯 숫자를 채우십시오. 값은 유한해야 하고 행렬식은 0이 아니어야 합니다. 특이 행렬은 shading을 선 하나로 접어 버립니다. 컴포넌트는 여섯 값을 shading 딕셔너리에 /Matrix로도 기록하지만, 직접 sh 호출에서는 PDFium이 칠하는 좌표계를 정하는 것이 콘텐츠 스트림의 cm입니다

여기서 순서가 중요하고, 이것이 세 번째 함정입니다. re 피연산자는 현재 변환 행렬로 해석됩니다. cm을 먼저 놓으면 페이지 크기 클립 사각형이 그라데이션과 함께 스케일되고 회전되고 비틀려서, 타원을 누르는 행렬이 보이는 영역도 누르고 페이지의 일부가 채우기를 잃습니다. 변환되지 않은 사용자 공간에서 먼저 클리핑한 뒤 행렬을 연결하면 기대한 결과, 즉 페이지 전체를 덮는 변환된 그라데이션이 나옵니다. Form XObject 스탬프와 그 여섯 숫자 행렬 가이드는 하나를 만들어야 할 때 각 행렬 성분이 무엇을 하는지 짚어 줍니다

procedure MakeEllipticalGlow(var Fill: TPdfShadingCreate);
begin
  BuildSealGlow(Fill);       // 450,160 중심의 radial 채우기
  Fill.Coords := [0, 0, 0, 0, 0, 90];  // 원점에 원들
  Fill.HasMatrix := True;
  Fill.MatrixA := 1.6;       // 수평으로 늘리기
  Fill.MatrixB := 0;
  Fill.MatrixC := 0;
  Fill.MatrixD := 0.8;       // 수직으로 압축
  Fill.MatrixE := 450;       // 그다음 도장 중심으로 이동
  Fill.MatrixF := 160;
end;

보고서 읽기와 한계 알기

TPdfShadingCreateReport는 무엇이 쓰였는지 말해 줍니다. SourceByteCount와 OutputByteCount, CreatedShadingObjectNumbers(입력 항목마다 하나의 객체 번호, 순서대로), AttachedShadingCount, Succeeded, ErrorMessage입니다. 검증 오류는 항목과 필드를 이름 지으므로, 예를 들어 엄격히 증가하지 않는 stop 목록 같은 것은 그대로 기록할 가치가 있습니다

if Pdf.SaveAsShadingCreated(OutFile, Fills, Report) then
  Writeln(Format('%d shading(s) attached, %d -> %d bytes',
    [Report.AttachedShadingCount, Report.SourceByteCount,
     Report.OutputByteCount]))
else
  Writeln('Rejected: ' + Report.ErrorMessage);

프로필은 의도적으로 한정됩니다. axial과 radial shading만 만듭니다. 함수 기반과 메시 shading(Type 1과 4에서 7)과 패턴 색 공간은 거부됩니다. 한 호출은 1에서 4096개 fill을 받고, 페이지 번호는 존재해야 하며, 리소스 이름은 문자, 숫자, 밑줄, 마이너스, 마침표의 127자로 제한됩니다. 암호화된 입력은 트레일러 단계에서 거부됩니다. 서명된 문서에는 AllowSignedDocument := True가 필요하고, 출력은 증분 업데이트가 아니라 전체 재작성이므로 기존 서명은 더 이상 검증되지 않으니, 그라데이션을 더한 뒤에 서명하십시오. 각 fill은 페이지 콘텐츠 끝에서, 이미 있는 것 위에 칠해집니다. 배경 효과라면 먼저 그라데이션을 만들고 텍스트와 이미지를 나중에 더하거나, fill이 전환 가능해야 할 때는 Delphi에서 PDF optional content 레이어 제어하기 글이 설명하듯 optional content를 쓰십시오

FAQ

SaveAsShadingCreated는 다중 stop 그라데이션을 지원하나요?

네. 위치가 0에서 1로 엄격히 증가하는 2에서 256개 항목을 Stops에 넘기십시오. stop 둘은 Type 2 함수를 만들고 셋 이상은 유도된 /Bounds와 /Encode 배열을 가진 Type 3 스티칭 함수를 만듭니다

파일이 유효해 보이는데 PDFium에서 그라데이션이 안 보이는 이유는요?

/ShadingType이 이름이 아니라 정수 2나 3인지, 그리고 sh 연산자가 같은 콘텐츠 스트림의 re W n 클립 안에서 도는지 확인하십시오. PDFium은 잘못된 shading 타입을 조용히 건너뛰고 빈 사각형의 shading 객체를 잘라냅니다

만들어진 shading은 어느 색 공간을 쓸 수 있나요?

TPdfShadingCreate는 DeviceGray, DeviceRGB, DeviceCMYK, CalGray, CalRGB, Lab과 1, 3, 4 성분 프로필의 ICCBased를 받습니다. Pattern과 Indexed 색 공간은 생성 프로필에 없습니다

그라데이션 생성, optional content, 스테이징 라이터의 나머지는 Delphi 및 C++Builder용 PDFium Component와 함께 제공되며, Delphi, C++Builder, Lazarus 프로젝트를 위해 PDFium을 감쌉니다