HotPDF는 THotPDF.FlattenLoadedPageRotation으로 PDF 페이지 회전을 평탄화합니다. 이 메서드는 회전된 페이지의 콘텐츠를 시계 방향 cm 변환으로 감싸고, 페이지가 실제로 가진 모든 페이지 박스를 다시 쓰며, 어노테이션 지오메트리, 외양 행렬, explicit destination, 태그 구조 지오메트리를 같은 각도로 돌린 다음 /Rotate를 0으로 세팅합니다. 뷰어에서 페이지는 동일해 보이지만 좌표계는 이제 바로 서 있습니다. 하류 도구, 프린트 RIP, 여러분 자신의 스탬핑 코드가 /Rotate를 무시하고 날 유저 공간에 배치하는 순간 이것이 중요해집니다
전형적인 방아쇠는 가로 페이지를 /Rotate 90을 단 세로 미디어로 기록하는 스캐너나 모바일 캡처 앱입니다. 모든 뷰어가 올바르게 보여 주므로 누구도 눈치채지 못하다가, 누군가 "오른쪽 아래"에 페이지 번호를 스탬프하면 그것이 왼쪽 가장자리를 따라 옆으로 눕거나, /MediaBox만 읽는 인포지션 단계가 가로 페이지에 세로 슬롯을 배치하면서 알게 됩니다. 평탄화는 행렬 한 줄짜리 작업처럼 들립니다. 실제로는 다섯 개의 페이지 박스, 세 종류의 어노테이션 지오메트리, 문서의 링크 타깃, 구조 트리를 건드리고, 그 각각은 ISO 32000-1에서 자기 규칙을 갖습니다
/Rotate는 PDF 페이지를 어느 쪽으로 돌릴까?
/Rotate는 표시와 인쇄를 위해 페이지를 시계 방향으로, 90도 배수로 돌립니다(ISO 32000-1 §7.7.3.3, 표 30). 90도에서 미디어의 왼쪽 가장자리가 위가 되고 위 가장자리가 오른쪽이 되므로, y가 아래로 가는 디바이스 공간에서 매핑은 X = (y - Bottom) * Scale, Y = (x - Left) * Scale입니다. 270도에서는 오른쪽 가장자리가 위가 됩니다. /Rotate는 /Resources, /MediaBox, /CropBox(§7.7.3.4)와 함께 네 개뿐인 상속 가능 페이지 어트리뷰트 중 하나이므로, 자체 /Rotate가 없는 페이지 딕셔너리도 /Pages 조상에게 돌아갈 수 있습니다. THotPDF.GetLoadedPageRotation은 /Parent 체인을 걷고 결과를 0–359로 정규화합니다. 여러분이 원하는 값은 그것이지 페이지의 날 키가 아닙니다
방향은 테스트를 살아남는 방식으로 틀리기 쉽고, 예전 HotPDF 빌드가 정확히 그렇게 했습니다. 옛 페이지-디바이스 행렬은 90과 270에서 y 성분을 맞바꿨는데, 이것은 회전이 아니라 대각선을 축으로 한 반사를 만듭니다. 회전 안 된 경우에 비해 행렬의 방향이 뒤집히는 것이죠. 두 각도 모두 여전히 "회전된 것처럼 보이고", 비트맵은 맞바뀐 너비와 높이를 가지며, 페이지에서 뷰로 갔다 돌아오는 왕복은 시작점을 반환하니, 크기 검사와 왕복 테스트는 모두 통과합니다. 유일하게 믿을 만한 검사는 코너 마커가 어디에 착지하는지, 레퍼런스 렌더러와 픽셀 단위로 비교하는 것입니다. 뷰어 모델, SIMD 렌더 백엔드, 하이라이트 매핑이 같은 행렬을 베꼈으므로 전부 함께 고쳐졌고, 평탄화 코드는 이제 렌더러와 같은 시계 방향 관례를 씁니다
FlattenLoadedPageRotation이 페이지를 다시 쓰는 방법
FlattenLoadedPageRotation(PageRange, Info)는 PageRange 안의 유효 회전이 90, 180, 270인 모든 페이지를 처리하고 평탄화한 페이지 수를 반환합니다. 빈 PageRange는 모든 페이지를 뜻하고, 그렇지 않으면 문자열은 익숙한 1 기반 '1-3,7' 문법을 쓰며, 범위를 벗어난 페이지 번호는 건너뛰는 대신 예외를 raise합니다. 원래 콘텐츠 스트림은 절대 재인코딩되지 않습니다. 메서드는 q 0 -1 1 0 -Bottom Width+Left cm(90도의 경우)를 담은 새 스트림을 페이지의 /Contents 앞에 붙이고, Q를 담은 스트림을 뒤에 붙이며, 마지막으로 명시적인 /Rotate 0을 페이지 딕셔너리에 써서 /Pages 노드의 상속 값이 페이지를 두 번 돌리지 못하게 합니다
var
Pdf: THotPDF;
Info: THPDFRotationFlattenInfo;
Flattened: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('scanned-batch.pdf') > 0 then
begin
// '' = 모든 페이지, 0도 페이지는 스캔만 하고 그대로 둠
Flattened := Pdf.FlattenLoadedPageRotation('', Info);
Writeln(Format('Scanned %d, flattened %d pages', [Info.ScannedPageCount, Info.FlattenedPageCount]));
Writeln(Format('Turned %d annotations, %d destinations, %d tagged geometry entries',
[Info.TransformedAnnotationCount, Info.TransformedDestinationCount,
Info.TransformedStructureGeometryCount]));
if Flattened > 0 then
Pdf.SaveLoadedDocument('scanned-batch-upright.pdf');
end;
finally
Pdf.Free;
end;
end;
THPDFRotationFlattenInfo 레코드는 버리지 말고 기록해 둘 가치가 있습니다. ScannedPageCount는 범위의 크기, FlattenedPageCount는 반환 값과 같고, 세 개의 Transformed... 카운터는 문서가 돌려진 페이지를 가리키는 링크, 북마크, 태그 지오메트리를 가졌는지 알려 줍니다. 모든 파일이 destination 0을 보고하는 배치는 문제없습니다. 그림 bounding box를 기대했는데 구조 지오메트리 0을 보고하는 태그 PDF/UA 파일은 손으로 들여다봐야 할 신호입니다
평탄화는 어떤 페이지 박스를 어떤 순서로 다시 쓸까?
평탄화는 페이지가 이미 가진 박스만 다시 쓰고, 하나를 쓰기 전에 모든 박스를 읽습니다. 순서가 중요한 이유는 디폴트 체인 때문입니다. GetLoadedPageBox(PageIndex, pbCropBox, ...)는 페이지에 /CropBox가 없을 때 /MediaBox를 반환하고, /BleedBox, /TrimBox, /ArtBox는 디폴트로 CropBox를 따릅니다(§14.11.2). 예전 버전은 한 번에 박스 하나를 읽고, 변환하고, 썼습니다. MediaBox를 먼저 다시 쓴 다음 "CropBox"를 읽었더니 이미 돌아간 MediaBox가 돌아와, 한 번 더 돌려서 페이지가 한 번도 가진 적 없는 CropBox를 썼고, 가로 페이지가 정사각형으로 잘리는 결과를 낳았습니다. 상속 규칙도 같은 방식으로 갈립니다. MediaBox와 CropBox는 /Parent 체인을 따라 찾지만, Bleed, Trim, ArtBox는 페이지 딕셔너리 자체에 있을 때만 유효하므로, /Pages 노드의 어긋난 /TrimBox는 없는 것으로 취급되고 페이지에 복사되지 않습니다
procedure DumpPageGeometry(Pdf: THotPDF; PageIndex: Integer);
var
L, B, R, T: Single;
begin
Writeln('Effective /Rotate: ', Pdf.GetLoadedPageRotation(PageIndex));
if Pdf.GetLoadedPageBox(PageIndex, pbMediaBox, L, B, R, T) then
Writeln(Format('MediaBox [%g %g %g %g]', [L, B, R, T]));
// /TrimBox 키가 없어도 True: 값은 CropBox, 다음은 MediaBox로 폴백
if Pdf.GetLoadedPageBox(PageIndex, pbTrimBox, L, B, R, T) then
Writeln(Format('TrimBox [%g %g %g %g]', [L, B, R, T]));
// Letter 프리셋, GetLoadedPageVisibleBox는 실패 시 출력을 그대로 둠
L := 0; B := 0; R := 612; T := 792;
Pdf.GetLoadedPageVisibleBox(PageIndex, L, B, R, T);
Writeln(Format('Visible [%g %g %g %g]', [L, B, R, T]));
end;
그 헬퍼를 평탄화 전후로 돌리면 숫자가 저절로 설명됩니다. MediaBox [0 0 612 792]인 90도 페이지의 평탄화된 MediaBox는 [0 0 792 612]가 됩니다. 다시 쓰인 모든 박스는 원래 MediaBox 원점을 기준으로 같은 시계 방향 회전으로 매핑되므로, 새 MediaBox는 항상 원점에서 시작하고 다른 박스는 그 안에서 자기 위치를 유지합니다. GetLoadedPageVisibleBox는 뷰어가 표시하고 프린터가 인쇄하는 것, 그러니까 MediaBox로 잘린 뒤 Left가 Right보다 작아지도록 정규화된 CropBox를 반환하며, HotPDF의 렌더러, SVG 내보내기, 뷰어, 인쇄 경로가 모두 그 같은 박스를 씁니다. 사람이 보는 페이지 크기가 필요하면 /MediaBox를 읽는 대신 GetLoadedPageVisibleBox를 호출하세요
/Rect만 돌리면 어노테이션이 왜 깨질까?
어노테이션이 깨지는 이유는 외양 스트림이 /Rect에 곧장 그려지지 않기 때문입니다. §12.5.5 아래에서 뷰어는 먼저 폼의 /BBox를 자기 /Matrix로 변환한 다음, 그 결과의 bounding box를 /Rect로 스케일하고 이동합니다. /Rect만 돌리면 200 × 40 스탬프가 40 × 200 슬롯에 짜부라져, 읽을 수 없게 옆으로 눕습니다. 그래서 FlattenLoadedPageRotation은 페이지의 시계 방향 회전을 각 외양 /Matrix에 오른쪽 곱합니다(90도의 경우 행 벡터 관례로 [0 -1 1 0 0 0]). /N, /R, /D 외양과 그 안의 모든 상태에 걸쳐서입니다. 하나의 외양 스트림은 여러 어노테이션이나 상태가 공유할 수 있으므로, 각 스트림은 호출당 정확히 한 번 돌아갑니다. 깔끔한 답이 없는 유일한 케이스는 다른 회전을 가진 페이지들에 걸쳐 공유되는 스트림입니다. 그것은 자기에게 먼저 닿은 페이지를 따릅니다
폼 필드와 포스트잇을 제자리에 두는 규칙이 둘 더 있습니다. 위젯의 /MK /R 엔트리(§12.5.6.19)는 반시계 방향 각도이므로, 페이지의 시계 방향 각도를 빼고 360으로 나머지를 취합니다. 이를 건너뛰면 다음 외양 재생성이 필드 텍스트를 엉뚱한 방향으로 그립니다. NoRotate 플래그(비트 위치 5, 값 16, §12.5.3)를 가진 어노테이션은 회전된 페이지에서 바로 서 있고 자기 /Rect의 왼쪽 위 코너를 축으로 삽니다. 그래서 평탄화는 너비, 높이, 바로 선 외양을 유지하고 그 코너만 회전이 놓이는 곳으로 옮깁니다. 어노테이션을 넘어, 메서드는 /QuadPoints, /Vertices, /L, /InkList도 돌리고, 페이지를 이름으로 지정하는 explicit destination을 다시 쓰며(/XYZ 점, /FitR 사각형, 90과 270도에서 맞바뀌는 /FitH / /FitV, §12.3.2.2), /Pg가 그 페이지인 구조 엘리먼트의 어트리뷰트 /BBox 엔트리 같은 태그 지오메트리를 변환합니다
평탄화가 덮지 않는 것은?
평탄화는 한 페이지 자신의 오브젝트에 대한 기하학적 재작성이고, 몇 가지 상황은 요란하게가 아니라 조용히 그 밖에 떨어져 나갑니다
- 유효 회전이 이미 0이거나 MediaBox가 없거나 너비 또는 높이가 0인 페이지는 오류 없이 건너뛰어집니다. 반환 값을 바꾸리라 기대한 페이지 수와 비교하세요
- 페이지 리소스에서 참조되는 Form XObject는 폼 공간의 자기
/BBox를 유지합니다. 바깥의cm이 이미 그것들을 돌리기 때문입니다. 구조 트리 스캔은/K와/A만 따르므로 페이지의 리소스나 어노테이션으로 두 번 걸어 들어가는 일은 없습니다 - destination은 평탄화된 페이지마다 모든 간접 오브젝트를 한 번씩 훑어 찾으므로, 회전 페이지가 수백 개인 큰 문서는 그 걷기 비용을 각 페이지마다 지불합니다
- HotPDF의 페이지 렌더러는 어노테이션을 그리지 않으므로, 돌려진 스탬프의 시각적 검사에는 먼저
FlattenLoadedAnnotations가 필요합니다
// 렌더러가 보여 줄 수 있도록 외양을 콘텐츠에 굽고,
// 그다음 /Rotate 제거 전후로 1페이지를 렌더링
Pdf.FlattenLoadedAnnotations('1');
Before := Pdf.RenderLoadedPageToBitmap(0, 96);
try
Pdf.FlattenLoadedPageRotation('1', Info);
After := Pdf.RenderLoadedPageToBitmap(0, 96);
try
Assert((Before.Width = After.Width) and (Before.Height = After.Height));
// 크기만이 아니라 여기서 코너 마커 픽셀을 비교
finally
After.Free;
end;
finally
Before.Free;
end;
더 깊은 배경으로, 이 이야기의 어노테이션 쪽은 평탄화 전에 어노테이션 외양 합성하기에서 이어지고, 전후 비교 뒤의 렌더러는 불러온 PDF 페이지를 비트맵으로 렌더링하기에서 다뤄지며, 불러온 PDF의 레닥션과 N-up 스티칭은 회전 접두사와 접미사가 의지하는 같은 콘텐츠 스트림 덧붙이기 기법을 보여 줍니다. FlattenLoadedPageRotation과 페이지 박스 리더를 포함한 HotPDF는 HotPDF Delphi PDF component 페이지에서 Delphi와 C++Builder용으로 구할 수 있습니다