PDF 페이지에 TrimBox가 없으면 유효 TrimBox는 페이지의 CropBox이고, CropBox마저 없으면 MediaBox입니다. BleedBox와 ArtBox도 같은 규칙을 따릅니다. PDF Library for Delphi인 PDFlibPas는 v3.539.44부터 이 기본 체인을 GetPageBox, HasPageBox, CapturePageEx에서 일관되게 적용하고, /Pages 노드에 얹힌 production 박스는 무시합니다. ISO 32000-1이 그것들의 상속을 허용하지 않기 때문입니다
인쇄 조판 작업을 돌리기 전까지는 각주처럼 읽힙니다. MediaBox가 6.25 × 9.25 in, CropBox가 6 × 9 in 트림으로 잡혀 있고, 내보낸 사람이 쓸 생각을 못 했으니 TrimBox는 없는 책 본문을 상상해 보세요. 트림 박스를 요청했더니 미디어 박스가 돌아오고, 인쇄판 위의 모든 셀이 이웃으로 1/8인치의 블리드와 슬러그를 끌고 들어갑니다. PDFlibPas는 정확히 이 영역에 결함이 있었고 v3.539.42와 v3.539.44에서 고쳐졌으며, 고쳐진 방식은 페이지 박스 시맨틱스를 어떤 PDF 라이브러리에서든 어떻게 구현해야 하는지 말해 줍니다
페이지에 TrimBox가 없을 때 어떤 박스가 적용될까?
답은 ISO 32000-1 §14.11.2의 고정된 기본 체인입니다. CropBox는 MediaBox를 기본으로 하고, BleedBox, TrimBox, ArtBox는 각각 CropBox를 기본으로 합니다. MediaBox로 바로 기본값을 삼는 것은 CropBox뿐입니다. 따라서 MediaBox만 정의한 페이지는 다섯 개의 동일한 박스를 갖고, MediaBox와 CropBox를 정의한 페이지는 CropBox와 같은 박스 네 개를 갖습니다
| 박스 | PDFlibPas BoxType | 없을 때 기본값 | /Pages에서 상속 가능 |
|---|---|---|---|
| MediaBox | 1 | 없음, 엔트리가 필수 | 예 |
| CropBox | 2 | MediaBox | 예 |
| BleedBox | 3 | CropBox | 아니요 |
| TrimBox | 4 | CropBox | 아니요 |
| ArtBox | 5 | CropBox | 아니요 |
두 단계 체인이 중요한 이유는 CropBox 자체가 상속될 수 있기 때문입니다. TrimBox도 CropBox도 자기 것을 두지 않은 페이지의 유효 TrimBox는 그것을 가진 가장 가까운 조상의 CropBox이고, 그마저 없으면 상속된 MediaBox입니다. 규격은 잊기 쉬운 규칙을 하나 더 얹습니다. crop, bleed, trim, art 박스는 미디어 박스를 넘어 확장되지 말아야 하고, 넘어가면 그와의 교집합으로 사실상 축소됩니다. PDFlibPas는 각 박스를 파일에 저장된 대로 보고하므로, 신뢰할 수 없는 입력을 다루는 밸리데이터는 MediaBox에 대해 직접 클램프해야 합니다
/Pages 노드는 어떤 페이지 속성을 물려줄 수 있을까?
정확히 넷입니다. Resources, MediaBox, CropBox, Rotate. ISO 32000-1 §7.7.3.4가 속성 상속을 정의하고 표 30은 그 네 페이지 오브젝트 엔트리만 상속 가능으로 표시합니다. BleedBox, TrimBox, ArtBox는 리프 페이지의 것입니다. /Pages 노드에 쓴 TrimBox는 상속되는 값이 아니라, 규격을 따르는 읽는 쪽이 무시하는 비표준 키입니다
그런 비표준 파일은 실제로 존재합니다. 보통 "모든 페이지가 이 트림을 가진다"의 약칭으로 루트 페이지 트리 노드에 TrimBox 하나만 얹혀 있죠. 그 약칭은 모든 키를 /Parent로 걷는 어떤 도구에서도 옳아 보이고, 그게 문제입니다. 파일이 이제 읽는 사람에 따라 두 가지 뜻을 갖습니다. 규격을 따르는 읽는 쪽은 TrimBox가 없다고 보고 CropBox를 쓰고, 모든 것을 상속하는 읽는 쪽은 부모 값을 봅니다. 인쇄 전 공정 파이프라인에서 그 애매함은 인쇄판 위에 착지합니다
PDF/X(ISO 15930) 워크플로는 완성 크기를 TrimBox에 의지하고, PDF/X 프로파일은 각 페이지가 TrimBox나 ArtBox를 선언하기를 요구합니다. /Pages 노드에 세워 둔 박스는 그 요구를 충족하지 못합니다. 키가 페이지 오브젝트에 닿지 않기 때문이죠. 프리플라이트는 그런 파일을 한쪽으로 조용히 읽는 대신 표시해야 합니다
PDFlibPas는 v3.539.44 전에 무엇을 잘못했을까?
PDFlibPas에는 별개의 결함 셋이 있었고, 모두 규격이 말하는 것과 독립된 두 코드 경로가 한 것 사이의 틈에 있었습니다. 첫째는 v3.539.42에서, 나머지 둘은 v3.539.44에서 고쳐졌습니다
캡처 중 production 박스가 MediaBox로 기본 설정됨
v3.539.42 전에는 페이지를 캡처 준비시키는 내부 루틴(상속된 엔트리를 페이지에 복사하고 빠진 박스를 채움)이 BleedBox, TrimBox, ArtBox에 빠져 있을 때 MediaBox 값을 주었습니다. 옵션 2부터 4인 CapturePageEx는 경계 사각형을 정확히 그 채워 넣은 엔트리들에서 읽으므로, CropBox만 정의한 페이지에서 트림 박스를 요청하면 미디어 박스 전체가 캡처됐습니다. GetPageBox는 이미 CropBox 기본값을 적용하고 있었고, CapturePageEx 레퍼런스는 처음부터 요청한 박스가 없으면 crop box가 쓰인다고 말하고 있었죠. 캡처 코드만 둘 다와 어긋났습니다. v3.539.42부터 세 production 박스는 페이지의 CropBox로 기본 설정됩니다. 그 시점에 그것은 이미 페이지 위에 있습니다(자기 것, 조상에서 복사된 것, 또는 MediaBox에서 채워진 것). MediaBox로 폴백하는 것은 CropBox 자신뿐입니다
두 상속 경로, 하나의 시맨틱 규칙
둘째 결함은 비표준 상속 자체였고, 미묘한 부분은 PDFlibPas가 박스를 두 독립 경로로 해석했다는 것입니다. 박스 조회(GetPageBox와 HasPageBox)는 한 헬퍼로 /Parent 체인을 걷고, 캡처는 별도의 로컬 헬퍼로 걸었습니다. 둘 다 production 박스를 포함해 모든 키를 상속했죠. 하나만 고쳤다면 단일 문서 안의 모순이 생겼을 겁니다. /Pages 노드에 폭 180포인트 TrimBox, 페이지에 폭 380포인트 CropBox가 있는 상태에서 GetPageBox는 여전히 트림 폭 180을 보고하는 동안 CapturePageEx는 380짜리 폼을 만들었을 테니까요. v3.539.44에서 두 경로 모두 /Parent 걷기를 네 상속 가능 키로 제한하고, production 박스는 리프에서만 읽으며, 엉뚱한 부모 엔트리는 파일에 그대로 남습니다. 삭제도 재작성도 하지 않죠
HasPageBox는 직접 부모 배열을 놓쳤다
HasPageBox는 페이지에 요청한 타입의 박스가 없으면 0, 페이지가 자기 박스를 갖고 있으면(직접 저장이든 indirect reference든) 1, MediaBox나 CropBox가 조상에서 상속됐으면 2를 반환합니다. 옛 코드는 상속된 값이 indirect reference일 때만 2를 돌려줬으므로, 상속된 직접 배열은 0을 돌려줬습니다. 수정은 역참조를 배열 검사에서 분리했고, 이제 두 표현 모두 2를 돌려줍니다. v3.539.44부터 BleedBox, TrimBox, ArtBox의 HasPageBox는 0이나 1만 반환할 수 있습니다
교훈은 페이지 박스를 훨씬 넘어 일반화됩니다. 한 조각의 규격 시맨틱스가 라이브러리 안에 두 구현 진입점을 갖는다면, 함께 고치고 행복 경로 파일 하나가 아니라 행렬로 테스트하세요. PDFlibPas 회귀 집합은 두 부모 박스 표현(직접 배열과 indirect 배열)에 세 리프 상태(없음, 직접 배열, indirect 배열)와 세 캡처 옵션(bleed, trim, art)을 교차해 18개 시나리오를 만들고, 각각이 조회 결과, 캡처된 경계, 합법적인 MediaBox와 CropBox 상속, 손대지 않은 부모 엔트리를 검사합니다
Delphi에서 유효 TrimBox는 어떻게 읽나?
선택된 페이지에서 GetPageBox(4, Dimension)를 호출하세요. PDFlibPas가 기본 체인을 대신 적용하므로, 페이지에 TrimBox가 있든 없든 결과는 유효 TrimBox입니다. 값이 어디에서 왔는지 알아야 할 때, 프리플라이트 리포트가 보통 그러하듯, HasPageBox와 짝지으세요
uses
System.SysUtils, PDFlibrary;
const
BOX_CROP = 2;
BOX_TRIM = 4;
DIM_LEFT = 0;
DIM_WIDTH = 2;
DIM_HEIGHT = 3;
DIM_BOTTOM = 5;
function DescribeTrim(Lib: TPDFlib; Page: Integer): string;
var
Source: string;
begin
Lib.SelectPage(Page);
if Lib.HasPageBox(BOX_TRIM) = 1 then
Source := 'own TrimBox'
else if Lib.HasPageBox(BOX_CROP) <> 0 then // 1 = 자기 것, 2 = 상속됨
Source := 'defaulted to the CropBox'
else
Source := 'defaulted to the MediaBox';
Result := Format('page %d: trim %.2f x %.2f pt at (%.2f, %.2f), %s',
[Page,
Lib.GetPageBox(BOX_TRIM, DIM_WIDTH),
Lib.GetPageBox(BOX_TRIM, DIM_HEIGHT),
Lib.GetPageBox(BOX_TRIM, DIM_LEFT),
Lib.GetPageBox(BOX_TRIM, DIM_BOTTOM),
Source]);
end;
var
Lib: TPDFlib;
Page: Integer;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('interior.pdf', '') = 1 then
for Page := 1 to Lib.PageCount do
Writeln(DescribeTrim(Lib, Page));
finally
Lib.Free;
end;
end.
GetPageBox와 SetPageBox는 문서의 현재 좌표 설정으로 동작합니다. 여기 예제들은 기본값으로 돕니다. 원점 0(왼쪽 아래, PDF 사용자 공간과 일치)과 측정 단위 points라서 Top 치수는 페이지 아래에서 위로 잰 윗 모서리입니다. SetOrigin(1) 이후에는 Top과 Bottom 치수가 대신 페이지 위에서 아래로 재지고, SetMeasurementUnits(1) 이후에는 모든 값이 밀리미터로 돌아옵니다. 폭과 높이는 원점에 좌우되지 않습니다
/Pages 노드에 표류한 production 박스 찾기
v3.539.44부터 박스 API는 /Pages 노드 위의 TrimBox를 더 이상 보지 않는데, 이게 맞습니다. 그래도 프리플라이트 도구는 보통 그런 파일을 규격대로 조용히 읽는 대신 보고하고 싶어 하죠. 페이지 트리 노드는 평범한 오브젝트이므로 저수준 오브젝트 API가 찾을 수 있습니다. GetMaxObjectNumber까지 오브젝트 번호를 걷고, 각각을 GetObjectToString으로 읽고, production 박스 키를 든 /Pages 딕셔너리를 찾으면 됩니다. 검사의 후반부는 PDF/X가 신경 쓰는 페이지별 테스트인데, 부모 TrimBox가 더 이상 세지 않으므로 HasPageBox가 이제 PDF/X 밸리데이터처럼 답합니다
procedure PreflightTrim(Lib: TPDFlib; Log: TStrings);
const
ProductionKeys: array[0..2] of string = ('/BleedBox', '/TrimBox', '/ArtBox');
var
ObjNum, K, Page, Missing: Integer;
Src: string;
begin
// 1. 페이지 트리 노드의 production 박스: 비표준이며 무시됨
for ObjNum := 1 to Lib.GetMaxObjectNumber do
begin
Src := ''; // 빈 번호는 텍스트를 반환하지 않음
Src := string(Lib.GetObjectToString(ObjNum));
if Pos('/Type /Pages', Src) = 0 then
Continue;
for K := Low(ProductionKeys) to High(ProductionKeys) do
if Pos(ProductionKeys[K] + ' ', Src) > 0 then
Log.Add(Format('object %d: %s on a /Pages node is not inheritable',
[ObjNum, ProductionKeys[K]]));
end;
// 2. PDF/X: 모든 페이지가 자기 TrimBox나 ArtBox가 필요
Missing := 0;
for Page := 1 to Lib.PageCount do
begin
Lib.SelectPage(Page);
if (Lib.HasPageBox(4) = 0) and (Lib.HasPageBox(5) = 0) then
begin
Inc(Missing);
Log.Add(Format('page %d: no TrimBox or ArtBox', [Page]));
end;
end;
// 3. 선택적 수리: 6.25 x 9.25 in 미디어 박스 안의 6 x 9 in 트림
// (포인트, 왼쪽 아래 원점: Left, Top, Width, Height)
if Missing > 0 then
Log.Add(Format('TrimBox written on %d pages',
[Lib.SetPageBoxRange('', 4, 9, 657, 432, 648)]));
end;
텍스트 매치는 실용적인 검사이지 파서가 아닙니다. PDFlibPas가 각 딕셔너리 엔트리를 키, 공백 하나, 값으로 직렬화하는 데 기대는데, GetObjectToString으로 읽어 돌린 오브젝트에는 그것이 성립합니다. 수리 단계는 반사 동작이 아니라 판단을 요합니다. 표류한 부모 값이야말로 저자의 의도일 수 있으니, 공식화하기 전에 작업 지시서와 대조하세요. 빈 범위의 SetPageBoxRange는 모든 페이지에 박스를 적용하고 갱신된 페이지 수를 반환합니다. 페이지의 기존 박스가 indirect 배열이어서 다른 페이지나 /Pages 노드가 공유할 수 있는 경우, SetPageBox는 공유 오브젝트를 재작성하는 대신 그 페이지에 새 직접 배열을 줍니다. BleedBox, TrimBox, ArtBox 설정은 잠기지 않은 문서를 그 엔트리들이 처음 나온 PDF 1.3으로 끌어올립니다
CapturePageEx로 TrimBox 위에 페이지 조판하기
CapturePageEx(Page, 3)는 페이지를 경계 상자가 유효 TrimBox인 Form XObject로 바꾸고, DrawCapturedPage는 그 폼을 다른 페이지 위에 임의 크기로 얹습니다. v3.539.42부터 TrimBox 없는 페이지의 옵션 3은 레퍼런스가 기술한 대로 슬러그 가득한 MediaBox가 아니라 CropBox를 줍니다
캡처의 두 성질이 코드 모양을 정합니다. 캡처는 파괴적입니다. 캡처된 페이지는 문서에서 제거되고 문서는 절대 0페이지로 떨어질 수 없으니, 무엇이든 캡처하기 전에 첫 출력 장을 덧붙이세요. 캡처는 또한 한 문서 안에서만 동작하므로, 모든 입력을 먼저 하나의 문서로 끌어 모으세요. 한 번의 패스로 PDF 소스를 콜레이트하고 인터리브하기의 기법이 그대로 통합니다
procedure ImposeTwoUp(const InFile, OutFile: string);
var
Lib: TPDFlib;
Captures: array of Integer;
SourceCount, I: Integer;
TrimW, TrimH: Double;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile(InFile, '') <> 1 then
raise Exception.Create('Cannot open ' + InFile);
SourceCount := Lib.PageCount;
// 페이지 1의 유효 트림 크기(이 레이아웃은 균일 트림을 가정)
Lib.SelectPage(1);
TrimW := Lib.GetPageBox(4, 2);
TrimH := Lib.GetPageBox(4, 3);
// 첫 장을 덧붙이고 크기를 정함, NewPage가 새 페이지를 선택
Lib.NewPage;
Lib.SetPageDimensions(2 * TrimW, TrimH);
// 캡처마다 페이지 1이 제거되므로 다음 소스 페이지가 올라옴
SetLength(Captures, SourceCount);
for I := 0 to SourceCount - 1 do
begin
Captures[I] := Lib.CapturePageEx(1, 3); // 3 = TrimBox
if Captures[I] = 0 then
raise Exception.CreateFmt('Capture of source page %d failed', [I + 1]);
end;
// 장만 남음: 장당 트림된 페이지 둘, 나란히
Lib.SelectPage(1);
for I := 0 to SourceCount - 1 do
begin
if (I > 0) and (I mod 2 = 0) then
Lib.NewPage; // 현재 장과 같은 크기
// 기본 원점: Top은 위쪽 모서리, 아래부터 측정
Lib.DrawCapturedPage(Captures[I], (I mod 2) * TrimW, TrimH, TrimW, TrimH);
end;
Lib.SaveToFile(OutFile);
finally
Lib.Free;
end;
end;
트림 기반 캡처는 TrimBox 밖의 모든 것을 잘라내는데, 디지털 교정쇄나 cut-and-stack 레이아웃에는 그게 원하는 바입니다. 인쇄 후 트림되는 인쇄판이라면 블리드가 살아남도록 옵션 2로 캡처하고 셀 사이를 블리드 폭만큼 벌리세요. 캡처는 소스 페이지를 제거하므로 그것들을 가리키던 북마크와 링크는 타깃을 잃습니다. 내비게이션이 필요한 문서를 편집하는 대신 별도 출력 파일로 조판하세요. 북마크를 깨지 않고 페이지 교체하기가 페이지 수술의 그 쪽을 다룹니다
소스가 온전해야 한다면 ImportPageAsFormXObject(SourceDocumentID, SourcePage, Options)을 쓰세요. 같은 0부터 4까지의 옵션 값을 받고(현재 문서에는 Lib.SelectedDocument를 넘김), 소스 페이지 트리를 그대로 두고, 상속된 페이지 회전을 폼 행렬로 정규화하며, DrawCapturedPage가 받아들이는 핸들을 반환합니다. CapturePageEx는 /Rotate를 되돌리지 않으므로 회전된 입력에는 그 단계가 먼저 필요하고, 페이지 박스를 깨지 않고 페이지 회전 평면화하기가 그럴 때 각 박스에 무슨 일이 일어나는지 보여 줍니다. /Pages 노드에 production 박스를 달고 있을 수 있는 입력에 대한 한 가지 주의입니다. 임포트 경로는 v3.539.44에서 정렬된 두 경로와 별개인 자기만의 조상 탐색으로 박스를 해석하므로, 소스 페이지에서 먼저 HasPageBox(4)를 검사하고 0이면 옵션 1(CropBox)을 넘기세요. 그러면 결과가 파일이 우연히 쓰인 방식이 아니라 규격에 묶입니다
페이지 박스 빠른 참조
- 유효 CropBox: 페이지 자신의 CropBox, 없으면 가장 가까운 상속 CropBox, 그것도 없으면 유효 MediaBox(ISO 32000-1 §14.11.2)
- 유효 BleedBox, TrimBox, ArtBox: 리프 페이지 자신의 엔트리, 없으면 유효 CropBox
Resources,MediaBox,CropBox,Rotate만/Pages노드에서 상속됨(§7.7.3.4, 표 30)./Pages노드의 production 박스는 무시됨GetPageBox(BoxType, Dimension): BoxType 1 MediaBox, 2 CropBox, 3 BleedBox, 4 TrimBox, 5 ArtBox, Dimension 0 Left, 1 Top, 2 Width, 3 Height, 4 Right, 5 BottomHasPageBox(BoxType): 0 박스 없음, 1 페이지 자신의 박스(직접 또는 indirect), 2 상속된 MediaBox나 CropBox(직접 또는 indirect)CapturePageEx(Page, Options): 0 MediaBox, 1 MediaBox 폴백이 있는 CropBox, 2부터 4는 CropBox 폴백이 있는 BleedBox, TrimBox, ArtBox- 박스 조회와 캡처 전반의 일관된 기본값과 상속을 위해 v3.539.44 이상으로 업그레이드
페이지 박스는 PDF의 조용한 기본값이 밀리미터의 일부로 재는 인쇄 전 공정 공차와 만나는 지점이며, 라이브러리는 그 기본값을 어디에서든 같은 방식으로 적용하거나, 아니면 하나의 질문에 두 답을 건네줍니다. 박스, 캡처, Form XObject API 전체는 PDFlibPas PDF Library for Delphi 제품 페이지에 문서화되어 있습니다