전체 글

gofpdf이 보관됨. gpdf로 마이그레이션하는 완전 가이드

gofpdf은 2021년 보관, 후속 go-pdf/fpdf도 2025년 중단. CJK 네이티브·의존성 제로의 순수 Go 라이브러리 gpdf로 옮기는 법.

TL;DR

gpdf는 순수 Go·외부 의존성 제로·네이티브 CJK 지원 PDF 라이브러리. AddUTF8Font 같은 번거로운 절차도, SetXY로 좌표를 직접 다루는 일도 없다. Bootstrap 식 12-컬럼 그리드로 선언적으로 작성할 수 있고, 벤치마크에선 gofpdf보다 약 10배 빠름. 마이그레이션은 대체로 "명령형 커서 조작"을 "선언형 빌더"로 바꾸는 작업이며, Before/After 다섯 쌍으로 전체 매핑을 살펴본다.

지난주 동료가 새 Go 프로젝트를 열고 go get github.com/jung-kurt/gofpdf를 실행했다. 10분 뒤 GitHub 배너 스크린샷이 날아왔다: "This repository has been archived by the owner. It is now read-only." 그리고 한마디, "잠깐, 포크도 아카이브된 거야?"

맞다. 둘 다 그렇다.

jung-kurt/gofpdf2021년 9월 8일 보관되었다. 커뮤니티 포크 go-pdf/fpdf의 마지막 릴리스는 2023년이고 2025년에 정식으로 보관되었다. Stack Overflow와 한국어 블로그의 Go PDF 관련 답변 대부분이 아직도 gofpdf를 가리키지만, 그건 4년 넘게 read-only 상태이고 후계자로 여겨지던 포크마저 사라졌다.

운영 중인 gofpdf 코드가 있다면 이 글은 마이그레이션 지도. 신규 프로젝트에서 검색 결과에 끌려 반사적으로 gofpdf를 집으려 했다면, 이 글이 그대로 대안이다.

왜 gofpdf은 정말로 돌아오지 않는가

오픈소스가 반드시 죽는 건 아니다. 원래 메인테이너가 손을 뗐어도 다른 누가 이어받는 경우가 있다. gofpdf도 그렇게 될 줄 알았고, 한동안은 실제로 그랬다. go-pdf/fpdf는 코드를 정리하고, 오래된 버그를 고치고, PR을 받으며 "진정한 후계"처럼 보였다.

그러다 2025년 초 포크도 보관되었다. README에는 이렇게 쓰여 있다: "이 프로젝트는 더 이상 적극적으로 유지되지 않습니다. 다른 라이브러리 사용을 고려해 주세요."

이유보다 결과가 중요하다. gofpdf에 의존하는 모든 Go 프로젝트는 지금 두 겹의 미유지 코드 위에 앉아 있다. 보안 이슈는 패치되지 않는다. PDF 2.0 스펙은 2020년에 나왔지만 gofpdf는 대부분 따라잡지 못했다. Go 1.25의 루프 변수 동작은 지금은 gofpdf와 문제없이 작동하지만 내일 깨진다면 포크해서 고치는 건 당신 몫이다.

"라이브러리에 버그가 있다"는 문제가 아니라 공급망 문제다.

한국 팀엔 특히 민감한 지점이 있다. 전자세금계산서, 전자문서 원본성 인증, 공공기관 납품용 PDF/A 요건 등에서 미유지 라이브러리로 기술 스택을 정당화하기 어렵다.

한국 팀이 gofpdf로 실제 하던 일

GitHub Issues와 한국 기술 블로그 글을 살피면 gofpdf의 주요 용도는 다음 네 가지다:

  1. 세금계산서·영수증·납품서 — 헤더, 거래처, 명세표, 합계, 푸터
  2. 리포트 — 헤더와 페이지 번호가 반복되는 다중 페이지 문서
  3. 증명서·양식 — 템플릿 이미지 위에 고정 위치로 텍스트를 얹음
  4. CJK 문서 — 한국어·일본어·중국어 혼용 송장, 배송 라벨

앞의 세 가지는 gpdf 빌더 API로 바로 덮인다. 네 번째 CJK가 gpdf가 gofpdf 대비 가장 격차를 크게 두는 영역. gofpdf은 AddUTF8Font를 호출하고 TTF 경로를 관리하며 기본 문자면 밖으로 글자가 나가지 않기를 기도해야 했다. gpdf는 CJK를 처음부터 일급 시민으로 다룬다 — TrueType 폰트를 등록하고 한국어를 쓰고, PDF가 나온다. 끝.

API 매핑표

아래 표가 치트시트. 이후 섹션에서 다섯 개의 구체적 Before/After를 본다.

하고 싶은 것gofpdfgpdf
문서 생성gofpdf.New("P", "mm", "A4", "")gpdf.NewDocument(gpdf.WithPageSize(document.A4))
페이지 추가pdf.AddPage()doc.AddPage() (*PageBuilder 반환)
폰트 지정pdf.SetFont("Arial", "B", 16)template.FontFamily(...) / template.Bold() / template.FontSize(16)
TTF 등록 (CJK)pdf.AddUTF8Font("noto", "", "NotoSansKR-Regular.ttf")gpdf.WithFont("NotoSansKR", ttfBytes) (생성 시 전달)
한 줄 텍스트pdf.Cell(40, 10, "hi")c.Text("hi")
자동 줄바꿈 텍스트pdf.MultiCell(0, 10, body, "", "L", false)c.Text(body) (자동 줄바꿈)
텍스트 색상pdf.SetTextColor(255, 0, 0)template.TextColor(pdf.Red) (텍스트별 옵션)
가로선pdf.Line(x1, y1, x2, y2)c.Line(template.LineThickness(document.Pt(1)))
이미지 삽입pdf.ImageOptions("logo.png", x, y, w, h, ...)c.Image(imgBytes, template.FitWidth(document.Mm(50)))
커서 위치pdf.SetXY(x, y)(없음 — 행/열로 작성, 또는 page.Absolute(x, y, fn))
모든 페이지 헤더pdf.SetHeaderFunc(fn)doc.Header(fn)
모든 페이지 푸터pdf.SetFooterFunc(fn)doc.Footer(fn)
페이지 번호pdf.PageNo()(수동)c.PageNumber() / c.TotalPages()
파일로 출력pdf.OutputFileAndClose("out.pdf")data, _ := doc.Generate(); os.WriteFile("out.pdf", data, 0o644)
io.Writer로 출력pdf.Output(w)doc.Render(w)

가장 큰 차이는 API 형태다. gofpdf는 명령형, gpdf는 선언형. gofpdf는 커서를 옮기며 쓴다. gpdf는 행과 열의 트리를 기술하고 레이아웃 엔진이 배치한다. 초반 몇 개는 gpdf 쪽이 더 길게 느껴진다. 세 번째쯤 되면 SetXY가 그리워지지 않는다.

단위 얘기. gofpdf는 생성 시 기본 단위("mm" / "pt" / "in")를 고른다. gpdf는 내부적으로 전부 pt로 고정하고, 호출부에서 document.Mm(20), document.Pt(12), document.Cm(1) 같은 헬퍼를 쓴다. CSS 감각에 가까운데, 헤더 여백을 document.Mm(15)로 잡고 난 뒤로는 단위를 의식하지 않게 된다.

Before / After 1: 가장 단순한 PDF

"hello world" 쌍. gofpdf의 간결함이 인기의 이유였다. gpdf 버전은 몇 줄 더 길다 — 커서를 움직이는 게 아니라 트리를 만들기 때문.

Before — gofpdf:

package main

import "github.com/jung-kurt/gofpdf"

func main() {
    pdf := gofpdf.New("P", "mm", "A4", "")
    pdf.AddPage()
    pdf.SetFont("Arial", "B", 24)
    pdf.Cell(40, 10, "Hello, World!")
    pdf.OutputFileAndClose("hello.pdf")
}

After — gpdf:

package main

import (
    "log"
    "os"

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

func main() {
    doc := gpdf.NewDocument(
        gpdf.WithPageSize(document.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.Text("Hello, World!", template.FontSize(24), template.Bold())
        })
    })

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

그리드가 일을 한다. AutoRow는 내용 높이로 결정되는 행을 추가하고 r.Col(12, ...)는 "12 그리드 전체를 차지하는 컬럼"을 뜻한다. Bootstrap과 같은 발상을 PDF 페이지에 적용한 것뿐.

Generate()는 바이트 슬라이스를 돌려준다. io.Writer로 흘려 보내고 싶다면 Render(w). "파일을 닫는" 단계가 없는 건 gpdf가 파일 핸들을 소유하지 않기 때문.

Before / After 2: 명세표

세금계산서 명세표는 gofpdf가 가장 수다스러워지는 지점. 내장 테이블이 없어서 Cell을 중첩 루프로 호출하고, 열 너비는 직접 계산하고, Ln(-1)로 개행한다. gofpdf 세금계산서 튜토리얼 글의 절반은 테이블 보일러플레이트로 채워져 있다.

Before — gofpdf:

pdf.SetFont("Arial", "B", 11)
pdf.SetFillColor(220, 220, 220)
pdf.CellFormat(80, 8, "품목",   "1", 0, "L", true, 0, "")
pdf.CellFormat(20, 8, "수량",   "1", 0, "C", true, 0, "")
pdf.CellFormat(30, 8, "단가",   "1", 0, "R", true, 0, "")
pdf.CellFormat(30, 8, "금액",   "1", 1, "R", true, 0, "")

pdf.SetFont("Arial", "", 11)
items := [][]string{
    {"프론트엔드 개발", "40h", "₩180,000", "₩7,200,000"},
    {"백엔드 개발",    "60h", "₩180,000", "₩10,800,000"},
    {"UI 디자인",      "20h", "₩150,000", "₩3,000,000"},
}
for _, row := range items {
    pdf.CellFormat(80, 8, row[0], "1", 0, "L", false, 0, "")
    pdf.CellFormat(20, 8, row[1], "1", 0, "C", false, 0, "")
    pdf.CellFormat(30, 8, row[2], "1", 0, "R", false, 0, "")
    pdf.CellFormat(30, 8, row[3], "1", 1, "R", false, 0, "")
}

머릿속에서 열 너비를 계산해 가며 써야 한다. 품목명이 줄바꿈되면 망가진다.

After — gpdf:

page.AutoRow(func(r *template.RowBuilder) {
    r.Col(12, func(c *template.ColBuilder) {
        c.Table(
            []string{"품목", "수량", "단가", "금액"},
            [][]string{
                {"프론트엔드 개발", "40h", "₩180,000", "₩7,200,000"},
                {"백엔드 개발",    "60h", "₩180,000", "₩10,800,000"},
                {"UI 디자인",      "20h", "₩150,000", "₩3,000,000"},
            },
            template.ColumnWidths(50, 15, 15, 20),
            template.TableHeaderStyle(
                template.Bold(),
                template.TextColor(pdf.White),
                template.BgColor(pdf.RGBHex(0x1A237E)),
            ),
            template.TableStripe(pdf.RGBHex(0xF5F5F5)),
        )
    })
})

ColumnWidths(50, 15, 15, 20)의 숫자는 절대 mm가 아니라, 표가 올라가는 컬럼 내의 비율. 같은 표를 r.Col(6, ...) 안에 넣어도 이 비율이 그대로 잘 맞는다. CellFormat을 래핑하지 않고는 닿을 수 없던 추상.

줄바꿈은 자동. 페이지 분리도 자동 — 표가 하단 여백을 넘으면 다음 페이지에 헤더 행이 다시 그려진다.

Before / After 3: 거추장스러운 절차 없이 한국어 쓰기

gofpdf을 포기한 결정적 이유가 여기. gofpdf에서 한국어를 출력하려면 AddUTF8Font를 호출하고, 디스크의 TTF를 가리키고, 폰트를 설정하고, 기도한다. 서브셋팅은 대부분 동작한다. 일부 TTF는 글리프 ID 충돌을 일으켜 깨진 문자를 낸다. 에러 메시지는 도움이 안 된다.

Before — gofpdf:

pdf := gofpdf.New("P", "mm", "A4", "")
pdf.AddUTF8Font("notosanskr", "", "NotoSansKR-Regular.ttf")
pdf.AddPage()
pdf.SetFont("notosanskr", "", 14)
pdf.Cell(0, 10, "안녕하세요, 세계.")
pdf.OutputFileAndClose("ko.pdf")

지뢰 두 개. TTF가 런타임에 지정 경로에 존재해야 하고, 그래서 Docker 이미지에 폰트를 함께 담아야 한다. Cell 너비를 0으로 주면 "오른쪽 여백까지"를 뜻하는데, CJK에서는 폭 추정이 전각 문자를 제대로 계산하지 못해 잘리는 일이 잦다.

After — gpdf:

package main

import (
    "log"
    "os"

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

func main() {
    fontData, err := os.ReadFile("NotoSansKR-Regular.ttf")
    if err != nil {
        log.Fatal(err)
    }

    doc := gpdf.NewDocument(
        gpdf.WithPageSize(document.A4),
        gpdf.WithMargins(document.UniformEdges(document.Mm(20))),
        gpdf.WithFont("NotoSansKR", fontData),
        gpdf.WithDefaultFont("NotoSansKR", 14),
    )

    page := doc.AddPage()
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(12, func(c *template.ColBuilder) {
            c.Text("안녕하세요, 세계.")
            c.Text("대한민국 서울특별시 강남구 테헤란로 427")
            c.Text("천 리 길도 한 걸음부터.")
        })
    })

    data, _ := doc.Generate()
    os.WriteFile("ko.pdf", data, 0o644)
}

다른 점이 둘.

첫째, 경로가 아니라 바이트를 넘긴다. //go:embed NotoSansKR-Regular.ttf로 TTF를 바이너리에 포함시키면 배포가 자기완결적이 된다. 프로덕션에서 "폰트를 찾을 수 없음"이 발생하지 않는다.

둘째, gpdf의 TrueType 서브셋팅은 CJK cmap 형식(4, 6, 12)과 Identity-H 인코딩을 이해한다. 출력 PDF에는 실제로 사용한 글리프만 들어간다 — NotoSansKR을 200자짜리 세금계산서에 넣으면 약 30 KB 서브셋이 된다. 4 MB 풀 임베드가 아니다. gofpdf로 한국어 한 페이지 PDF가 5 MB가 되는 걸 본 적이 있다면 가장 먼저 느끼는 차이가 이것.

나눔스퀘어, Pretendard, 본고딕 등 한국어 폰트별 심화 주제는 별도 글에서 다룰 예정.

Before / After 4: 모든 페이지 헤더 + 푸터 페이지 번호

gofpdf의 반복 크롬 패턴은 SetHeaderFunc / SetFooterFunc. 각각 현재 커서에 대해 실행되는 func()를 받는다. 페이지 번호는 pdf.PageNo() + pdf.AliasNbPages().

Before — gofpdf:

pdf := gofpdf.New("P", "mm", "A4", "")
pdf.SetHeaderFunc(func() {
    pdf.SetFont("Arial", "B", 12)
    pdf.Cell(0, 10, "ACME 주식회사")
    pdf.Ln(15)
})
pdf.SetFooterFunc(func() {
    pdf.SetY(-15)
    pdf.SetFont("Arial", "I", 8)
    pdf.CellFormat(0, 10,
        fmt.Sprintf("Page %d/{nb}", pdf.PageNo()),
        "", 0, "C", false, 0, "")
})
pdf.AliasNbPages("")
pdf.AddPage()
// ... body ...

{nb}는 gofpdf가 출력 시 전체 페이지 수로 치환하는 센티넬. 동작은 하지만 "알고 있어야만 쓴다"는 부류.

After — gpdf:

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

doc.Header(func(p *template.PageBuilder) {
    p.AutoRow(func(r *template.RowBuilder) {
        r.Col(12, func(c *template.ColBuilder) {
            c.Text("ACME 주식회사", template.Bold(), template.FontSize(12))
            c.Line(template.LineColor(pdf.Gray(0.7)))
            c.Spacer(document.Mm(4))
        })
    })
})

doc.Footer(func(p *template.PageBuilder) {
    p.AutoRow(func(r *template.RowBuilder) {
        r.Col(6, func(c *template.ColBuilder) {
            c.Text("ACME 주식회사",
                template.FontSize(8), template.TextColor(pdf.Gray(0.5)))
        })
        r.Col(6, func(c *template.ColBuilder) {
            // "페이지 X / Y" — 둘 다 플레이스홀더로,
            // 페이지네이션 완료 후 레이아웃 엔진이 해결한다.
            c.PageNumber(template.AlignRight(),
                template.FontSize(8), template.TextColor(pdf.Gray(0.5)))
        })
    })
})

for i := 0; i < 10; i++ {
    page := doc.AddPage()
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(12, func(c *template.ColBuilder) {
            c.Text(fmt.Sprintf("%d 페이지 본문.", i+1))
        })
    })
}

PageNumberTotalPages는 플레이스홀더. 레이아웃 엔진이 페이지 수를 확정한 뒤 확장된다. {nb} 센티넬도, SetY(-15)로 푸터를 바닥에 박아두는 작업도 필요 없다 — 푸터는 그냥 트리일 뿐이고, 엔진이 매 페이지 자동으로 공간을 확보한다.

Before / After 5: HTTP 핸들러에 바이트 반환

실제 운영 중인 gofpdf 코드 대부분은 파일이 아니라 io.Writer에 쓴다. 주로 application/pdf를 반환하는 http.ResponseWriter. 이 쌍에서 gpdf API가 gofpdf에 가장 가깝다.

Before — gofpdf:

func handler(w http.ResponseWriter, r *http.Request) {
    pdf := gofpdf.New("P", "mm", "A4", "")
    pdf.AddPage()
    pdf.SetFont("Arial", "", 12)
    pdf.Cell(0, 10, "생성 시간: "+time.Now().Format(time.RFC3339))

    w.Header().Set("Content-Type", "application/pdf")
    if err := pdf.Output(w); err != nil {
        http.Error(w, err.Error(), 500)
    }
}

After — gpdf:

func handler(w http.ResponseWriter, r *http.Request) {
    doc := gpdf.NewDocument(
        gpdf.WithPageSize(document.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.Text("생성 시간: " + time.Now().Format(time.RFC3339))
        })
    })

    w.Header().Set("Content-Type", "application/pdf")
    if err := doc.Render(w); err != nil {
        http.Error(w, err.Error(), 500)
    }
}

형태는 같다. doc.Render(w)가 PDF를 응답으로 바로 흘려보낸다. Content-Length를 세우고 싶다면 먼저 Generate()로 바이트를 받아 len()을 찍는다.

"충분히 빠름"은 얼마나 빠른가

gpdf는 실전 워크로드에서 gofpdf보다 약 10배 빠름. 아래 수치는 _benchmark/benchmark_test.go를 Apple M1, Go 1.25에서 돌린 결과다.

벤치마크gpdfgofpdfgopdfMaroto v2
단일 페이지13 µs132 µs423 µs237 µs
4×10 테이블108 µs241 µs835 µs8.6 ms
100 페이지683 µs11.7 ms8.6 ms19.8 ms
복잡한 CJK 세금계산서133 µs254 µs997 µs10.4 ms

합성 벤치마크가 아니다. 테이블 벤치는 4열 10행 세금계산서 명세, 100페이지 벤치는 헤더와 페이지 번호가 반복되는 리포트 — 실제 프로덕션 코드 모양에 맞춰져 있다.

의미 얘기도 가볍게. 13 µs/단일 페이지 = 싱글 코어로 초당 7만 5천 장의 hello-world PDF, 108 µs/테이블 포함 = 초당 약 9,000장의 세금계산서. 핵심은 자랑이 아니라, "PDF 생성을 캐시해야 할까? 비동기 큐로 빼야 할까?" 같은 고민이 사라진다는 것. 대부분의 워크로드는 요청 경로에서 동기 생성으로 충분하다.

마이그레이션에서 잃는 것

가이드가 현실의 간극을 가리면 의미가 없다. gpdf가 아직 gofpdf 대비 약한 지점을 솔직히 적는다:

  • 임의 각도 선, 베지어, 복잡한 패스. c.Line()은 컬럼을 가로지르는 수평선을 그린다. CAD 도면이나 자체 차트 지오메트리는 아직 못 담는다. (차트를 이미지로 사전 렌더링해서 삽입하는 건 문제없이 된다.)
  • SetXY 기반 절대 좌표 코드. page.Absolute(x, y, fn)로 비슷한 것은 할 수 있지만, 기존 코드가 2,000줄 SetXY + Cell이라면 이전은 사실상 재작성. 다만 재작성본이 보통 원본의 절반 길이가 되는 게 위안.
  • AcroForm (입력 가능 폼). gpdf는 아직 생성하지 않는다. PDF가 뷰어에서 사용자가 채우는 템플릿이라면 당분간 AcroForm 지원 라이브러리에 남는 선택지.
  • 주석·북마크. 기본 아웃라인은 되지만 리치 주석은 미지원.

이 중 어느 것도 당신에게 걸리지 않는다면 마이그레이션은 매끄럽게 끝난다. 걸린다면 Issue를 열어 달라 — 로드맵은 요청 기반으로 돌아간다.

FAQ

gpdf는 gofpdf의 포크인가? 아니다. gpdf는 순수 Go로 처음부터 다시 구현했다. PDF 와이어 포맷, 레이아웃 엔진, TrueType 서브셋팅 — 전부 새로 작성. gofpdf나 그 포크와 공유하는 코드는 없다. 왜 포크가 아닌 재구현이냐면, gofpdf 아키텍처는 "단일 변경 가능 커서"를 전제로 만들어져 있어서 선언형 그리드를 뒤에 얹으면 기존 호출이 전부 깨지기 때문.

외부 의존성은? 코어는 제로. go get github.com/gpdf-dev/gpdfgo mod graph | grep gpdf를 치면 한 줄만 나온다. gpdf-pro 확장(HTML→PDF, AES 암호화, 서명, PDF/A)은 HTML 파싱을 위해 golang.org/x/net을 끌어오지만, 이건 옵트인이고 마이그레이션에 필수가 아니다.

CGO는? gofpdf은 CGO-free였는데 gpdf는? 똑같이 순수 Go, CGO 없음. GOOS=linux GOARCH=arm64 go build로 크로스 컴파일해 정적 바이너리를 배포할 수 있다. Distroless나 Alpine에서는 CGO 툴체인이 없는 것만으로 이미지 크기가 절반이 되기 때문에 중요한 포인트.

기존 gofpdf 코드가 SetXY 투성이. 재작성 없이 이전할 수 있나?page.Absolute(x, y, fn)를 래핑하면 비슷한 감각은 낸다. 다만 코드 전체가 커서 조작 중심이라면, 레이아웃 엔진 모델로의 이전은 문법이 아니라 사고방식 전환이다. 많은 팀이 "재작성한 쪽이 원본보다 짧다"고 말한다.

전자세금계산서, 전자문서 인증은? 타임스탬프와 전자서명은 gpdf-pro에서 구현 중. 구체적 요구가 있다면 Issue로 우선순위를 올려 달라.

go-pdf/fpdf가 아카이브 해제되면? 선택지가 하나 더 생길 뿐. gpdf의 베팅은 "gofpdf이 영원히 아카이브"가 아니라 "커서 기반 + 싱글바이트 폰트 + CJK 미지원이라는 아키텍처 자체가 누가 유지해도 막다른 길"이라는 쪽. 2026년의 PDF 생성은 플로터를 조작하는 것보다 웹 페이지를 짜는 것에 가깝고, API도 그걸 반영해야 한다.

gpdf 사용해 보기

gpdf는 Go의 PDF 생성 라이브러리. MIT, 의존성 제로, CJK 지원.

go get github.com/gpdf-dev/gpdf

⭐ GitHub에서 스타 누르기 · 문서

다음으로 읽을 것

  • 12-컬럼 그리드는 gpdf에서 어떻게 동작하나 (곧 공개)
  • gpdf에 한국어 폰트 넣기 (곧 공개)
  • Quickstart — 5분 세팅, go.mod 포함