[{"data":1,"prerenderedAt":2901},["ShallowReactive",2],{"blog-ja-invoice-pdf-go-under-50-lines":3},{"id":4,"title":5,"author":6,"body":10,"date":2867,"description":2868,"draft":2869,"extension":2870,"howTo":2871,"image":2892,"meta":2893,"navigation":130,"path":2894,"seo":2895,"stem":2896,"tags":2897,"updated":2892,"__hash__":2900},"blogJa/ja/blog/012.invoice-pdf-go-under-50-lines.md","Go で請求書 PDF を 50 行以下で作る",{"name":7,"url":8,"avatar":9},"野田大貴","https://nadai.dev/ja/about","https://nadai.dev/og-default.png",{"type":11,"value":12,"toc":2847},"minimark",[13,18,36,39,64,70,74,77,80,96,99,104,107,110,1144,1151,1159,1162,1165,1168,1239,1245,1256,1264,1274,1278,1314,1339,1350,1353,1460,1475,1487,1494,1505,1508,1711,1714,1732,1752,1755,1766,1769,1820,1840,1843,1950,1963,1974,1976,1979,2124,2132,2145,2149,2152,2162,2291,2294,2304,2412,2415,2425,2509,2515,2539,2546,2549,2616,2622,2634,2637,2640,2650,2668,2688,2694,2697,2701,2707,2728,2738,2760,2770,2785,2789,2792,2804,2818,2821,2843],[14,15,17],"h2",{"id":16},"tldr","TL;DR",[19,20,21,22,26,27,31,32,35],"p",{},"Go で動く請求書 PDF を ",[23,24,25],"strong",{},"50 行"," で書く。",[28,29,30],"code",{},"main.go"," 1 つ、",[28,33,34],{},"go get"," 1 回、Chromium なし、CGO なし、テンプレート言語なし、HTML なし。表もストライプも右寄せ合計もあり。コード全体は下に置く。残りは各ブロックの解説と、このスタイルが破綻する条件の話。",[19,37,38],{},"先にコードだけ読みたい人向け:",[40,41,46],"pre",{"className":42,"code":43,"language":44,"meta":45,"style":45},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","go get github.com/gpdf-dev/gpdf\n","bash","",[28,47,48],{"__ignoreMap":45},[49,50,53,57,61],"span",{"class":51,"line":52},"line",1,[49,54,56],{"class":55},"sBMFI","go",[49,58,60],{"class":59},"sfazB"," get",[49,62,63],{"class":59}," github.com/gpdf-dev/gpdf\n",[19,65,66,67,69],{},"あとは次のセクションの ",[28,68,30],{}," を貼るだけ。",[14,71,73],{"id":72},"なぜ50-行以下という閾値にこだわるのか","なぜ「50 行以下」という閾値にこだわるのか",[19,75,76],{},"正直に書くと、「go 請求書 pdf」で検索して出てくる記事の多くは、(a) ヘッドレス Chromium を立てろと言うか、(b) 1 つの表を描くのに PDF の低レベル演算子を 400 行書く例を載せているか、のどちらか。両方とも技術的には間違いではない。でも、やりたいことの形と合っていない。",[19,78,79],{},"まともな請求書に必要なのは:",[81,82,83,87,90,93],"ul",{},[84,85,86],"li",{},"発行元と宛先の情報が載ったヘッダ",[84,88,89],{},"請求番号と支払期日",[84,91,92],{},"明細行の表",[84,94,95],{},"合計金額",[19,97,98],{},"4 つ。だからコードも 4 ブロックで済むはず。1 画面に収まらない時点で、ライブラリの選択が間違っている。",[19,100,101,103],{},[23,102,25],{}," は、普通のエディタで 1 画面に収まる限界値。そしてレビュアーがスクロールせずに全部読む限界でもある。これを切れると、生成結果を Slack に貼るだけで他人がライブラリを理解できる。そのラインを狙う。",[19,105,106],{},"下のコードは gofmt 済み、import 完全展開、エラーも全部拾っている。隠されたヘルパーパッケージも小細工もない。見えているものがそのままコンパイルされる。",[14,108,25],{"id":109},"_50-行",[40,111,114],{"className":112,"code":113,"language":56,"meta":45,"style":45},"language-go shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","package main\n\nimport (\n    \"log\"\n    \"os\"\n\n    \"github.com/gpdf-dev/gpdf\"\n    \"github.com/gpdf-dev/gpdf/document\"\n    \"github.com/gpdf-dev/gpdf/pdf\"\n    \"github.com/gpdf-dev/gpdf/template\"\n)\n\nfunc main() {\n    doc := gpdf.NewDocument(template.WithPageSize(document.A4))\n    page := doc.AddPage()\n    page.AutoRow(func(r *template.RowBuilder) {\n        r.Col(6, func(c *template.ColBuilder) {\n            c.Text(\"ACME 株式会社\", template.FontSize(22), template.Bold())\n            c.Text(\"東京都千代田区丸の内 1-1-1\")\n        })\n        r.Col(6, func(c *template.ColBuilder) {\n            c.Text(\"請求書 #INV-2026-001\", template.Bold(), template.AlignRight())\n            c.Text(\"支払期日: 2026-03-31\", template.AlignRight())\n        })\n    })\n    page.AutoRow(func(r *template.RowBuilder) {\n        r.Col(12, func(c *template.ColBuilder) {\n            c.Spacer(document.Mm(6))\n            c.Table(\n                []string{\"品目\", \"数量\", \"単価\", \"金額\"},\n                [][]string{\n                    {\"フロントエンド開発\", \"40 時間\", \"¥18,000\", \"¥720,000\"},\n                    {\"バックエンド開発\", \"60 時間\", \"¥18,000\", \"¥1,080,000\"},\n                    {\"UI/UX デザイン\", \"20 時間\", \"¥15,000\", \"¥300,000\"},\n                },\n                template.ColumnWidths(40, 15, 20, 25),\n                template.TableHeaderStyle(template.Bold(), template.BgColor(pdf.RGBHex(0xF0F0F0))),\n                template.TableStripe(pdf.RGBHex(0xFAFAFA)),\n            )\n            c.Text(\"合計: ¥2,100,000\", template.AlignRight(), template.Bold(), template.FontSize(14))\n        })\n    })\n    b, err := doc.Generate()\n    if err != nil {\n        log.Fatal(err)\n    }\n    if err := os.WriteFile(\"invoice.pdf\", b, 0644); err != nil {\n        log.Fatal(err)\n    }\n}\n",[28,115,116,125,132,142,154,164,169,179,189,199,209,215,220,236,279,298,331,370,419,439,445,476,514,542,547,553,578,610,635,648,699,710,752,792,833,839,873,918,944,950,999,1004,1009,1031,1047,1065,1071,1118,1133,1138],{"__ignoreMap":45},[49,117,118,122],{"class":51,"line":52},[49,119,121],{"class":120},"sMK4o","package",[49,123,124],{"class":55}," main\n",[49,126,128],{"class":51,"line":127},2,[49,129,131],{"emptyLinePlaceholder":130},true,"\n",[49,133,135,139],{"class":51,"line":134},3,[49,136,138],{"class":137},"s7zQu","import",[49,140,141],{"class":120}," (\n",[49,143,145,148,151],{"class":51,"line":144},4,[49,146,147],{"class":120},"    \"",[49,149,150],{"class":55},"log",[49,152,153],{"class":120},"\"\n",[49,155,157,159,162],{"class":51,"line":156},5,[49,158,147],{"class":120},[49,160,161],{"class":55},"os",[49,163,153],{"class":120},[49,165,167],{"class":51,"line":166},6,[49,168,131],{"emptyLinePlaceholder":130},[49,170,172,174,177],{"class":51,"line":171},7,[49,173,147],{"class":120},[49,175,176],{"class":55},"github.com/gpdf-dev/gpdf",[49,178,153],{"class":120},[49,180,182,184,187],{"class":51,"line":181},8,[49,183,147],{"class":120},[49,185,186],{"class":55},"github.com/gpdf-dev/gpdf/document",[49,188,153],{"class":120},[49,190,192,194,197],{"class":51,"line":191},9,[49,193,147],{"class":120},[49,195,196],{"class":55},"github.com/gpdf-dev/gpdf/pdf",[49,198,153],{"class":120},[49,200,202,204,207],{"class":51,"line":201},10,[49,203,147],{"class":120},[49,205,206],{"class":55},"github.com/gpdf-dev/gpdf/template",[49,208,153],{"class":120},[49,210,212],{"class":51,"line":211},11,[49,213,214],{"class":120},")\n",[49,216,218],{"class":51,"line":217},12,[49,219,131],{"emptyLinePlaceholder":130},[49,221,223,226,230,233],{"class":51,"line":222},13,[49,224,225],{"class":120},"func",[49,227,229],{"class":228},"s2Zo4"," main",[49,231,232],{"class":120},"()",[49,234,235],{"class":120}," {\n",[49,237,239,243,246,249,252,255,258,261,263,266,268,271,273,276],{"class":51,"line":238},14,[49,240,242],{"class":241},"sTEyZ","    doc ",[49,244,245],{"class":120},":=",[49,247,248],{"class":241}," gpdf",[49,250,251],{"class":120},".",[49,253,254],{"class":228},"NewDocument",[49,256,257],{"class":120},"(",[49,259,260],{"class":241},"template",[49,262,251],{"class":120},[49,264,265],{"class":228},"WithPageSize",[49,267,257],{"class":120},[49,269,270],{"class":241},"document",[49,272,251],{"class":120},[49,274,275],{"class":241},"A4",[49,277,278],{"class":120},"))\n",[49,280,282,285,287,290,292,295],{"class":51,"line":281},15,[49,283,284],{"class":241},"    page ",[49,286,245],{"class":120},[49,288,289],{"class":241}," doc",[49,291,251],{"class":120},[49,293,294],{"class":228},"AddPage",[49,296,297],{"class":120},"()\n",[49,299,301,304,306,309,312,316,319,321,323,326,329],{"class":51,"line":300},16,[49,302,303],{"class":241},"    page",[49,305,251],{"class":120},[49,307,308],{"class":228},"AutoRow",[49,310,311],{"class":120},"(func(",[49,313,315],{"class":314},"sHdIc","r",[49,317,318],{"class":120}," *",[49,320,260],{"class":55},[49,322,251],{"class":120},[49,324,325],{"class":55},"RowBuilder",[49,327,328],{"class":120},")",[49,330,235],{"class":120},[49,332,334,337,339,342,344,348,351,354,357,359,361,363,366,368],{"class":51,"line":333},17,[49,335,336],{"class":241},"        r",[49,338,251],{"class":120},[49,340,341],{"class":228},"Col",[49,343,257],{"class":120},[49,345,347],{"class":346},"sbssI","6",[49,349,350],{"class":120},",",[49,352,353],{"class":120}," func(",[49,355,356],{"class":314},"c",[49,358,318],{"class":120},[49,360,260],{"class":55},[49,362,251],{"class":120},[49,364,365],{"class":55},"ColBuilder",[49,367,328],{"class":120},[49,369,235],{"class":120},[49,371,373,376,378,381,383,386,389,391,393,396,398,401,403,406,409,411,413,416],{"class":51,"line":372},18,[49,374,375],{"class":241},"            c",[49,377,251],{"class":120},[49,379,380],{"class":228},"Text",[49,382,257],{"class":120},[49,384,385],{"class":120},"\"",[49,387,388],{"class":59},"ACME 株式会社",[49,390,385],{"class":120},[49,392,350],{"class":120},[49,394,395],{"class":241}," template",[49,397,251],{"class":120},[49,399,400],{"class":228},"FontSize",[49,402,257],{"class":120},[49,404,405],{"class":346},"22",[49,407,408],{"class":120},"),",[49,410,395],{"class":241},[49,412,251],{"class":120},[49,414,415],{"class":228},"Bold",[49,417,418],{"class":120},"())\n",[49,420,422,424,426,428,430,432,435,437],{"class":51,"line":421},19,[49,423,375],{"class":241},[49,425,251],{"class":120},[49,427,380],{"class":228},[49,429,257],{"class":120},[49,431,385],{"class":120},[49,433,434],{"class":59},"東京都千代田区丸の内 1-1-1",[49,436,385],{"class":120},[49,438,214],{"class":120},[49,440,442],{"class":51,"line":441},20,[49,443,444],{"class":120},"        })\n",[49,446,448,450,452,454,456,458,460,462,464,466,468,470,472,474],{"class":51,"line":447},21,[49,449,336],{"class":241},[49,451,251],{"class":120},[49,453,341],{"class":228},[49,455,257],{"class":120},[49,457,347],{"class":346},[49,459,350],{"class":120},[49,461,353],{"class":120},[49,463,356],{"class":314},[49,465,318],{"class":120},[49,467,260],{"class":55},[49,469,251],{"class":120},[49,471,365],{"class":55},[49,473,328],{"class":120},[49,475,235],{"class":120},[49,477,479,481,483,485,487,489,492,494,496,498,500,502,505,507,509,512],{"class":51,"line":478},22,[49,480,375],{"class":241},[49,482,251],{"class":120},[49,484,380],{"class":228},[49,486,257],{"class":120},[49,488,385],{"class":120},[49,490,491],{"class":59},"請求書 #INV-2026-001",[49,493,385],{"class":120},[49,495,350],{"class":120},[49,497,395],{"class":241},[49,499,251],{"class":120},[49,501,415],{"class":228},[49,503,504],{"class":120},"(),",[49,506,395],{"class":241},[49,508,251],{"class":120},[49,510,511],{"class":228},"AlignRight",[49,513,418],{"class":120},[49,515,517,519,521,523,525,527,530,532,534,536,538,540],{"class":51,"line":516},23,[49,518,375],{"class":241},[49,520,251],{"class":120},[49,522,380],{"class":228},[49,524,257],{"class":120},[49,526,385],{"class":120},[49,528,529],{"class":59},"支払期日: 2026-03-31",[49,531,385],{"class":120},[49,533,350],{"class":120},[49,535,395],{"class":241},[49,537,251],{"class":120},[49,539,511],{"class":228},[49,541,418],{"class":120},[49,543,545],{"class":51,"line":544},24,[49,546,444],{"class":120},[49,548,550],{"class":51,"line":549},25,[49,551,552],{"class":120},"    })\n",[49,554,556,558,560,562,564,566,568,570,572,574,576],{"class":51,"line":555},26,[49,557,303],{"class":241},[49,559,251],{"class":120},[49,561,308],{"class":228},[49,563,311],{"class":120},[49,565,315],{"class":314},[49,567,318],{"class":120},[49,569,260],{"class":55},[49,571,251],{"class":120},[49,573,325],{"class":55},[49,575,328],{"class":120},[49,577,235],{"class":120},[49,579,581,583,585,587,589,592,594,596,598,600,602,604,606,608],{"class":51,"line":580},27,[49,582,336],{"class":241},[49,584,251],{"class":120},[49,586,341],{"class":228},[49,588,257],{"class":120},[49,590,591],{"class":346},"12",[49,593,350],{"class":120},[49,595,353],{"class":120},[49,597,356],{"class":314},[49,599,318],{"class":120},[49,601,260],{"class":55},[49,603,251],{"class":120},[49,605,365],{"class":55},[49,607,328],{"class":120},[49,609,235],{"class":120},[49,611,613,615,617,620,622,624,626,629,631,633],{"class":51,"line":612},28,[49,614,375],{"class":241},[49,616,251],{"class":120},[49,618,619],{"class":228},"Spacer",[49,621,257],{"class":120},[49,623,270],{"class":241},[49,625,251],{"class":120},[49,627,628],{"class":228},"Mm",[49,630,257],{"class":120},[49,632,347],{"class":346},[49,634,278],{"class":120},[49,636,638,640,642,645],{"class":51,"line":637},29,[49,639,375],{"class":241},[49,641,251],{"class":120},[49,643,644],{"class":228},"Table",[49,646,647],{"class":120},"(\n",[49,649,651,654,658,661,663,666,668,670,673,676,678,680,682,685,687,689,691,694,696],{"class":51,"line":650},30,[49,652,653],{"class":120},"                []",[49,655,657],{"class":656},"spNyl","string",[49,659,660],{"class":120},"{",[49,662,385],{"class":120},[49,664,665],{"class":59},"品目",[49,667,385],{"class":120},[49,669,350],{"class":120},[49,671,672],{"class":120}," \"",[49,674,675],{"class":59},"数量",[49,677,385],{"class":120},[49,679,350],{"class":120},[49,681,672],{"class":120},[49,683,684],{"class":59},"単価",[49,686,385],{"class":120},[49,688,350],{"class":120},[49,690,672],{"class":120},[49,692,693],{"class":59},"金額",[49,695,385],{"class":120},[49,697,698],{"class":120},"},\n",[49,700,702,705,707],{"class":51,"line":701},31,[49,703,704],{"class":120},"                [][]",[49,706,657],{"class":656},[49,708,709],{"class":120},"{\n",[49,711,713,716,718,721,723,725,727,730,732,734,736,739,741,743,745,748,750],{"class":51,"line":712},32,[49,714,715],{"class":120},"                    {",[49,717,385],{"class":120},[49,719,720],{"class":59},"フロントエンド開発",[49,722,385],{"class":120},[49,724,350],{"class":120},[49,726,672],{"class":120},[49,728,729],{"class":59},"40 時間",[49,731,385],{"class":120},[49,733,350],{"class":120},[49,735,672],{"class":120},[49,737,738],{"class":59},"¥18,000",[49,740,385],{"class":120},[49,742,350],{"class":120},[49,744,672],{"class":120},[49,746,747],{"class":59},"¥720,000",[49,749,385],{"class":120},[49,751,698],{"class":120},[49,753,755,757,759,762,764,766,768,771,773,775,777,779,781,783,785,788,790],{"class":51,"line":754},33,[49,756,715],{"class":120},[49,758,385],{"class":120},[49,760,761],{"class":59},"バックエンド開発",[49,763,385],{"class":120},[49,765,350],{"class":120},[49,767,672],{"class":120},[49,769,770],{"class":59},"60 時間",[49,772,385],{"class":120},[49,774,350],{"class":120},[49,776,672],{"class":120},[49,778,738],{"class":59},[49,780,385],{"class":120},[49,782,350],{"class":120},[49,784,672],{"class":120},[49,786,787],{"class":59},"¥1,080,000",[49,789,385],{"class":120},[49,791,698],{"class":120},[49,793,795,797,799,802,804,806,808,811,813,815,817,820,822,824,826,829,831],{"class":51,"line":794},34,[49,796,715],{"class":120},[49,798,385],{"class":120},[49,800,801],{"class":59},"UI/UX デザイン",[49,803,385],{"class":120},[49,805,350],{"class":120},[49,807,672],{"class":120},[49,809,810],{"class":59},"20 時間",[49,812,385],{"class":120},[49,814,350],{"class":120},[49,816,672],{"class":120},[49,818,819],{"class":59},"¥15,000",[49,821,385],{"class":120},[49,823,350],{"class":120},[49,825,672],{"class":120},[49,827,828],{"class":59},"¥300,000",[49,830,385],{"class":120},[49,832,698],{"class":120},[49,834,836],{"class":51,"line":835},35,[49,837,838],{"class":120},"                },\n",[49,840,842,845,847,850,852,855,857,860,862,865,867,870],{"class":51,"line":841},36,[49,843,844],{"class":241},"                template",[49,846,251],{"class":120},[49,848,849],{"class":228},"ColumnWidths",[49,851,257],{"class":120},[49,853,854],{"class":346},"40",[49,856,350],{"class":120},[49,858,859],{"class":346}," 15",[49,861,350],{"class":120},[49,863,864],{"class":346}," 20",[49,866,350],{"class":120},[49,868,869],{"class":346}," 25",[49,871,872],{"class":120},"),\n",[49,874,876,878,880,883,885,887,889,891,893,895,897,900,902,905,907,910,912,915],{"class":51,"line":875},37,[49,877,844],{"class":241},[49,879,251],{"class":120},[49,881,882],{"class":228},"TableHeaderStyle",[49,884,257],{"class":120},[49,886,260],{"class":241},[49,888,251],{"class":120},[49,890,415],{"class":228},[49,892,504],{"class":120},[49,894,395],{"class":241},[49,896,251],{"class":120},[49,898,899],{"class":228},"BgColor",[49,901,257],{"class":120},[49,903,904],{"class":241},"pdf",[49,906,251],{"class":120},[49,908,909],{"class":228},"RGBHex",[49,911,257],{"class":120},[49,913,914],{"class":346},"0xF0F0F0",[49,916,917],{"class":120},"))),\n",[49,919,921,923,925,928,930,932,934,936,938,941],{"class":51,"line":920},38,[49,922,844],{"class":241},[49,924,251],{"class":120},[49,926,927],{"class":228},"TableStripe",[49,929,257],{"class":120},[49,931,904],{"class":241},[49,933,251],{"class":120},[49,935,909],{"class":228},[49,937,257],{"class":120},[49,939,940],{"class":346},"0xFAFAFA",[49,942,943],{"class":120},")),\n",[49,945,947],{"class":51,"line":946},39,[49,948,949],{"class":120},"            )\n",[49,951,953,955,957,959,961,963,966,968,970,972,974,976,978,980,982,984,986,988,990,992,994,997],{"class":51,"line":952},40,[49,954,375],{"class":241},[49,956,251],{"class":120},[49,958,380],{"class":228},[49,960,257],{"class":120},[49,962,385],{"class":120},[49,964,965],{"class":59},"合計: ¥2,100,000",[49,967,385],{"class":120},[49,969,350],{"class":120},[49,971,395],{"class":241},[49,973,251],{"class":120},[49,975,511],{"class":228},[49,977,504],{"class":120},[49,979,395],{"class":241},[49,981,251],{"class":120},[49,983,415],{"class":228},[49,985,504],{"class":120},[49,987,395],{"class":241},[49,989,251],{"class":120},[49,991,400],{"class":228},[49,993,257],{"class":120},[49,995,996],{"class":346},"14",[49,998,278],{"class":120},[49,1000,1002],{"class":51,"line":1001},41,[49,1003,444],{"class":120},[49,1005,1007],{"class":51,"line":1006},42,[49,1008,552],{"class":120},[49,1010,1012,1015,1017,1020,1022,1024,1026,1029],{"class":51,"line":1011},43,[49,1013,1014],{"class":241},"    b",[49,1016,350],{"class":120},[49,1018,1019],{"class":241}," err ",[49,1021,245],{"class":120},[49,1023,289],{"class":241},[49,1025,251],{"class":120},[49,1027,1028],{"class":228},"Generate",[49,1030,297],{"class":120},[49,1032,1034,1037,1039,1042,1045],{"class":51,"line":1033},44,[49,1035,1036],{"class":137},"    if",[49,1038,1019],{"class":241},[49,1040,1041],{"class":120},"!=",[49,1043,1044],{"class":120}," nil",[49,1046,235],{"class":120},[49,1048,1050,1053,1055,1058,1060,1063],{"class":51,"line":1049},45,[49,1051,1052],{"class":241},"        log",[49,1054,251],{"class":120},[49,1056,1057],{"class":228},"Fatal",[49,1059,257],{"class":120},[49,1061,1062],{"class":241},"err",[49,1064,214],{"class":120},[49,1066,1068],{"class":51,"line":1067},46,[49,1069,1070],{"class":120},"    }\n",[49,1072,1074,1076,1078,1080,1083,1085,1088,1090,1092,1095,1097,1099,1102,1104,1107,1110,1112,1114,1116],{"class":51,"line":1073},47,[49,1075,1036],{"class":137},[49,1077,1019],{"class":241},[49,1079,245],{"class":120},[49,1081,1082],{"class":241}," os",[49,1084,251],{"class":120},[49,1086,1087],{"class":228},"WriteFile",[49,1089,257],{"class":120},[49,1091,385],{"class":120},[49,1093,1094],{"class":59},"invoice.pdf",[49,1096,385],{"class":120},[49,1098,350],{"class":120},[49,1100,1101],{"class":241}," b",[49,1103,350],{"class":120},[49,1105,1106],{"class":346}," 0644",[49,1108,1109],{"class":120},");",[49,1111,1019],{"class":241},[49,1113,1041],{"class":120},[49,1115,1044],{"class":120},[49,1117,235],{"class":120},[49,1119,1121,1123,1125,1127,1129,1131],{"class":51,"line":1120},48,[49,1122,1052],{"class":241},[49,1124,251],{"class":120},[49,1126,1057],{"class":228},[49,1128,257],{"class":120},[49,1130,1062],{"class":241},[49,1132,214],{"class":120},[49,1134,1136],{"class":51,"line":1135},49,[49,1137,1070],{"class":120},[49,1139,1141],{"class":51,"line":1140},50,[49,1142,1143],{"class":120},"}\n",[19,1145,1146,1147,1150],{},"※ 上のコードをそのまま ",[28,1148,1149],{},"go run ."," すると日本語は「豆腐」(□) になる。日本語フォントの埋め込みは最後に別途扱う。まず構造を見てほしい。",[19,1152,1153,1155,1156,1158],{},[28,1154,1149],{}," でカレントディレクトリに ",[28,1157,1094],{}," が出力される。M1 の手元では数ミリ秒で終わる。実際の PDF 生成部分は 150 µs 未満。残りはプロセスの起動時間。",[14,1160,1161],{"id":1161},"各ブロックが何をしているか",[1163,1164,138],"h3",{"id":138},[19,1166,1167],{},"gpdf からは 4 つのパッケージ:",[81,1169,1170,1183,1216,1230],{},[84,1171,1172,1174,1175,1178,1179,1182],{},[28,1173,176],{}," — ファサード。使うのは ",[28,1176,1177],{},"gpdf.NewDocument"," だけで、実体は ",[28,1180,1181],{},"template.New"," の薄いラッパ。",[84,1184,1185,1187,1188,1190,1191,1190,1194,1190,1197,1190,1200,1190,1203,1206,1207,1190,1209,1190,1212,1215],{},[28,1186,186],{}," — 単位 (",[28,1189,628],{},", ",[28,1192,1193],{},"Pt",[28,1195,1196],{},"Cm",[28,1198,1199],{},"In",[28,1201,1202],{},"Em",[28,1204,1205],{},"Pct",")、ページサイズ (",[28,1208,275],{},[28,1210,1211],{},"Letter",[28,1213,1214],{},"Legal",")、マージン。",[84,1217,1218,1220,1221,1190,1223,1190,1226,1229],{},[28,1219,196],{}," — 色プリミティブ (",[28,1222,909],{},[28,1224,1225],{},"Gray",[28,1227,1228],{},"pdf.White"," のような定数)。",[84,1231,1232,1234,1235,1238],{},[28,1233,206],{}," — Builder API 本体。",[28,1236,1237],{},"template."," で始まるもの (オプション、レイアウト関数、スタイル修飾) は全部ここから出る。",[19,1240,1241,1242,1244],{},"4 パッケージ分割が多いと思うかもしれないが、これは意図的。",[28,1243,904],{}," は低レベル Writer なのでほぼ直接触らないが、色は Text / Table / Line で共有するため外に出してある。残り 3 つはどの gpdf コードでも必ず import する。",[19,1246,1247,1248,1251,1252,1255],{},"外部依存ゼロ。",[28,1249,1250],{},"go get github.com/gpdf-dev/gpdf"," 後の ",[28,1253,1254],{},"go.mod",":",[40,1257,1262],{"className":1258,"code":1260,"language":1261},[1259],"language-text","require github.com/gpdf-dev/gpdf v1.x.x\n","text",[28,1263,1260],{"__ignoreMap":45},[19,1265,1266,1269,1270,1273],{},[28,1267,1268],{},"require"," ブロックはこれだけ。",[28,1271,1272],{},"indirect"," が雪崩れ込むことはない。",[1163,1275,1277],{"id":1276},"document-の構築","Document の構築",[40,1279,1281],{"className":112,"code":1280,"language":56,"meta":45,"style":45},"doc := gpdf.NewDocument(template.WithPageSize(document.A4))\n",[28,1282,1283],{"__ignoreMap":45},[49,1284,1285,1288,1290,1292,1294,1296,1298,1300,1302,1304,1306,1308,1310,1312],{"class":51,"line":52},[49,1286,1287],{"class":241},"doc ",[49,1289,245],{"class":120},[49,1291,248],{"class":241},[49,1293,251],{"class":120},[49,1295,254],{"class":228},[49,1297,257],{"class":120},[49,1299,260],{"class":241},[49,1301,251],{"class":120},[49,1303,265],{"class":228},[49,1305,257],{"class":120},[49,1307,270],{"class":241},[49,1309,251],{"class":120},[49,1311,275],{"class":241},[49,1313,278],{"class":120},[19,1315,1316,1318,1319,1322,1323,1326,1327,1330,1331,1334,1335,1338],{},[28,1317,1177],{}," は ",[28,1320,1321],{},"...template.Option"," 可変引数。ページサイズ、マージン、デフォルトフォント、メタデータ、カスタムフォント、すべて ",[28,1324,1325],{},"WithXxx"," オプション。US レター用紙なら ",[28,1328,1329],{},"document.A4"," を ",[28,1332,1333],{},"document.Letter"," に差し替える。マージンのデフォルトは 20 mm。もっと狭くしたければ ",[28,1336,1337],{},"template.WithMargins(document.UniformEdges(document.Mm(15)))","。",[19,1340,1341,1342,1345,1346,1349],{},"上のコードはライブラリ同梱の Latin フォントで描画される。日本語を出すには TTF を ",[28,1343,1344],{},"template.WithFont"," で登録する必要がある。それが gpdf に来る人の一番の用件だが、ここでは別トピック。",[23,1347,1348],{},"日本語対応","の節で扱う。",[1163,1351,1352],{"id":1352},"ヘッダ行",[40,1354,1356],{"className":112,"code":1355,"language":56,"meta":45,"style":45},"page.AutoRow(func(r *template.RowBuilder) {\n    r.Col(6, func(c *template.ColBuilder) { ... })\n    r.Col(6, func(c *template.ColBuilder) { ... })\n})\n",[28,1357,1358,1383,1421,1455],{"__ignoreMap":45},[49,1359,1360,1363,1365,1367,1369,1371,1373,1375,1377,1379,1381],{"class":51,"line":52},[49,1361,1362],{"class":241},"page",[49,1364,251],{"class":120},[49,1366,308],{"class":228},[49,1368,311],{"class":120},[49,1370,315],{"class":314},[49,1372,318],{"class":120},[49,1374,260],{"class":55},[49,1376,251],{"class":120},[49,1378,325],{"class":55},[49,1380,328],{"class":120},[49,1382,235],{"class":120},[49,1384,1385,1388,1390,1392,1394,1396,1398,1400,1402,1404,1406,1408,1410,1412,1415,1418],{"class":51,"line":127},[49,1386,1387],{"class":241},"    r",[49,1389,251],{"class":120},[49,1391,341],{"class":228},[49,1393,257],{"class":120},[49,1395,347],{"class":346},[49,1397,350],{"class":120},[49,1399,353],{"class":120},[49,1401,356],{"class":314},[49,1403,318],{"class":120},[49,1405,260],{"class":55},[49,1407,251],{"class":120},[49,1409,365],{"class":55},[49,1411,328],{"class":120},[49,1413,1414],{"class":120}," {",[49,1416,1417],{"class":120}," ...",[49,1419,1420],{"class":120}," })\n",[49,1422,1423,1425,1427,1429,1431,1433,1435,1437,1439,1441,1443,1445,1447,1449,1451,1453],{"class":51,"line":134},[49,1424,1387],{"class":241},[49,1426,251],{"class":120},[49,1428,341],{"class":228},[49,1430,257],{"class":120},[49,1432,347],{"class":346},[49,1434,350],{"class":120},[49,1436,353],{"class":120},[49,1438,356],{"class":314},[49,1440,318],{"class":120},[49,1442,260],{"class":55},[49,1444,251],{"class":120},[49,1446,365],{"class":55},[49,1448,328],{"class":120},[49,1450,1414],{"class":120},[49,1452,1417],{"class":120},[49,1454,1420],{"class":120},[49,1456,1457],{"class":51,"line":144},[49,1458,1459],{"class":120},"})\n",[19,1461,1462,1463,1466,1467,1470,1471,1474],{},"gofpdf や gopdf から来た人が驚く部分: gpdf は ",[23,1464,1465],{},"12 カラムグリッド","を採用している。Bootstrap と同じメンタルモデル。1 行は水平方向に 12 単位分。",[28,1468,1469],{},"r.Col(6, ...)"," はその半分。",[28,1472,1473],{},"Col(6)"," が 2 つで合計 12、行をちょうど埋める。",[19,1476,1477,1479,1480,1483,1484,1486],{},[28,1478,308],{}," は「いちばん背の高いカラムに合わせて行の高さを決める」方式。カード状の固定高レイアウトなら ",[28,1481,1482],{},"FixedRow(height, fn)"," もある。請求書なら ",[28,1485,308],{}," で正解。",[19,1488,1489,1490,1493],{},"各カラムの中では ",[28,1491,1492],{},"c.Text(...)"," を縦に積んでいくだけ。明示的な座標指定はない。ビルダーが内部でカーソルを持っていて、各要素の描画高さぶんだけカーソルが進む。",[19,1495,1496,1497,1500,1501,1504],{},"右側カラムは ",[28,1498,1499],{},"template.AlignRight()"," で右寄せ。テキストオプションは合成可能で、",[28,1502,1503],{},"c.Text(\"請求書\", template.Bold(), template.AlignRight(), template.FontSize(20))"," のように 1 回の呼び出しで 3 つの修飾子を重ねられる。順番は関係ない。",[1163,1506,1507],{"id":1507},"明細テーブル",[40,1509,1511],{"className":112,"code":1510,"language":56,"meta":45,"style":45},"c.Table(\n    []string{\"品目\", \"数量\", \"単価\", \"金額\"},\n    [][]string{\n        {\"フロントエンド開発\", \"40 時間\", \"¥18,000\", \"¥720,000\"},\n        ...\n    },\n    template.ColumnWidths(40, 15, 20, 25),\n    template.TableHeaderStyle(template.Bold(), template.BgColor(pdf.RGBHex(0xF0F0F0))),\n    template.TableStripe(pdf.RGBHex(0xFAFAFA)),\n)\n",[28,1512,1513,1523,1564,1573,1610,1615,1620,1647,1685,1707],{"__ignoreMap":45},[49,1514,1515,1517,1519,1521],{"class":51,"line":52},[49,1516,356],{"class":241},[49,1518,251],{"class":120},[49,1520,644],{"class":228},[49,1522,647],{"class":120},[49,1524,1525,1528,1530,1532,1534,1536,1538,1540,1542,1544,1546,1548,1550,1552,1554,1556,1558,1560,1562],{"class":51,"line":127},[49,1526,1527],{"class":120},"    []",[49,1529,657],{"class":656},[49,1531,660],{"class":120},[49,1533,385],{"class":120},[49,1535,665],{"class":59},[49,1537,385],{"class":120},[49,1539,350],{"class":120},[49,1541,672],{"class":120},[49,1543,675],{"class":59},[49,1545,385],{"class":120},[49,1547,350],{"class":120},[49,1549,672],{"class":120},[49,1551,684],{"class":59},[49,1553,385],{"class":120},[49,1555,350],{"class":120},[49,1557,672],{"class":120},[49,1559,693],{"class":59},[49,1561,385],{"class":120},[49,1563,698],{"class":120},[49,1565,1566,1569,1571],{"class":51,"line":134},[49,1567,1568],{"class":120},"    [][]",[49,1570,657],{"class":656},[49,1572,709],{"class":120},[49,1574,1575,1578,1580,1582,1584,1586,1588,1590,1592,1594,1596,1598,1600,1602,1604,1606,1608],{"class":51,"line":144},[49,1576,1577],{"class":120},"        {",[49,1579,385],{"class":120},[49,1581,720],{"class":59},[49,1583,385],{"class":120},[49,1585,350],{"class":120},[49,1587,672],{"class":120},[49,1589,729],{"class":59},[49,1591,385],{"class":120},[49,1593,350],{"class":120},[49,1595,672],{"class":120},[49,1597,738],{"class":59},[49,1599,385],{"class":120},[49,1601,350],{"class":120},[49,1603,672],{"class":120},[49,1605,747],{"class":59},[49,1607,385],{"class":120},[49,1609,698],{"class":120},[49,1611,1612],{"class":51,"line":156},[49,1613,1614],{"class":120},"        ...\n",[49,1616,1617],{"class":51,"line":166},[49,1618,1619],{"class":120},"    },\n",[49,1621,1622,1625,1627,1629,1631,1633,1635,1637,1639,1641,1643,1645],{"class":51,"line":171},[49,1623,1624],{"class":241},"    template",[49,1626,251],{"class":120},[49,1628,849],{"class":228},[49,1630,257],{"class":120},[49,1632,854],{"class":346},[49,1634,350],{"class":120},[49,1636,859],{"class":346},[49,1638,350],{"class":120},[49,1640,864],{"class":346},[49,1642,350],{"class":120},[49,1644,869],{"class":346},[49,1646,872],{"class":120},[49,1648,1649,1651,1653,1655,1657,1659,1661,1663,1665,1667,1669,1671,1673,1675,1677,1679,1681,1683],{"class":51,"line":181},[49,1650,1624],{"class":241},[49,1652,251],{"class":120},[49,1654,882],{"class":228},[49,1656,257],{"class":120},[49,1658,260],{"class":241},[49,1660,251],{"class":120},[49,1662,415],{"class":228},[49,1664,504],{"class":120},[49,1666,395],{"class":241},[49,1668,251],{"class":120},[49,1670,899],{"class":228},[49,1672,257],{"class":120},[49,1674,904],{"class":241},[49,1676,251],{"class":120},[49,1678,909],{"class":228},[49,1680,257],{"class":120},[49,1682,914],{"class":346},[49,1684,917],{"class":120},[49,1686,1687,1689,1691,1693,1695,1697,1699,1701,1703,1705],{"class":51,"line":191},[49,1688,1624],{"class":241},[49,1690,251],{"class":120},[49,1692,927],{"class":228},[49,1694,257],{"class":120},[49,1696,904],{"class":241},[49,1698,251],{"class":120},[49,1700,909],{"class":228},[49,1702,257],{"class":120},[49,1704,940],{"class":346},[49,1706,943],{"class":120},[49,1708,1709],{"class":51,"line":201},[49,1710,214],{"class":120},[19,1712,1713],{},"位置引数 3 つ、形状を決めるオプション 3 つ。これだけ。",[19,1715,1716,1719,1720,1723,1724,1727,1728,1731],{},[28,1717,1718],{},"ColumnWidths(40, 15, 20, 25)"," は",[23,1721,1722],{},"カラム幅のパーセンテージ","。絶対値の pt ではない。4 つの数字の合計は 100。",[28,1725,1726],{},"40, 20, 20, 20"," (合計 100) なら問題なし。",[28,1729,1730],{},"40, 15, 20, 30"," (合計 105) を渡しても描画はされるが、最後のカラムがはみ出す。これが唯一のハマりどころ。合計 100 にするか、はみ出しを受け入れるか。合計不一致を強制エラーにするかは検討したが、あえて 90% 合計で右に 10% 余白を欲しいレイアウトもあるので、ゆるくしてある。",[19,1733,1734,1736,1737,1190,1739,1190,1742,1190,1744,1747,1748,1751],{},[28,1735,882],{}," は通常のテキストオプション (",[28,1738,415],{},[28,1740,1741],{},"TextColor",[28,1743,899],{},[28,1745,1746],{},"AlignCenter",") をそのまま受け取る。暗色ヘッダが欲しければ 1 行で済む。",[28,1749,1750],{},"TableStripe(color)"," は縞模様 (ゼブラ) の背景色。省略すれば行の背景は透明。",[19,1753,1754],{},"ここで書かないこと:",[81,1756,1757,1760,1763],{},[84,1758,1759],{},"行の高さは指定しない。テーブルが各セルを計測して、一番高いセルに合わせる。",[84,1761,1762],{},"フォントは指定しない。テーブルはカラムのデフォルトを継承し、カラムはドキュメントのデフォルトを継承する。",[84,1764,1765],{},"ページ分割も書かない。テーブルがページを超えると gpdf が自動で分割し、継続ページの先頭にヘッダを再描画する。上の 3 行なら 1 ページに確実に収まるが、100 行でも同じコードで動く。",[1163,1767,1768],{"id":1768},"合計",[40,1770,1772],{"className":112,"code":1771,"language":56,"meta":45,"style":45},"c.Text(\"合計: ¥2,100,000\", template.AlignRight(), template.Bold(), template.FontSize(14))\n",[28,1773,1774],{"__ignoreMap":45},[49,1775,1776,1778,1780,1782,1784,1786,1788,1790,1792,1794,1796,1798,1800,1802,1804,1806,1808,1810,1812,1814,1816,1818],{"class":51,"line":52},[49,1777,356],{"class":241},[49,1779,251],{"class":120},[49,1781,380],{"class":228},[49,1783,257],{"class":120},[49,1785,385],{"class":120},[49,1787,965],{"class":59},[49,1789,385],{"class":120},[49,1791,350],{"class":120},[49,1793,395],{"class":241},[49,1795,251],{"class":120},[49,1797,511],{"class":228},[49,1799,504],{"class":120},[49,1801,395],{"class":241},[49,1803,251],{"class":120},[49,1805,415],{"class":228},[49,1807,504],{"class":120},[49,1809,395],{"class":241},[49,1811,251],{"class":120},[49,1813,400],{"class":228},[49,1815,257],{"class":120},[49,1817,996],{"class":346},[49,1819,278],{"class":120},[19,1821,1822,1823,1825,1826,1828,1829,1832,1833,1835,1836,1839],{},"特別なことはしていない。テーブルの外に ",[28,1824,380],{}," を置いて、右寄せ、少し大きく。テーブルとの間隔は行のカーソルが自然に進むぶんで、明示的な ",[28,1827,619],{}," はない。余白が欲しければ ",[28,1830,1831],{},"c.Table(...)"," と ",[28,1834,1492],{}," の間に ",[28,1837,1838],{},"c.Spacer(document.Mm(3))"," を挟む。",[1163,1841,1842],{"id":1842},"生成と書き出し",[40,1844,1846],{"className":112,"code":1845,"language":56,"meta":45,"style":45},"b, err := doc.Generate()\nif err != nil { log.Fatal(err) }\nif err := os.WriteFile(\"invoice.pdf\", b, 0644); err != nil { log.Fatal(err) }\n",[28,1847,1848,1867,1896],{"__ignoreMap":45},[49,1849,1850,1853,1855,1857,1859,1861,1863,1865],{"class":51,"line":52},[49,1851,1852],{"class":241},"b",[49,1854,350],{"class":120},[49,1856,1019],{"class":241},[49,1858,245],{"class":120},[49,1860,289],{"class":241},[49,1862,251],{"class":120},[49,1864,1028],{"class":228},[49,1866,297],{"class":120},[49,1868,1869,1872,1874,1876,1878,1880,1883,1885,1887,1889,1891,1893],{"class":51,"line":127},[49,1870,1871],{"class":137},"if",[49,1873,1019],{"class":241},[49,1875,1041],{"class":120},[49,1877,1044],{"class":120},[49,1879,1414],{"class":120},[49,1881,1882],{"class":241}," log",[49,1884,251],{"class":120},[49,1886,1057],{"class":228},[49,1888,257],{"class":120},[49,1890,1062],{"class":241},[49,1892,328],{"class":120},[49,1894,1895],{"class":120}," }\n",[49,1897,1898,1900,1902,1904,1906,1908,1910,1912,1914,1916,1918,1920,1922,1924,1926,1928,1930,1932,1934,1936,1938,1940,1942,1944,1946,1948],{"class":51,"line":134},[49,1899,1871],{"class":137},[49,1901,1019],{"class":241},[49,1903,245],{"class":120},[49,1905,1082],{"class":241},[49,1907,251],{"class":120},[49,1909,1087],{"class":228},[49,1911,257],{"class":120},[49,1913,385],{"class":120},[49,1915,1094],{"class":59},[49,1917,385],{"class":120},[49,1919,350],{"class":120},[49,1921,1101],{"class":241},[49,1923,350],{"class":120},[49,1925,1106],{"class":346},[49,1927,1109],{"class":120},[49,1929,1019],{"class":241},[49,1931,1041],{"class":120},[49,1933,1044],{"class":120},[49,1935,1414],{"class":120},[49,1937,1882],{"class":241},[49,1939,251],{"class":120},[49,1941,1057],{"class":228},[49,1943,257],{"class":120},[49,1945,1062],{"class":241},[49,1947,328],{"class":120},[49,1949,1895],{"class":120},[19,1951,1952,1318,1955,1958,1959,1962],{},[28,1953,1954],{},"doc.Generate()",[28,1956,1957],{},"([]byte, error)"," を返す。ファイルシステムには触れない。返ってきたバイト列はそのまま完全な PDF。ディスクに書くもよし、S3 にアップするもよし、HTTP レスポンスに ",[28,1960,1961],{},"w.Write(b)"," で流すもよし、メールに添付するもよし。一時ファイルもクリーンアップも不要。",[19,1964,1965,1966,1969,1970,1973],{},"バッファを経由せずにストリーム書き込みしたいなら ",[28,1967,1968],{},"doc.Render(w io.Writer)"," が用意されている。請求書のサイズ (数 KB) なら差は誤差。1 万ページの帳票なら ",[28,1971,1972],{},"Render"," を使う。",[14,1975,1348],{"id":1348},[19,1977,1978],{},"上のコードは Latin-1 フォントしか持っていないので、日本語は豆腐 (□) になる。TTF を 1 行加えれば直る。Noto Sans JP を使う例:",[40,1980,1982],{"className":112,"code":1981,"language":56,"meta":45,"style":45},"ttf, err := os.ReadFile(\"NotoSansJP-Regular.ttf\")\nif err != nil { log.Fatal(err) }\ndoc := gpdf.NewDocument(\n    template.WithPageSize(document.A4),\n    template.WithFont(\"NotoSansJP\", ttf),\n    template.WithDefaultFont(\"NotoSansJP\", 10),\n)\n",[28,1983,1984,2013,2039,2053,2071,2096,2120],{"__ignoreMap":45},[49,1985,1986,1989,1991,1993,1995,1997,1999,2002,2004,2006,2009,2011],{"class":51,"line":52},[49,1987,1988],{"class":241},"ttf",[49,1990,350],{"class":120},[49,1992,1019],{"class":241},[49,1994,245],{"class":120},[49,1996,1082],{"class":241},[49,1998,251],{"class":120},[49,2000,2001],{"class":228},"ReadFile",[49,2003,257],{"class":120},[49,2005,385],{"class":120},[49,2007,2008],{"class":59},"NotoSansJP-Regular.ttf",[49,2010,385],{"class":120},[49,2012,214],{"class":120},[49,2014,2015,2017,2019,2021,2023,2025,2027,2029,2031,2033,2035,2037],{"class":51,"line":127},[49,2016,1871],{"class":137},[49,2018,1019],{"class":241},[49,2020,1041],{"class":120},[49,2022,1044],{"class":120},[49,2024,1414],{"class":120},[49,2026,1882],{"class":241},[49,2028,251],{"class":120},[49,2030,1057],{"class":228},[49,2032,257],{"class":120},[49,2034,1062],{"class":241},[49,2036,328],{"class":120},[49,2038,1895],{"class":120},[49,2040,2041,2043,2045,2047,2049,2051],{"class":51,"line":134},[49,2042,1287],{"class":241},[49,2044,245],{"class":120},[49,2046,248],{"class":241},[49,2048,251],{"class":120},[49,2050,254],{"class":228},[49,2052,647],{"class":120},[49,2054,2055,2057,2059,2061,2063,2065,2067,2069],{"class":51,"line":144},[49,2056,1624],{"class":241},[49,2058,251],{"class":120},[49,2060,265],{"class":228},[49,2062,257],{"class":120},[49,2064,270],{"class":241},[49,2066,251],{"class":120},[49,2068,275],{"class":241},[49,2070,872],{"class":120},[49,2072,2073,2075,2077,2080,2082,2084,2087,2089,2091,2094],{"class":51,"line":156},[49,2074,1624],{"class":241},[49,2076,251],{"class":120},[49,2078,2079],{"class":228},"WithFont",[49,2081,257],{"class":120},[49,2083,385],{"class":120},[49,2085,2086],{"class":59},"NotoSansJP",[49,2088,385],{"class":120},[49,2090,350],{"class":120},[49,2092,2093],{"class":241}," ttf",[49,2095,872],{"class":120},[49,2097,2098,2100,2102,2105,2107,2109,2111,2113,2115,2118],{"class":51,"line":166},[49,2099,1624],{"class":241},[49,2101,251],{"class":120},[49,2103,2104],{"class":228},"WithDefaultFont",[49,2106,257],{"class":120},[49,2108,385],{"class":120},[49,2110,2086],{"class":59},[49,2112,385],{"class":120},[49,2114,350],{"class":120},[49,2116,2117],{"class":346}," 10",[49,2119,872],{"class":120},[49,2121,2122],{"class":51,"line":171},[49,2123,214],{"class":120},[19,2125,2126,2128,2129,2131],{},[28,2127,2104],{}," を使うとこのドキュメント全体のデフォルトが NotoSansJP になるので、以降の ",[28,2130,1492],{}," はそのまま日本語で描画される。フォントサブセット化は gpdf が自動でやるので、NotoSansJP の 3 MB TTF が PDF に全部埋め込まれる心配はない。実際に使った字だけが埋め込まれる。",[19,2133,2134,2135,1832,2140,2144],{},"詳しくは ",[2136,2137,2139],"a",{"href":2138},"/ja/blog/embed-japanese-font","gpdf で日本語フォントを埋め込むには",[2136,2141,2143],{"href":2142},"/ja/blog/tofu-boxes-japanese","PDF で日本語が豆腐になる原因"," を参照。",[14,2146,2148],{"id":2147},"_50-行を崩さずに見栄えを整える","50 行を崩さずに見栄えを整える",[19,2150,2151],{},"上のバージョンは機能はしているが地味。1 行の追加でかなり印象が変わる。",[19,2153,2154,2157,2158,2161],{},[23,2155,2156],{},"ブランドカラー。"," 16 進値 (例えば紺色の ",[28,2159,2160],{},"0x1A237E",") を会社名とテーブルヘッダに流す:",[40,2163,2165],{"className":112,"code":2164,"language":56,"meta":45,"style":45},"brand := pdf.RGBHex(0x1A237E)\nc.Text(\"ACME 株式会社\", template.FontSize(22), template.Bold(), template.TextColor(brand))\n// ...\ntemplate.TableHeaderStyle(template.Bold(), template.TextColor(pdf.White), template.BgColor(brand)),\n",[28,2166,2167,2187,2238,2244],{"__ignoreMap":45},[49,2168,2169,2172,2174,2177,2179,2181,2183,2185],{"class":51,"line":52},[49,2170,2171],{"class":241},"brand ",[49,2173,245],{"class":120},[49,2175,2176],{"class":241}," pdf",[49,2178,251],{"class":120},[49,2180,909],{"class":228},[49,2182,257],{"class":120},[49,2184,2160],{"class":346},[49,2186,214],{"class":120},[49,2188,2189,2191,2193,2195,2197,2199,2201,2203,2205,2207,2209,2211,2213,2215,2217,2219,2221,2223,2225,2227,2229,2231,2233,2236],{"class":51,"line":127},[49,2190,356],{"class":241},[49,2192,251],{"class":120},[49,2194,380],{"class":228},[49,2196,257],{"class":120},[49,2198,385],{"class":120},[49,2200,388],{"class":59},[49,2202,385],{"class":120},[49,2204,350],{"class":120},[49,2206,395],{"class":241},[49,2208,251],{"class":120},[49,2210,400],{"class":228},[49,2212,257],{"class":120},[49,2214,405],{"class":346},[49,2216,408],{"class":120},[49,2218,395],{"class":241},[49,2220,251],{"class":120},[49,2222,415],{"class":228},[49,2224,504],{"class":120},[49,2226,395],{"class":241},[49,2228,251],{"class":120},[49,2230,1741],{"class":228},[49,2232,257],{"class":120},[49,2234,2235],{"class":241},"brand",[49,2237,278],{"class":120},[49,2239,2240],{"class":51,"line":134},[49,2241,2243],{"class":2242},"sHwdD","// ...\n",[49,2245,2246,2248,2250,2252,2254,2256,2258,2260,2262,2264,2266,2268,2270,2272,2274,2277,2279,2281,2283,2285,2287,2289],{"class":51,"line":144},[49,2247,260],{"class":241},[49,2249,251],{"class":120},[49,2251,882],{"class":228},[49,2253,257],{"class":120},[49,2255,260],{"class":241},[49,2257,251],{"class":120},[49,2259,415],{"class":228},[49,2261,504],{"class":120},[49,2263,395],{"class":241},[49,2265,251],{"class":120},[49,2267,1741],{"class":228},[49,2269,257],{"class":120},[49,2271,904],{"class":241},[49,2273,251],{"class":120},[49,2275,2276],{"class":241},"White",[49,2278,408],{"class":120},[49,2280,395],{"class":241},[49,2282,251],{"class":120},[49,2284,899],{"class":228},[49,2286,257],{"class":120},[49,2288,2235],{"class":241},[49,2290,943],{"class":120},[19,2292,2293],{},"追加 2 行、変更 1 行。まだ 50 行以下。",[19,2295,2296,2299,2300,2303],{},[23,2297,2298],{},"小計と消費税。"," 合計の上に小計と税額を分けて出すなら、",[28,2301,2302],{},"c.Text"," を 3 つ積む:",[40,2305,2307],{"className":112,"code":2306,"language":56,"meta":45,"style":45},"c.Text(\"小計: ¥2,100,000\", template.AlignRight())\nc.Text(\"消費税 (10%): ¥210,000\",  template.AlignRight())\nc.Text(\"合計:    ¥2,310,000\",      template.AlignRight(), template.Bold(), template.FontSize(14))\n",[28,2308,2309,2336,2364],{"__ignoreMap":45},[49,2310,2311,2313,2315,2317,2319,2321,2324,2326,2328,2330,2332,2334],{"class":51,"line":52},[49,2312,356],{"class":241},[49,2314,251],{"class":120},[49,2316,380],{"class":228},[49,2318,257],{"class":120},[49,2320,385],{"class":120},[49,2322,2323],{"class":59},"小計: ¥2,100,000",[49,2325,385],{"class":120},[49,2327,350],{"class":120},[49,2329,395],{"class":241},[49,2331,251],{"class":120},[49,2333,511],{"class":228},[49,2335,418],{"class":120},[49,2337,2338,2340,2342,2344,2346,2348,2351,2353,2355,2358,2360,2362],{"class":51,"line":127},[49,2339,356],{"class":241},[49,2341,251],{"class":120},[49,2343,380],{"class":228},[49,2345,257],{"class":120},[49,2347,385],{"class":120},[49,2349,2350],{"class":59},"消費税 (10%): ¥210,000",[49,2352,385],{"class":120},[49,2354,350],{"class":120},[49,2356,2357],{"class":241},"  template",[49,2359,251],{"class":120},[49,2361,511],{"class":228},[49,2363,418],{"class":120},[49,2365,2366,2368,2370,2372,2374,2376,2379,2381,2383,2386,2388,2390,2392,2394,2396,2398,2400,2402,2404,2406,2408,2410],{"class":51,"line":134},[49,2367,356],{"class":241},[49,2369,251],{"class":120},[49,2371,380],{"class":228},[49,2373,257],{"class":120},[49,2375,385],{"class":120},[49,2377,2378],{"class":59},"合計:    ¥2,310,000",[49,2380,385],{"class":120},[49,2382,350],{"class":120},[49,2384,2385],{"class":241},"      template",[49,2387,251],{"class":120},[49,2389,511],{"class":228},[49,2391,504],{"class":120},[49,2393,395],{"class":241},[49,2395,251],{"class":120},[49,2397,415],{"class":228},[49,2399,504],{"class":120},[49,2401,395],{"class":241},[49,2403,251],{"class":120},[49,2405,400],{"class":228},[49,2407,257],{"class":120},[49,2409,996],{"class":346},[49,2411,278],{"class":120},[19,2413,2414],{},"ここで 50 行の予算を 2 行オーバーする。「50」という数字を売るか「消費税行を足してもレイアウトが崩れない」を売るか。我々は後者を採る。",[19,2416,2417,2420,2421,2424],{},[23,2418,2419],{},"合計の上に罫線。"," 小計ブロックと合計の間に ",[28,2422,2423],{},"c.Line()"," を挟む:",[40,2426,2428],{"className":112,"code":2427,"language":56,"meta":45,"style":45},"c.Spacer(document.Mm(2))\nc.Line(template.LineThickness(document.Pt(0.5)))\nc.Spacer(document.Mm(2))\n",[28,2429,2430,2453,2487],{"__ignoreMap":45},[49,2431,2432,2434,2436,2438,2440,2442,2444,2446,2448,2451],{"class":51,"line":52},[49,2433,356],{"class":241},[49,2435,251],{"class":120},[49,2437,619],{"class":228},[49,2439,257],{"class":120},[49,2441,270],{"class":241},[49,2443,251],{"class":120},[49,2445,628],{"class":228},[49,2447,257],{"class":120},[49,2449,2450],{"class":346},"2",[49,2452,278],{"class":120},[49,2454,2455,2457,2459,2462,2464,2466,2468,2471,2473,2475,2477,2479,2481,2484],{"class":51,"line":127},[49,2456,356],{"class":241},[49,2458,251],{"class":120},[49,2460,2461],{"class":228},"Line",[49,2463,257],{"class":120},[49,2465,260],{"class":241},[49,2467,251],{"class":120},[49,2469,2470],{"class":228},"LineThickness",[49,2472,257],{"class":120},[49,2474,270],{"class":241},[49,2476,251],{"class":120},[49,2478,1193],{"class":228},[49,2480,257],{"class":120},[49,2482,2483],{"class":346},"0.5",[49,2485,2486],{"class":120},")))\n",[49,2488,2489,2491,2493,2495,2497,2499,2501,2503,2505,2507],{"class":51,"line":134},[49,2490,356],{"class":241},[49,2492,251],{"class":120},[49,2494,619],{"class":228},[49,2496,257],{"class":120},[49,2498,270],{"class":241},[49,2500,251],{"class":120},[49,2502,628],{"class":228},[49,2504,257],{"class":120},[49,2506,2450],{"class":346},[49,2508,278],{"class":120},[19,2510,2511,2514],{},[23,2512,2513],{},"適格請求書 (インボイス制度) 対応。"," 登録番号を発行元セクションに 1 行追加するだけ。レイアウトの変更なし:",[40,2516,2518],{"className":112,"code":2517,"language":56,"meta":45,"style":45},"c.Text(\"登録番号: T1234567890123\")\n",[28,2519,2520],{"__ignoreMap":45},[49,2521,2522,2524,2526,2528,2530,2532,2535,2537],{"class":51,"line":52},[49,2523,356],{"class":241},[49,2525,251],{"class":120},[49,2527,380],{"class":228},[49,2529,257],{"class":120},[49,2531,385],{"class":120},[49,2533,2534],{"class":59},"登録番号: T1234567890123",[49,2536,385],{"class":120},[49,2538,214],{"class":120},[19,2540,2541,2542,2545],{},"税率ごとの区分 (8%/10%) を明細表に入れる場合は、カラムを 5 つに増やして ",[28,2543,2544],{},"ColumnWidths(30, 10, 15, 15, 10, 20)"," みたいに調整する。ここまで来るとレイアウトの範疇で、ライブラリ機能というより設計の話。",[14,2547,2548],{"id":2548},"実行",[40,2550,2552],{"className":42,"code":2551,"language":44,"meta":45,"style":45},"mkdir invoice-demo\ncd invoice-demo\ngo mod init example.com/invoice-demo\ngo get github.com/gpdf-dev/gpdf\n# main.go を貼る\ngo run .\nopen invoice.pdf    # macOS; Linux は xdg-open, Windows は start\n",[28,2553,2554,2562,2569,2582,2590,2595,2605],{"__ignoreMap":45},[49,2555,2556,2559],{"class":51,"line":52},[49,2557,2558],{"class":55},"mkdir",[49,2560,2561],{"class":59}," invoice-demo\n",[49,2563,2564,2567],{"class":51,"line":127},[49,2565,2566],{"class":228},"cd",[49,2568,2561],{"class":59},[49,2570,2571,2573,2576,2579],{"class":51,"line":134},[49,2572,56],{"class":55},[49,2574,2575],{"class":59}," mod",[49,2577,2578],{"class":59}," init",[49,2580,2581],{"class":59}," example.com/invoice-demo\n",[49,2583,2584,2586,2588],{"class":51,"line":144},[49,2585,56],{"class":55},[49,2587,60],{"class":59},[49,2589,63],{"class":59},[49,2591,2592],{"class":51,"line":156},[49,2593,2594],{"class":2242},"# main.go を貼る\n",[49,2596,2597,2599,2602],{"class":51,"line":166},[49,2598,56],{"class":55},[49,2600,2601],{"class":59}," run",[49,2603,2604],{"class":59}," .\n",[49,2606,2607,2610,2613],{"class":51,"line":171},[49,2608,2609],{"class":55},"open",[49,2611,2612],{"class":59}," invoice.pdf",[49,2614,2615],{"class":2242},"    # macOS; Linux は xdg-open, Windows は start\n",[19,2617,2618,2619,2621],{},"最初の ",[28,2620,34],{}," でソース約 3 MB が落ちてくる (コンパイル済みバイナリはない)。モジュールはキャッシュされるので 2 回目以降は一瞬。",[19,2623,2624,2626,2627,1330,2630,2633],{},[28,2625,1149],{}," が何も言わず終わるのに PDF が見つからない場合は、別のディレクトリに書かれていないか確認する。プログラムはカレントディレクトリをそのまま出力先として使う。読み取り専用ファイルシステムの Docker 内で走らせる場合は ",[28,2628,2629],{},"os.WriteFile",[28,2631,2632],{},"io.Copy(w, bytes.NewReader(b))"," に差し替えて HTTP レスポンスに流す。",[14,2635,2636],{"id":2636},"このパターンが崩れるとき",[19,2638,2639],{},"50 行バージョンは、以下の 4 つのどれかが起きるまでは穏やかにスケールする。",[19,2641,2642,2645,2646,2649],{},[23,2643,2644],{},"明細がデータになる。"," ハードコードされたスライスではなく DB クエリや JSON ペイロードから明細が来るようになった場合、テーブル部分のコードは変わらない。",[28,2647,2648],{},"[][]string"," を構築する処理が加わるだけ。これは破綻ではなく想定通りの形。",[19,2651,2652,2655,2656,2659,2660,2663,2664,2667],{},[23,2653,2654],{},"レイアウトを使い回したくなる。"," ループで複数の請求書を生成し始めた瞬間に、",[28,2657,2658],{},"main"," に全部書くのは止める。",[28,2661,2662],{},"func renderInvoice(doc *template.Document, inv Invoice)"," に切り出す。50 行テンプレの骨格は残ったまま、",[28,2665,2666],{},"doc"," とデータを引数で回すだけ。",[19,2669,2670,2673,2674,2678,2679,2682,2683,2687],{},[23,2671,2672],{},"レイアウトに分岐が入る。"," 発注番号列がある請求書・ない請求書、消費税行がある客・ない客。条件分岐が増えてくると、Builder API は冗長に感じ始める。そのときは ",[2136,2675,2677],{"href":2676},"/ja/docs/guide/json-templates","JSON スキーマ入口"," (",[28,2680,2681],{},"gpdf.NewDocumentFromJSON",") か ",[2136,2684,2686],{"href":2685},"/ja/docs/guide/go-templates","Go テンプレート入口"," が合う。構造をテンプレートファイルで宣言して、データだけ流す形になる。",[19,2689,2690,2693],{},[23,2691,2692],{},"CJK テキストが必要になる。"," 上のコードで日本語・中国語・韓国語を書くと豆腐になる。ドキュメント構築時に TTF を 1 つ登録するだけで直る (上の「日本語対応」節を参照)。",[19,2695,2696],{},"どれも「ゼロから書き直し」にはならない。全部インクリメンタルな追加。50 行バージョンはそのまま進化させていける出発地点。",[14,2698,2700],{"id":2699},"faq","FAQ",[19,2702,2703,2706],{},[23,2704,2705],{},"この形式で商用請求書を作っても大丈夫ですか?","\n大丈夫。gpdf は MIT ライセンス。商用クローズドソース製品に組み込むのも自由。表記義務もなし。GitHub で Star をくれると嬉しい。",[19,2708,2709,2716,2717,2720,2721,2723,2724,2727],{},[23,2710,2711,2712,2715],{},"バイトスライスを経由せず ",[28,2713,2714],{},"io.Writer"," に直接書けますか?","\n書ける。",[28,2718,2719],{},"doc.Render(w io.Writer) error","。上の ",[28,2722,1954],{}," は「",[28,2725,2726],{},"[]byte"," がほしい」というよくあるケースのための便利メソッド。",[19,2729,2730,2733,2734,2737],{},[23,2731,2732],{},"実際どのくらい速いですか?","\n上の 50 行は M1 で約 100 µs で PDF を生成する (3 行テーブルとテキストレイアウトが大半)。単一ページの hello world は ",[23,2735,2736],{},"13 µs","。夜間バッチで一斉発行するような大量処理では、gpdf 自体がボトルネックにはならず、データを供給する側が先に詰まる。",[19,2739,2740,2743,2744,2747,2748,2751,2752,2755,2756,1338],{},[23,2741,2742],{},"gpdf を使わずに Go で請求書 PDF を作ることもできますよね?","\nできる。",[28,2745,2746],{},"jung-kurt/gofpdf"," (アーカイブ済みだが動く)、",[28,2749,2750],{},"signintech/gopdf"," (低レベル)、",[28,2753,2754],{},"johnfercher/maroto"," (別のレイアウト抽象)。どれで書いても、上の 50 行と同等のものを作るには冗長になる。詳細は ",[2136,2757,2759],{"href":2758},"/ja/blog/go-pdf-library-showdown-2026","2026 年版 Go PDF ライブラリ比較",[19,2761,2762,2769],{},[23,2763,2764,2765,2768],{},"なぜ ",[28,2766,2767],{},"gpdf.Invoice"," のようなヘルパーがないのですか?","\n請求書のフォーマットが国ごとに違って、どう簡略化しても誰かを切り捨てるから。日本の適格請求書、米国の W-9 付き、ブラジルの NFe。それぞれ要件が違う。コンストラクタ 1 つで全部は受けきれない。50 行の出発点を渡して、そこから派生させてもらう方が現実的。Builder API がそのヘルパー。",[19,2771,2772,2775,2776,2779,2780,2784],{},[23,2773,2774],{},"PDF/A などの規格適合はできますか?","\nデフォルトは標準 PDF 1.7。PDF/A-2b (長期保存用規格) が必要なら ",[28,2777,2778],{},"gpdf.WithPDFA(pdfa.Level2B)"," をドキュメント構築時に渡す。これは別記事の ",[2136,2781,2783],{"href":2782},"/ja/docs/guide/pdf-a","純 Go で PDF/A-2b を作る"," で扱う。",[14,2786,2788],{"id":2787},"gpdf-を使ってみる","gpdf を使ってみる",[19,2790,2791],{},"gpdf は Go の PDF 生成ライブラリ。MIT ライセンス、依存ゼロ、CJK ネイティブ対応、ベンチマーク対象のワークロードで他ライブラリの 10〜30 倍速い。",[40,2793,2794],{"className":42,"code":43,"language":44,"meta":45,"style":45},[28,2795,2796],{"__ignoreMap":45},[49,2797,2798,2800,2802],{"class":51,"line":52},[49,2799,56],{"class":55},[49,2801,60],{"class":59},[49,2803,63],{"class":59},[19,2805,2806,2812,2813],{},[2136,2807,2811],{"href":2808,"rel":2809},"https://github.com/gpdf-dev/gpdf",[2810],"nofollow","⭐ GitHub で Star"," · ",[2136,2814,2817],{"href":2815,"rel":2816},"https://gpdf.dev/ja/docs/quickstart",[2810],"ドキュメントを読む",[14,2819,2820],{"id":2820},"次に読む",[81,2822,2823,2830,2836],{},[84,2824,2825,2829],{},[2136,2826,2828],{"href":2827},"/ja/blog/why-gpdf-is-faster","なぜ gpdf は他の Go PDF ライブラリより 10〜30 倍速いのか"," — 上の「数百マイクロ秒」の根拠となるベンチマーク数字。",[84,2831,2832,2835],{},[2136,2833,2834],{"href":2758},"Go PDF ライブラリ比較 2026"," — 同じ請求書を gofpdf / gopdf / Maroto で書くと何行になるか。",[84,2837,2838,2842],{},[2136,2839,2841],{"href":2840},"/ja/blog/12-column-grid","gpdf の 12 カラムグリッドの仕組み"," — 上のヘッダ行で使ったレイアウトモデルの詳しい話。",[2844,2845,2846],"style",{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}",{"title":45,"searchDepth":127,"depth":127,"links":2848},[2849,2850,2851,2852,2860,2861,2862,2863,2864,2865,2866],{"id":16,"depth":127,"text":17},{"id":72,"depth":127,"text":73},{"id":109,"depth":127,"text":25},{"id":1161,"depth":127,"text":1161,"children":2853},[2854,2855,2856,2857,2858,2859],{"id":138,"depth":134,"text":138},{"id":1276,"depth":134,"text":1277},{"id":1352,"depth":134,"text":1352},{"id":1507,"depth":134,"text":1507},{"id":1768,"depth":134,"text":1768},{"id":1842,"depth":134,"text":1842},{"id":1348,"depth":127,"text":1348},{"id":2147,"depth":127,"text":2148},{"id":2548,"depth":127,"text":2548},{"id":2636,"depth":127,"text":2636},{"id":2699,"depth":127,"text":2700},{"id":2787,"depth":127,"text":2788},{"id":2820,"depth":127,"text":2820},"2026-04-21","動く請求書 PDF の完全コードを Go で 50 行。依存ゼロ、Chromium も CGO も不要。gpdf だけで header / table / total まで全部入り。",false,"md",{"name":2872,"totalTime":2873,"tools":2874,"steps":2876},"gpdf で請求書 PDF を Go で生成する","PT10M",[2875],"Go 1.22+",[2877,2880,2883,2886,2889],{"name":2878,"text":2879},"gpdf をインストールする","モジュール内で go get github.com/gpdf-dev/gpdf を実行する。gpdf は依存ゼロなので go.sum は 1 行、CGO も入らない。",{"name":2881,"text":2882},"Document を組み立てる","gpdf.NewDocument に template.WithPageSize(document.A4) を渡す。doc.AddPage() で PageBuilder が返り、そこに AutoRow を積んでいく。",{"name":2884,"text":2885},"ヘッダを 6 カラム × 2 で組む","page.AutoRow の中で r.Col(6, ...) を 2 回呼ぶ。左に会社情報、右に INVOICE 番号と期日を template.AlignRight() で右寄せ。",{"name":2887,"text":2888},"明細テーブルを描画する","12 カラムのセル内で c.Table にヘッダと行を渡す。template.ColumnWidths と template.TableHeaderStyle で幅とスタイルを指定する。",{"name":2890,"text":2891},"PDF バイト列を書き出す","doc.Generate() で []byte を得て、os.WriteFile(\"invoice.pdf\", b, 0644) で保存する。一時ファイルも外部バイナリも不要。",null,{},"/ja/blog/invoice-pdf-go-under-50-lines",{"title":5,"description":2868},"ja/blog/012.invoice-pdf-go-under-50-lines",[2898,2899],"tutorial","templates","mgnAQ6vdHPiNJ8W5m8uHTXcTfKI3THxMp0Iq3ix6kJw",1779199022206]