From c704c427be949eec15c99349f67536ea6e57dc73 Mon Sep 17 00:00:00 2001 From: Taiki Noda Date: Wed, 29 Apr 2026 12:19:12 +0900 Subject: [PATCH 1/3] feat(template): add borders and backgrounds for tables, images, and boxes --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- CHANGELOG.md | 12 +- README.md | 132 ++++++++++++- README_es.md | 130 ++++++++++++- README_ja.md | 130 ++++++++++++- README_ko.md | 130 ++++++++++++- README_pt.md | 130 ++++++++++++- README_zh.md | 130 ++++++++++++- _examples/builder/35_border_test.go | 181 ++++++++++++++++++ .../builder/testdata/golden/35_box_border.pdf | Bin 0 -> 1484 bytes _examples/gotemplate/35_border_test.go | 135 +++++++++++++ _examples/json/35_border_test.go | 132 +++++++++++++ _examples/testdata/golden/35_border.pdf | Bin 0 -> 17440 bytes document/layout/block.go | 30 ++- document/layout/engine.go | 8 + document/layout/paging.go | 2 + template/border.go | 87 +++++++++ template/component.go | 133 ++++++++++++- template/grid.go | 86 ++++++++- template/schema.go | 169 ++++++++++++---- 20 files changed, 1701 insertions(+), 58 deletions(-) create mode 100644 _examples/builder/35_border_test.go create mode 100644 _examples/builder/testdata/golden/35_box_border.pdf create mode 100644 _examples/gotemplate/35_border_test.go create mode 100644 _examples/json/35_border_test.go create mode 100644 _examples/testdata/golden/35_border.pdf create mode 100644 template/border.go diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 818636f..06a26ff 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -63,7 +63,7 @@ body: id: gpdf-version attributes: label: gpdf Version - placeholder: "v1.0.5" + placeholder: "v1.0.6" validations: required: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f5ba8b..0027564 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] +## [1.0.6] - 2026-04-20 + +### Added +- Minimum display size constraints for images and QR codes — raise layout overflow to the next page when the target box would render below `minWidth` / `minHeight` (#19) + - Builder: `MinDisplayWidth(v)` / `MinDisplayHeight(v)` options on Image and QR + - JSON / GoTemplate schema: `image.minWidth` / `image.minHeight` / `qr.minWidth` / `qr.minHeight` + - Layout engine propagates overflow when the constraint is violated (`document/layout/block.go`) +- Example tests: `_examples/{builder,json,gotemplate}/34_image_min_size_test.go` with shared golden + ## [1.0.5] - 2026-04-19 ### Fixed @@ -132,7 +141,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/). - Reed-Solomon coefficient order in QR code encoder - binary.Write return value handling for errcheck lint -[Unreleased]: https://github.com/gpdf-dev/gpdf/compare/v1.0.5...HEAD +[Unreleased]: https://github.com/gpdf-dev/gpdf/compare/v1.0.6...HEAD +[1.0.6]: https://github.com/gpdf-dev/gpdf/compare/v1.0.5...v1.0.6 [1.0.5]: https://github.com/gpdf-dev/gpdf/compare/v1.0.4...v1.0.5 [1.0.4]: https://github.com/gpdf-dev/gpdf/compare/v1.0.3...v1.0.4 [1.0.3]: https://github.com/gpdf-dev/gpdf/compare/v1.0.2...v1.0.3 diff --git a/README.md b/README.md index 8eaf37b..78cd6d5 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,8 @@ A pure Go, zero-dependency PDF generation library with a layered architecture an - **12-column grid system** — Bootstrap-style responsive layout - **TrueType font support** — embed custom fonts with subsetting - **CJK ready** — full CJK text support from day one -- **Tables** — headers, column widths, striped rows, vertical alignment +- **Tables** — headers, column widths, striped rows, vertical alignment, outer + per-cell borders +- **Borders & backgrounds** — apply to tables, images, and box containers (solid / dashed / dotted) - **Headers & Footers** — consistent across all pages with page numbers - **Lists** — bulleted and numbered lists - **QR codes** — pure Go QR code generation with error correction levels @@ -297,6 +298,40 @@ c.Table( ) ``` +Table borders — outer frame, per-cell grid, or both: + +```go +outer := template.Border( + template.BorderWidth(document.Pt(1)), + template.BorderColor(pdf.RGBHex(0x1A237E)), +) +grid := template.Border( + template.BorderWidth(document.Pt(0.5)), + template.BorderColor(pdf.Gray(0.5)), +) + +// Outer frame only +c.Table(header, rows, template.WithTableBorder(outer)) + +// Cell grid only (Excel-style grid lines) +c.Table(header, rows, template.WithTableCellBorder(grid)) + +// Outer frame + cell grid + background +c.Table(header, rows, + template.WithTableBorder(outer), + template.WithTableCellBorder(grid), + template.WithTableBackground(pdf.RGBHex(0xFAFAFA)), +) + +// Dashed cell grid +dashed := template.Border( + template.BorderWidth(document.Pt(0.75)), + template.BorderColor(pdf.RGBHex(0x0D47A1)), + template.BorderLine(document.BorderDashed), +) +c.Table(header, rows, template.WithTableCellBorder(dashed)) +``` + ### Images Embed JPEG and PNG images with optional fit options: @@ -327,6 +362,38 @@ page.AutoRow(func(r *template.RowBuilder) { }) ``` +Image with a border and a solid backdrop (handy for transparent PNGs): + +```go +c.Image(pngData, + template.FitWidth(document.Mm(60)), + template.WithImageBorder(template.Border( + template.BorderWidth(document.Pt(2)), + template.BorderColor(pdf.RGBHex(0xE53935)), + )), + template.WithImageBackground(pdf.RGBHex(0xFFF8E1)), +) +``` + +### Boxes + +Wrap arbitrary column content in a styled rectangular container with a +border, fill, and padding: + +```go +c.Box(func(c *template.ColBuilder) { + c.Text("Inside a box") + c.Text("with two lines of body copy") +}, + template.WithBoxBorder(template.Border( + template.BorderWidth(document.Pt(1)), + template.BorderColor(pdf.RGBHex(0x1A237E)), + )), + template.WithBoxBackground(pdf.RGBHex(0xE8EAF6)), + template.WithBoxPadding(document.UniformEdges(document.Mm(4))), +) +``` + ### Lines & Spacers Horizontal rules with color and thickness: @@ -558,6 +625,27 @@ doc, err := template.FromJSON(schema, nil) data, _ := doc.Generate() ``` +Tables and images accept the same border / background keys as the builder API: + +```jsonc +{"span": 12, "table": { + "header": ["Name", "Age", "City"], + "rows": [["Alice","30","Tokyo"], ["Bob","25","NYC"]], + "border": {"width": "1pt", "color": "#1A237E"}, // outer frame + "cellBorder": {"width": "0.5pt", "color": "gray(0.5)", "style": "dashed"}, // grid lines + "background": "#FAFAFA" +}} + +{"span": 12, "image": { + "src": "...", + "width": "60mm", + "border": {"widths": ["2pt","2pt","2pt","2pt"], "color": "#E53935"}, + "background": "#FFF8E1" +}} +``` + +`style` accepts `solid` (default), `dashed`, `dotted`, or `none`. Use `widths` for per-edge `[top, right, bottom, left]` or `width` for a uniform value. + ### Go Template Integration Use Go templates with JSON schema for dynamic content: @@ -788,6 +876,7 @@ doc.Render(f) | `c.TotalPages(opts...)` | Add total page count | | `c.Line(opts...)` | Add a horizontal line | | `c.Spacer(height)` | Add vertical space | +| `c.Box(fn, opts...)` | Wrap content in a styled box (border / fill / padding) | ### Page-Level Content @@ -844,6 +933,10 @@ doc.Render(f) | `template.TableHeaderStyle(opts...)` | Style the header row | | `template.TableStripe(color)` | Set alternating row color | | `template.TableCellVAlign(align)` | Set cell vertical alignment (Top/Middle/Bottom) | +| `template.WithTableBorder(spec)` | Draw an outer border around the table | +| `template.WithTableCellBorder(spec)` | Draw the same border around every header + body cell (grid lines) | +| `template.WithTableBorderCollapse(b)` | Mark adjacent cell borders for collapsing | +| `template.WithTableBackground(color)` | Fill the table's outer box | ### Image Options @@ -851,6 +944,43 @@ doc.Render(f) |---|---| | `template.FitWidth(value)` | Scale to fit width (keeps aspect ratio) | | `template.FitHeight(value)` | Scale to fit height (keeps aspect ratio) | +| `template.MinDisplayWidth(v)` | Overflow to next page if shrunk below this width | +| `template.MinDisplayHeight(v)` | Overflow to next page if shrunk below this height | +| `template.WithImageBorder(spec)` | Draw a border around the image | +| `template.WithImageBackground(color)` | Fill the image's box (useful for transparent PNGs) | + +### Box Options + +| Option | Description | +|---|---| +| `template.WithBoxBorder(spec)` | Draw a border around the box | +| `template.WithBoxBackground(color)` | Fill the box | +| `template.WithBoxPadding(edges)` | Inner spacing | +| `template.WithBoxMargin(edges)` | Outer spacing | +| `template.WithBoxWidth(value)` | Explicit width | +| `template.WithBoxHeight(value)` | Explicit height | + +### Border Helpers + +Build a `BorderSpec` once and apply it with `WithTableBorder`, +`WithTableCellBorder`, `WithImageBorder`, `WithBoxBorder`, or +`WithTextBorder`: + +```go +spec := template.Border( + template.BorderWidth(document.Pt(1)), // uniform edges + template.BorderColor(pdf.RGBHex(0x1A237E)), + template.BorderLine(document.BorderSolid), // BorderSolid | BorderDashed | BorderDotted +) +``` + +| Option | Description | +|---|---| +| `template.Border(opts...)` | Build a `BorderSpec` (default 1pt black solid) | +| `template.BorderWidth(v)` | Uniform width on all four edges | +| `template.BorderWidths(t, r, b, l)` | Per-edge widths in CSS order | +| `template.BorderColor(c)` | Edge color | +| `template.BorderLine(style)` | Line style: `BorderSolid`, `BorderDashed`, `BorderDotted`, `BorderNone` | ### QR Code Options diff --git a/README_es.md b/README_es.md index 2b7844a..17671cf 100644 --- a/README_es.md +++ b/README_es.md @@ -17,7 +17,8 @@ Biblioteca de generación de PDF en Go puro, sin dependencias externas, con arqu - **Sistema de cuadrícula de 12 columnas** — diseño responsivo estilo Bootstrap - **Soporte de fuentes TrueType** — incrustación de fuentes personalizadas con subconjuntos - **Listo para CJK** — soporte completo de texto chino, japonés y coreano desde el primer día -- **Tablas** — encabezados, anchos de columna, filas alternadas, alineación vertical +- **Tablas** — encabezados, anchos de columna, filas alternadas, alineación vertical, bordes externos + por celda +- **Bordes y fondos** — aplicables a tablas, imágenes y contenedores Box (solid / dashed / dotted) - **Encabezados y pies de página** — con números de página, consistentes en todas las páginas - **Listas** — listas con viñetas y numeradas - **Códigos QR** — generación de QR en Go puro (niveles de corrección de errores) @@ -279,6 +280,40 @@ c.Table( ) ``` +Bordes de tabla — marco externo, cuadrícula por celda, o ambos: + +```go +outer := template.Border( + template.BorderWidth(document.Pt(1)), + template.BorderColor(pdf.RGBHex(0x1A237E)), +) +grid := template.Border( + template.BorderWidth(document.Pt(0.5)), + template.BorderColor(pdf.Gray(0.5)), +) + +// Solo marco externo +c.Table(header, rows, template.WithTableBorder(outer)) + +// Solo cuadrícula (líneas estilo Excel) +c.Table(header, rows, template.WithTableCellBorder(grid)) + +// Marco + cuadrícula + fondo +c.Table(header, rows, + template.WithTableBorder(outer), + template.WithTableCellBorder(grid), + template.WithTableBackground(pdf.RGBHex(0xFAFAFA)), +) + +// Cuadrícula con líneas discontinuas +dashed := template.Border( + template.BorderWidth(document.Pt(0.75)), + template.BorderColor(pdf.RGBHex(0x0D47A1)), + template.BorderLine(document.BorderDashed), +) +c.Table(header, rows, template.WithTableCellBorder(dashed)) +``` + ### Imágenes Incrustar imágenes JPEG y PNG con opciones de ajuste: @@ -289,6 +324,37 @@ c.Image(imgData, template.FitWidth(document.Mm(80))) // Ajustar al ancho c.Image(imgData, template.FitHeight(document.Mm(30))) // Ajustar a la altura ``` +Imagen con borde y fondo sólido (útil para PNG transparentes): + +```go +c.Image(pngData, + template.FitWidth(document.Mm(60)), + template.WithImageBorder(template.Border( + template.BorderWidth(document.Pt(2)), + template.BorderColor(pdf.RGBHex(0xE53935)), + )), + template.WithImageBackground(pdf.RGBHex(0xFFF8E1)), +) +``` + +### Cajas (Box) + +Envuelve contenido arbitrario de columna en un contenedor rectangular con borde, relleno y padding: + +```go +c.Box(func(c *template.ColBuilder) { + c.Text("Dentro de una caja") + c.Text("con dos líneas de texto") +}, + template.WithBoxBorder(template.Border( + template.BorderWidth(document.Pt(1)), + template.BorderColor(pdf.RGBHex(0x1A237E)), + )), + template.WithBoxBackground(pdf.RGBHex(0xE8EAF6)), + template.WithBoxPadding(document.UniformEdges(document.Mm(4))), +) +``` + ### Líneas y espaciadores ```go @@ -621,6 +687,27 @@ doc, err := template.FromJSON(schema, nil) data, _ := doc.Generate() ``` +Las tablas y las imágenes aceptan las mismas claves `border` / `background` que la API del builder: + +```jsonc +{"span": 12, "table": { + "header": ["Nombre", "Cant.", "Precio"], + "rows": [["A","1","$100"], ["B","2","$200"]], + "border": {"width": "1pt", "color": "#1A237E"}, // marco externo + "cellBorder": {"width": "0.5pt", "color": "gray(0.5)", "style": "dashed"}, // líneas de cuadrícula + "background": "#FAFAFA" +}} + +{"span": 12, "image": { + "src": "...", + "width": "60mm", + "border": {"widths": ["2pt","2pt","2pt","2pt"], "color": "#E53935"}, + "background": "#FFF8E1" +}} +``` + +`style` acepta `solid` (por defecto), `dashed`, `dotted` o `none`. Usa `widths` para anchuras por borde en orden CSS `[top, right, bottom, left]`, o `width` para un valor uniforme. + ### Integración con Go Templates Use plantillas Go con esquemas JSON para contenido dinámico: @@ -727,6 +814,7 @@ doc.Render(f) | `c.TotalPages(opts...)` | Agregar total de páginas | | `c.Line(opts...)` | Agregar una línea horizontal | | `c.Spacer(height)` | Agregar espacio vertical | +| `c.Box(fn, opts...)` | Envolver contenido en un Box estilizado (borde / relleno / padding) | ### Contenido a nivel de página @@ -783,6 +871,10 @@ doc.Render(f) | `template.TableHeaderStyle(opts...)` | Estilo de la fila de encabezado | | `template.TableStripe(color)` | Color de filas alternadas | | `template.TableCellVAlign(align)` | Alineación vertical de celda (Top/Middle/Bottom) | +| `template.WithTableBorder(spec)` | Dibujar un borde exterior alrededor de la tabla | +| `template.WithTableCellBorder(spec)` | Dibujar el mismo borde en cada celda de encabezado y cuerpo (líneas de cuadrícula) | +| `template.WithTableBorderCollapse(b)` | Marcar bordes de celdas adyacentes para fusionarse | +| `template.WithTableBackground(color)` | Rellenar la caja externa de la tabla | ### Opciones de imagen @@ -790,6 +882,42 @@ doc.Render(f) |---|---| | `template.FitWidth(value)` | Escalar al ancho (mantiene proporción) | | `template.FitHeight(value)` | Escalar a la altura (mantiene proporción) | +| `template.MinDisplayWidth(v)` | Pasar a la siguiente página si se reduce por debajo de este ancho | +| `template.MinDisplayHeight(v)` | Pasar a la siguiente página si se reduce por debajo de esta altura | +| `template.WithImageBorder(spec)` | Dibujar un borde alrededor de la imagen | +| `template.WithImageBackground(color)` | Rellenar la caja de la imagen (útil para PNG transparentes) | + +### Opciones de Box + +| Opción | Descripción | +|---|---| +| `template.WithBoxBorder(spec)` | Dibujar un borde alrededor del Box | +| `template.WithBoxBackground(color)` | Rellenar el Box | +| `template.WithBoxPadding(edges)` | Espaciado interior | +| `template.WithBoxMargin(edges)` | Espaciado exterior | +| `template.WithBoxWidth(value)` | Ancho explícito | +| `template.WithBoxHeight(value)` | Altura explícita | + +### Helpers de borde + +Construye un `BorderSpec` una vez y aplícalo con `WithTableBorder`, +`WithTableCellBorder`, `WithImageBorder`, `WithBoxBorder` o `WithTextBorder`: + +```go +spec := template.Border( + template.BorderWidth(document.Pt(1)), // bordes uniformes + template.BorderColor(pdf.RGBHex(0x1A237E)), + template.BorderLine(document.BorderSolid), // BorderSolid | BorderDashed | BorderDotted +) +``` + +| Opción | Descripción | +|---|---| +| `template.Border(opts...)` | Construye un `BorderSpec` (por defecto 1pt sólido negro) | +| `template.BorderWidth(v)` | Ancho uniforme en los cuatro bordes | +| `template.BorderWidths(t, r, b, l)` | Anchos por borde en orden CSS | +| `template.BorderColor(c)` | Color del borde | +| `template.BorderLine(style)` | Estilo de línea: `BorderSolid` / `BorderDashed` / `BorderDotted` / `BorderNone` | ### Opciones de código QR diff --git a/README_ja.md b/README_ja.md index a10cdb8..3f37197 100644 --- a/README_ja.md +++ b/README_ja.md @@ -17,7 +17,8 @@ - **12カラムグリッドシステム** — Bootstrap風のレスポンシブレイアウト - **TrueTypeフォント対応** — カスタムフォントの埋め込みとサブセット化 - **CJK対応** — 日中韓テキストを初日からフルサポート -- **テーブル** — ヘッダー、カラム幅指定、ストライプ行、垂直揃え +- **テーブル** — ヘッダー、カラム幅指定、ストライプ行、垂直揃え、外周罫線とセル罫線 +- **罫線と背景** — テーブル / 画像 / Box コンテナに適用可能(solid / dashed / dotted) - **ヘッダー&フッター** — ページ番号付きで全ページに一貫表示 - **リスト** — 箇条書きリストと番号付きリスト - **QRコード** — 純GoのQRコード生成(誤り訂正レベル対応) @@ -279,6 +280,40 @@ c.Table( ) ``` +テーブル罫線 — 外周フレーム、セルグリッド、または両方: + +```go +outer := template.Border( + template.BorderWidth(document.Pt(1)), + template.BorderColor(pdf.RGBHex(0x1A237E)), +) +grid := template.Border( + template.BorderWidth(document.Pt(0.5)), + template.BorderColor(pdf.Gray(0.5)), +) + +// 外周フレームのみ +c.Table(header, rows, template.WithTableBorder(outer)) + +// セルグリッドのみ(Excel 風グリッド線) +c.Table(header, rows, template.WithTableCellBorder(grid)) + +// 外周フレーム + セルグリッド + 背景 +c.Table(header, rows, + template.WithTableBorder(outer), + template.WithTableCellBorder(grid), + template.WithTableBackground(pdf.RGBHex(0xFAFAFA)), +) + +// 破線セルグリッド +dashed := template.Border( + template.BorderWidth(document.Pt(0.75)), + template.BorderColor(pdf.RGBHex(0x0D47A1)), + template.BorderLine(document.BorderDashed), +) +c.Table(header, rows, template.WithTableCellBorder(dashed)) +``` + ### 画像 JPEGとPNG画像の埋め込み(フィットオプション対応): @@ -289,6 +324,37 @@ c.Image(imgData, template.FitWidth(document.Mm(80))) // 幅に合わせる c.Image(imgData, template.FitHeight(document.Mm(30))) // 高さに合わせる ``` +罫線と背景色を指定(透過 PNG に背景を敷く用途に便利): + +```go +c.Image(pngData, + template.FitWidth(document.Mm(60)), + template.WithImageBorder(template.Border( + template.BorderWidth(document.Pt(2)), + template.BorderColor(pdf.RGBHex(0xE53935)), + )), + template.WithImageBackground(pdf.RGBHex(0xFFF8E1)), +) +``` + +### Box コンテナ + +カラム内の任意の内容を、罫線・塗りつぶし・パディング付きの矩形コンテナでラップ: + +```go +c.Box(func(c *template.ColBuilder) { + c.Text("Box の中") + c.Text("本文 2 行") +}, + template.WithBoxBorder(template.Border( + template.BorderWidth(document.Pt(1)), + template.BorderColor(pdf.RGBHex(0x1A237E)), + )), + template.WithBoxBackground(pdf.RGBHex(0xE8EAF6)), + template.WithBoxPadding(document.UniformEdges(document.Mm(4))), +) +``` + ### 罫線とスペーサー ```go @@ -490,6 +556,27 @@ doc, err := template.FromJSON(schema, nil) data, _ := doc.Generate() ``` +テーブルと画像はビルダー API と同じ border / background キーを受け付けます: + +```jsonc +{"span": 12, "table": { + "header": ["商品", "数量", "単価"], + "rows": [["A","1","¥100"], ["B","2","¥200"]], + "border": {"width": "1pt", "color": "#1A237E"}, // 外周フレーム + "cellBorder": {"width": "0.5pt", "color": "gray(0.5)", "style": "dashed"}, // グリッド線 + "background": "#FAFAFA" +}} + +{"span": 12, "image": { + "src": "...", + "width": "60mm", + "border": {"widths": ["2pt","2pt","2pt","2pt"], "color": "#E53935"}, + "background": "#FFF8E1" +}} +``` + +`style` には `solid`(デフォルト) / `dashed` / `dotted` / `none` を指定可能。CSS 順で `[top, right, bottom, left]` を別指定する場合は `widths`、一律の場合は `width` を使用。 + ### Goテンプレート統合 GoテンプレートとJSONスキーマで動的コンテンツを生成: @@ -720,6 +807,7 @@ doc.Render(f) | `c.TotalPages(opts...)` | 総ページ数を追加 | | `c.Line(opts...)` | 水平線を追加 | | `c.Spacer(height)` | 垂直スペースを追加 | +| `c.Box(fn, opts...)` | スタイル付き Box にコンテンツをラップ(罫線 / 塗り / パディング) | ### ページレベルコンテンツ @@ -776,6 +864,10 @@ doc.Render(f) | `template.TableHeaderStyle(opts...)` | ヘッダー行のスタイル設定 | | `template.TableStripe(color)` | 交互行の色を設定 | | `template.TableCellVAlign(align)` | セルの垂直揃え (Top/Middle/Bottom) | +| `template.WithTableBorder(spec)` | テーブル外周に罫線を描画 | +| `template.WithTableCellBorder(spec)` | 全ヘッダー・ボディセルに同じ罫線を描画(グリッド線) | +| `template.WithTableBorderCollapse(b)` | 隣接セル境界の collapse を有効化 | +| `template.WithTableBackground(color)` | テーブルの外側 Box を塗りつぶし | ### 画像オプション @@ -783,6 +875,42 @@ doc.Render(f) |---|---| | `template.FitWidth(value)` | 幅に合わせてスケール(アスペクト比維持) | | `template.FitHeight(value)` | 高さに合わせてスケール(アスペクト比維持) | +| `template.MinDisplayWidth(v)` | この幅を下回る場合は次ページへ送る | +| `template.MinDisplayHeight(v)` | この高さを下回る場合は次ページへ送る | +| `template.WithImageBorder(spec)` | 画像の周囲に罫線を描画 | +| `template.WithImageBackground(color)` | 画像の Box を塗りつぶし(透過 PNG に便利) | + +### Box オプション + +| オプション | 説明 | +|---|---| +| `template.WithBoxBorder(spec)` | Box の周囲に罫線を描画 | +| `template.WithBoxBackground(color)` | Box を塗りつぶし | +| `template.WithBoxPadding(edges)` | 内側のパディング | +| `template.WithBoxMargin(edges)` | 外側のマージン | +| `template.WithBoxWidth(value)` | 明示的な幅 | +| `template.WithBoxHeight(value)` | 明示的な高さ | + +### 罫線ヘルパー + +`BorderSpec` を一度組み立てて、`WithTableBorder` / `WithTableCellBorder` / +`WithImageBorder` / `WithBoxBorder` / `WithTextBorder` で適用します: + +```go +spec := template.Border( + template.BorderWidth(document.Pt(1)), // 全 4 辺一律 + template.BorderColor(pdf.RGBHex(0x1A237E)), + template.BorderLine(document.BorderSolid), // BorderSolid | BorderDashed | BorderDotted +) +``` + +| オプション | 説明 | +|---|---| +| `template.Border(opts...)` | `BorderSpec` を構築(デフォルト: 1pt black solid) | +| `template.BorderWidth(v)` | 全 4 辺に同じ幅 | +| `template.BorderWidths(t, r, b, l)` | CSS 順で各辺別の幅 | +| `template.BorderColor(c)` | 辺の色 | +| `template.BorderLine(style)` | 線スタイル: `BorderSolid` / `BorderDashed` / `BorderDotted` / `BorderNone` | ### QRコードオプション diff --git a/README_ko.md b/README_ko.md index 36972f1..9011f4a 100644 --- a/README_ko.md +++ b/README_ko.md @@ -17,7 +17,8 @@ - **12컬럼 그리드 시스템** — Bootstrap 스타일의 반응형 레이아웃 - **TrueType 폰트 지원** — 커스텀 폰트 임베딩 및 서브셋팅 - **CJK 지원** — 첫날부터 한중일 텍스트 완벽 지원 -- **테이블** — 헤더, 컬럼 너비, 줄무늬 행, 수직 정렬 +- **테이블** — 헤더, 컬럼 너비, 줄무늬 행, 수직 정렬, 외곽 + 셀별 테두리 +- **테두리 및 배경** — 테이블, 이미지, Box 컨테이너에 적용 (solid / dashed / dotted) - **머리글 및 바닥글** — 페이지 번호 포함, 모든 페이지에서 일관된 표시 - **리스트** — 글머리 기호 목록 및 번호 목록 - **QR 코드** — 순수 Go QR 코드 생성 (오류 정정 레벨 지원) @@ -279,6 +280,40 @@ c.Table( ) ``` +테이블 테두리 — 외곽 프레임, 셀 그리드, 또는 둘 다: + +```go +outer := template.Border( + template.BorderWidth(document.Pt(1)), + template.BorderColor(pdf.RGBHex(0x1A237E)), +) +grid := template.Border( + template.BorderWidth(document.Pt(0.5)), + template.BorderColor(pdf.Gray(0.5)), +) + +// 외곽만 +c.Table(header, rows, template.WithTableBorder(outer)) + +// 셀 그리드만 (Excel 스타일 그리드 라인) +c.Table(header, rows, template.WithTableCellBorder(grid)) + +// 외곽 + 셀 그리드 + 배경 +c.Table(header, rows, + template.WithTableBorder(outer), + template.WithTableCellBorder(grid), + template.WithTableBackground(pdf.RGBHex(0xFAFAFA)), +) + +// 점선 셀 그리드 +dashed := template.Border( + template.BorderWidth(document.Pt(0.75)), + template.BorderColor(pdf.RGBHex(0x0D47A1)), + template.BorderLine(document.BorderDashed), +) +c.Table(header, rows, template.WithTableCellBorder(dashed)) +``` + ### 이미지 JPEG 및 PNG 이미지 임베딩 (맞춤 옵션 지원): @@ -289,6 +324,37 @@ c.Image(imgData, template.FitWidth(document.Mm(80))) // 너비에 맞춤 c.Image(imgData, template.FitHeight(document.Mm(30))) // 높이에 맞춤 ``` +테두리와 단색 배경이 있는 이미지 (투명 PNG에 유용): + +```go +c.Image(pngData, + template.FitWidth(document.Mm(60)), + template.WithImageBorder(template.Border( + template.BorderWidth(document.Pt(2)), + template.BorderColor(pdf.RGBHex(0xE53935)), + )), + template.WithImageBackground(pdf.RGBHex(0xFFF8E1)), +) +``` + +### Box 컨테이너 + +임의의 컬럼 콘텐츠를 테두리, 채우기, 안쪽 여백이 있는 사각형 컨테이너로 감싸기: + +```go +c.Box(func(c *template.ColBuilder) { + c.Text("박스 안") + c.Text("두 줄의 본문") +}, + template.WithBoxBorder(template.Border( + template.BorderWidth(document.Pt(1)), + template.BorderColor(pdf.RGBHex(0x1A237E)), + )), + template.WithBoxBackground(pdf.RGBHex(0xE8EAF6)), + template.WithBoxPadding(document.UniformEdges(document.Mm(4))), +) +``` + ### 선 및 간격 ```go @@ -589,6 +655,27 @@ doc, err := template.FromJSON(schema, nil) data, _ := doc.Generate() ``` +테이블과 이미지는 빌더 API와 동일한 border / background 키를 받습니다: + +```jsonc +{"span": 12, "table": { + "header": ["이름", "수량", "단가"], + "rows": [["A","1","₩100"], ["B","2","₩200"]], + "border": {"width": "1pt", "color": "#1A237E"}, // 외곽 프레임 + "cellBorder": {"width": "0.5pt", "color": "gray(0.5)", "style": "dashed"}, // 그리드 라인 + "background": "#FAFAFA" +}} + +{"span": 12, "image": { + "src": "...", + "width": "60mm", + "border": {"widths": ["2pt","2pt","2pt","2pt"], "color": "#E53935"}, + "background": "#FFF8E1" +}} +``` + +`style`은 `solid`(기본) / `dashed` / `dotted` / `none`을 받습니다. 변별적으로 지정할 때는 CSS 순서 `[top, right, bottom, left]`로 `widths`를, 균일한 폭은 `width`를 사용하세요. + ### Go 템플릿 통합 Go 템플릿과 JSON 스키마로 동적 콘텐츠 생성: @@ -727,6 +814,7 @@ doc.Render(f) | `c.TotalPages(opts...)` | 전체 페이지 수 추가 | | `c.Line(opts...)` | 수평선 추가 | | `c.Spacer(height)` | 수직 공간 추가 | +| `c.Box(fn, opts...)` | 스타일이 적용된 Box로 콘텐츠 감싸기 (테두리 / 채우기 / 안쪽 여백) | ### 페이지 레벨 콘텐츠 @@ -783,6 +871,10 @@ doc.Render(f) | `template.TableHeaderStyle(opts...)` | 헤더 행 스타일 설정 | | `template.TableStripe(color)` | 교차 행 색상 설정 | | `template.TableCellVAlign(align)` | 셀 수직 정렬 (Top/Middle/Bottom) | +| `template.WithTableBorder(spec)` | 테이블 외곽에 테두리 그리기 | +| `template.WithTableCellBorder(spec)` | 모든 헤더 및 본문 셀에 동일한 테두리 그리기 (그리드 라인) | +| `template.WithTableBorderCollapse(b)` | 인접 셀 테두리 병합 활성화 | +| `template.WithTableBackground(color)` | 테이블의 외곽 Box 채우기 | ### 이미지 옵션 @@ -790,6 +882,42 @@ doc.Render(f) |---|---| | `template.FitWidth(value)` | 너비에 맞춰 스케일 (비율 유지) | | `template.FitHeight(value)` | 높이에 맞춰 스케일 (비율 유지) | +| `template.MinDisplayWidth(v)` | 이 너비 미만으로 축소되면 다음 페이지로 이동 | +| `template.MinDisplayHeight(v)` | 이 높이 미만으로 축소되면 다음 페이지로 이동 | +| `template.WithImageBorder(spec)` | 이미지 주위에 테두리 그리기 | +| `template.WithImageBackground(color)` | 이미지의 Box 채우기 (투명 PNG에 유용) | + +### Box 옵션 + +| 옵션 | 설명 | +|---|---| +| `template.WithBoxBorder(spec)` | Box 주위에 테두리 그리기 | +| `template.WithBoxBackground(color)` | Box 채우기 | +| `template.WithBoxPadding(edges)` | 안쪽 여백 | +| `template.WithBoxMargin(edges)` | 바깥쪽 여백 | +| `template.WithBoxWidth(value)` | 명시적 너비 | +| `template.WithBoxHeight(value)` | 명시적 높이 | + +### 테두리 헬퍼 + +`BorderSpec`을 한 번 만들고 `WithTableBorder` / `WithTableCellBorder` / +`WithImageBorder` / `WithBoxBorder` / `WithTextBorder`로 적용: + +```go +spec := template.Border( + template.BorderWidth(document.Pt(1)), // 4면 균일 + template.BorderColor(pdf.RGBHex(0x1A237E)), + template.BorderLine(document.BorderSolid), // BorderSolid | BorderDashed | BorderDotted +) +``` + +| 옵션 | 설명 | +|---|---| +| `template.Border(opts...)` | `BorderSpec` 빌드 (기본 1pt 검정 실선) | +| `template.BorderWidth(v)` | 4면 균일 폭 | +| `template.BorderWidths(t, r, b, l)` | CSS 순서로 변별 폭 | +| `template.BorderColor(c)` | 모서리 색상 | +| `template.BorderLine(style)` | 선 스타일: `BorderSolid` / `BorderDashed` / `BorderDotted` / `BorderNone` | ### QR 코드 옵션 diff --git a/README_pt.md b/README_pt.md index 5e45da7..ebbcace 100644 --- a/README_pt.md +++ b/README_pt.md @@ -17,7 +17,8 @@ Biblioteca de geração de PDF em Go puro, sem dependências externas, com arqui - **Sistema de grade de 12 colunas** — layout responsivo estilo Bootstrap - **Suporte a fontes TrueType** — incorporação de fontes personalizadas com subconjuntos - **Pronto para CJK** — suporte completo a texto chinês, japonês e coreano desde o primeiro dia -- **Tabelas** — cabeçalhos, larguras de coluna, linhas alternadas, alinhamento vertical +- **Tabelas** — cabeçalhos, larguras de coluna, linhas alternadas, alinhamento vertical, bordas externas + por célula +- **Bordas e fundos** — aplicáveis a tabelas, imagens e contêineres Box (solid / dashed / dotted) - **Cabeçalhos e rodapés** — com números de página, consistentes em todas as páginas - **Listas** — listas com marcadores e numeradas - **QR codes** — geração de QR code em Go puro (níveis de correção de erros) @@ -279,6 +280,40 @@ c.Table( ) ``` +Bordas de tabela — moldura externa, grade por célula, ou ambos: + +```go +outer := template.Border( + template.BorderWidth(document.Pt(1)), + template.BorderColor(pdf.RGBHex(0x1A237E)), +) +grid := template.Border( + template.BorderWidth(document.Pt(0.5)), + template.BorderColor(pdf.Gray(0.5)), +) + +// Apenas moldura externa +c.Table(header, rows, template.WithTableBorder(outer)) + +// Apenas grade (linhas estilo Excel) +c.Table(header, rows, template.WithTableCellBorder(grid)) + +// Moldura + grade + fundo +c.Table(header, rows, + template.WithTableBorder(outer), + template.WithTableCellBorder(grid), + template.WithTableBackground(pdf.RGBHex(0xFAFAFA)), +) + +// Grade com linhas tracejadas +dashed := template.Border( + template.BorderWidth(document.Pt(0.75)), + template.BorderColor(pdf.RGBHex(0x0D47A1)), + template.BorderLine(document.BorderDashed), +) +c.Table(header, rows, template.WithTableCellBorder(dashed)) +``` + ### Imagens Incorporar imagens JPEG e PNG com opções de ajuste: @@ -289,6 +324,37 @@ c.Image(imgData, template.FitWidth(document.Mm(80))) // Ajustar à largura c.Image(imgData, template.FitHeight(document.Mm(30))) // Ajustar à altura ``` +Imagem com borda e fundo sólido (útil para PNGs transparentes): + +```go +c.Image(pngData, + template.FitWidth(document.Mm(60)), + template.WithImageBorder(template.Border( + template.BorderWidth(document.Pt(2)), + template.BorderColor(pdf.RGBHex(0xE53935)), + )), + template.WithImageBackground(pdf.RGBHex(0xFFF8E1)), +) +``` + +### Caixas (Box) + +Envolva conteúdo arbitrário de coluna em um contêiner retangular com borda, preenchimento e padding: + +```go +c.Box(func(c *template.ColBuilder) { + c.Text("Dentro de uma caixa") + c.Text("com duas linhas de texto") +}, + template.WithBoxBorder(template.Border( + template.BorderWidth(document.Pt(1)), + template.BorderColor(pdf.RGBHex(0x1A237E)), + )), + template.WithBoxBackground(pdf.RGBHex(0xE8EAF6)), + template.WithBoxPadding(document.UniformEdges(document.Mm(4))), +) +``` + ### Linhas e espaçadores ```go @@ -562,6 +628,27 @@ doc, err := template.FromJSON(schema, nil) data, _ := doc.Generate() ``` +Tabelas e imagens aceitam as mesmas chaves `border` / `background` da API do builder: + +```jsonc +{"span": 12, "table": { + "header": ["Nome", "Qtd.", "Preço"], + "rows": [["A","1","R$100"], ["B","2","R$200"]], + "border": {"width": "1pt", "color": "#1A237E"}, // moldura externa + "cellBorder": {"width": "0.5pt", "color": "gray(0.5)", "style": "dashed"}, // linhas de grade + "background": "#FAFAFA" +}} + +{"span": 12, "image": { + "src": "...", + "width": "60mm", + "border": {"widths": ["2pt","2pt","2pt","2pt"], "color": "#E53935"}, + "background": "#FFF8E1" +}} +``` + +`style` aceita `solid` (padrão), `dashed`, `dotted` ou `none`. Use `widths` para larguras por borda em ordem CSS `[top, right, bottom, left]`, ou `width` para um valor uniforme. + ### Integração com Go Templates Use templates Go com esquemas JSON para conteúdo dinâmico: @@ -727,6 +814,7 @@ merged, _ := gpdf.Merge( | `c.TotalPages(opts...)` | Adicionar total de páginas | | `c.Line(opts...)` | Adicionar uma linha horizontal | | `c.Spacer(height)` | Adicionar espaço vertical | +| `c.Box(fn, opts...)` | Envolver conteúdo em um Box estilizado (borda / preenchimento / padding) | ### Conteúdo em nível de página @@ -783,6 +871,10 @@ merged, _ := gpdf.Merge( | `template.TableHeaderStyle(opts...)` | Estilo da linha de cabeçalho | | `template.TableStripe(color)` | Cor de linhas alternadas | | `template.TableCellVAlign(align)` | Alinhamento vertical da célula (Top/Middle/Bottom) | +| `template.WithTableBorder(spec)` | Desenhar uma borda externa ao redor da tabela | +| `template.WithTableCellBorder(spec)` | Desenhar a mesma borda em cada célula de cabeçalho e corpo (linhas de grade) | +| `template.WithTableBorderCollapse(b)` | Marcar bordas de células adjacentes para colapso | +| `template.WithTableBackground(color)` | Preencher a caixa externa da tabela | ### Opções de imagem @@ -790,6 +882,42 @@ merged, _ := gpdf.Merge( |---|---| | `template.FitWidth(value)` | Escalar à largura (mantém proporção) | | `template.FitHeight(value)` | Escalar à altura (mantém proporção) | +| `template.MinDisplayWidth(v)` | Transbordar para a próxima página se reduzido abaixo desta largura | +| `template.MinDisplayHeight(v)` | Transbordar para a próxima página se reduzido abaixo desta altura | +| `template.WithImageBorder(spec)` | Desenhar uma borda ao redor da imagem | +| `template.WithImageBackground(color)` | Preencher a caixa da imagem (útil para PNGs transparentes) | + +### Opções de Box + +| Opção | Descrição | +|---|---| +| `template.WithBoxBorder(spec)` | Desenhar uma borda ao redor do Box | +| `template.WithBoxBackground(color)` | Preencher o Box | +| `template.WithBoxPadding(edges)` | Espaçamento interno | +| `template.WithBoxMargin(edges)` | Espaçamento externo | +| `template.WithBoxWidth(value)` | Largura explícita | +| `template.WithBoxHeight(value)` | Altura explícita | + +### Helpers de borda + +Construa um `BorderSpec` uma vez e aplique-o com `WithTableBorder`, +`WithTableCellBorder`, `WithImageBorder`, `WithBoxBorder` ou `WithTextBorder`: + +```go +spec := template.Border( + template.BorderWidth(document.Pt(1)), // bordas uniformes + template.BorderColor(pdf.RGBHex(0x1A237E)), + template.BorderLine(document.BorderSolid), // BorderSolid | BorderDashed | BorderDotted +) +``` + +| Opção | Descrição | +|---|---| +| `template.Border(opts...)` | Constrói um `BorderSpec` (padrão 1pt preto sólido) | +| `template.BorderWidth(v)` | Largura uniforme nas quatro bordas | +| `template.BorderWidths(t, r, b, l)` | Larguras por borda em ordem CSS | +| `template.BorderColor(c)` | Cor da borda | +| `template.BorderLine(style)` | Estilo de linha: `BorderSolid` / `BorderDashed` / `BorderDotted` / `BorderNone` | ### Opções de QR code diff --git a/README_zh.md b/README_zh.md index 4bf1c46..eba5983 100644 --- a/README_zh.md +++ b/README_zh.md @@ -17,7 +17,8 @@ - **12 列网格系统** — Bootstrap 风格的响应式布局 - **TrueType 字体支持** — 自定义字体嵌入与子集化 - **CJK 就绪** — 从第一天起完整支持中日韩文本 -- **表格** — 表头、列宽、条纹行、垂直对齐 +- **表格** — 表头、列宽、条纹行、垂直对齐、外边框和单元格边框 +- **边框和背景** — 适用于表格、图片和 Box 容器(solid / dashed / dotted) - **页眉和页脚** — 带页码,所有页面统一显示 - **列表** — 无序列表和有序列表 - **二维码** — 纯 Go 二维码生成(支持纠错等级) @@ -279,6 +280,40 @@ c.Table( ) ``` +表格边框 — 外框、单元格网格、或两者: + +```go +outer := template.Border( + template.BorderWidth(document.Pt(1)), + template.BorderColor(pdf.RGBHex(0x1A237E)), +) +grid := template.Border( + template.BorderWidth(document.Pt(0.5)), + template.BorderColor(pdf.Gray(0.5)), +) + +// 仅外框 +c.Table(header, rows, template.WithTableBorder(outer)) + +// 仅单元格网格(Excel 风格网格线) +c.Table(header, rows, template.WithTableCellBorder(grid)) + +// 外框 + 单元格网格 + 背景 +c.Table(header, rows, + template.WithTableBorder(outer), + template.WithTableCellBorder(grid), + template.WithTableBackground(pdf.RGBHex(0xFAFAFA)), +) + +// 虚线单元格网格 +dashed := template.Border( + template.BorderWidth(document.Pt(0.75)), + template.BorderColor(pdf.RGBHex(0x0D47A1)), + template.BorderLine(document.BorderDashed), +) +c.Table(header, rows, template.WithTableCellBorder(dashed)) +``` + ### 图片 嵌入 JPEG 和 PNG 图片(支持缩放选项): @@ -289,6 +324,37 @@ c.Image(imgData, template.FitWidth(document.Mm(80))) // 按宽度缩放 c.Image(imgData, template.FitHeight(document.Mm(30))) // 按高度缩放 ``` +带边框和实色背景的图片(适用于透明 PNG): + +```go +c.Image(pngData, + template.FitWidth(document.Mm(60)), + template.WithImageBorder(template.Border( + template.BorderWidth(document.Pt(2)), + template.BorderColor(pdf.RGBHex(0xE53935)), + )), + template.WithImageBackground(pdf.RGBHex(0xFFF8E1)), +) +``` + +### Box 容器 + +将任意列内容包装在带边框、填充和内边距的样式化矩形容器中: + +```go +c.Box(func(c *template.ColBuilder) { + c.Text("盒子里") + c.Text("两行正文") +}, + template.WithBoxBorder(template.Border( + template.BorderWidth(document.Pt(1)), + template.BorderColor(pdf.RGBHex(0x1A237E)), + )), + template.WithBoxBackground(pdf.RGBHex(0xE8EAF6)), + template.WithBoxPadding(document.UniformEdges(document.Mm(4))), +) +``` + ### 线条和间距 ```go @@ -621,6 +687,27 @@ doc, err := template.FromJSON(schema, nil) data, _ := doc.Generate() ``` +表格和图片接受与构建器 API 相同的 border / background 键: + +```jsonc +{"span": 12, "table": { + "header": ["名称", "数量", "单价"], + "rows": [["A","1","¥100"], ["B","2","¥200"]], + "border": {"width": "1pt", "color": "#1A237E"}, // 外框 + "cellBorder": {"width": "0.5pt", "color": "gray(0.5)", "style": "dashed"}, // 网格线 + "background": "#FAFAFA" +}} + +{"span": 12, "image": { + "src": "...", + "width": "60mm", + "border": {"widths": ["2pt","2pt","2pt","2pt"], "color": "#E53935"}, + "background": "#FFF8E1" +}} +``` + +`style` 接受 `solid`(默认)/ `dashed` / `dotted` / `none`。使用 `widths` 按 CSS 顺序 `[top, right, bottom, left]` 指定各边宽度;使用 `width` 设置统一宽度。 + ### Go 模板集成 使用 Go 模板和 JSON 模式生成动态内容: @@ -727,6 +814,7 @@ doc.Render(f) | `c.TotalPages(opts...)` | 添加总页数 | | `c.Line(opts...)` | 添加水平线 | | `c.Spacer(height)` | 添加垂直间距 | +| `c.Box(fn, opts...)` | 在带样式的 Box 中包装内容(边框 / 填充 / 内边距) | ### 页面级内容 @@ -783,6 +871,10 @@ doc.Render(f) | `template.TableHeaderStyle(opts...)` | 设置表头行样式 | | `template.TableStripe(color)` | 设置交替行颜色 | | `template.TableCellVAlign(align)` | 设置单元格垂直对齐(Top/Middle/Bottom) | +| `template.WithTableBorder(spec)` | 在表格周围绘制外边框 | +| `template.WithTableCellBorder(spec)` | 在每个表头和正文单元格周围绘制相同的边框(网格线) | +| `template.WithTableBorderCollapse(b)` | 启用相邻单元格边框合并 | +| `template.WithTableBackground(color)` | 填充表格的外部 Box | ### 图片选项 @@ -790,6 +882,42 @@ doc.Render(f) |---|---| | `template.FitWidth(value)` | 按宽度缩放(保持宽高比) | | `template.FitHeight(value)` | 按高度缩放(保持宽高比) | +| `template.MinDisplayWidth(v)` | 低于此宽度时溢出到下一页 | +| `template.MinDisplayHeight(v)` | 低于此高度时溢出到下一页 | +| `template.WithImageBorder(spec)` | 在图片周围绘制边框 | +| `template.WithImageBackground(color)` | 填充图片的 Box(适用于透明 PNG) | + +### Box 选项 + +| 选项 | 说明 | +|---|---| +| `template.WithBoxBorder(spec)` | 在 Box 周围绘制边框 | +| `template.WithBoxBackground(color)` | 填充 Box | +| `template.WithBoxPadding(edges)` | 内边距 | +| `template.WithBoxMargin(edges)` | 外边距 | +| `template.WithBoxWidth(value)` | 显式宽度 | +| `template.WithBoxHeight(value)` | 显式高度 | + +### 边框辅助函数 + +构建一次 `BorderSpec` 并通过 `WithTableBorder` / `WithTableCellBorder` / +`WithImageBorder` / `WithBoxBorder` / `WithTextBorder` 应用: + +```go +spec := template.Border( + template.BorderWidth(document.Pt(1)), // 四边统一 + template.BorderColor(pdf.RGBHex(0x1A237E)), + template.BorderLine(document.BorderSolid), // BorderSolid | BorderDashed | BorderDotted +) +``` + +| 选项 | 说明 | +|---|---| +| `template.Border(opts...)` | 构建 `BorderSpec`(默认 1pt 黑色实线) | +| `template.BorderWidth(v)` | 四边统一宽度 | +| `template.BorderWidths(t, r, b, l)` | 按 CSS 顺序设置各边宽度 | +| `template.BorderColor(c)` | 边线颜色 | +| `template.BorderLine(style)` | 线型:`BorderSolid` / `BorderDashed` / `BorderDotted` / `BorderNone` | ### 二维码选项 diff --git a/_examples/builder/35_border_test.go b/_examples/builder/35_border_test.go new file mode 100644 index 0000000..1c35090 --- /dev/null +++ b/_examples/builder/35_border_test.go @@ -0,0 +1,181 @@ +package builder_test + +import ( + "image/color" + "testing" + + "github.com/gpdf-dev/gpdf/_examples/testutil" + "github.com/gpdf-dev/gpdf/document" + "github.com/gpdf-dev/gpdf/pdf" + "github.com/gpdf-dev/gpdf/template" +) + +// TestExample_35_Border exercises the template.Border / WithTableBorder / +// WithTableCellBorder / WithImageBorder API surface added for issue #22. +// +// The page contains four tables that exercise each border pattern in turn, +// followed by a bordered image. The same golden file is shared with the +// JSON and Go-template entry points to ensure the three input methods +// produce byte-identical output. +func TestExample_35_Border(t *testing.T) { + doc := template.New( + template.WithPageSize(document.A4), + template.WithMargins(document.UniformEdges(document.Mm(20))), + ) + + page := doc.AddPage() + + header := []string{"Name", "Age", "City"} + rows := [][]string{ + {"Alice", "30", "Tokyo"}, + {"Bob", "25", "New York"}, + {"Charlie", "35", "London"}, + } + + addLabel := func(label string) { + page.AutoRow(func(r *template.RowBuilder) { + r.Col(12, func(c *template.ColBuilder) { + c.Text(label, template.FontSize(11), template.Bold()) + c.Spacer(document.Mm(2)) + }) + }) + } + + page.AutoRow(func(r *template.RowBuilder) { + r.Col(12, func(c *template.ColBuilder) { + c.Text("Borders & backgrounds", template.FontSize(18), template.Bold()) + c.Spacer(document.Mm(5)) + }) + }) + + outerBorder := template.Border( + template.BorderWidth(document.Pt(1)), + template.BorderColor(pdf.RGBHex(0x1A237E)), + ) + cellBorder := template.Border( + template.BorderWidth(document.Pt(0.5)), + template.BorderColor(pdf.Gray(0.5)), + ) + dashedCellBorder := template.Border( + template.BorderWidth(document.Pt(0.75)), + template.BorderColor(pdf.RGBHex(0x0D47A1)), + template.BorderLine(document.BorderDashed), + ) + + // Pattern A — outer frame only. + addLabel("A. Outer border only") + page.AutoRow(func(r *template.RowBuilder) { + r.Col(12, func(c *template.ColBuilder) { + c.Table(header, rows, + template.WithTableBorder(outerBorder), + template.WithTableBackground(pdf.RGBHex(0xF5F5F5)), + template.ColumnWidths(40, 20, 40), + ) + c.Spacer(document.Mm(5)) + }) + }) + + // Pattern B — cell grid only. + addLabel("B. Cell grid only") + page.AutoRow(func(r *template.RowBuilder) { + r.Col(12, func(c *template.ColBuilder) { + c.Table(header, rows, + template.WithTableCellBorder(cellBorder), + template.ColumnWidths(40, 20, 40), + ) + c.Spacer(document.Mm(5)) + }) + }) + + // Pattern C — outer frame + cell grid (Excel-style). + addLabel("C. Outer frame + cell grid") + page.AutoRow(func(r *template.RowBuilder) { + r.Col(12, func(c *template.ColBuilder) { + c.Table(header, rows, + template.WithTableBorder(outerBorder), + template.WithTableCellBorder(cellBorder), + template.WithTableBackground(pdf.RGBHex(0xFAFAFA)), + template.ColumnWidths(40, 20, 40), + ) + c.Spacer(document.Mm(5)) + }) + }) + + // Pattern D — dashed cell border with row stripes. + addLabel("D. Dashed cell grid with stripes") + page.AutoRow(func(r *template.RowBuilder) { + r.Col(12, func(c *template.ColBuilder) { + c.Table(header, rows, + template.WithTableCellBorder(dashedCellBorder), + template.TableStripe(pdf.RGBHex(0xE3F2FD)), + template.ColumnWidths(40, 20, 40), + ) + c.Spacer(document.Mm(5)) + }) + }) + + // Bordered image. + imageBorder := template.Border( + template.BorderWidths( + document.Pt(2), + document.Pt(2), + document.Pt(2), + document.Pt(2), + ), + template.BorderColor(pdf.RGBHex(0xE53935)), + template.BorderLine(document.BorderSolid), + ) + + pngData := testutil.TestImagePNG(t, 200, 100, color.RGBA{R: 66, G: 133, B: 244, A: 255}) + + addLabel("E. Image with border + background") + page.AutoRow(func(r *template.RowBuilder) { + r.Col(12, func(c *template.ColBuilder) { + c.Image( + pngData, + template.FitWidth(document.Mm(60)), + template.WithImageBorder(imageBorder), + template.WithImageBackground(pdf.RGBHex(0xFFF8E1)), + ) + }) + }) + + testutil.GeneratePDFSharedGolden(t, "35_border.pdf", doc) +} + +// TestExample_35b_BoxBorder demonstrates the new ColBuilder.Box container with +// border, background, and padding. Builder-only because the JSON / Go-template +// schema does not yet expose a "box" element. +func TestExample_35b_BoxBorder(t *testing.T) { + doc := template.New( + template.WithPageSize(document.A4), + template.WithMargins(document.UniformEdges(document.Mm(20))), + ) + + page := doc.AddPage() + + page.AutoRow(func(r *template.RowBuilder) { + r.Col(12, func(c *template.ColBuilder) { + c.Text("Box container", template.FontSize(18), template.Bold()) + c.Spacer(document.Mm(5)) + }) + }) + + page.AutoRow(func(r *template.RowBuilder) { + r.Col(12, func(c *template.ColBuilder) { + c.Box(func(c *template.ColBuilder) { + c.Text("This paragraph lives inside a Box with a 1pt border, a", template.FontSize(11)) + c.Text("light fill, and 4mm of padding on every side.", template.FontSize(11)) + }, + template.WithBoxBorder(template.Border( + template.BorderWidth(document.Pt(1)), + template.BorderColor(pdf.RGBHex(0x1A237E)), + )), + template.WithBoxBackground(pdf.RGBHex(0xE8EAF6)), + template.WithBoxPadding(document.UniformEdges(document.Mm(4))), + ) + }) + }) + + testutil.GeneratePDF(t, "35_box_border.pdf", doc) +} diff --git a/_examples/builder/testdata/golden/35_box_border.pdf b/_examples/builder/testdata/golden/35_box_border.pdf new file mode 100644 index 0000000000000000000000000000000000000000..bd539ab1cbc94fca530dd7c5f1db64747a0647e6 GIT binary patch literal 1484 zcmcIkO>f&U48053f8eD+3#_teTVFuYL%IeVwqaeGTd{*0%Z>}z%4DZ&`|Bq;A9WFI z*r5{wN2ba5NP>??S0ARQNF~XDfxEpUXJ?R1OlPOA?IHPSn+Mz13v)VgO##XE`nG>s zSxDyLiJL!22H*2@V%s7@6HOf@YRk*XhpMJa1Oabs)9R5%A^cFmXzfCz=905U{d87W-{fu4m8`_{!3^)&2^4r zc0MAKxuSWR$y{&Go)F)f>aeE&NP%zPuuM{}B)pVyz_Fj16cFRg2u)dr0%R&EQylp) zHuGiO!OD13d9zwVQ$N@a>b9#33kKp){i?$}8o*T#x6T*VzX!9MuRoJuO4XjEsjFoV zW!*F(wk;sPzXw+a+M=l23S0~J!TKkNtaOJRHMX7qt=hy$Cl_JVZQFMc#WRECt1W61 z)4gHAz%{3-0+q*k{Q=39@xd9=jql8MZtWML(6|ByNe>rPJsel7qC6RS;n*uM zcf0I2pcke$jjKihkKUGHQz*jr2ZCrb4&}S84N(VO#slq(jfGlY+rry{A7kp!5BZ@@ zi_n?Zc!(mDv$OTy)Q$DS{{7UJ4q`(>*JsYf9uD{G`q_et)pfn`{ZJdGxgtkLm*1x3 EH_0U{@ki3mVQ0Z%^;h+ERW+;F>?bcTE1XHpYL3RQ9&a}9A2Kw)hJQ)<3ytNyUwkpj z?&o3%+B{UtQJ6i-a>5yD0KU0Qv-7zZpHd9@zsBdw_@sG>j6ORzGs6rvZBLDCoVBW1{?i^ZYh8`%MnZ(!Hl9WK28Z#o#mv7cL-DW`HKy zYK0cXa_%KL206D%voS;iB$iY;8jP8SFeOQ>uE-n+*~X+STRaQ{2IBps6a<0%#t>ma z&hB~X9q0S zh@pNKQE-lu{yNg^G_F`vfeFyKdt_mFYU(f5H4GJ6vz(4Twc0>lmC;9VX`%=^XMIv! zI8h_n=omFh!JcBxb}21Qj48NgIgbw$S~eqQ@wI`NPCXSSXUE@f}H5;b%)Zx=k z%;UpEjm?MiK%t?^Y9Yq(yR+Z36zazQQtimliTcg;iAx*D@_%I=3^UiHx zB9oPOdo*;B+GmbRG$Qj7w++{uk&r=Ox8&63WJlS&)%-K$G|je9cU#cospkM~DPs@` zqH^xdlSs}U=Qd-tHFFh7U~SYnI0y-7>+q{?*5OyiYMa)T3aPFm=-ATLa}`?zx{{8u zjb*eB8&{@P*l32j+})X?x@a3rmI=FBy6S7}njmsn>l#Fwz@CDgxzu4en83PttfjKP zJg)*1>iE?5)fgJad)GP&kJY$sWSW~hu5oPzcf}!1p(*X!)=;6BVd&G*CbEpC-R^Ej zNKN<|z~&)5YIM}?Ag8`u-wr#Ur6#ATS^bRuy#<>MR}Ts*+zeo|oJUwqlNNd|$u-Q!IEINQRV6&V@Ph9{xM71mL zDWkB{B8Jzh&^5XU(8YB9I-8ra9{sR{4K=XIW%-cl9Vo&G%&=S2!xo|-w6~JLW)8R% z>#Zf_<_>EY2kaRMcoOsacd3c<3ic3i5e|~@^l*SG-}Um{?|O$=C;Ep8hT4@X`}P28 zSJJgByb-4n$VcUL2v@K-cnqCxkr?HxWIf?^?Zd;(x4~14oEmS>3=AbyBrcr5JMP|7 zE|M$kk`qp2P5M@CKEx}9in6t;=e)K>dbmk9fNIiD4tV5Q7fS|@lKzt}mJC+9jWy}p z?S&z;UcJhv&;?6RnAo8_s3x2TZc>46W0?ZmU#jANJYF z$r5Sb+mtZG8%!Qn{&1>1?3451kdN#UW-aiI-UIDww@w$Yd}5!r&_`_U<-#ELaAD~ zyWsVAGQGBI2Nylh6Qyq-oIx7Kof5bn!}0LC_XUF{rQAU%<^(=RJ5{qXf?h7RYg&l? zlhm{|z(@9a^UUJgya#&MnhyKb%E=OIdd*(QA^xDNTI~_1T4#NFJ{RV2m|PobL1q^1ifcohPs4a9DSqrAbTF67Y@fG+PyG~ zplsj_K1WvFMk))3X8|g|NSE)+s&B17wLafA`(x#H*z#*@U1C9fB|mb8y?50|&$0EQ zEQ~2sWK_IHf{(4C^N%>Z0EklO4H`J_F`))QO%sOW0Mt}feGVfxyBm$4-w||?!z)HdHxQ4ClDCC$`3i>u&tJ-e?;S#(?T4$07bjF z_bOZGkHsuoBXLfIJoiEVhaho&1l({LB60k~6`WCEPEruJ2<5g zIXXhXs8c8=a8Lo9*?`8sUe4F(J7K~VC}iZ;mLP0+)q*dJeEv9tQwly_Jj`!DvF?Is zF?Rv-TG>?&I+<>!t9)SzyeqO>I3pn)DKCb@AYuj)i*7$EuaSgAV6pf?_xc*d)7CX{ zL(Mh5EsHUHb?(Vh`0!SYU}3~O0dw4I2^O!()wNAAU9Pf19_jJ?{x*kGGvpjy<+ 0 { + refW = bc.constraints.PageWidth + } + if bc.constraints.PageHeight > 0 { + refH = bc.constraints.PageHeight + } + } + + x := pos.X.Resolve(refW, defaultFontSize) + y := pos.Y.Resolve(refH, defaultFontSize) // Determine available space for child layout. - availW := bc.contentWidth - x + availW := refW - x if box.BoxStyle.Width.Unit != document.UnitAuto && box.BoxStyle.Width.Amount > 0 { - availW = box.BoxStyle.Width.Resolve(bc.contentWidth, defaultFontSize) + availW = box.BoxStyle.Width.Resolve(refW, defaultFontSize) } - availH := bc.contentHeight - y + availH := refH - y if box.BoxStyle.Height.Unit != document.UnitAuto && box.BoxStyle.Height.Amount > 0 { - availH = box.BoxStyle.Height.Resolve(bc.contentHeight, defaultFontSize) + availH = box.BoxStyle.Height.Resolve(refH, defaultFontSize) } if availW < 0 { availW = 0 diff --git a/document/layout/engine.go b/document/layout/engine.go index ed5b675..3324f95 100644 --- a/document/layout/engine.go +++ b/document/layout/engine.go @@ -22,6 +22,14 @@ type Constraints struct { AvailableWidth float64 // AvailableHeight is the maximum vertical space in points. AvailableHeight float64 + // PageWidth is the physical page width in points, used to resolve + // absolute positions with OriginPage. Zero means fall back to + // AvailableWidth (single-box layout outside a paginator). + PageWidth float64 + // PageHeight is the physical page height in points, used to resolve + // absolute positions with OriginPage. Zero means fall back to + // AvailableHeight. + PageHeight float64 // FontResolver provides font metrics and text measurement. FontResolver FontResolver } diff --git a/document/layout/paging.go b/document/layout/paging.go index 4d31272..131839d 100644 --- a/document/layout/paging.go +++ b/document/layout/paging.go @@ -93,6 +93,8 @@ func (p *Paginator) Paginate(doc *document.Document) []PageLayout { constraints := Constraints{ AvailableWidth: contentWidth, AvailableHeight: bodyHeight, + PageWidth: pageSize.Width, + PageHeight: pageSize.Height, FontResolver: p.fontResolver, } diff --git a/template/border.go b/template/border.go new file mode 100644 index 0000000..607348d --- /dev/null +++ b/template/border.go @@ -0,0 +1,87 @@ +package template + +import ( + "github.com/gpdf-dev/gpdf/document" + "github.com/gpdf-dev/gpdf/pdf" +) + +// BorderSpec describes how to draw a border around a box-like element such +// as a Table, Image, or generic Box. Construct it with [Border] and one or +// more BorderOption values, then apply with [WithTableBorder], +// [WithImageBorder], [WithBoxBorder], or [WithTextBorder]. +// +// The zero value of BorderSpec disables the border on all four edges. +type BorderSpec struct { + top, right, bottom, left document.Value + color pdf.Color + style document.BorderStyle + hasWidth bool +} + +// BorderOption configures a [BorderSpec]. +type BorderOption func(*BorderSpec) + +// Border builds a [BorderSpec] from one or more [BorderOption] values. +// +// Defaults when no width is supplied: 1 pt uniform width, solid line, black. +// If width is supplied via [BorderWidth] or [BorderWidths], those values win. +func Border(opts ...BorderOption) BorderSpec { + spec := BorderSpec{ + color: pdf.Black, + style: document.BorderSolid, + } + for _, opt := range opts { + opt(&spec) + } + if !spec.hasWidth { + w := document.Pt(1) + spec.top, spec.right, spec.bottom, spec.left = w, w, w, w + } + return spec +} + +// BorderWidth sets the same width for all four edges. +func BorderWidth(v document.Value) BorderOption { + return func(s *BorderSpec) { + s.top, s.right, s.bottom, s.left = v, v, v, v + s.hasWidth = true + } +} + +// BorderWidths sets per-edge widths in CSS order: top, right, bottom, left. +func BorderWidths(top, right, bottom, left document.Value) BorderOption { + return func(s *BorderSpec) { + s.top, s.right, s.bottom, s.left = top, right, bottom, left + s.hasWidth = true + } +} + +// BorderColor sets the color used for all edges. +func BorderColor(c pdf.Color) BorderOption { + return func(s *BorderSpec) { s.color = c } +} + +// BorderLine sets the line style used for all edges. +// Use [document.BorderSolid], [document.BorderDashed], or [document.BorderDotted]. +func BorderLine(style document.BorderStyle) BorderOption { + return func(s *BorderSpec) { s.style = style } +} + +// toEdges converts the spec to a [document.BorderEdges] value, applying the +// uniform color and line style to every side. Edges with zero width fall back +// to [document.BorderNone] so the renderer skips them. +func (s BorderSpec) toEdges() document.BorderEdges { + side := func(w document.Value) document.BorderSide { + st := s.style + if w.Amount == 0 { + st = document.BorderNone + } + return document.BorderSide{Width: w, Style: st, Color: s.color} + } + return document.BorderEdges{ + Top: side(s.top), + Right: side(s.right), + Bottom: side(s.bottom), + Left: side(s.left), + } +} diff --git a/template/component.go b/template/component.go index 451c00e..cce98bd 100644 --- a/template/component.go +++ b/template/component.go @@ -83,12 +83,14 @@ func Strikethrough() TextOption { type ImageOption func(*imageConfig) type imageConfig struct { - width document.Value - height document.Value - minWidth document.Value - minHeight document.Value - fitMode document.ImageFitMode - align document.TextAlign + width document.Value + height document.Value + minWidth document.Value + minHeight document.Value + fitMode document.ImageFitMode + align document.TextAlign + border *BorderSpec + background *pdf.Color } // FitWidth sets the image to fit within the specified width. @@ -139,6 +141,22 @@ func MinDisplayHeight(height document.Value) ImageOption { } } +// WithImageBorder draws a border around the image using the given [BorderSpec]. +// Build a spec with [Border] and [BorderWidth], [BorderColor], etc. +func WithImageBorder(spec BorderSpec) ImageOption { + return func(cfg *imageConfig) { + cfg.border = &spec + } +} + +// WithImageBackground fills the image's box with the given color before the +// image is drawn. Useful for transparent PNGs that need a solid backdrop. +func WithImageBackground(c pdf.Color) ImageOption { + return func(cfg *imageConfig) { + cfg.background = &c + } +} + // --- Table Options --- // TableOption configures a Table element. @@ -151,6 +169,11 @@ type tableConfig struct { columnWidths []float64 cellVAlign document.VerticalAlign hasCellVAlign bool + border *BorderSpec + cellBorder *BorderSpec + background *pdf.Color + borderCollapse bool + hasCollapse bool } // TableHeaderStyle sets the header background and text color. @@ -189,6 +212,104 @@ func TableCellVAlign(align document.VerticalAlign) TableOption { } } +// WithTableBorder draws a border around the table using the given [BorderSpec]. +func WithTableBorder(spec BorderSpec) TableOption { + return func(cfg *tableConfig) { + cfg.border = &spec + } +} + +// WithTableBorderCollapse merges adjacent cell borders into a single line, +// like CSS border-collapse: collapse. The default is separated borders. +func WithTableBorderCollapse(collapse bool) TableOption { + return func(cfg *tableConfig) { + cfg.borderCollapse = collapse + cfg.hasCollapse = true + } +} + +// WithTableBackground fills the table's outer box with the given color. +func WithTableBackground(c pdf.Color) TableOption { + return func(cfg *tableConfig) { + cfg.background = &c + } +} + +// WithTableCellBorder draws the same border around every header and body +// cell, producing a uniform grid. Combine with [WithTableBorder] for an +// outer frame, or use alone for cell-only grid lines. +func WithTableCellBorder(spec BorderSpec) TableOption { + return func(cfg *tableConfig) { + cfg.cellBorder = &spec + } +} + +// --- Box Options --- + +// BoxOption configures a styled Box container created with [ColBuilder.Box]. +type BoxOption func(*boxConfig) + +type boxConfig struct { + border *BorderSpec + background *pdf.Color + padding document.Edges + hasPadding bool + margin document.Edges + hasMargin bool + width document.Value + height document.Value +} + +// WithBoxBorder draws a border around the box using the given [BorderSpec]. +func WithBoxBorder(spec BorderSpec) BoxOption { + return func(cfg *boxConfig) { + cfg.border = &spec + } +} + +// WithBoxBackground fills the box with the given color. +func WithBoxBackground(c pdf.Color) BoxOption { + return func(cfg *boxConfig) { + cfg.background = &c + } +} + +// WithBoxPadding sets uniform or per-edge padding inside the box. +func WithBoxPadding(e document.Edges) BoxOption { + return func(cfg *boxConfig) { + cfg.padding = e + cfg.hasPadding = true + } +} + +// WithBoxMargin sets uniform or per-edge margin around the box. +func WithBoxMargin(e document.Edges) BoxOption { + return func(cfg *boxConfig) { + cfg.margin = e + cfg.hasMargin = true + } +} + +// WithBoxWidth sets an explicit width for the box. +func WithBoxWidth(v document.Value) BoxOption { + return func(cfg *boxConfig) { cfg.width = v } +} + +// WithBoxHeight sets an explicit height for the box. +func WithBoxHeight(v document.Value) BoxOption { + return func(cfg *boxConfig) { cfg.height = v } +} + +// --- Text border --- + +// WithTextBorder draws a border around a text paragraph. Background color is +// available via the existing [BgColor] option. +func WithTextBorder(spec BorderSpec) TextOption { + return func(s *document.Style) { + s.Border = spec.toEdges() + } +} + // --- List Options --- // ListOption configures a List element. diff --git a/template/grid.go b/template/grid.go index b29cc07..0c14f61 100644 --- a/template/grid.go +++ b/template/grid.go @@ -215,10 +215,57 @@ func (c *ColBuilder) Image(src []byte, opts ...ImageOption) { if imgCfg.minHeight.Amount > 0 { imgNode.MinDisplayHeight = imgCfg.minHeight } + if imgCfg.border != nil { + imgNode.ImgStyle.Border = imgCfg.border.toEdges() + } + if imgCfg.background != nil { + imgNode.ImgStyle.Background = imgCfg.background + } c.nodes = append(c.nodes, imgNode) } +// Box adds a styled rectangular container that wraps the content built +// inside fn. Use [BoxOption] values such as [WithBoxBorder], +// [WithBoxBackground], or [WithBoxPadding] to style the container. +// +// c.Box(func(c *template.ColBuilder) { +// c.Text("Inside a bordered box") +// }, template.WithBoxBorder(template.Border( +// template.BorderWidth(document.Pt(1)), +// template.BorderColor(pdf.RGBHex(0x1A237E)), +// )), template.WithBoxPadding(document.UniformEdges(document.Mm(4)))) +func (c *ColBuilder) Box(fn func(c *ColBuilder), opts ...BoxOption) { + cfg := boxConfig{} + for _, opt := range opts { + opt(&cfg) + } + inner := &ColBuilder{doc: c.doc} + if fn != nil { + fn(inner) + } + box := &document.Box{ + Content: inner.buildNodes(), + BoxStyle: document.BoxStyle{ + Width: cfg.width, + Height: cfg.height, + }, + } + if cfg.border != nil { + box.BoxStyle.Border = cfg.border.toEdges() + } + if cfg.background != nil { + box.BoxStyle.Background = cfg.background + } + if cfg.hasPadding { + box.BoxStyle.Padding = cfg.padding + } + if cfg.hasMargin { + box.BoxStyle.Margin = cfg.margin + } + c.nodes = append(c.nodes, box) +} + // Table adds a table with header and body rows. func (c *ColBuilder) Table(header []string, rows [][]string, opts ...TableOption) { tbl := &document.Table{} @@ -228,6 +275,8 @@ func (c *ColBuilder) Table(header []string, rows [][]string, opts ...TableOption opt(&tblCfg) } + cellOuter := tableCellOuterStyle(&tblCfg) + // Build header row. if len(header) > 0 { headerRow := document.TableRow{} @@ -244,8 +293,9 @@ func (c *ColBuilder) Table(header []string, rows [][]string, opts ...TableOption Content: []document.DocumentNode{ &document.Text{Content: h, TextStyle: cellStyle}, }, - ColSpan: 1, - RowSpan: 1, + ColSpan: 1, + RowSpan: 1, + CellStyle: cellOuter, }) } tbl.Header = []document.TableRow{headerRow} @@ -266,8 +316,9 @@ func (c *ColBuilder) Table(header []string, rows [][]string, opts ...TableOption Content: []document.DocumentNode{ &document.Text{Content: cell, TextStyle: cellStyle}, }, - ColSpan: 1, - RowSpan: 1, + ColSpan: 1, + RowSpan: 1, + CellStyle: cellOuter, }) } tbl.Body = append(tbl.Body, bodyRow) @@ -282,9 +333,36 @@ func (c *ColBuilder) Table(header []string, rows [][]string, opts ...TableOption } } + applyTableDecoration(tbl, &tblCfg) + c.nodes = append(c.nodes, tbl) } +// applyTableDecoration copies border, background, and border-collapse +// settings from a tableConfig onto the constructed Table node. +func applyTableDecoration(tbl *document.Table, cfg *tableConfig) { + if cfg.border != nil { + tbl.TableStyle.Border = cfg.border.toEdges() + } + if cfg.background != nil { + tbl.TableStyle.Background = cfg.background + } + if cfg.hasCollapse { + tbl.TableStyle.BorderCollapse = cfg.borderCollapse + } +} + +// tableCellOuterStyle builds the outer per-cell Style (border, background, +// padding) shared by every header and body cell. Returns the zero Style when +// no cell-level decoration is configured. +func tableCellOuterStyle(cfg *tableConfig) document.Style { + var s document.Style + if cfg.cellBorder != nil { + s.Border = cfg.cellBorder.toEdges() + } + return s +} + // Line adds a horizontal line (rule) to the column. func (c *ColBuilder) Line(opts ...LineOption) { cfg := lineConfig{ diff --git a/template/schema.go b/template/schema.go index 7be4142..e5000d3 100644 --- a/template/schema.go +++ b/template/schema.go @@ -127,23 +127,41 @@ type SchemaStyle struct { // SchemaImage defines an image element. type SchemaImage struct { - Src string `json:"src"` // base64, data URI, or file path - Width string `json:"width,omitempty"` // dimension - Height string `json:"height,omitempty"` // dimension - MinWidth string `json:"minWidth,omitempty"` // minimum display width; overflow to next page when violated - MinHeight string `json:"minHeight,omitempty"` // minimum display height; overflow to next page when violated - Fit string `json:"fit,omitempty"` // "contain"|"cover"|"stretch"|"original" - Align string `json:"align,omitempty"` // "left"|"center"|"right" + Src string `json:"src"` // base64, data URI, or file path + Width string `json:"width,omitempty"` // dimension + Height string `json:"height,omitempty"` // dimension + MinWidth string `json:"minWidth,omitempty"` // minimum display width; overflow to next page when violated + MinHeight string `json:"minHeight,omitempty"` // minimum display height; overflow to next page when violated + Fit string `json:"fit,omitempty"` // "contain"|"cover"|"stretch"|"original" + Align string `json:"align,omitempty"` // "left"|"center"|"right" + Border *SchemaBorder `json:"border,omitempty"` // optional border around the image + Background string `json:"background,omitempty"` // "#RRGGBB" or named, drawn behind the image } // SchemaTable defines a table element. type SchemaTable struct { - Header []string `json:"header"` - Rows [][]string `json:"rows"` - ColumnWidths []float64 `json:"columnWidths,omitempty"` - HeaderStyle *SchemaStyle `json:"headerStyle,omitempty"` - StripeColor string `json:"stripeColor,omitempty"` - CellVAlign string `json:"cellVAlign,omitempty"` // "top", "middle", "bottom" + Header []string `json:"header"` + Rows [][]string `json:"rows"` + ColumnWidths []float64 `json:"columnWidths,omitempty"` + HeaderStyle *SchemaStyle `json:"headerStyle,omitempty"` + StripeColor string `json:"stripeColor,omitempty"` + CellVAlign string `json:"cellVAlign,omitempty"` // "top", "middle", "bottom" + Border *SchemaBorder `json:"border,omitempty"` + CellBorder *SchemaBorder `json:"cellBorder,omitempty"` // applies to every header + body cell + BorderCollapse *bool `json:"borderCollapse,omitempty"` + Background string `json:"background,omitempty"` // "#RRGGBB" or named +} + +// SchemaBorder defines border styling for table/image/box elements. +// +// Width is the uniform edge width (e.g. "1pt", "2mm"). Use Widths for per-edge +// values in CSS order [top, right, bottom, left]. If both are set, Widths wins. +// Style accepts "solid" (default), "dashed", "dotted", or "none". +type SchemaBorder struct { + Width string `json:"width,omitempty"` + Widths []string `json:"widths,omitempty"` + Color string `json:"color,omitempty"` + Style string `json:"style,omitempty"` } // SchemaList defines a list element. @@ -383,6 +401,61 @@ func appendColorOpt(opts []TextOption, s string, fn func(pdf.Color) TextOption) return opts } +// parseBorderLineStyle converts a string into a [document.BorderStyle]. +// Empty string returns BorderSolid (matches the [Border] default). +func parseBorderLineStyle(s string) (document.BorderStyle, bool) { + switch strings.ToLower(strings.TrimSpace(s)) { + case "", "solid": + return document.BorderSolid, true + case "dashed": + return document.BorderDashed, true + case "dotted": + return document.BorderDotted, true + case "none": + return document.BorderNone, true + default: + return document.BorderSolid, false + } +} + +// parseSchemaBorder turns a SchemaBorder into a [BorderSpec]. Returns ok=false +// when the schema is nil or the width/widths cannot be parsed. +func parseSchemaBorder(b *SchemaBorder) (BorderSpec, bool) { + if b == nil { + return BorderSpec{}, false + } + var bopts []BorderOption + switch { + case len(b.Widths) == 4: + var edges [4]document.Value + for i, w := range b.Widths { + v, err := parseValue(w) + if err != nil { + return BorderSpec{}, false + } + edges[i] = v + } + bopts = append(bopts, BorderWidths(edges[0], edges[1], edges[2], edges[3])) + case b.Width != "": + v, err := parseValue(b.Width) + if err != nil { + return BorderSpec{}, false + } + bopts = append(bopts, BorderWidth(v)) + } + if b.Color != "" { + if c, err := parseColor(b.Color); err == nil { + bopts = append(bopts, BorderColor(c)) + } + } + if b.Style != "" { + if st, ok := parseBorderLineStyle(b.Style); ok { + bopts = append(bopts, BorderLine(st)) + } + } + return Border(bopts...), true +} + // decodeBase64Image decodes a base64-encoded image string. // Supports both raw base64 and data URI format (data:image/...;base64,...). func decodeBase64Image(s string) ([]byte, error) { @@ -619,27 +692,7 @@ func buildSchemaImage(c *ColBuilder, img *SchemaImage) { if err != nil { return // silently skip, consistent with builder API pattern } - var opts []ImageOption - if img.Width != "" { - if v, err := parseValue(img.Width); err == nil { - opts = append(opts, FitWidth(v)) - } - } - if img.Height != "" { - if v, err := parseValue(img.Height); err == nil { - opts = append(opts, FitHeight(v)) - } - } - if img.MinWidth != "" { - if v, err := parseValue(img.MinWidth); err == nil { - opts = append(opts, MinDisplayWidth(v)) - } - } - if img.MinHeight != "" { - if v, err := parseValue(img.MinHeight); err == nil { - opts = append(opts, MinDisplayHeight(v)) - } - } + opts := schemaImageDimensionOpts(img) if img.Fit != "" { if mode, ok := parseFitMode(img.Fit); ok { opts = append(opts, WithFitMode(mode)) @@ -650,9 +703,43 @@ func buildSchemaImage(c *ColBuilder, img *SchemaImage) { opts = append(opts, WithAlign(align)) } } + if spec, ok := parseSchemaBorder(img.Border); ok { + opts = append(opts, WithImageBorder(spec)) + } + if img.Background != "" { + if c2, err := parseColor(img.Background); err == nil { + opts = append(opts, WithImageBackground(c2)) + } + } c.Image(data, opts...) } +// schemaImageDimensionOpts converts the dimension fields of a SchemaImage +// (width, height, minWidth, minHeight) into the matching ImageOption slice. +func schemaImageDimensionOpts(img *SchemaImage) []ImageOption { + dims := []struct { + raw string + fn func(document.Value) ImageOption + }{ + {img.Width, FitWidth}, + {img.Height, FitHeight}, + {img.MinWidth, MinDisplayWidth}, + {img.MinHeight, MinDisplayHeight}, + } + var opts []ImageOption + for _, d := range dims { + if d.raw == "" { + continue + } + v, err := parseValue(d.raw) + if err != nil { + continue + } + opts = append(opts, d.fn(v)) + } + return opts +} + // parseFitMode converts a fit mode string to an ImageFitMode constant. func parseFitMode(s string) (document.ImageFitMode, bool) { switch strings.ToLower(s) { @@ -719,6 +806,20 @@ func buildSchemaTable(c *ColBuilder, tbl *SchemaTable) { opts = append(opts, TableCellVAlign(align)) } } + if spec, ok := parseSchemaBorder(tbl.Border); ok { + opts = append(opts, WithTableBorder(spec)) + } + if spec, ok := parseSchemaBorder(tbl.CellBorder); ok { + opts = append(opts, WithTableCellBorder(spec)) + } + if tbl.BorderCollapse != nil { + opts = append(opts, WithTableBorderCollapse(*tbl.BorderCollapse)) + } + if tbl.Background != "" { + if c2, err := parseColor(tbl.Background); err == nil { + opts = append(opts, WithTableBackground(c2)) + } + } c.Table(tbl.Header, tbl.Rows, opts...) } From 055ac15afc94377ab9ef223fbaddf7366f80578c Mon Sep 17 00:00:00 2001 From: Taiki Noda Date: Wed, 29 Apr 2026 19:19:06 +0900 Subject: [PATCH 2/3] chore: bump version to v1.0.7 --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- CHANGELOG.md | 12 +++++++++++- internal/buildinfo/version.go | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 06a26ff..959f871 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -63,7 +63,7 @@ body: id: gpdf-version attributes: label: gpdf Version - placeholder: "v1.0.6" + placeholder: "v1.0.7" validations: required: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 0027564..a45fff8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] +## [1.0.7] - 2026-04-29 + +### Added +- Borders and backgrounds for tables, images, and boxes + - Builder: `Border(opts...)` with `BorderWidth`, `BorderWidths`, `BorderColor`, `BorderLine`; applied via `WithTableBorder`, `WithImageBorder`, `WithBoxBorder`, `WithTextBorder`, plus `WithImageBackground` / table & box background options + - JSON / GoTemplate schema: `SchemaBorder { width, widths, color, style }`; `image.border` / `image.background`; `table.border` / `table.cellBorder` / `table.borderCollapse` / `table.background` + - Layout engine support: borders participate in box-model sizing and pagination (`document/layout/{block,engine,paging}.go`) +- Example tests: `_examples/{builder,json,gotemplate}/35_border_test.go` with shared golden + ## [1.0.6] - 2026-04-20 ### Added @@ -141,7 +150,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/). - Reed-Solomon coefficient order in QR code encoder - binary.Write return value handling for errcheck lint -[Unreleased]: https://github.com/gpdf-dev/gpdf/compare/v1.0.6...HEAD +[Unreleased]: https://github.com/gpdf-dev/gpdf/compare/v1.0.7...HEAD +[1.0.7]: https://github.com/gpdf-dev/gpdf/compare/v1.0.6...v1.0.7 [1.0.6]: https://github.com/gpdf-dev/gpdf/compare/v1.0.5...v1.0.6 [1.0.5]: https://github.com/gpdf-dev/gpdf/compare/v1.0.4...v1.0.5 [1.0.4]: https://github.com/gpdf-dev/gpdf/compare/v1.0.3...v1.0.4 diff --git a/internal/buildinfo/version.go b/internal/buildinfo/version.go index bd6ff3d..408136a 100644 --- a/internal/buildinfo/version.go +++ b/internal/buildinfo/version.go @@ -3,4 +3,4 @@ package buildinfo // Version is the library version. It is the single source of truth used by // the public gpdf.Version constant and the default PDF Producer metadata. -const Version = "1.0.6" +const Version = "1.0.7" From 51f36d49ba3c193808690d06b1deaf5b2901e958 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:24:43 +0000 Subject: [PATCH 3/3] update coverage badge to 84.4% --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 78cd6d5..c61549b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Go Reference](https://pkg.go.dev/badge/github.com/gpdf-dev/gpdf.svg)](https://pkg.go.dev/github.com/gpdf-dev/gpdf) [![CI](https://github.com/gpdf-dev/gpdf/actions/workflows/check-code.yml/badge.svg)](https://github.com/gpdf-dev/gpdf/actions/workflows/check-code.yml) -![coverage](https://img.shields.io/badge/coverage-85.8%25-green) +![coverage](https://img.shields.io/badge/coverage-84.4%25-green) [![Go Report Card](https://goreportcard.com/badge/github.com/gpdf-dev/gpdf)](https://goreportcard.com/report/github.com/gpdf-dev/gpdf) [![Go Version](https://img.shields.io/badge/Go-%3E%3D1.22-blue)](https://go.dev/) [![Website](https://img.shields.io/badge/Website-gpdf.dev-blue)](https://gpdf.dev/)