전체 글

gpdf에서 한 단락에 두 글꼴을 섞는 방법

gpdf에서 한 단락 안에 여러 글꼴을 섞으려면 c.RichText를 쓰고 각 span에 template.FontFamily를 지정한다. c.Text는 문자열 전체에 한 글꼴만 적용한다.

질문을 다시 말하면

한 단락 — 한 문장, 라벨, 표 셀 — 이 있고, 그 일부를 다른 글꼴로 쓰고 싶다. Helvetica 한 줄 안에 고정폭 코드 조각을. ASCII 주문 번호 옆에 일본어 이름을 Noto Sans JP로. 텍스트를 별도 블록으로 쪼개지 않고 단락 중간에 글꼴을 바꾸려면 어떻게 하나.

짧은 답

c.Text는 여기서 맞는 도구가 아니다. 문자열 전체에 하나의 document.Style — 글꼴 패밀리도 하나 — 을 적용한다. 원하는 건 c.RichText이고, 여기선 각 span이 자기 스타일을 갖는다:

c.RichText(func(rt *template.RichTextBuilder) {
    rt.Span("Run ")
    rt.Span("gofmt ./...", template.FontFamily("Courier"))
    rt.Span(" before you commit.")
})

span 3개, 글꼴 2개, 단락 1개. 레이아웃 엔진은 워드 프로세서처럼 span 경계를 넘나들며 줄을 바꾸므로, 고정폭 조각은 주변 Helvetica와 인라인으로 흐른다.

CourierWithFont 호출 없이 동작하는 건 PDF Standard 14 글꼴 중 하나이기 때문이다 — Helvetica, Times-Roman처럼 모든 뷰어가 이미 갖고 있다. 두 번째 글꼴이 직접 제공하는 TrueType 파일(브랜드 글꼴, CJK 글꼴)이라면 한 번 등록하고 이름으로 참조한다. 아래에서 더 다룬다.

동작하는 코드 (Helvetica + Courier, 글꼴 파일 없음)

package main

import (
    "log"
    "os"

    "github.com/gpdf-dev/gpdf"
    "github.com/gpdf-dev/gpdf/document"
    "github.com/gpdf-dev/gpdf/pdf"
    "github.com/gpdf-dev/gpdf/template"
)

func main() {
    doc := gpdf.NewDocument(
        gpdf.WithPageSize(gpdf.A4),
        gpdf.WithMargins(document.UniformEdges(document.Mm(20))),
    )

    page := doc.AddPage()
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(12, func(c *template.ColBuilder) {
            c.RichText(func(rt *template.RichTextBuilder) {
                rt.Span("Run ")
                rt.Span("gofmt ./...", template.FontFamily("Courier"))
                rt.Span(" before every commit. ")
                rt.Span("It is not optional", template.Bold(), template.Italic())
                rt.Span(".")
            })
            c.RichText(func(rt *template.RichTextBuilder) {
                rt.Span("The field is ")
                rt.Span("created_at", template.FontFamily("Courier"), template.TextColor(pdf.RGBHex(0xB00020)))
                rt.Span(" — not ")
                rt.Span("createdAt", template.FontFamily("Courier"))
                rt.Span(".")
            })
        })
    })

    data, err := doc.Generate()
    if err != nil {
        log.Fatal(err)
    }
    if err := os.WriteFile("mixed-fonts.pdf", data, 0o644); err != nil {
        log.Fatal(err)
    }
}

본문은 Helvetica(기본값) 그대로, 인라인 식별자는 Courier로 바뀌고, 한 span은 기본 글꼴 위에 bold + italic을 얹는다. WithFont 없음, 임베드 글꼴 데이터 없음 — PDF는 Helvetica, Helvetica-BoldOblique, Courier를 임베드되지 않은 Type 1 항목으로 참조하며, 모든 리더가 이미 갖고 있는 글꼴이다.

RichText가 span들에 하는 일

rt.Span은 자기 스타일 복사본을 가진 document.RichTextFragment가 된다. 옵션 없이 호출한 span은 블록 스타일 — RichText에서는 열의 기본값, 즉 문서의 기본 글꼴과 크기 — 을 상속한다. template.FontFamily("Courier")로 호출한 span은 그 필드만 덮어쓰고 나머지는 그대로 둔다.

레이아웃 시 gpdf는 각 fragment를 단어 단위 run으로 나누고, 각 run을 그 run 자신의 글꼴 메트릭으로 측정한 다음 — 그래서 같은 줄의 Courier 단어와 Helvetica 단어가 올바른 폭이 된다 — 탐욕 알고리즘으로 run을 줄에 채운다. 한 줄의 run들은 베이스라인을 공유하므로, 24 pt span 옆에 12 pt span이 오면 아래쪽에서 정렬되고 줄 높이는 큰 쪽에 맞춰 늘어난다.

여기서 한 가지 헷갈리는 게 있다: c.RichText의 두 번째 인자는 단락 수준 스타일이고, span별 옵션은 조각 수준이다.

옵션어디에 두나
FontFamily / FontSize / Bold / Italic / TextColor / Underline / Strikethroughspan별 — 각 rt.Span에 전달
AlignLeft / AlignCenter / AlignRight / AlignJustify, 줄 높이, TextIndent단락 수준 — c.RichText의 두 번째 인자로 전달

개별 rt.SpanAlignRight()를 붙여도 아무 일도 일어나지 않는다. 정렬은 줄의 속성이지 조각의 속성이 아니다.

진짜 사례: 라틴 글꼴 옆에 CJK 글꼴

문장 안 고정폭은 쉬운 버전이다. 사람들이 실제로 씨름하는 건 한 줄 안에서 서구 글꼴과 CJK 글꼴을 섞는 것 — 영어 라벨과 일본어 값, 제품 코드와 商品名. 두 가지를 알아야 한다.

첫째, gpdf는 문자 체계로 글꼴을 고르지 않는다. 어떤 span의 패밀리가 Helvetica인데 텍스트가 日本語라면 두부 글자(□)가 나온다 — Helvetica에는 CJK 글리프가 없고, gpdf는 다른 등록된 글꼴을 몰래 끌어와 메우지 않는다. CJK span에는 직접 CJK 패밀리를 붙여야 한다:

ttf, _ := os.ReadFile("NotoSansJP-Regular.ttf")

doc := gpdf.NewDocument(
    gpdf.WithFont("NotoSansJP", ttf),
)
// ...
c.RichText(func(rt *template.RichTextBuilder) {
    rt.Span("Customer: ")                                 // 기본값 → Helvetica
    rt.Span("山田 太郎", template.FontFamily("NotoSansJP")) // CJK → Noto Sans JP
    rt.Span("  (ID 10293)")                               // 다시 Helvetica
})

둘째 — 그리고 이건 입 밖으로 말할 가치가 있는데 — 일본어 CJK 글꼴 대부분은 이미 쓸 만한 라틴 글리프를 갖고 있다. Noto Sans JP, IPAex, Source Han Sans, 전부 ID 10293을 잘 그린다. 그러니 span별 혼용에 손대기 전에, 정말 두 글꼴이 필요한지 아니면 그냥 습관으로 여기까지 온 건지 따져 보라. 문서 전체가 "일본어 + 약간의 ASCII"라면 가장 간단한 건 gpdf.WithDefaultFont("NotoSansJP", 11)이고 아예 섞지 않는 것이다. RichText + FontFamily는 정말로 다른 인상을 원할 때 쓴다 — 숫자엔 깔끔한 기하학적 라틴체, 본문엔 휴머니스트 CJK체 — 단지 문자가 표시되게 하려는 게 아니라.

c.Text로 충분할 때

문자열 전체가 한 글꼴이면 c.Text를 계속 써라 — 더 가볍고 읽기도 낫다. c.Text("発行日: 2026-05-11", template.FontFamily("NotoSansJP"))은 줄 전체가 한 글꼴이고 c.Text가 처리한다. RichText가 값을 하는 건 스타일이 문자열 안에서 바뀔 때뿐이다. 단일 스타일 한 줄을 그저 할 수 있다는 이유로 RichText 콜백으로 감싸지 마라.

관련 레시피

gpdf 써 보기

gpdf는 PDF를 생성하는 Go 라이브러리다. MIT 라이선스, 외부 의존성 제로, CJK 기본 지원.

go get github.com/gpdf-dev/gpdf

⭐ Star on GitHub · 문서 읽기