- Dropflow는 기초 CSS 표준인 inline, block, float, positioning, 향후 table을 탐구하기 위해 만든 CSS 레이아웃 엔진이며, flexbox와 grid는 대상이 아님
- Node와 node-canvas로 백엔드에서 PDF·이미지 생성에 쓸 수 있고, 브라우저에서는 canvas에 줄바꿈이 있는 리치 텍스트를 렌더링할 수 있음
- 텍스트 레이아웃은 양방향·RTL 텍스트, 그래핌 단위 폰트 폴백, 색이 있는 발음 구별 기호, OpenType/TrueType 폰트 등록, HarfBuzz 기반 shaping을 지원함
- CSS 지원 범위는 30개 이상 속성으로,
float,clear,display: inline-block,position: relative,overflow,z-index,zoom등이 동작하고table,absolute,fixed,transform등은 계획 단계임 - 최고 성능은 HTML/CSS 파싱을 건너뛰는 hyperscript API와 재사용 가능한 style 객체에서 나오며, 2019 MacBook Pro 기준 500개 넘는 문단의 The Little Prince를 HTML에서 이미지로 160ms 미만에 변환함
Dropflow가 다루는 CSS 레이아웃 범위
- Dropflow는 CSS 레이아웃 엔진으로, foundational CSS standards의 범위를 탐구하기 위해 만들어짐
- 대상은 inline, block, float, positioning, 향후 tables임
- flexbox와 grid는 대상이 아님
- 고품질 텍스트 레이아웃 구현을 갖추고 있으며, 세계 여러 언어를 표시할 수 있음
- 사용처는 두 가지로 제시됨
- Node와 node-canvas를 이용해 백엔드에서 PDF나 이미지 생성
- 브라우저에서 canvas에 줄바꿈되는 리치 텍스트 렌더링
주요 기능
- 30개 이상 CSS 속성을 지원하며,
float처럼 복잡한 속성도 포함함 - 텍스트 관련 기능이 넓음
- 양방향 텍스트와 RTL 텍스트
- 그래핌 단위 폰트 폴백
- 색이 있는 발음 구별 기호
- 바람직한 줄바꿈 처리, 예를 들어 시작 padding을 다음 줄로 가져감
- 최적화된 shaping
- 입력 방식은 HTML/CSS뿐 아니라, 스타일을 객체로 넘기는 hyperscript
h()API도 지원함 - OpenType/TrueType 버퍼는 등록 가능하며, 등록이 필수임
<img>는 JPEG, BMP, PNG, GIF를 지원하지만 백엔드별 지원은 다를 수 있음- 상속·캐스케이드된 스타일은 두 번 계산하지 않음
- 타입이 완비되어 있고 테스트가 많으며 빠른 동작을 목표로 함
CSS 지원 상태
- Inline formatting에서 동작하는 항목에는
color,direction,font-family,font-size,font-stretch,font-style,font-weight,line-height,text-align,vertical-align,white-space,word-break,overflow-wrap,word-wrap,word-spacing등이 있음 - Inline formatting에서
font-variant,letter-spacing,tab-size,text-decoration,unicode-bidi는 계획됨 상태임 - Block formatting에서는
clear와float가 동작함writing-mode의horizontal-tb,vertical-lr,vertical-rl은 부분 구현 상태임- BFC에는 구현됐지만 IFC에는 아직 구현되지 않음
- Boxes and positioning에서는 여러 박스·배치 속성이 동작함
background-clip,background-color,border-color,border-style,border-widthtop,right,bottom,leftbox-sizingdisplay: block,inline,inline-block,flow-root,noneheight,margin,padding,overflow,width,z-index,zoomposition: relative
- Boxes and positioning에서
display: table,min/max-height,min/max-width,position: absolute,position: fixed,transform은 계획됨 상태임
기본 사용 흐름
- Dropflow는 브라우저와 같은 방식으로 상속·계산된 스타일이 있는 DOM을 기반으로 동작함
- 일반적인 흐름은 다음과 같음
- 레이아웃 전에
FontFace로 폰트 등록 flow.style()로 스타일 객체 생성flow.h()로 DOM 생성flow.dom()으로 렌더링할 트리 준비renderToCanvas()로 canvas 전체에 레이아웃과 페인트 수행
- 레이아웃 전에
- HTML API도 제공하지만, 성능이 중요하지 않거나 학습 목적일 때만 권장됨
- 파싱은 추가 시간을 만들고 번들 크기를 크게 늘림
- HTML 파싱은 @fb55 덕분에 빠르다고 적혀 있음
- 현재는
styleHTML 속성만 지원하며class는 아직 동작하지 않음
성능 특성
- 성능은 최상위 목표이며, 정확성 다음으로 중요함
- 예제 기준 성능 수치가 제시됨
- 여러 inline span과 다른 폰트가 있는 8개 문단을 HTML에서 이미지로 변환하는 데 2019 MacBook Pro에서 9ms, 2012 MacBook Pro에서 13ms
- 500개 넘는 문단의 The Little Prince를 HTML에서 이미지로 변환하는 데 2019 MacBook Pro에서 160ms 미만, 2012 MacBook Pro에서 250ms 미만
- 10글자 단어를 생성하고 레이아웃만 수행하는 데 2019 MacBook Pro에서 25µs 미만, 2012 MacBook Pro에서 50µs 미만
- 가장 빠른 성능은 hyperscript API로 DOM을 직접 만들고 일반적인 HTML/CSS 파싱 단계를 건너뛸 때 얻을 수 있음
- style 객체를 재사용하면 이점을 더 많이 얻을 수 있음
- 다른 너비에서 reflow하는 작업은 레이아웃 트리를 다시 만드는 것보다 빠름
API 구성
- 기본 단계는 두 가지임
- 폰트 등록
- Hyperscript API나 Parse API로 DOM 생성
- 단순 사용에서는 DOM을 canvas에 바로 렌더링할 수 있음
renderToCanvas(el, canvas)는 canvas의 width와 height를 viewport 크기로 사용해 전체 레이아웃을 렌더링함
- 더 낮은 수준 API를 쓰면 레이아웃을 보존할 수 있음
- 의존 리소스 로드
- DOM의 layout 생성
- layout reflow
- HTML5 canvas 같은 대상에 paint
- 이 방식은 다른 크기로 reflow하거나, 보이지 않는 레이아웃을 페인트하지 않거나, intrinsics를 얻는 데 사용할 수 있음
폰트 처리
- Dropflow 프로그램의 첫 단계는 CSS font 속성으로 선택될 폰트를 등록하는 것임
- Dropflow는 시스템 폰트를 검색하지 않으므로, 최소 한 번
FontFace를 만들고 추가해야 함 - 폰트 등록 API는 CSS Font Loading API의 부분집합을 구현하고, 비표준
loadSync메서드를 추가함 - 서버 측에서는
file:///URL을readFileSync로 동기 로드할 수 있음 ArrayBuffer는 브라우저처럼 생성자에서 즉시 로드됨registerNotoFonts는 모든 Noto Sans 폰트 패밀리를 등록함- 폰트는 FontSource가 배포하고 jsDelivr가 호스팅함
- Noto Sans 폰트가 200개를 넘고 CJK 폰트의
unicodeRange문자열이 커서 큰 import임 - 브라우저 프로덕션 사용에서는 개별 폰트를 등록하는 편이 더 낫다고 되어 있음
- Latin에는 italic 폰트가 등록되고, 모든 스크립트에는 normal 400과 bold 700이 등록됨
- 중국어·한국어·일본어는 공통 Unicode 코드 포인트를 공유하지만 문자가 다르게 렌더링될 수 있으므로, 가능하면 특정 언어용 폰트를 쓰는 편이 더 좋음
레이아웃, reflow, paint
layout(el)은 box tree, fragmentation tree, glyph를 포함하는 layout을 만듦- box tree는 DOM tree와 대체로 대응하지만, 익명 텍스트 콘텐츠 때문에 더 많은 box가 생기거나
display: none때문에 더 적을 수 있음
- box tree는 DOM tree와 대체로 대응하지만, 익명 텍스트 콘텐츠 때문에 더 많은 box가 생기거나
reflow(layout, width = 640, height = 480)는 box를 배치하고 텍스트를 줄로 나눠 paint 가능한 상태로 만듦- block box의 margin collapsing
- HarfBuzz에 텍스트 전달
- 폰트 폴백 반복
- 줄바꿈과 break point에 따른 reshaping
- float 배치와
clear처리 direction과 텍스트 방향에 따른 shaped text span과 background 배치float,inline-block,absolute콘텐츠의 intrinsics 계산- normal flow 이후
position처리
- Paint 대상은 현재 Canvas와 SVG를 지원함
paintToCanvas는 브라우저 canvas, node-canvas, 유사한 표준 준수 context에 paint함paintToSvg는 SVG 문자열을 만들고,FontFace에 넘긴 URL을 참조하는@font-face규칙을 포함함paintToSvgElements는 기존 SVG 안에 그릴 때 쓰도록<svg>와@font-face규칙을 추가하지 않음paintToHtml은 절대 위치 요소의 flat list를 생성하며, 사용을 권장하지는 않지만 개발 중 유용할 수 있음
DOM API와 환경 훅
- Hyperscript와 Parse API에서 얻은 루트
HTMLElement는 browser의querySelector계열처럼 tag name,id,class로 요소를 찾는 메서드를 제공함query(selector)는 하나의HTMLElement또는null을 반환함queryAll(selector)는HTMLElement[]를 반환함
HTMLElement는 연결된 render box를 가질 수 있음- 일반적으로 하나지만, inline과 block 콘텐츠가 섞인 경우 여러 개일 수 있음
BlockContainer는 absolute positioned element, floated element, inline-block, block-level element에 생성됨ReplacedBox는 이미지에 생성됨
- Dropflow는 다양한 환경에 맞게 조정할 수 있도록 설계됨
- 브라우저에서는
fetch로 폰트와 이미지를 로드하고, 폰트 버퍼를document.fonts에 등록함 - Nodejs에서는
fs.readFileSync로 폰트를 동기 로드할 수 있음 - canvas 백엔드와
node-canvas가 있으면node-canvas의registerFont를 호출함 node-canvas는 font buffer를 지원하지 않으므로file://URL을 사용해야 함
- 브라우저에서는
@napi-rs/canvas나skia-canvas를 쓰려면flow.environment.registerFont를 해당 폰트 등록 API에 연결하는 몇 줄의 코드가 필요함- 환경에는 6개 훅이 있음
wasmLocatorregisterFontresolveUrlresolveUrlSynccreateDecodedImagedestroyDecodedImage
HarfBuzz 기반 텍스트 shaping
- Glyph layout은 WebAssembly로 컴파일된 HarfBuzz가 수행함
measureTextAPI로 텍스트 span 위치를 잡는 방식으로는 얻기 어려운 정확성을 목표로 함- 예로 Google Sheets에서
"AV"의"V"만 다른 색으로 칠하면 kerning이 사라져 글자가 원래보다 멀어짐- 두 번의
measureText와fillText호출이 글자별로 발생하면서 contextual glyph advance가 사라지기 때문임
- 두 번의
- Dropflow는 색이 바뀌는 지점이 아니라 더 거친 shaping boundary에서 HarfBuzz를 사용해 폰트를 더 정확히 지원함
- WebAssembly로 컴파일된 HarfBuzz는
CanvasRenderingContext2D의measureText와 비슷한 성능 지표를 낼 수 있음measureText만큼 빠르지는 않지만 크게 느리지는 않다고 되어 있음- 둘 다 텍스트 레이아웃 스택의 지배적인 병목은 아니라고 되어 있음
measureText기반 텍스트 레이아웃은 빠르게 동작하려면 word cache가 필요하며, GSuite 앱이 이 방식을 쓴다고 되어 있음- word cache는 공백을 가로지르는 효과가 있는 폰트를 지원하지 못함
- 그런 폰트를 지원하려면 문단의 break index에서 이진 검색이 필요하고, 전체 문단을 HarfBuzz에 넘기는 것보다 훨씬 느리다고 되어 있음
- 색이 있는 발음 구별 기호는
measureText로는 불가능함
의존 프로젝트
- Dropflow는
package.json의존성이 없지만, 여러 프로젝트의 작업을 활용함 - JavaScript 의존성은 프로젝트에 체크인되어 있으며, 초점 유지와 dependency-of-dependency 문제를 피하기 위해 다양한 정도로 수정됨
- 주요 프로젝트는 다음과 같음
- harfbuzz: font shaping과 필수 font API 제공
- Tehreer/SheenBidi: bidi boundary 계산
- foliojs/linebreak: Unicode break index 제공
- peggyjs/peggy: CSS parser 생성
- fb55/htmlparser2: HTML parsing
- google/emoji-segmenter: emoji segmenting
- foliojs/grapheme-breaker: Unicode grapheme boundary 제공, Unicode 15용으로 크게 수정됨
- foliojs/unicode-trie: 빠른 Unicode data에 사용되며, 미사용 부분 제거를 위해 크게 수정됨