전체 글

signintech/gopdf 에서 gpdf 로 — 좌표 계산이 사라진다

signintech/gopdf 는 동작하지만 셀, 선, 헤더 모두가 (x, y) 계산식이다. 이 글은 gopdf API 를 gpdf 로 매핑한다 — 같은 Go, 좌표 계산은 사라진다.

TL;DR

gpdf 는 12 컬럼 레이아웃 엔진을 갖춘 순수 Go PDF 라이브러리. signintech/gopdf 는 PDF 좌표계의 저수준 바인딩. gopdf 를 한동안 운영해 온 코드베이스가 이제 거의 SetXY, Cell, 너비 산술로 채워져 있다면, 이 글은 그 호출들이 레이아웃 엔진 아래에서 무엇으로 축약되는지를 보여준다.

지난주에 한 분과 signintech/gopdf 기반 송장 생성기를 리팩토링했다. 5 년치 누적. 명세 행 테이블을 그리는 함수가 280 줄. 그중 실제로 일을 하는 건 약 40 줄 — 금액 포맷, 날짜 포맷, 행마다 반복. 나머지 240 줄은 좌표 계산이었다: x 위치 계산, y 추적, SetXY 호출, Cell 호출, Br 호출, Line(x1, y1, x2, y2) 로 테두리 그리기, 행이 페이지에 들어가는지 판단, 안 들어가면 수동 AddPage 와 헤더 재출력.

이게 프로덕션의 gopdf 다. 나쁜 라이브러리는 아니다. CGO 없는 얇고 빠른 PDF 이미징 모델 바인딩 — 이름 그대로다. 커서가 있고 좌표가 있고, 엔지니어가 레이아웃 엔진 역할을 한다.

이 글은 gopdf API 를 함수 단위로 gpdf 에 매핑한다. 결론은 제목에 있다: 대부분의 줄은 사라진다, 왜냐하면 그것들은 런타임이 대신 해줄 수 있는 레이아웃 수학이었기 때문이다.

signintech/gopdf 의 장점 — 그리고 그렇지 않은 점

「이전하자」서사로 들어가기 전에 솔직히 말하면, gopdf 에는 진짜 미덕이 있다.

활발히 유지보수된다. 순수 Go (CGO 없음) 라서 크로스 컴파일과 Alpine 이미지가 그냥 된다. CJK 를 포함한 TrueType 폰트를 지원한다. 출력이 빠르다 — gopdf 는 무거운 엔진 없이 PDF wire format 을 직접 쓰기 때문에 이미징 프리미티브 면에서는 gpdf 와 같은 영역에 있다. API 는 내부 PDF 모델에 그대로 매핑된다: 현재 점이 있고, 그것을 옮기고, 거기에 그린다. 이미 PDF 좌표로 사고하는 사람에게 gopdf 는 편하다.

편하지 않은 부분은 레이아웃 시스템이 없다는 점이다. 행, 열, flex, 그리드 개념이 없다. 자동 페이지 분할이 없다: 콘텐츠가 하단 여백을 넘으면 그대로 넘는다 (또는 페이지를 벗어난다), 직접 AddPage 를 호출할 때까지. 테이블은 프리미티브로 존재하지 않는다 — 프로젝트마다 다시 구현하는 패턴이다: 셀별 Cell 호출, 수동 테두리 선, 자체 페이지 분할 로직.

한 페이지짜리 수료증이나 매우 통제된 고정 양식이라면 커서 모델로 충분하다. 송장, 보고서, 명세서, 가변 길이 콘텐츠를 포함하는 모든 것은 — 좌표 계산이 문서 표면적에 비례해 늘어난다. 그게 gpdf 가 겨냥한 워크로드다.

멘탈 모델 전환

이게 코드의 읽는 맛을 실제로 바꾸는 부분이다. gpdf 에는 gopdf 에 없는 두 가지 아이디어가 있다.

선언적 트리. 렌더러에게 어디 그릴지 말하지 않는다. page → row → column → content 의 트리를 기술하면 레이아웃 엔진이 한 패스로 위치를 해결한다. 진행할 커서가 없다. 연속된 두 r.Col(...) 는 서로를 모른다.

12 컬럼 그리드. 각 행은 암묵적으로 12 단위 너비. 그것을 컬럼으로 소비한다: r.Col(8, ...) 는 2/3, r.Col(4, ...) 는 1/3. Bootstrap 과 Tailwind 가 HTML 에서 쓰는 같은 사고를 PDF 로 가져온 것. pageWidth - leftMargin - rightMargin 을 4 로 나누는 대신 r.Col(3, ...) 를 4 번 쓴다.

이 두 가지만으로 수학의 대부분이 사라진다. 뒤따르는 Before/After 쌍은 모두 같은 방식으로 축약된다: 커서를 진행시키는 명령형 루프가 작은 선언적 트리가 된다.

API 매핑 표

치트 시트 먼저. 뒤 섹션에서 다섯 쌍을 구체적으로 다룬다.

하고 싶은 일signintech/gopdfgpdf
구성pdf := gopdf.GoPdf{}; pdf.Start(gopdf.Config{...})doc := gpdf.NewDocument(gpdf.WithPageSize(document.A4), ...)
페이지 크기 설정Config{PageSize: gopdf.PageSizeA4}gpdf.WithPageSize(document.A4)
페이지 추가pdf.AddPage()page := doc.AddPage()
커서 이동pdf.SetX(40); pdf.SetY(80) (곳곳에서)(커서 없음)
한 줄 텍스트pdf.SetXY(x, y); pdf.Cell(nil, "hi")c.Text("hi") (컬럼 내부)
줄바꿈 텍스트pdf.MultiCell(&gopdf.Rect{W: 200, H: 100}, body)c.Text(body) (자동 줄바꿈)
줄 바꾸기pdf.Br(20)(행 사이 암묵적; 필요 시 c.Spacer(document.Mm(4)))
폰트 등록pdf.AddTTFFont("noto", "fonts/Noto.ttf")gpdf.WithFont("Noto", ttfBytes) (구성 시점)
활성 폰트 설정pdf.SetFont("noto", "", 14)텍스트마다 template.FontFamily("Noto"), template.FontSize(14)
색상pdf.SetTextColor(26, 35, 126)template.TextColor(pdf.RGBHex(0x1A237E))
가로 선pdf.Line(40, 100, 555, 100)c.Line(template.LineColor(pdf.Gray(0.7)))
사각형pdf.RectFromUpperLeftWithStyle(x, y, w, h, "FD")c.Box(template.BgColor(...), template.Border(...))
이미지pdf.Image("logo.png", x, y, &gopdf.Rect{W: 100, H: 50})c.Image(imgBytes, template.FitWidth(document.Mm(35)))
수동 테이블수십 개의 Cell + Line + SetXYc.Table(headers, rows, template.ColumnWidths(...))
헤더 / 푸터pdf.AddHeader(fn) / pdf.AddFooter(fn)doc.Header(fn) / doc.Footer(fn)
페이지 번호자체 카운터로 "Page %d of %d" 포맷c.PageNumber() / c.TotalPages() (플레이스홀더)
암호화Config{Protection: PDFProtectionConfig{...}}gpdf.WithEncryption(gpdf.AES256, "user", "owner", perms)
출력pdf.WritePdf("out.pdf")data, _ := doc.Generate(); os.WriteFile("out.pdf", data, 0o644)
writer 로 출력pdf.Write(w) / pdf.ToBuffer()doc.Render(w)

구조적 변화 두 가지. 첫째, 커서가 사라진다. 표에서 (곳곳에서) 표시한 행은 과장이 아니다 — 실제 프로덕션 gopdf 코드에서 SetXY 호출 수가 Cell 을 능가한다. gpdf 에서는 모두 0 으로 축약된다. 둘째, 픽셀이 백분율이 된다. Rect{W: 200, H: 100} 는 「이 컬럼은 컨테이너의 12 단위 중 4 를 차지한다」가 된다. 같은 컬럼을 절반 너비 행에 넣어도 비율이 유지되어 변경 불필요.

Before / After 1: hello world

가장 짧은 차이. 오른쪽에서 무엇이 빠졌는지 본다.

Before — signintech/gopdf:

package main

import (
    "log"

    "github.com/signintech/gopdf"
)

func main() {
    pdf := gopdf.GoPdf{}
    pdf.Start(gopdf.Config{PageSize: *gopdf.PageSizeA4})
    pdf.AddPage()

    if err := pdf.AddTTFFont("helvetica", "fonts/Helvetica.ttf"); err != nil {
        log.Fatal(err)
    }
    if err := pdf.SetFont("helvetica", "", 24); err != nil {
        log.Fatal(err)
    }

    pdf.SetX(40)
    pdf.SetY(80)
    if err := pdf.Cell(nil, "Hello, World!"); err != nil {
        log.Fatal(err)
    }

    if err := pdf.WritePdf("hello.pdf"); err != nil {
        log.Fatal(err)
    }
}

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)
    }
}

빠진 두 가지. Helvetica 는 표준 14 폰트의 일부이고 gpdf 가 번들로 제공하므로 런타임에 TTF 파일이 필요하지 않다. SetX(40); SetY(80) 도 사라졌다 — 행이 페이지 여백 안에 자동으로 배치된다. 추가된 것: 12 단위 전체를 차지하는 단일 컬럼 행. "Hello, World!" 에 이 비계는 무거워 보이지만, 100 페이지 보고서를 떠받치는 것도 같은 비계다, 라는 게 핵심.

Before / After 2: 4 컬럼 헤더 행

좌표 계산이 가장 두드러지는 곳. 페이지를 가로지르는 4 개 등폭 셀의 헤더가 필요하다: 페이지 너비 빼기 여백, 4 로 나누기. gopdf 에서는 그 나눗셈을 직접 쓴다. gpdf 에서는 12 단위를 4 등분한다.

Before — signintech/gopdf:

const (
    pageWidth   = 595.28 // A4 (pt)
    leftMargin  = 40.0
    rightMargin = 40.0
    rowY        = 100.0
    rowH        = 24.0
)

contentWidth := pageWidth - leftMargin - rightMargin // 515.28
colW := contentWidth / 4                              // 128.82

pdf.SetFont("helvetica-bold", "", 11)
pdf.SetFillColor(26, 35, 126)
pdf.SetTextColor(255, 255, 255)

headers := []string{"품목", "수량", "단가", "금액"}
for i, h := range headers {
    x := leftMargin + colW*float64(i)
    pdf.RectFromUpperLeftWithStyle(x, rowY, colW, rowH, "F")

    pdf.SetXY(x+6, rowY+7)
    if err := pdf.Cell(nil, h); err != nil {
        log.Fatal(err)
    }
}

pdf.SetTextColor(0, 0, 0)

상수 4 개, 너비 뺄셈 1 회, 나눗셈 1 회, colW*float64(i) 의 루프 — 그 float 변환은 Go 의 * 가 int 를 float64 로 자동 승격하지 않기 때문에만 존재한다. gpdf 버전에는 어느 것도 없다.

After — gpdf:

page.AutoRow(func(r *template.RowBuilder) {
    headers := []string{"품목", "수량", "단가", "금액"}
    for _, h := range headers {
        r.Col(3, func(c *template.ColBuilder) {
            c.Box(
                template.BgColor(pdf.RGBHex(0x1A237E)),
                template.Padding(document.Mm(2), document.Mm(3)),
            )
            c.Text(h,
                template.Bold(), template.FontSize(11),
                template.TextColor(pdf.White),
            )
        })
    }
})

r.Col(3, ...) 4 회 합계 12. 너비는 그리드가 처리. A4 를 Letter 로 바꾸거나 여백을 줄여도 이 코드는 pageWidth 에 일절 의존하지 않으므로 배치는 그대로 정확하다. 1 컬럼만 다른 셋의 두 배 너비로 만들고 싶다면 그 컬럼을 r.Col(6, ...) 로, 다른 하나를 r.Col(2, ...) 로 바꾸면 된다. 산술 없음.

Before / After 3: 페이지를 넘는 송장 테이블

본판. gopdf 에서 여러 페이지에 걸친 테이블을 그리는 건 거의 다 부기다: 현재 y 추적, 각 행 그리기, 다음 행이 들어가는지 검사, 안 들어가면 AddPage 호출 후 헤더 다시 그리기. 상태 기계가 코드 안에 산다.

Before — signintech/gopdf:

func drawInvoiceTable(pdf *gopdf.GoPdf, items [][4]string) error {
    const (
        pageH       = 841.89 // A4 높이
        bottomLimit = pageH - 40
        rowH        = 22.0
        leftX       = 40.0
    )
    cols := []float64{260, 80, 80, 95}

    drawHeader := func(y float64) float64 {
        pdf.SetFont("helvetica-bold", "", 11)
        pdf.SetFillColor(26, 35, 126)
        pdf.SetTextColor(255, 255, 255)
        x := leftX
        for i, h := range []string{"품목", "수량", "단가", "금액"} {
            pdf.RectFromUpperLeftWithStyle(x, y, cols[i], rowH, "F")
            pdf.SetXY(x+6, y+7)
            if err := pdf.Cell(nil, h); err != nil {
                log.Println(err)
            }
            x += cols[i]
        }
        pdf.SetTextColor(0, 0, 0)
        pdf.SetFont("helvetica", "", 11)
        return y + rowH
    }

    y := drawHeader(100)
    for _, row := range items {
        if y+rowH > bottomLimit {
            pdf.AddPage()
            y = drawHeader(60)
        }

        x := leftX
        for i, cell := range row {
            pdf.RectFromUpperLeftWithStyle(x, y, cols[i], rowH, "D")
            pdf.SetXY(x+6, y+7)
            if err := pdf.Cell(nil, cell); err != nil {
                return err
            }
            x += cols[i]
        }
        y += rowH
    }
    return nil
}

테이블 함수 30 줄, 그중 데이터에 관한 건 5 줄. 나머지는 레이아웃: 하드코딩된 높이, 하드코딩된 하한, 페이지 분할 후 헤더 재출력 클로저, for 루프 두 개, 셀당 커서 두 번 이동. 이게 gopdf 테이블의 중앙값.

After — gpdf:

page.AutoRow(func(r *template.RowBuilder) {
    r.Col(12, func(c *template.ColBuilder) {
        c.Table(
            []string{"품목", "수량", "단가", "금액"},
            items, // [][]string
            template.ColumnWidths(55, 15, 15, 15),
            template.TableHeaderStyle(
                template.Bold(),
                template.TextColor(pdf.White),
                template.BgColor(pdf.RGBHex(0x1A237E)),
            ),
            template.TableStripe(pdf.RGBHex(0xF5F5F5)),
        )
    })
})

이게 전부. 페이지 분할 자동. 본문이 이어지는 페이지에서 헤더 자동 반복. 줄무늬 행은 옵션 하나. 컬럼 너비는 컨테이너 백분율이므로 같은 테이블을 r.Col(6, ...) 안에 넣으면 비율을 유지한 채 절반 크기로 렌더링된다. 25 줄짜리 gopdf 부기 함수가 사라진다.

구체적인 숫자 하나. 100 행 송장 렌더링은 gpdf 에서 108 µs, signintech/gopdf 에서 약 2.4 ms — gopdf 쪽 수치는 작성한 셀별 패턴에 따라 달라진다. 배수가 헤드라인이 아니라 함수 자체가 사라진다는 게 헤드라인.

Before / After 4: 단락 옆의 이미지

흔한 패턴: 왼쪽에 회사 로고, 오른쪽에 주소 블록.

Before — signintech/gopdf:

const (
    leftX  = 40.0
    rightX = 380.0
    blockY = 50.0
)

if err := pdf.Image("logo.png", leftX, blockY, &gopdf.Rect{W: 100, H: 60}); err != nil {
    log.Fatal(err)
}

pdf.SetFont("helvetica-bold", "", 14)
pdf.SetXY(rightX, blockY)
if err := pdf.Cell(nil, "ACME 주식회사"); err != nil {
    log.Fatal(err)
}

pdf.SetFont("helvetica", "", 10)
pdf.SetXY(rightX, blockY+20)
pdf.Cell(nil, "서울특별시 강남구 테헤란로 123")
pdf.SetXY(rightX, blockY+34)
pdf.Cell(nil, "06234")
pdf.SetXY(rightX, blockY+48)
pdf.Cell(nil, "[email protected]")

명시적 y 좌표 6 개, 오른쪽 블록은 rightX = 380 부터 시작 — 누군가 로고 너비를 100 으로 정하고 오른쪽 블록은 240 픽셀 간격이 필요하다고 정한 결과. 로고를 오른쪽으로 옮기면 모든 숫자가 바뀐다.

After — gpdf:

//go:embed logo.png
var logoData []byte

page.AutoRow(func(r *template.RowBuilder) {
    r.Col(4, func(c *template.ColBuilder) {
        c.Image(logoData, template.FitWidth(document.Mm(35)))
    })
    r.Col(8, func(c *template.ColBuilder) {
        c.Text("ACME 주식회사", template.Bold(), template.FontSize(14))
        c.Text("서울특별시 강남구 테헤란로 123")
        c.Text("06234")
        c.Text("[email protected]")
    })
})

두 컬럼, 4 + 8 = 12. 이미지는 고정 너비에 맞추고 높이는 종횡비에서 gpdf 가 계산. 각 c.Text 는 이전 줄 아래로 흐른다 — Br 도 y 산술도 없음. 로고를 오른쪽으로 옮기려면 컬럼 순서만 바꾸면 된다.

Before / After 5: 푸터의 페이지 번호

gopdf 에서는 카운트를 직접 유지한다. 렌더링이 단일 패스이고 첫 푸터를 그릴 시점에 총 페이지 수를 모르기 때문. 많은 코드베이스가 두 패스 우회를 한다: 한 번 렌더링해서 페이지 수 세고, 다시 렌더링해서 총수를 박아 넣는다.

Before — signintech/gopdf:

totalPages := 0
pdf.AddFooter(func() {
    totalPages++
})

buildContent(&pdf)
finalTotal := totalPages

pdf2 := gopdf.GoPdf{}
pdf2.Start(gopdf.Config{PageSize: *gopdf.PageSizeA4})
pageNum := 0
pdf2.AddFooter(func() {
    pageNum++
    pdf2.SetFont("helvetica", "", 8)
    pdf2.SetXY(40, 800)
    pdf2.Cell(nil, fmt.Sprintf("%d / %d 페이지", pageNum, finalTotal))
})
buildContent(&pdf2)
pdf2.WritePdf("report.pdf")

gopdf 코드를 유지보수해 봤다면 이걸 써본 적이 있다. FAQ 어디에도 없지만, 출력을 파싱하지 않고 정직한 「X / Y 페이지」 푸터를 얻는 유일한 방법.

After — gpdf:

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) {
            c.Stack(template.AlignRight(), func(c *template.ColBuilder) {
                c.PageNumber(template.Inline())
                c.Text(" / ", template.Inline())
                c.TotalPages(template.Inline())
                c.Text(" 페이지", template.Inline())
            }, template.FontSize(8), template.TextColor(pdf.Gray(0.5)))
        })
    })
})

PageNumberTotalPages 는 플레이스홀더. 레이아웃 엔진이 먼저 페이지 분할하고 총수를 해결한 뒤 써넣는다. 한 패스, 수동 카운트 없음, 이중 렌더링 없음.

한국어 텍스트: 수동 서브셋 없이

signintech/gopdf 도 CJK 를 지원하지만 문자 집합 부기를 직접 한다. TTF 추가, 문자 매핑 설정, 등록한 서브셋 외 글리프가 들어오면 두부 박스가 나온다. gpdf 의 TrueType 서브세터는 cmap (포맷 4, 6, 12) 을 따라 실제로 사용한 글리프만 임베드한다 — 수동 서브셋 목록은 존재하지 않는다.

//go:embed NotoSansKR-Regular.ttf
var notoKR []byte

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

page := doc.AddPage()
page.AutoRow(func(r *template.RowBuilder) {
    r.Col(12, func(c *template.ColBuilder) {
        c.Text("안녕하세요, 세계.")
        c.Text("전자세금계산서")
        c.Text("서울특별시 강남구 테헤란로 123")
    })
})

200 자 한국어 송장의 경우 폰트 서브셋은 ~30 KB. 4 MB 풀 임베드 대신.

벤치마크

같은 하드웨어, 같은 워크로드, Apple M1 과 Go 1.25.

벤치gpdfsignintech/gopdfgofpdfMaroto v2
단일 페이지13 µs423 µs132 µs237 µs
4×10 송장 테이블108 µs835 µs241 µs8.6 ms
100 페이지 보고서683 µs8.6 ms11.7 ms19.8 ms
복잡한 CJK 송장133 µs997 µs254 µs10.4 ms

수치는 gpdf/_benchmark/benchmark_test.go 출처.

단일 코어에서 테이블 페이지당 108 µs = 초당 약 9,000 송장. 대부분의 워크로드에서 PDF 생성을 요청 경로에 둘 수 있다.

gopdf 에는 있고 gpdf 에 없는 것

정직 섹션. gopdf 사용이 다음에 의존한다면 이전이 이 글만으로 끝나지 않는다.

  • ImportPage. 기존 PDF 에서 한 페이지를 가져와 그 위에 콘텐츠를 찍는 「PDF 템플릿」 워크플로. gpdf 의 overlay (gpdf.Overlay) 가 일반적인 경우를 다루지만 같은 UseImportedTemplate 프리미티브는 노출하지 않는다.
  • 다각형과 타원 프리미티브. gpdf 의 프리미티브는 사각형, 선, 이미지, 텍스트, 테이블이고 임의 경로 그리기는 일급이 아니다. 데이터 시각화는 차트 라이브러리로 PNG/SVG 렌더 후 임베드.
  • 직접 커서 위치 지정. 픽셀 정확 배치 (예: 정확히 (420, 240) 에 도장) 가 필요하다면 page.Absolute(x, y, fn) 가 있지만 탈출구다.
  • PlaceHolderText / FillInPlaceHoldText. 일반적인 「나중에 채울 슬롯」 메커니즘은 gpdf 에 아직 없다; PageNumber / TotalPages 플레이스홀더는 페이지 번호 시나리오만 다룬다.

송장, 명세서, 보고서, 수료증, 계약서, 영수증, 배송 라벨, 거래명세서, CJK 문서 — gopdf 청구의 대부분이 실제로 생성하는 것 — 에 대해서는 교체가 완전하다.

FAQ

gpdf 는 signintech/gopdf 의 포크? 아니다. gpdf 는 순수 Go 의 클린 재구현. 공유 코드도 혈통도 없다.

둘 다 순수 Go, CGO-free. 갈아탈 실익은? 레이아웃 엔진. 위 이전 섹션의 80% 가 좌표 계산 제거이고, 그게 일상 코드의 읽는 맛 차이. 벤치마크는 부수적. MIT 라이선스는 양쪽 동일하므로 라이선스는 요인이 아니다.

점진 이전이 가능한가? 가능. 두 라이브러리는 충돌하지 않는다. 각자 독립된 []byte 출력을 만든다. 한 섹션은 gpdf, 다른 섹션은 gopdf 로 렌더링하고 gpdf.Merge(a, b) 로 붙인다. 실제로는 문서 단위로 한 번에 옮기는 편이 편하다 — 같은 파일에 두 멘탈 모델이 공존하면 혼란을 부른다.

기존 코드는 pdf.Image(path, ...) 로 디스크에서 로고를 읽는다. 임베드해야 하나? 필수는 아니다. c.Image(imageBytes, ...) 는 바이트를 받으므로 os.ReadFile 로 런타임에 읽으면 된다. 하지만 //go:embed 가 더 나은 기본값: 컨테이너 이미지가 쓰기 가능 파일시스템을 기대하지 않게 되고, 프로덕션에서 자산이 사라지는 사고가 사라진다.

gopdf.PageSizeA4 같은 페이지 크기 상수? gpdf 의 document.A4, document.Letter, document.Legal 등이 같은 집합을 다룬다. 커스텀 크기는 document.PageSize(document.Mm(210), document.Mm(297)).

pdf.Rotate 로 대각선 워터마크를 한다. 등가 기능은?page.Absolute(x, y, fn) 가 회전 옵션을 받으므로 「페이지 대각선 워터마크」는 page.Absolute 한 번 호출.

자동 재작성 도구가 있나? 아직 없다. 단순 부분 매핑 (SetXY/Cellr.Col/c.Text) 은 기계적이지만 테이블 재작성은 구조적 — 부기 코드를 번역이 아니라 삭제. 일반적인 생성기 수동 이전은 문서 유형당 몇 시간.

gpdf 시도

gpdf 는 Go 용 PDF 생성 라이브러리. MIT 라이선스, 외부 의존성 없음, 네이티브 CJK 지원, 12 컬럼 그리드 레이아웃.

go get github.com/gpdf-dev/gpdf

⭐ Star on GitHub · 문서 읽기

다음 읽을거리