PDF Library for Delphi는 실제 2차원 레이아웃으로 HTML을 PDF 페이지에 렌더링합니다. display: flex와 display: grid는 쌓인 블록으로 격하되지 않고 제대로 측정되어 배치되며, 각주는 자신의 참조를 담은 박스의 맨 아래에 예약되고, 번호 매김은 열과 페이지에 걸쳐 계속 이어집니다. 진입점은 익숙한 그대로, 단일 박스용 DrawHTMLTextBox와 다단 흐름용 DrawHTMLStory입니다
이것이 중요한 이유는 오늘날 대부분의 리포트 콘텐츠가 HTML로 들어오기 때문입니다. 템플릿은 CSS를 쓰는 사람들이 작성하고, 대시보드는 카드 형태로 디자인되는데, 플렉스 행을 조용히 네 개의 쌓인 블록으로 무너뜨리는 렌더러는 디자인과 전혀 닮지 않은 문서를 만들어냅니다. 이 기능이 생기기 전에는 이 엔진이 측정할 수 있는 2차원 컨테이너가 표 하나뿐이었으므로, 모든 카드 레이아웃을 손으로 표로 다시 작성해야 했습니다
레이아웃 모델에서 무엇이 바뀌었을까?
이전의 메인 루프는 단일 라인 박스 하나를 유지하며 페이지 아래로 내려갔습니다. 이 모델은 인라인 콘텐츠와 쌓인 블록은 완벽하게 다루지만, 자식들이 서로에 대한 관계로 크기가 정해지는 컨테이너는 표현할 수 없습니다. 표는 자신만의 2패스 측정 방식을 가진 유일한 예외였습니다
플렉스와 그리드는 각각 컨테이너 자식들에 대한 제한된 측정 패스를 추가하며, 중요한 단어는 제한된입니다. 플렉스 컨테이너는 직계 자식 최대 256개를 고정 배열로 측정합니다. 그리드는 결정론적인 자동 배치를 위해 최대 64×64 셀의 점유 행렬을 사용합니다. 이런 상한이 존재하는 이유는, 악의적이거나 단순히 생성된 스타일시트가 무한 재귀나 제곱 배치 메모리를 유발하지 못하게 하기 위해서이며, HTML이 고객이 직접 편집하는 템플릿에서 나올 때는 실제로 걱정해야 할 문제입니다
플렉스 아이템은 크기를 어떻게 얻을까
가로 방향에서, 컨테이너는 각 아이템의 basis를 grow와 shrink 가중치와 함께 합산한 다음, 남는 공간을, 양수든 음수든, 그 가중치에 따라 분배합니다. flex-wrap이 있으면 각 줄이 독립적으로 풀리므로, 두 줄로 나뉘는 행은 컨테이너 전체가 아니라 줄마다 남는 공간을 배정합니다. 세로 방향에서는 명시적인 높이나 콘텐츠 높이 중 하나를 대상으로 같은 주축 분배가 실행됩니다
justify-content, align-items, gap, 역방향 지시자는 이미 측정이 끝난 지오메트리 위에서 동작합니다. 박스를 옮길 뿐, 아이템 콘텐츠를 다시 측정하는 일은 결코 없습니다. 이 분리 덕분에 복잡한 대시보드도 자식들을 여러 번 측정하지 않게 됩니다
uses
PDFlibrary;
var
Lib: TPDFlib;
Html, Remainder: WideString;
begin
Lib := TPDFlib.Create;
try
Lib.NewDocument;
Lib.SetPageSize('A4');
Lib.NewPage;
Html :=
'<div style="display:flex; gap:12px;">' +
' <div style="flex:2 1 0; background:#f4f6f8; padding:8px;">' +
' <b>Revenue</b><br/>EUR 4,182,300</div>' +
' <div style="flex:1 1 0; background:#f4f6f8; padding:8px;">' +
' <b>Margin</b><br/>18.4%</div>' +
' <div style="flex:1 1 0; background:#f4f6f8; padding:8px;">' +
' <b>Backlog</b><br/>92 days</div>' +
'</div>';
Remainder := Lib.DrawHTMLTextBox(40, 40, 515, 120, Html);
if Remainder <> '' then
Log('content did not fit - carry the remainder to the next box');
Lib.SaveToFile('dashboard.pdf');
finally
Lib.Free;
end;
end;
반환 값은 이어짐 문자열로, 이것이 모든 HTML 드로잉 진입점이 무엇이 들어가지 못했는지 보고하는 방식입니다. 이를 다음 박스나 다음 페이지에 넘기면 흐름이 멈춘 지점부터 다시 이어집니다
그리드 배치, 그리고 트랙이 가질 수 있는 값
그리드 트랙은 고정 길이, 백분율, fr 단위, 단순한 repeat() 표현식, minmax()를 받아들입니다. 자동 배치는 점유 행렬을 결정론적으로 채우므로, 같은 HTML은 항상 같은 배치를 만들어냅니다. 명시적인 좌표는 겹치는 것이 허용되는데, 이는 의도적인 설계입니다. 카드 위에 배지를 겹쳐 놓는 디자인은 실수가 아니라 의도를 표현하는 것입니다. 한쪽 축만 명시적으로 주어졌을 때는 나머지 축에서만 배치를 탐색합니다
여러 행에 걸치는 아이템은 자신의 측정된 높이를 자신이 걸쳐 있는 행들에 평균으로 나누어 되돌려주므로, 세로로 긴 스패닝 아이템이 이웃한 행을 짧게 만든 채 하나의 행만 짓누르는 일을 막아줍니다:
Html :=
'<div style="display:grid; grid-template-columns:repeat(3, 1fr); ' +
' gap:10px;">' +
' <div style="grid-row:span 2; background:#eef;">Site plan</div>' +
' <div>Inspector</div>' +
' <div>Date</div>' +
' <div style="grid-column:2 / span 2;">Findings summary</div>' +
'</div>';
Remainder := Lib.DrawHTMLTextBox(40, 180, 515, 260, Html);
플렉스와 그리드의 자식은 다른 모든 콘텐츠와 같은 HTML 렌더러를 거쳐 그려지며, 바로 이 점이 이 기능을 별세계가 아니라 실제로 쓸 수 있게 만들어 줍니다. 폰트, CSS 캐스케이드, 링크, 이미지, 표, 그리고 더 중첩된 플렉스나 그리드 컨테이너 모두가 플렉스 아이템 안에서 최상위 레벨에서와 정확히 똑같이 동작하며, 바깥쪽 레이아웃 계획은 최종 텍스트와 사각형 명령을 기록해 두므로 반복해서 그릴 때는 이미 존재하는 측정 캐시를 재사용합니다
각주는 왜 페이지 나눔 문제일까?
각주는 자신의 참조를 담은 단락 뒤에 흘러가는 콘텐츠가 아닙니다. 자신의 참조와 같은 박스의 맨 아래에 나타나야 하는 콘텐츠입니다. 이는 평소의 측정 순서를 뒤집는데, 본문 텍스트에 쓸 수 있는 공간이 아직 배치되지 않은 콘텐츠에 달려 있게 되기 때문입니다
그래서 렌더러는 참조를 만나는 순간 각주를 측정하고, 그 각주 영역을 현재 제한된 박스의 본문 높이 예산에서 빼둡니다. 참조와 지금까지의 본문 텍스트, 각주가 모두 함께 들어갈 수 없다면, 각주 표시와 그 이후 모든 것이 함께 이어짐 문자열로 넘어갑니다. 이 규칙이 두 가지 전형적인 실패를 막아줍니다. 각주가 본문 텍스트에 겹쳐 인쇄되는 것과, 참조는 이전 페이지에 있는데 각주만 뒤에 남겨지는 것입니다
제한된 박스 안에서는 각주 영역이 맨 아래에 구분선과 함께 고정됩니다. 박스 높이가 고정되지 않은 제한 없는 측정에서는 각주 영역이 본문 바로 뒤를 따라갑니다. 번호 매김은 이어짐 스택의 확장 필드로 운반되므로, DrawHTMLTextBox와 DrawHTMLStory는 열과 페이지에 걸쳐 순서를 계속 이어가며, 그 필드가 존재하기 전에 만들어진 이어짐 문자열도 여전히 올바르게 재개됩니다
// Footnotes inside a multi-column story keep one running sequence
Html := LoadTemplate('chapter.html'); // uses float:footnote markers
Remainder := Lib.DrawHTMLStory(40, 40, 515, 700,
2, // columns
16, // gutter in points
20, // maximum pages for this story
Html);
if Remainder <> '' then
Log('story exceeded its page budget');
템플릿 작성자를 위한 실무 가이드
문서화된 상한 안에서 설계하십시오. 직계 자식이 256개보다 많은 플렉스 컨테이너는 거의 언제나 플렉스 옷을 입은 데이터 표이며, 표 경로가 어차피 그것을 더 잘 측정해 줍니다. 64×64보다 큰 그리드는 스프레드시트이며 같은 조언이 적용됩니다. 다단 본문 텍스트의 경우, 열과 하이픈 처리 동작은 하이픈 처리와 균형 잡힌 텍스트 열에서 각 열 안의 흐름이 어떻게 보이는지를 결정합니다
레이아웃이 맞아떨어져야 할 때는 그리기 전에 측정하십시오. GetHTMLTextHeight는 주어진 너비가 필요로 하는 높이를 보고하며, 이는 잉크를 쓰기 전에 레이아웃 하나와 다른 레이아웃 사이에서 저렴하게 판단할 수 있는 방법입니다. 그리고 비어 있지 않은 이어짐 문자열은 예외가 아니라 정상으로 취급하십시오. 이는 긴 콘텐츠가 페이지로 나뉘는 메커니즘이지 오류 신호가 아닙니다
HTML이 손으로 작성한 템플릿이 아니라 리포트 엔진에서 나온다면, 데이터 세트 리포트 엔진의 데이터 주도 경로가 이와 잘 어울리며, 플렉스와 그리드가 그다음에 배치할 마크업을 만들어냅니다. 그리고 같은 콘텐츠가 다시 PDF를 벗어나야 할 때는 PDF를 Markdown과 DOCX로 내보내기의 시맨틱 내보내기 경로가 왕복을 완성해 줍니다
HTML 레이아웃, 리포트 생성, 시맨틱 내보내기는 Delphi, C++Builder, Free Pascal용 하나의 라이브러리에 속해 있습니다. 전체 기능 목록은 Delphi용 PDF Library 페이지에 있습니다