템플릿 문서
템플릿은 JSON 객체 하나입니다. 페이지, 재사용 스타일 목록, 그리고 노드의 평면 배열로 이루어집니다. 노드 목록은 중첩하지 않습니다 — 노드가 parent 에 부모의 id 를 적어 관계를 나타냅니다.
아래 필드는 전부 필수입니다. 노드의 props 는 모든 노드 타입의 props 를 합친 것이라, 그 타입이 쓰지 않는 키도 들고 있어야 합니다. 안 쓰는 것은 비워 두십시오 — 문자열은 "", 숫자는 0, 배열은 []. 키를 지우면 스키마 위반이지만 비우는 것은 아닙니다.
최상위
v integer
스키마 버전. 현재 1. 버전이 없으면 추측하지 않고 거부합니다.
unit const
좌표 단위. 언제나 "px" 이며 96 dpi 기준입니다 — A4 는 794 × 1123 입니다.
px
page object
용지, 여백, 그리고 모든 페이지에 반복되는 머리글·바닥글.
fonts array
이 문서가 필요로 하는 폰트. 빈 배열이면 기본 폰트를 씁니다.
styles array
재사용하는 글자·상자 스타일. 노드가 id 로 참조합니다.
nodes array
문서의 모든 노드를 담은 평면 배열. 중첩은 parent 로 나타냅니다.
page
size enum
용지 크기.
a4a5letterlegal
orientation enum
용지 방향. size 뒤에 적용되므로 a5 + landscape 는 가로로 긴 A5 입니다.
portraitlandscape
margin object
인쇄 여백(px). 노드 좌표는 여백 안쪽에서 시작합니다.
margin.t number
위 여백.
margin.r number
오른쪽 여백.
margin.b number
아래 여백. 바닥글 자리를 위해 보통 위쪽보다 큽니다.
margin.l number
왼쪽 여백.
header string
모든 페이지 상단에 반복되는 문구. 빈 문자열이면 없음. {{ … }} 를 쓸 수 있습니다.
footer string
모든 페이지 하단에 반복되는 문구. 빈 문자열이면 없음. {{ … }} 와 [[page]] · [[pages]] 를 쓸 수 있습니다.
fonts
family string
폰트 이름. styles[].fontFamily 가 찾는 이름과 같아야 합니다.
src enum
폰트의 출처. "preinstalled" 는 기본 내장, "r2" 는 직접 올린 폰트입니다.
preinstalledr2
styles
id string
문서 내 유일한 스타일 id. nodes[].styleId 가 가리킵니다.
fontFamily string
폰트 이름. 한글·중국어·일본어가 들어갈 수 있는 글자에는 "Noto Sans CJK KR" 을 쓰십시오 — 라틴 전용 폰트는 그 글자를 통째로 떨어뜨립니다.
fontSize number
글자 크기(px).
fontWeight integer
글자 굵기. 400 이 보통, 700 이 굵게.
italic boolean
기울임.
color string
글자 색 #RRGGBB.
bg string
배경색 #RRGGBB 또는 "transparent".
align enum
가로 정렬.
leftcenterrightjustify
valign enum
노드 상자 안에서의 세로 정렬.
topmiddlebottom
lineHeight number
줄 간격. 글자 크기의 배수입니다.
border string
CSS border 축약형 — 예: "1px solid #ddd". 없으면 "none".
radius number
모서리 둥글기(px).
opacity number
불투명도 0~1.
nodes
id string
문서 내 유일한 노드 id. n_ 접두는 관례일 뿐 규칙은 아닙니다.
parent string
부모 노드의 id. 빈 문자열이면 페이지 직속입니다. 없는 부모를 가리키거나 순환이 생기면 위반입니다.
order integer
같은 부모 안에서의 순서. 겹칠 때 무엇이 위에 그려지는지도 이 값이 정합니다.
type enum
노드의 종류.
labelimagelinerectcircletablechartcodehtml
x number
부모 기준 X 좌표(px).
y number
부모 기준 Y 좌표(px).
w number
너비(px).
h number
높이(px). 0 이면 자동 높이입니다. 배열이 바인딩된 표에서는 예약 높이가 됩니다 — "늘어나는 표" 참조.
show string
표시 조건. 빈 문자열이면 항상 표시합니다. 그 밖의 값은 data 기준으로 계산하는 표현식입니다.
styleId string
styles[] 의 id. 빈 문자열이면 기본 스타일.
props object
타입별 설정. 모든 키가 필수이며 안 쓰는 것은 비워 둡니다.
nodes[].props
text string
label 전용. 그릴 텍스트. {{ … }} 가능. 그 밖의 타입은 "".
src string
image 전용. data: URI 또는 업로드한 자산 키. 그 밖의 타입은 "".
fit enum
image 전용. 이미지를 상자에 채우는 방식. 그 밖의 타입은 "".
""containcoverfill
thickness number
line 전용. 선 두께(px). 그 밖의 타입은 0.
stroke string
line·rect·circle. 선 색 #RRGGBB. 그 밖의 타입은 "".
fill string
rect·circle. 채움 색 #RRGGBB. 그 밖의 타입은 "".
repeat string
rect 전용. 이 경로의 배열 항목마다 그룹을 한 벌씩 그립니다 — 예: "data.companies". 안에 든 것이 함께 반복됩니다. 그 밖의 타입은 "".
as string
rect 전용. 반복 그룹 안에서 지금 항목을 부를 이름 — "co" 로 두면 {{ co.name }} 으로 씁니다. 비우면 "block". "item" 은 쓸 수 없습니다: 그 이름은 표의 행이 쓰므로, 나눠 두어야 그룹 안의 표가 둘 다 읽습니다.
break enum
rect 전용. "page" 면 반복할 때마다 새 페이지에서 시작합니다. 비우면 이어 붙습니다.
""page
keep enum
rect 전용. "together" 면 한 벌이 페이지 경계에서 갈리지 않습니다. 한 페이지보다 큰 벌은 어차피 갈립니다.
""together
padding number
table 전용. 칸 안 여백(px). 키가 없으면 선이 있을 때만 기본 여백이 붙습니다. 그 밖의 타입은 0.
align enum
table 전용. 격자 표 칸의 가로 정렬 기본값. 칸이 따로 정하면 그쪽이 이깁니다. 반복 표는 열마다(columns[].align) 정합니다.
""leftcenterright
valign enum
table 전용. 칸 안 세로 정렬. 비우면 가운데. 칸이 따로 정하면 그쪽이 이깁니다.
""topmiddlebottom
wrap enum
table 전용. 칸 안 줄바꿈. 비우거나 "on" 이면 넘치는 글이 다음 줄로, "off" 면 한 줄로 두고 넘치는 만큼 잘라 … 를 붙입니다.
""onoff
bind string
table 전용. 반복할 배열의 경로 — 예: "data.items". 그 밖의 타입은 "".
headerHeight number
table 전용. 머리 행 높이(px). 그 밖의 타입은 0.
rowHeight number
table 전용. 본문 행 높이(px). 그 밖의 타입은 0.
max integer
table 전용. 그릴 최대 행 수. 넘는 행은 버립니다. 그 밖의 타입은 0.
columns array
table 전용. 왼쪽부터의 열 정의. 그 밖의 타입은 [].
columns[].header string
머리 셀 문구.
columns[].cell string
본문 셀 템플릿. item 이 현재 행이므로 "{{ item.name }}" 처럼 그 필드를 읽습니다.
columns[].width number
열 너비(px). 합이 표 너비와 맞아야 합니다.
columns[].align enum
셀 정렬.
leftcenterright
rows integer
table 전용. bind 가 빈 격자 표의 행 수. 반복 표와 그 밖의 타입은 0.
cells array
table 전용. 격자 표의 칸. 없는 자리는 빈 칸이므로 다 적지 않아도 됩니다. 반복 표와 그 밖의 타입은 [].
cells[].r integer
행 번호. 0부터.
cells[].c integer
열 번호. 0부터.
cells[].span integer
가로 병합 칸 수. 1 이 기본.
cells[].rowspan integer
세로 병합 칸 수. 1 이 기본.
cells[].text string
칸 내용. "{{ data.x }}" 로 값을 넣을 수 있습니다.
cells[].align enum
칸 글자 정렬.
leftcenterright
cells[].valign enum
이 칸만 세로 정렬. 비우면 표의 값을 따릅니다.
""topmiddlebottom
cells[].wrap enum
이 칸만 줄바꿈. 비우면 표의 값을 따릅니다.
""onoff
cells[].src string
칸 안 이미지. data:image/ 로 시작하는 값만 받습니다 — 외부 주소는 요청이 나가므로 렌더가 거부합니다.
cells[].fit enum
칸 이미지 맞춤. 비우면 contain.
""containcoverfill
cells[].bg string
칸 배경색 #RRGGBB. 비우면 배경 없음.
cells[].head boolean
머리 칸. 머리 행은 r=0 줄에, 머리 열은 c=0 줄에 켭니다.
cells[].border enum
"none" 이면 이 칸만 선을 그리지 않습니다.
""none
chartKind enum
chart 전용. "bar"(기본) · "line" · "pie" · "donut". 그 밖의 타입은 "".
""barlinepiedonut
codeKind enum
code 전용. 그릴 코드 종류 — "qr"(기본) · "code128" · "ean13". 그 밖의 타입은 "".
""qrcode128ean13
ecc enum
code 전용이고 QR 에만. 오류정정 수준 L · M · Q · H — 비우면 M 입니다. 높을수록 더 많이 훼손돼도 읽히지만 모듈이 늘어 같은 글자가 더 촘촘한 코드가 됩니다.
""LMQH
html string
html 전용. 서식 있는 본문. 허용목록에 있는 태그만 남습니다. 그 밖의 태그는 글은 남고 태그만 사라지며, script · style · iframe 은 내용까지 사라집니다. 그 밖의 타입은 "".
표현식
모든 문자열 필드에 {{ … }} 표현식을 넣을 수 있습니다. 렌더 요청과 함께 보낸 data 를 기준으로 계산됩니다.
표의 셀 안에서 item 은 바인딩된 배열의 현재 행을 가리킵니다. 표 밖에서는 정의되지 않습니다.
쪽번호는 문법이 다릅니다 — [[page]] 와 [[pages]] 이며 page.header · page.footer 에서만 동작합니다. 전체 쪽수가 정해지는 배치 이후에 채워지기 때문입니다.
서식 있는 글
html 노드는 마크업 한 덩어리를 받습니다. 라벨을 줄지어 놓는 것으로는 못 하는 일입니다 — 줄 하나가 늘면 손으로 잡아 둔 배치가 전부 다시 흐르기 때문입니다. 약관, 안내문, 각주처럼 흘러야 하는 글에 씁니다.
저희는 마크업을 걸러내지 않고 다시 짓습니다. 보내신 HTML 을 읽어서, 목록에 있는 태그와 속성만 새로 씁니다. 이 차이가 중요합니다 — 저희가 이해하지 못한 것은 *저희가 미리 생각해 뒀는지에 기대는 대신* 아예 출력에 존재할 수 없습니다.
허용: p · div · span · br · hr · blockquote · pre · h1~h6 · b · strong · i · em · u · s · small · sub · sup · code · a · ul · ol · li · table 과 그 행·칸 · img. 목록 밖 태그는 태그만 사라지고 글은 남습니다 — 쓰신 글을 잃지 않습니다. script · style · iframe 같은 것은 내용까지 사라집니다. 그 내용은 글이 아니기 때문입니다.
속성은 더 좁습니다: style(선언마다 검사하고 position 은 제외) · 칸의 colspan · rowspan · 링크의 href · 이미지의 src · alt · width · height. class 와 id 는 없습니다 — 페이지를 배치하는 저희 클래스와 부딪힙니다. 이미지는 data: URI 여야 합니다. image 노드와 같은 규칙이고, 원격 이미지를 그리려면 렌더 도중에 가져와야 하기 때문입니다. 링크는 어디든 가리켜도 됩니다. 링크는 가져오기가 아닙니다.
html 안의 {{ … }} 는 데이터가 무엇이든 글자가 되지 마크업이 되지 않습니다. 속성 안에 있으면 그 값도 직접 쓴 값과 똑같이 검사합니다 — 값으로 style 에 url(…) 을 밀어 넣거나 href 에 javascript: 를 넣을 수 없습니다. 상한: 20,000자 · 중첩 32단계 · 태그 2,000개. 넘으면 자르고 warnings 로 알려 드립니다.
차트
chart 노드는 표가 쓰는 두 필드를 그대로 씁니다. bind 가 배열이고, columns 가 각 항목에서 무엇을 읽을지 정합니다 — columns[0].cell 이 라벨, columns[1].cell 이 값, columns[1].header 가 제목입니다. 새 필드는 chartKind 하나입니다: "bar" · "line" · "pie" · "donut".
축은 둥근 최댓값을 고릅니다 — 1 · 2 · 5 에 10의 거듭제곱을 곱한 값 — 그래야 눈금이 사람이 고를 법한 숫자로 읽힙니다. 숫자가 아닌 값은 렌더를 실패시키지 않고 0으로 셉니다. 읽을 수 없는 칸이 빈 칸으로 나오는 것과 같습니다. 파이와 도넛은 절댓값을 씁니다 — 음수 조각은 뜻이 없기 때문입니다.
차트는 SVG 로 그리므로 PDF 안에서 벡터입니다. 항목이 많으면 라벨을 겹쳐 찍는 대신 솎아 냅니다. 범례는 파이·도넛 아래 한 줄에 고정이고 움직이지 않습니다 — 스스로 자리를 찾는 범례는 여러분의 조판을 함께 움직입니다.
QR 코드와 바코드
code 노드는 글자를 스캔되는 그림으로 바꿉니다. 값을 text 에 넣고 codeKind 를 고르면 됩니다 — "qr" · "code128" · "ean13". SVG 로 그리므로 PDF 에 벡터로 들어갑니다. 어떤 인쇄 해상도에서도 선명하고, 막대 경계가 규격이 말하는 자리에 정확히 놓입니다. 마지막이 중요합니다 — 엉뚱한 시점에 래스터로 바뀐 바코드는 스캐너가 잘못 읽는 바코드입니다.
QR 은 정사각형으로 두십시오. 상자를 채우려고 코드를 늘리지 않습니다 — 찌그러지면 스캔이 안 됩니다 — 그래서 직사각형 상자는 긴 쪽에 여백이 남습니다. 코드 둘레의 여백(quiet zone)은 규격의 일부라 저희가 확보해 둡니다. 잘라내려 하지 마십시오.
ean13 은 12자리를 받아 체크digit 을 저희가 계산합니다. 13자리를 주면 마지막 자리를 검사합니다. code128 은 ASCII 32~126 을 담습니다. 인코딩할 수 없는 값이면 — 길이가 틀렸거나, 담을 수 없는 글자거나, QR 용량을 넘겼거나 — 빈 상자를 그리는 대신 오류로 실패합니다. 빈 상자는 라벨이 인쇄된 뒤에야 발견되기 때문입니다.
지원하는 것은 이 셋입니다. 100종을 다루는 라이브러리가 있지만, 가장 작은 것도 저희 워커 전체보다 무겁습니다. 저희가 그리지 않는 것이 필요하면 어떤 것이고 왜 필요한지 알려 주십시오.
늘어나는 표
배열이 바인딩된 표는 높이가 정해져 있지 않습니다 — 3행일 수도 300행일 수도 있습니다. 반복 그룹도 마찬가지입니다. 그래서 그런 노드가 처음 나오는 자리부터 그 뒤 전부가 고정 좌표가 아니라 문서 흐름으로 배치되고, 앞의 것이 길어지면 뒤의 내용이 함께 밀려납니다.
표에 적은 h 는 상한이 아니라 예약 높이입니다. 표는 그보다 길어질 수 있습니다. 표 뒤에 놓은 노드는 그 예약 높이의 아래끝을 기준으로, 그려 둔 간격을 그대로 유지합니다.
늘어나는 표에는 현실적인 h 를 주십시오. 0 으로 두면 표 아래에 배치한 노드들이 표 뒤가 아니라 표의 시작 지점에서 그려집니다.
구역 전체를 반복하기
표는 행을 반복합니다. 사각형은 그 안에 든 것을 통째로 반복합니다 — props.repeat 에 배열 경로를 적으면 항목마다 그룹이 한 벌씩, 자식까지 함께 그려집니다. 요청 하나로 여러 레코드를 담은 문서를 만드는 방법입니다. 지점별 명세서, 회사별 요약처럼요. 레코드마다 요청을 나눌 필요가 없습니다.
그룹 안에서 지금 항목은 props.as 에 적은 이름으로 부릅니다. as 가 "co" 면 {{ co.company }} 가 지금 항목의 company 를 읽습니다. 비우면 "block" 입니다. "item" 이 기본값이 아닌 것은 일부러입니다 — 그 이름은 표의 행이 쓰므로, 나눠 두어야 그룹 안의 표가 둘을 함께 읽을 수 있습니다. 레코드는 {{ co.company }}, 행은 {{ item.name }} 입니다. data · item · index 는 이름으로 쓸 수 없습니다.
props.break 를 "page" 로 두면 반복할 때마다 새 페이지에서 시작합니다. 한 페이지보다 큰 그룹은 알아서 나뉘므로 직접 셀 필요가 없습니다. 그룹 안의 표도 흐르기 때문에, 그려 둔 그룹보다 길어지면 그 그룹 안의 나머지를 밀어냅니다.
한 벌 안에서의 쪽번호는 [[bpage]] · [[bpages]] 입니다 — "이 청구서의 1/2쪽"이지 문서 전체의 번호가 아닙니다. page.footer 에서만 됩니다. [[page]] 는 Chromium 이 인쇄하면서 채우는데, 그쪽은 어디서 한 벌이 시작했는지 모릅니다. 그래서 이 바닥글은 인쇄가 끝난 뒤 저희가 직접 그립니다. 같은 이유로 이때의 바닥글은 Latin-1 만 쓸 수 있습니다 — 표준 폰트는 실을 수 있어도 한글 폰트는 못 싣습니다.
경로가 없거나, 배열이 아니거나, 비어 있으면 그룹은 그냥 그려지지 않습니다 — 오류가 아닙니다. 반복은 표의 행과 같은 노드 예산을 쓰므로, 배열이 아주 크면 무한히 늘어나는 대신 잘립니다. 그룹끼리 겹쳐 넣을 수는 없습니다.