[{"data":1,"prerenderedAt":2718},["ShallowReactive",2],{"blog-en-invoice-pdf-go-under-50-lines":3},{"id":4,"title":5,"author":6,"body":10,"date":2684,"description":2685,"draft":2686,"extension":2687,"howTo":2688,"image":2709,"meta":2710,"navigation":131,"path":2711,"seo":2712,"stem":2713,"tags":2714,"updated":2709,"__hash__":2717},"blog/blog/012.invoice-pdf-go-under-50-lines.md","Generate an invoice PDF in Go in under 50 lines",{"name":7,"url":8,"avatar":9},"Taiki Noda","https://nadai.dev/en/about","https://nadai.dev/og-default.png",{"type":11,"value":12,"toc":2665},"minimark",[13,18,36,39,64,70,74,77,80,96,99,104,107,111,1145,1154,1158,1163,1166,1238,1247,1258,1266,1277,1281,1317,1342,1349,1353,1460,1475,1484,1491,1502,1506,1709,1712,1730,1750,1753,1764,1768,1819,1835,1839,1946,1960,1970,1974,1977,1991,2120,2123,2133,2240,2243,2252,2336,2348,2352,2419,2425,2438,2442,2445,2455,2473,2494,2508,2511,2515,2521,2542,2552,2574,2588,2602,2606,2609,2621,2635,2639,2661],[14,15,17],"h2",{"id":16},"tldr","TL;DR",[19,20,21,22,26,27,31,32,35],"p",{},"A working invoice PDF in Go, end-to-end, in ",[23,24,25],"strong",{},"50 lines",". One ",[28,29,30],"code",{},"main.go",", one ",[28,33,34],{},"go get",", no Chromium, no CGO, no templating language, no HTML. Table, striped rows, right-aligned totals. It runs. The code is below, and the rest of this post is what each block does and where the pattern stops scaling.",[19,37,38],{},"If you just want to read the code first:",[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],{},"Then paste ",[28,68,30],{}," from the next section.",[14,71,73],{"id":72},"why-under-50-lines-is-the-threshold-we-care-about","Why \"under 50 lines\" is the threshold we care about",[19,75,76],{},"The honest reason this post exists: most people Googling \"generate invoice pdf in go\" find blog posts that either (a) recommend spawning headless Chromium, or (b) show 400 lines of low-level PDF operators to render a single table. Both answers are technically correct. Neither is what the task is.",[19,78,79],{},"A reasonable invoice has:",[81,82,83,87,90,93],"ul",{},[84,85,86],"li",{},"A header with your company and the customer's",[84,88,89],{},"An invoice number and a due date",[84,91,92],{},"A line-item table",[84,94,95],{},"A total",[19,97,98],{},"That's four things. It should be four blocks of code. If it takes more than one screen, the library is wrong.",[19,100,101,103],{},[23,102,25],{}," is roughly the limit where the code still fits on one screen in a normal editor. It's also the threshold where a reviewer will read it end-to-end instead of skipping to the tests. Hitting it means you can paste the result into a Slack message and someone can learn the library from that message alone. That's the bar.",[19,105,106],{},"The code below is gofmt'd, all imports expanded, all error paths honored. No clever tricks, no helper package hidden elsewhere. What you see is what compiles.",[14,108,110],{"id":109},"the-50-lines","The 50 lines",[40,112,115],{"className":113,"code":114,"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 Corp\", template.FontSize(22), template.Bold())\n            c.Text(\"123 Business St, San Francisco\")\n        })\n        r.Col(6, func(c *template.ColBuilder) {\n            c.Text(\"INVOICE #INV-2026-001\", template.Bold(), template.AlignRight())\n            c.Text(\"Due: 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{\"Description\", \"Qty\", \"Unit Price\", \"Amount\"},\n                [][]string{\n                    {\"Frontend dev\", \"40 hrs\", \"$150.00\", \"$6,000.00\"},\n                    {\"Backend dev\", \"60 hrs\", \"$150.00\", \"$9,000.00\"},\n                    {\"UI/UX design\", \"20 hrs\", \"$120.00\", \"$2,400.00\"},\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(\"Total: $17,400.00\", 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,116,117,126,133,143,155,165,170,180,190,200,210,216,221,237,280,299,332,371,420,440,446,477,515,543,548,554,579,611,636,649,700,711,753,793,834,840,874,919,945,951,1000,1005,1010,1032,1048,1066,1072,1119,1134,1139],{"__ignoreMap":45},[49,118,119,123],{"class":51,"line":52},[49,120,122],{"class":121},"sMK4o","package",[49,124,125],{"class":55}," main\n",[49,127,129],{"class":51,"line":128},2,[49,130,132],{"emptyLinePlaceholder":131},true,"\n",[49,134,136,140],{"class":51,"line":135},3,[49,137,139],{"class":138},"s7zQu","import",[49,141,142],{"class":121}," (\n",[49,144,146,149,152],{"class":51,"line":145},4,[49,147,148],{"class":121},"    \"",[49,150,151],{"class":55},"log",[49,153,154],{"class":121},"\"\n",[49,156,158,160,163],{"class":51,"line":157},5,[49,159,148],{"class":121},[49,161,162],{"class":55},"os",[49,164,154],{"class":121},[49,166,168],{"class":51,"line":167},6,[49,169,132],{"emptyLinePlaceholder":131},[49,171,173,175,178],{"class":51,"line":172},7,[49,174,148],{"class":121},[49,176,177],{"class":55},"github.com/gpdf-dev/gpdf",[49,179,154],{"class":121},[49,181,183,185,188],{"class":51,"line":182},8,[49,184,148],{"class":121},[49,186,187],{"class":55},"github.com/gpdf-dev/gpdf/document",[49,189,154],{"class":121},[49,191,193,195,198],{"class":51,"line":192},9,[49,194,148],{"class":121},[49,196,197],{"class":55},"github.com/gpdf-dev/gpdf/pdf",[49,199,154],{"class":121},[49,201,203,205,208],{"class":51,"line":202},10,[49,204,148],{"class":121},[49,206,207],{"class":55},"github.com/gpdf-dev/gpdf/template",[49,209,154],{"class":121},[49,211,213],{"class":51,"line":212},11,[49,214,215],{"class":121},")\n",[49,217,219],{"class":51,"line":218},12,[49,220,132],{"emptyLinePlaceholder":131},[49,222,224,227,231,234],{"class":51,"line":223},13,[49,225,226],{"class":121},"func",[49,228,230],{"class":229},"s2Zo4"," main",[49,232,233],{"class":121},"()",[49,235,236],{"class":121}," {\n",[49,238,240,244,247,250,253,256,259,262,264,267,269,272,274,277],{"class":51,"line":239},14,[49,241,243],{"class":242},"sTEyZ","    doc ",[49,245,246],{"class":121},":=",[49,248,249],{"class":242}," gpdf",[49,251,252],{"class":121},".",[49,254,255],{"class":229},"NewDocument",[49,257,258],{"class":121},"(",[49,260,261],{"class":242},"template",[49,263,252],{"class":121},[49,265,266],{"class":229},"WithPageSize",[49,268,258],{"class":121},[49,270,271],{"class":242},"document",[49,273,252],{"class":121},[49,275,276],{"class":242},"A4",[49,278,279],{"class":121},"))\n",[49,281,283,286,288,291,293,296],{"class":51,"line":282},15,[49,284,285],{"class":242},"    page ",[49,287,246],{"class":121},[49,289,290],{"class":242}," doc",[49,292,252],{"class":121},[49,294,295],{"class":229},"AddPage",[49,297,298],{"class":121},"()\n",[49,300,302,305,307,310,313,317,320,322,324,327,330],{"class":51,"line":301},16,[49,303,304],{"class":242},"    page",[49,306,252],{"class":121},[49,308,309],{"class":229},"AutoRow",[49,311,312],{"class":121},"(func(",[49,314,316],{"class":315},"sHdIc","r",[49,318,319],{"class":121}," *",[49,321,261],{"class":55},[49,323,252],{"class":121},[49,325,326],{"class":55},"RowBuilder",[49,328,329],{"class":121},")",[49,331,236],{"class":121},[49,333,335,338,340,343,345,349,352,355,358,360,362,364,367,369],{"class":51,"line":334},17,[49,336,337],{"class":242},"        r",[49,339,252],{"class":121},[49,341,342],{"class":229},"Col",[49,344,258],{"class":121},[49,346,348],{"class":347},"sbssI","6",[49,350,351],{"class":121},",",[49,353,354],{"class":121}," func(",[49,356,357],{"class":315},"c",[49,359,319],{"class":121},[49,361,261],{"class":55},[49,363,252],{"class":121},[49,365,366],{"class":55},"ColBuilder",[49,368,329],{"class":121},[49,370,236],{"class":121},[49,372,374,377,379,382,384,387,390,392,394,397,399,402,404,407,410,412,414,417],{"class":51,"line":373},18,[49,375,376],{"class":242},"            c",[49,378,252],{"class":121},[49,380,381],{"class":229},"Text",[49,383,258],{"class":121},[49,385,386],{"class":121},"\"",[49,388,389],{"class":59},"ACME Corp",[49,391,386],{"class":121},[49,393,351],{"class":121},[49,395,396],{"class":242}," template",[49,398,252],{"class":121},[49,400,401],{"class":229},"FontSize",[49,403,258],{"class":121},[49,405,406],{"class":347},"22",[49,408,409],{"class":121},"),",[49,411,396],{"class":242},[49,413,252],{"class":121},[49,415,416],{"class":229},"Bold",[49,418,419],{"class":121},"())\n",[49,421,423,425,427,429,431,433,436,438],{"class":51,"line":422},19,[49,424,376],{"class":242},[49,426,252],{"class":121},[49,428,381],{"class":229},[49,430,258],{"class":121},[49,432,386],{"class":121},[49,434,435],{"class":59},"123 Business St, San Francisco",[49,437,386],{"class":121},[49,439,215],{"class":121},[49,441,443],{"class":51,"line":442},20,[49,444,445],{"class":121},"        })\n",[49,447,449,451,453,455,457,459,461,463,465,467,469,471,473,475],{"class":51,"line":448},21,[49,450,337],{"class":242},[49,452,252],{"class":121},[49,454,342],{"class":229},[49,456,258],{"class":121},[49,458,348],{"class":347},[49,460,351],{"class":121},[49,462,354],{"class":121},[49,464,357],{"class":315},[49,466,319],{"class":121},[49,468,261],{"class":55},[49,470,252],{"class":121},[49,472,366],{"class":55},[49,474,329],{"class":121},[49,476,236],{"class":121},[49,478,480,482,484,486,488,490,493,495,497,499,501,503,506,508,510,513],{"class":51,"line":479},22,[49,481,376],{"class":242},[49,483,252],{"class":121},[49,485,381],{"class":229},[49,487,258],{"class":121},[49,489,386],{"class":121},[49,491,492],{"class":59},"INVOICE #INV-2026-001",[49,494,386],{"class":121},[49,496,351],{"class":121},[49,498,396],{"class":242},[49,500,252],{"class":121},[49,502,416],{"class":229},[49,504,505],{"class":121},"(),",[49,507,396],{"class":242},[49,509,252],{"class":121},[49,511,512],{"class":229},"AlignRight",[49,514,419],{"class":121},[49,516,518,520,522,524,526,528,531,533,535,537,539,541],{"class":51,"line":517},23,[49,519,376],{"class":242},[49,521,252],{"class":121},[49,523,381],{"class":229},[49,525,258],{"class":121},[49,527,386],{"class":121},[49,529,530],{"class":59},"Due: 2026-03-31",[49,532,386],{"class":121},[49,534,351],{"class":121},[49,536,396],{"class":242},[49,538,252],{"class":121},[49,540,512],{"class":229},[49,542,419],{"class":121},[49,544,546],{"class":51,"line":545},24,[49,547,445],{"class":121},[49,549,551],{"class":51,"line":550},25,[49,552,553],{"class":121},"    })\n",[49,555,557,559,561,563,565,567,569,571,573,575,577],{"class":51,"line":556},26,[49,558,304],{"class":242},[49,560,252],{"class":121},[49,562,309],{"class":229},[49,564,312],{"class":121},[49,566,316],{"class":315},[49,568,319],{"class":121},[49,570,261],{"class":55},[49,572,252],{"class":121},[49,574,326],{"class":55},[49,576,329],{"class":121},[49,578,236],{"class":121},[49,580,582,584,586,588,590,593,595,597,599,601,603,605,607,609],{"class":51,"line":581},27,[49,583,337],{"class":242},[49,585,252],{"class":121},[49,587,342],{"class":229},[49,589,258],{"class":121},[49,591,592],{"class":347},"12",[49,594,351],{"class":121},[49,596,354],{"class":121},[49,598,357],{"class":315},[49,600,319],{"class":121},[49,602,261],{"class":55},[49,604,252],{"class":121},[49,606,366],{"class":55},[49,608,329],{"class":121},[49,610,236],{"class":121},[49,612,614,616,618,621,623,625,627,630,632,634],{"class":51,"line":613},28,[49,615,376],{"class":242},[49,617,252],{"class":121},[49,619,620],{"class":229},"Spacer",[49,622,258],{"class":121},[49,624,271],{"class":242},[49,626,252],{"class":121},[49,628,629],{"class":229},"Mm",[49,631,258],{"class":121},[49,633,348],{"class":347},[49,635,279],{"class":121},[49,637,639,641,643,646],{"class":51,"line":638},29,[49,640,376],{"class":242},[49,642,252],{"class":121},[49,644,645],{"class":229},"Table",[49,647,648],{"class":121},"(\n",[49,650,652,655,659,662,664,667,669,671,674,677,679,681,683,686,688,690,692,695,697],{"class":51,"line":651},30,[49,653,654],{"class":121},"                []",[49,656,658],{"class":657},"spNyl","string",[49,660,661],{"class":121},"{",[49,663,386],{"class":121},[49,665,666],{"class":59},"Description",[49,668,386],{"class":121},[49,670,351],{"class":121},[49,672,673],{"class":121}," \"",[49,675,676],{"class":59},"Qty",[49,678,386],{"class":121},[49,680,351],{"class":121},[49,682,673],{"class":121},[49,684,685],{"class":59},"Unit Price",[49,687,386],{"class":121},[49,689,351],{"class":121},[49,691,673],{"class":121},[49,693,694],{"class":59},"Amount",[49,696,386],{"class":121},[49,698,699],{"class":121},"},\n",[49,701,703,706,708],{"class":51,"line":702},31,[49,704,705],{"class":121},"                [][]",[49,707,658],{"class":657},[49,709,710],{"class":121},"{\n",[49,712,714,717,719,722,724,726,728,731,733,735,737,740,742,744,746,749,751],{"class":51,"line":713},32,[49,715,716],{"class":121},"                    {",[49,718,386],{"class":121},[49,720,721],{"class":59},"Frontend dev",[49,723,386],{"class":121},[49,725,351],{"class":121},[49,727,673],{"class":121},[49,729,730],{"class":59},"40 hrs",[49,732,386],{"class":121},[49,734,351],{"class":121},[49,736,673],{"class":121},[49,738,739],{"class":59},"$150.00",[49,741,386],{"class":121},[49,743,351],{"class":121},[49,745,673],{"class":121},[49,747,748],{"class":59},"$6,000.00",[49,750,386],{"class":121},[49,752,699],{"class":121},[49,754,756,758,760,763,765,767,769,772,774,776,778,780,782,784,786,789,791],{"class":51,"line":755},33,[49,757,716],{"class":121},[49,759,386],{"class":121},[49,761,762],{"class":59},"Backend dev",[49,764,386],{"class":121},[49,766,351],{"class":121},[49,768,673],{"class":121},[49,770,771],{"class":59},"60 hrs",[49,773,386],{"class":121},[49,775,351],{"class":121},[49,777,673],{"class":121},[49,779,739],{"class":59},[49,781,386],{"class":121},[49,783,351],{"class":121},[49,785,673],{"class":121},[49,787,788],{"class":59},"$9,000.00",[49,790,386],{"class":121},[49,792,699],{"class":121},[49,794,796,798,800,803,805,807,809,812,814,816,818,821,823,825,827,830,832],{"class":51,"line":795},34,[49,797,716],{"class":121},[49,799,386],{"class":121},[49,801,802],{"class":59},"UI/UX design",[49,804,386],{"class":121},[49,806,351],{"class":121},[49,808,673],{"class":121},[49,810,811],{"class":59},"20 hrs",[49,813,386],{"class":121},[49,815,351],{"class":121},[49,817,673],{"class":121},[49,819,820],{"class":59},"$120.00",[49,822,386],{"class":121},[49,824,351],{"class":121},[49,826,673],{"class":121},[49,828,829],{"class":59},"$2,400.00",[49,831,386],{"class":121},[49,833,699],{"class":121},[49,835,837],{"class":51,"line":836},35,[49,838,839],{"class":121},"                },\n",[49,841,843,846,848,851,853,856,858,861,863,866,868,871],{"class":51,"line":842},36,[49,844,845],{"class":242},"                template",[49,847,252],{"class":121},[49,849,850],{"class":229},"ColumnWidths",[49,852,258],{"class":121},[49,854,855],{"class":347},"40",[49,857,351],{"class":121},[49,859,860],{"class":347}," 15",[49,862,351],{"class":121},[49,864,865],{"class":347}," 20",[49,867,351],{"class":121},[49,869,870],{"class":347}," 25",[49,872,873],{"class":121},"),\n",[49,875,877,879,881,884,886,888,890,892,894,896,898,901,903,906,908,911,913,916],{"class":51,"line":876},37,[49,878,845],{"class":242},[49,880,252],{"class":121},[49,882,883],{"class":229},"TableHeaderStyle",[49,885,258],{"class":121},[49,887,261],{"class":242},[49,889,252],{"class":121},[49,891,416],{"class":229},[49,893,505],{"class":121},[49,895,396],{"class":242},[49,897,252],{"class":121},[49,899,900],{"class":229},"BgColor",[49,902,258],{"class":121},[49,904,905],{"class":242},"pdf",[49,907,252],{"class":121},[49,909,910],{"class":229},"RGBHex",[49,912,258],{"class":121},[49,914,915],{"class":347},"0xF0F0F0",[49,917,918],{"class":121},"))),\n",[49,920,922,924,926,929,931,933,935,937,939,942],{"class":51,"line":921},38,[49,923,845],{"class":242},[49,925,252],{"class":121},[49,927,928],{"class":229},"TableStripe",[49,930,258],{"class":121},[49,932,905],{"class":242},[49,934,252],{"class":121},[49,936,910],{"class":229},[49,938,258],{"class":121},[49,940,941],{"class":347},"0xFAFAFA",[49,943,944],{"class":121},")),\n",[49,946,948],{"class":51,"line":947},39,[49,949,950],{"class":121},"            )\n",[49,952,954,956,958,960,962,964,967,969,971,973,975,977,979,981,983,985,987,989,991,993,995,998],{"class":51,"line":953},40,[49,955,376],{"class":242},[49,957,252],{"class":121},[49,959,381],{"class":229},[49,961,258],{"class":121},[49,963,386],{"class":121},[49,965,966],{"class":59},"Total: $17,400.00",[49,968,386],{"class":121},[49,970,351],{"class":121},[49,972,396],{"class":242},[49,974,252],{"class":121},[49,976,512],{"class":229},[49,978,505],{"class":121},[49,980,396],{"class":242},[49,982,252],{"class":121},[49,984,416],{"class":229},[49,986,505],{"class":121},[49,988,396],{"class":242},[49,990,252],{"class":121},[49,992,401],{"class":229},[49,994,258],{"class":121},[49,996,997],{"class":347},"14",[49,999,279],{"class":121},[49,1001,1003],{"class":51,"line":1002},41,[49,1004,445],{"class":121},[49,1006,1008],{"class":51,"line":1007},42,[49,1009,553],{"class":121},[49,1011,1013,1016,1018,1021,1023,1025,1027,1030],{"class":51,"line":1012},43,[49,1014,1015],{"class":242},"    b",[49,1017,351],{"class":121},[49,1019,1020],{"class":242}," err ",[49,1022,246],{"class":121},[49,1024,290],{"class":242},[49,1026,252],{"class":121},[49,1028,1029],{"class":229},"Generate",[49,1031,298],{"class":121},[49,1033,1035,1038,1040,1043,1046],{"class":51,"line":1034},44,[49,1036,1037],{"class":138},"    if",[49,1039,1020],{"class":242},[49,1041,1042],{"class":121},"!=",[49,1044,1045],{"class":121}," nil",[49,1047,236],{"class":121},[49,1049,1051,1054,1056,1059,1061,1064],{"class":51,"line":1050},45,[49,1052,1053],{"class":242},"        log",[49,1055,252],{"class":121},[49,1057,1058],{"class":229},"Fatal",[49,1060,258],{"class":121},[49,1062,1063],{"class":242},"err",[49,1065,215],{"class":121},[49,1067,1069],{"class":51,"line":1068},46,[49,1070,1071],{"class":121},"    }\n",[49,1073,1075,1077,1079,1081,1084,1086,1089,1091,1093,1096,1098,1100,1103,1105,1108,1111,1113,1115,1117],{"class":51,"line":1074},47,[49,1076,1037],{"class":138},[49,1078,1020],{"class":242},[49,1080,246],{"class":121},[49,1082,1083],{"class":242}," os",[49,1085,252],{"class":121},[49,1087,1088],{"class":229},"WriteFile",[49,1090,258],{"class":121},[49,1092,386],{"class":121},[49,1094,1095],{"class":59},"invoice.pdf",[49,1097,386],{"class":121},[49,1099,351],{"class":121},[49,1101,1102],{"class":242}," b",[49,1104,351],{"class":121},[49,1106,1107],{"class":347}," 0644",[49,1109,1110],{"class":121},");",[49,1112,1020],{"class":242},[49,1114,1042],{"class":121},[49,1116,1045],{"class":121},[49,1118,236],{"class":121},[49,1120,1122,1124,1126,1128,1130,1132],{"class":51,"line":1121},48,[49,1123,1053],{"class":242},[49,1125,252],{"class":121},[49,1127,1058],{"class":229},[49,1129,258],{"class":121},[49,1131,1063],{"class":242},[49,1133,215],{"class":121},[49,1135,1137],{"class":51,"line":1136},49,[49,1138,1071],{"class":121},[49,1140,1142],{"class":51,"line":1141},50,[49,1143,1144],{"class":121},"}\n",[19,1146,1147,1150,1151,1153],{},[28,1148,1149],{},"go run ."," produces ",[28,1152,1095],{}," in the current directory. On an M1 the whole program finishes in a few milliseconds — the actual PDF generation is under 150 µs, the rest is process startup.",[14,1155,1157],{"id":1156},"what-each-block-is-doing","What each block is doing",[1159,1160,1162],"h3",{"id":1161},"imports","Imports",[19,1164,1165],{},"Four packages from gpdf:",[81,1167,1168,1181,1214,1229],{},[84,1169,1170,1172,1173,1176,1177,1180],{},[28,1171,177],{}," — the facade. The only thing we use from it is ",[28,1174,1175],{},"gpdf.NewDocument",", which is a thin wrapper over ",[28,1178,1179],{},"template.New"," with sensible defaults.",[84,1182,1183,1185,1186,1188,1189,1188,1192,1188,1195,1188,1198,1188,1201,1204,1205,1188,1207,1188,1210,1213],{},[28,1184,187],{}," — units (",[28,1187,629],{},", ",[28,1190,1191],{},"Pt",[28,1193,1194],{},"Cm",[28,1196,1197],{},"In",[28,1199,1200],{},"Em",[28,1202,1203],{},"Pct","), page sizes (",[28,1206,276],{},[28,1208,1209],{},"Letter",[28,1211,1212],{},"Legal","), margins.",[84,1215,1216,1218,1219,1188,1221,1224,1225,1228],{},[28,1217,197],{}," — color primitives (",[28,1220,910],{},[28,1222,1223],{},"Gray",", named constants like ",[28,1226,1227],{},"pdf.White",").",[84,1230,1231,1233,1234,1237],{},[28,1232,207],{}," — the builder API. Everything that starts with ",[28,1235,1236],{},"template."," (options, layout functions, style modifiers) comes from here.",[19,1239,1240,1241,1243,1244,1246],{},"If the four-package split feels like a lot, that's by design. The ",[28,1242,905],{}," package is the low-level writer — you almost never touch it directly — but it exposes color types because colors are shared between text and tables and lines, and shoving them into ",[28,1245,261],{}," would make that package enormous. The other three you'll import in every file.",[19,1248,1249,1250,1253,1254,1257],{},"No external dependencies. ",[28,1251,1252],{},"go.mod"," after ",[28,1255,1256],{},"go get github.com/gpdf-dev/gpdf",":",[40,1259,1264],{"className":1260,"code":1262,"language":1263},[1261],"language-text","require github.com/gpdf-dev/gpdf v1.x.x\n","text",[28,1265,1262],{"__ignoreMap":45},[19,1267,1268,1269,1272,1273,1276],{},"That's the whole ",[28,1270,1271],{},"require"," block. No ",[28,1274,1275],{},"indirect"," explosion.",[1159,1278,1280],{"id":1279},"document-construction","Document construction",[40,1282,1284],{"className":113,"code":1283,"language":56,"meta":45,"style":45},"doc := gpdf.NewDocument(template.WithPageSize(document.A4))\n",[28,1285,1286],{"__ignoreMap":45},[49,1287,1288,1291,1293,1295,1297,1299,1301,1303,1305,1307,1309,1311,1313,1315],{"class":51,"line":52},[49,1289,1290],{"class":242},"doc ",[49,1292,246],{"class":121},[49,1294,249],{"class":242},[49,1296,252],{"class":121},[49,1298,255],{"class":229},[49,1300,258],{"class":121},[49,1302,261],{"class":242},[49,1304,252],{"class":121},[49,1306,266],{"class":229},[49,1308,258],{"class":121},[49,1310,271],{"class":242},[49,1312,252],{"class":121},[49,1314,276],{"class":242},[49,1316,279],{"class":121},[19,1318,1319,1321,1322,1325,1326,1329,1330,1333,1334,1337,1338,1341],{},[28,1320,1175],{}," takes a variadic ",[28,1323,1324],{},"...template.Option",". All configuration — page size, margins, default font, metadata, custom fonts — is a ",[28,1327,1328],{},"WithXxx"," option. If you want US Letter, swap ",[28,1331,1332],{},"document.A4"," for ",[28,1335,1336],{},"document.Letter",". Default margins are 20 mm; override with ",[28,1339,1340],{},"template.WithMargins(document.UniformEdges(document.Mm(15)))"," if you want tighter.",[19,1343,1344,1345,1348],{},"The invoice above ships with whatever the library's default font is (Helvetica equivalent, built-in, no TTF required). For Japanese, Chinese, or Korean invoices you register a TTF with ",[28,1346,1347],{},"template.WithFont"," — that's a different post and it's the main reason people come to gpdf, but for a dollar-USD invoice you don't need it.",[1159,1350,1352],{"id":1351},"the-header-row","The header row",[40,1354,1356],{"className":113,"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":242},"page",[49,1364,252],{"class":121},[49,1366,309],{"class":229},[49,1368,312],{"class":121},[49,1370,316],{"class":315},[49,1372,319],{"class":121},[49,1374,261],{"class":55},[49,1376,252],{"class":121},[49,1378,326],{"class":55},[49,1380,329],{"class":121},[49,1382,236],{"class":121},[49,1384,1385,1388,1390,1392,1394,1396,1398,1400,1402,1404,1406,1408,1410,1412,1415,1418],{"class":51,"line":128},[49,1386,1387],{"class":242},"    r",[49,1389,252],{"class":121},[49,1391,342],{"class":229},[49,1393,258],{"class":121},[49,1395,348],{"class":347},[49,1397,351],{"class":121},[49,1399,354],{"class":121},[49,1401,357],{"class":315},[49,1403,319],{"class":121},[49,1405,261],{"class":55},[49,1407,252],{"class":121},[49,1409,366],{"class":55},[49,1411,329],{"class":121},[49,1413,1414],{"class":121}," {",[49,1416,1417],{"class":121}," ...",[49,1419,1420],{"class":121}," })\n",[49,1422,1423,1425,1427,1429,1431,1433,1435,1437,1439,1441,1443,1445,1447,1449,1451,1453],{"class":51,"line":135},[49,1424,1387],{"class":242},[49,1426,252],{"class":121},[49,1428,342],{"class":229},[49,1430,258],{"class":121},[49,1432,348],{"class":347},[49,1434,351],{"class":121},[49,1436,354],{"class":121},[49,1438,357],{"class":315},[49,1440,319],{"class":121},[49,1442,261],{"class":55},[49,1444,252],{"class":121},[49,1446,366],{"class":55},[49,1448,329],{"class":121},[49,1450,1414],{"class":121},[49,1452,1417],{"class":121},[49,1454,1420],{"class":121},[49,1456,1457],{"class":51,"line":145},[49,1458,1459],{"class":121},"})\n",[19,1461,1462,1463,1466,1467,1470,1471,1474],{},"This is the part that surprises people coming from gofpdf or gopdf: gpdf uses a ",[23,1464,1465],{},"12-column grid",", same mental model as Bootstrap. A row has 12 units of horizontal space. ",[28,1468,1469],{},"r.Col(6, ...)"," takes half. Two ",[28,1472,1473],{},"Col(6)"," calls add up to 12, which fills the row exactly.",[19,1476,1477,1479,1480,1483],{},[28,1478,309],{}," means the row's height is whatever its tallest column needs. If you want a fixed-height row — useful for card layouts — there's ",[28,1481,1482],{},"FixedRow(height, fn)",". For an invoice, auto is what you want.",[19,1485,1486,1487,1490],{},"Inside each column, you stack ",[28,1488,1489],{},"c.Text(...)"," calls top to bottom. No explicit positioning. The builder tracks a cursor internally and advances it by the rendered height of each element.",[19,1492,1493,1494,1497,1498,1501],{},"The right-side column uses ",[28,1495,1496],{},"template.AlignRight()",". Text options are composable — ",[28,1499,1500],{},"c.Text(\"INVOICE\", template.Bold(), template.AlignRight(), template.FontSize(20))"," just layers three modifiers on one call. The order doesn't matter.",[1159,1503,1505],{"id":1504},"the-items-table","The items table",[40,1507,1509],{"className":113,"code":1508,"language":56,"meta":45,"style":45},"c.Table(\n    []string{\"Description\", \"Qty\", \"Unit Price\", \"Amount\"},\n    [][]string{\n        {\"Frontend dev\", \"40 hrs\", \"$150.00\", \"$6,000.00\"},\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,1510,1511,1521,1562,1571,1608,1613,1618,1645,1683,1705],{"__ignoreMap":45},[49,1512,1513,1515,1517,1519],{"class":51,"line":52},[49,1514,357],{"class":242},[49,1516,252],{"class":121},[49,1518,645],{"class":229},[49,1520,648],{"class":121},[49,1522,1523,1526,1528,1530,1532,1534,1536,1538,1540,1542,1544,1546,1548,1550,1552,1554,1556,1558,1560],{"class":51,"line":128},[49,1524,1525],{"class":121},"    []",[49,1527,658],{"class":657},[49,1529,661],{"class":121},[49,1531,386],{"class":121},[49,1533,666],{"class":59},[49,1535,386],{"class":121},[49,1537,351],{"class":121},[49,1539,673],{"class":121},[49,1541,676],{"class":59},[49,1543,386],{"class":121},[49,1545,351],{"class":121},[49,1547,673],{"class":121},[49,1549,685],{"class":59},[49,1551,386],{"class":121},[49,1553,351],{"class":121},[49,1555,673],{"class":121},[49,1557,694],{"class":59},[49,1559,386],{"class":121},[49,1561,699],{"class":121},[49,1563,1564,1567,1569],{"class":51,"line":135},[49,1565,1566],{"class":121},"    [][]",[49,1568,658],{"class":657},[49,1570,710],{"class":121},[49,1572,1573,1576,1578,1580,1582,1584,1586,1588,1590,1592,1594,1596,1598,1600,1602,1604,1606],{"class":51,"line":145},[49,1574,1575],{"class":121},"        {",[49,1577,386],{"class":121},[49,1579,721],{"class":59},[49,1581,386],{"class":121},[49,1583,351],{"class":121},[49,1585,673],{"class":121},[49,1587,730],{"class":59},[49,1589,386],{"class":121},[49,1591,351],{"class":121},[49,1593,673],{"class":121},[49,1595,739],{"class":59},[49,1597,386],{"class":121},[49,1599,351],{"class":121},[49,1601,673],{"class":121},[49,1603,748],{"class":59},[49,1605,386],{"class":121},[49,1607,699],{"class":121},[49,1609,1610],{"class":51,"line":157},[49,1611,1612],{"class":121},"        ...\n",[49,1614,1615],{"class":51,"line":167},[49,1616,1617],{"class":121},"    },\n",[49,1619,1620,1623,1625,1627,1629,1631,1633,1635,1637,1639,1641,1643],{"class":51,"line":172},[49,1621,1622],{"class":242},"    template",[49,1624,252],{"class":121},[49,1626,850],{"class":229},[49,1628,258],{"class":121},[49,1630,855],{"class":347},[49,1632,351],{"class":121},[49,1634,860],{"class":347},[49,1636,351],{"class":121},[49,1638,865],{"class":347},[49,1640,351],{"class":121},[49,1642,870],{"class":347},[49,1644,873],{"class":121},[49,1646,1647,1649,1651,1653,1655,1657,1659,1661,1663,1665,1667,1669,1671,1673,1675,1677,1679,1681],{"class":51,"line":182},[49,1648,1622],{"class":242},[49,1650,252],{"class":121},[49,1652,883],{"class":229},[49,1654,258],{"class":121},[49,1656,261],{"class":242},[49,1658,252],{"class":121},[49,1660,416],{"class":229},[49,1662,505],{"class":121},[49,1664,396],{"class":242},[49,1666,252],{"class":121},[49,1668,900],{"class":229},[49,1670,258],{"class":121},[49,1672,905],{"class":242},[49,1674,252],{"class":121},[49,1676,910],{"class":229},[49,1678,258],{"class":121},[49,1680,915],{"class":347},[49,1682,918],{"class":121},[49,1684,1685,1687,1689,1691,1693,1695,1697,1699,1701,1703],{"class":51,"line":192},[49,1686,1622],{"class":242},[49,1688,252],{"class":121},[49,1690,928],{"class":229},[49,1692,258],{"class":121},[49,1694,905],{"class":242},[49,1696,252],{"class":121},[49,1698,910],{"class":229},[49,1700,258],{"class":121},[49,1702,941],{"class":347},[49,1704,944],{"class":121},[49,1706,1707],{"class":51,"line":202},[49,1708,215],{"class":121},[19,1710,1711],{},"Three arguments, three shape-defining options. Nothing more.",[19,1713,1714,1717,1718,1721,1722,1725,1726,1729],{},[28,1715,1716],{},"ColumnWidths(40, 15, 20, 25)"," is ",[23,1719,1720],{},"percentages of the containing column",", not absolute points. The four widths sum to 100. If you hand it ",[28,1723,1724],{},"40, 20, 20, 20"," (sum 100), that works; if you hand it ",[28,1727,1728],{},"40, 15, 20, 30"," (sum 105), the table still renders but the last column overflows — that's the one gotcha. Sum-to-100 or you get ugly output. We considered failing loudly on sum mismatch and decided against it; some layouts genuinely want a 90% total with 10% whitespace on the right, and forcing a check would break that.",[19,1731,1732,1734,1735,1188,1737,1188,1740,1188,1742,1745,1746,1749],{},[28,1733,883],{}," takes the same text options everything else takes (",[28,1736,416],{},[28,1738,1739],{},"TextColor",[28,1741,900],{},[28,1743,1744],{},"AlignCenter","). If you want an opinionated dark-on-light header, this is one line. ",[28,1747,1748],{},"TableStripe(color)"," alternates the row background — the zebra effect. If you don't want stripes, omit it; rows render with transparent backgrounds.",[19,1751,1752],{},"What you don't do here:",[81,1754,1755,1758,1761],{},[84,1756,1757],{},"You don't specify row heights. The table measures each cell and picks the row height from the tallest.",[84,1759,1760],{},"You don't specify fonts. The table inherits from the column's default, which inherits from the document.",[84,1762,1763],{},"You don't handle pagination. If the table overflows the page, gpdf breaks it across pages and re-draws the header on each continuation page. The 50-line version above has three rows and clearly fits; if you push it to 100 rows, pagination is automatic.",[1159,1765,1767],{"id":1766},"the-total","The total",[40,1769,1771],{"className":113,"code":1770,"language":56,"meta":45,"style":45},"c.Text(\"Total: $17,400.00\", template.AlignRight(), template.Bold(), template.FontSize(14))\n",[28,1772,1773],{"__ignoreMap":45},[49,1774,1775,1777,1779,1781,1783,1785,1787,1789,1791,1793,1795,1797,1799,1801,1803,1805,1807,1809,1811,1813,1815,1817],{"class":51,"line":52},[49,1776,357],{"class":242},[49,1778,252],{"class":121},[49,1780,381],{"class":229},[49,1782,258],{"class":121},[49,1784,386],{"class":121},[49,1786,966],{"class":59},[49,1788,386],{"class":121},[49,1790,351],{"class":121},[49,1792,396],{"class":242},[49,1794,252],{"class":121},[49,1796,512],{"class":229},[49,1798,505],{"class":121},[49,1800,396],{"class":242},[49,1802,252],{"class":121},[49,1804,416],{"class":229},[49,1806,505],{"class":121},[49,1808,396],{"class":242},[49,1810,252],{"class":121},[49,1812,401],{"class":229},[49,1814,258],{"class":121},[49,1816,997],{"class":347},[49,1818,279],{"class":121},[19,1820,1821,1822,1824,1825,1828,1829,1832,1833,252],{},"Nothing clever. It's another ",[28,1823,381],{}," call outside the table, right-aligned, slightly bigger. The visual distance from the table comes from the row's natural cursor advance — there's no explicit spacer above. If you want breathing room, add ",[28,1826,1827],{},"c.Spacer(document.Mm(3))"," between ",[28,1830,1831],{},"c.Table(...)"," and ",[28,1834,1489],{},[1159,1836,1838],{"id":1837},"generate-and-write","Generate and write",[40,1840,1842],{"className":113,"code":1841,"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,1843,1844,1863,1892],{"__ignoreMap":45},[49,1845,1846,1849,1851,1853,1855,1857,1859,1861],{"class":51,"line":52},[49,1847,1848],{"class":242},"b",[49,1850,351],{"class":121},[49,1852,1020],{"class":242},[49,1854,246],{"class":121},[49,1856,290],{"class":242},[49,1858,252],{"class":121},[49,1860,1029],{"class":229},[49,1862,298],{"class":121},[49,1864,1865,1868,1870,1872,1874,1876,1879,1881,1883,1885,1887,1889],{"class":51,"line":128},[49,1866,1867],{"class":138},"if",[49,1869,1020],{"class":242},[49,1871,1042],{"class":121},[49,1873,1045],{"class":121},[49,1875,1414],{"class":121},[49,1877,1878],{"class":242}," log",[49,1880,252],{"class":121},[49,1882,1058],{"class":229},[49,1884,258],{"class":121},[49,1886,1063],{"class":242},[49,1888,329],{"class":121},[49,1890,1891],{"class":121}," }\n",[49,1893,1894,1896,1898,1900,1902,1904,1906,1908,1910,1912,1914,1916,1918,1920,1922,1924,1926,1928,1930,1932,1934,1936,1938,1940,1942,1944],{"class":51,"line":135},[49,1895,1867],{"class":138},[49,1897,1020],{"class":242},[49,1899,246],{"class":121},[49,1901,1083],{"class":242},[49,1903,252],{"class":121},[49,1905,1088],{"class":229},[49,1907,258],{"class":121},[49,1909,386],{"class":121},[49,1911,1095],{"class":59},[49,1913,386],{"class":121},[49,1915,351],{"class":121},[49,1917,1102],{"class":242},[49,1919,351],{"class":121},[49,1921,1107],{"class":347},[49,1923,1110],{"class":121},[49,1925,1020],{"class":242},[49,1927,1042],{"class":121},[49,1929,1045],{"class":121},[49,1931,1414],{"class":121},[49,1933,1878],{"class":242},[49,1935,252],{"class":121},[49,1937,1058],{"class":229},[49,1939,258],{"class":121},[49,1941,1063],{"class":242},[49,1943,329],{"class":121},[49,1945,1891],{"class":121},[19,1947,1948,1951,1952,1955,1956,1959],{},[28,1949,1950],{},"doc.Generate()"," returns ",[28,1953,1954],{},"([]byte, error)",". It doesn't touch the filesystem. The byte slice is a complete PDF — you can write it to disk, upload it to S3, stream it as an HTTP response with ",[28,1957,1958],{},"w.Write(b)",", or embed it in an email attachment. No temp files, no cleanup.",[19,1961,1962,1963,1966,1967,252],{},"If you want streaming instead of a buffered slice, ",[28,1964,1965],{},"doc.Render(w io.Writer)"," exists and writes directly. For an invoice, which is kilobytes, the difference is noise. For a 10,000-page report you'd use ",[28,1968,1969],{},"Render",[14,1971,1973],{"id":1972},"making-it-prettier-without-breaking-50-lines","Making it prettier (without breaking 50 lines)",[19,1975,1976],{},"The version above is functional but plain. A few single-line additions change the look significantly.",[19,1978,1979,1982,1983,1986,1987,1990],{},[23,1980,1981],{},"Brand color."," Pick a hex (navy blue at ",[28,1984,1985],{},"0x1A237E",", a teal at ",[28,1988,1989],{},"0x00796B",", whatever) and thread it through the two places readers notice most — the company name and the table header:",[40,1992,1994],{"className":113,"code":1993,"language":56,"meta":45,"style":45},"brand := pdf.RGBHex(0x1A237E)\nc.Text(\"ACME Corp\", template.FontSize(22), template.Bold(), template.TextColor(brand))\n// ...\ntemplate.TableHeaderStyle(template.Bold(), template.TextColor(pdf.White), template.BgColor(brand)),\n",[28,1995,1996,2016,2067,2073],{"__ignoreMap":45},[49,1997,1998,2001,2003,2006,2008,2010,2012,2014],{"class":51,"line":52},[49,1999,2000],{"class":242},"brand ",[49,2002,246],{"class":121},[49,2004,2005],{"class":242}," pdf",[49,2007,252],{"class":121},[49,2009,910],{"class":229},[49,2011,258],{"class":121},[49,2013,1985],{"class":347},[49,2015,215],{"class":121},[49,2017,2018,2020,2022,2024,2026,2028,2030,2032,2034,2036,2038,2040,2042,2044,2046,2048,2050,2052,2054,2056,2058,2060,2062,2065],{"class":51,"line":128},[49,2019,357],{"class":242},[49,2021,252],{"class":121},[49,2023,381],{"class":229},[49,2025,258],{"class":121},[49,2027,386],{"class":121},[49,2029,389],{"class":59},[49,2031,386],{"class":121},[49,2033,351],{"class":121},[49,2035,396],{"class":242},[49,2037,252],{"class":121},[49,2039,401],{"class":229},[49,2041,258],{"class":121},[49,2043,406],{"class":347},[49,2045,409],{"class":121},[49,2047,396],{"class":242},[49,2049,252],{"class":121},[49,2051,416],{"class":229},[49,2053,505],{"class":121},[49,2055,396],{"class":242},[49,2057,252],{"class":121},[49,2059,1739],{"class":229},[49,2061,258],{"class":121},[49,2063,2064],{"class":242},"brand",[49,2066,279],{"class":121},[49,2068,2069],{"class":51,"line":135},[49,2070,2072],{"class":2071},"sHwdD","// ...\n",[49,2074,2075,2077,2079,2081,2083,2085,2087,2089,2091,2093,2095,2097,2099,2101,2103,2106,2108,2110,2112,2114,2116,2118],{"class":51,"line":145},[49,2076,261],{"class":242},[49,2078,252],{"class":121},[49,2080,883],{"class":229},[49,2082,258],{"class":121},[49,2084,261],{"class":242},[49,2086,252],{"class":121},[49,2088,416],{"class":229},[49,2090,505],{"class":121},[49,2092,396],{"class":242},[49,2094,252],{"class":121},[49,2096,1739],{"class":229},[49,2098,258],{"class":121},[49,2100,905],{"class":242},[49,2102,252],{"class":121},[49,2104,2105],{"class":242},"White",[49,2107,409],{"class":121},[49,2109,396],{"class":242},[49,2111,252],{"class":121},[49,2113,900],{"class":229},[49,2115,258],{"class":121},[49,2117,2064],{"class":242},[49,2119,944],{"class":121},[19,2121,2122],{},"Two new lines, one changed line. Still under 50.",[19,2124,2125,2128,2129,2132],{},[23,2126,2127],{},"Tax and subtotal."," If the line above the total needs to break out subtotal and tax, stack three ",[28,2130,2131],{},"c.Text"," calls:",[40,2134,2136],{"className":113,"code":2135,"language":56,"meta":45,"style":45},"c.Text(\"Subtotal: $17,400.00\", template.AlignRight())\nc.Text(\"Tax (10%): $1,740.00\",  template.AlignRight())\nc.Text(\"Total:    $19,140.00\",  template.AlignRight(), template.Bold(), template.FontSize(14))\n",[28,2137,2138,2165,2193],{"__ignoreMap":45},[49,2139,2140,2142,2144,2146,2148,2150,2153,2155,2157,2159,2161,2163],{"class":51,"line":52},[49,2141,357],{"class":242},[49,2143,252],{"class":121},[49,2145,381],{"class":229},[49,2147,258],{"class":121},[49,2149,386],{"class":121},[49,2151,2152],{"class":59},"Subtotal: $17,400.00",[49,2154,386],{"class":121},[49,2156,351],{"class":121},[49,2158,396],{"class":242},[49,2160,252],{"class":121},[49,2162,512],{"class":229},[49,2164,419],{"class":121},[49,2166,2167,2169,2171,2173,2175,2177,2180,2182,2184,2187,2189,2191],{"class":51,"line":128},[49,2168,357],{"class":242},[49,2170,252],{"class":121},[49,2172,381],{"class":229},[49,2174,258],{"class":121},[49,2176,386],{"class":121},[49,2178,2179],{"class":59},"Tax (10%): $1,740.00",[49,2181,386],{"class":121},[49,2183,351],{"class":121},[49,2185,2186],{"class":242},"  template",[49,2188,252],{"class":121},[49,2190,512],{"class":229},[49,2192,419],{"class":121},[49,2194,2195,2197,2199,2201,2203,2205,2208,2210,2212,2214,2216,2218,2220,2222,2224,2226,2228,2230,2232,2234,2236,2238],{"class":51,"line":135},[49,2196,357],{"class":242},[49,2198,252],{"class":121},[49,2200,381],{"class":229},[49,2202,258],{"class":121},[49,2204,386],{"class":121},[49,2206,2207],{"class":59},"Total:    $19,140.00",[49,2209,386],{"class":121},[49,2211,351],{"class":121},[49,2213,2186],{"class":242},[49,2215,252],{"class":121},[49,2217,512],{"class":229},[49,2219,505],{"class":121},[49,2221,396],{"class":242},[49,2223,252],{"class":121},[49,2225,416],{"class":229},[49,2227,505],{"class":121},[49,2229,396],{"class":242},[49,2231,252],{"class":121},[49,2233,401],{"class":229},[49,2235,258],{"class":121},[49,2237,997],{"class":347},[49,2239,279],{"class":121},[19,2241,2242],{},"You've just blown the 50-line budget by two lines. Whether that's a real cost depends on whether you're selling the number \"50\" or the ability to add a tax line without rewriting the layout. We'd pick the tax line.",[19,2244,2245,2248,2249,1257],{},[23,2246,2247],{},"A rule above the total."," Between the subtotal block and the total, drop a ",[28,2250,2251],{},"c.Line()",[40,2253,2255],{"className":113,"code":2254,"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,2256,2257,2280,2314],{"__ignoreMap":45},[49,2258,2259,2261,2263,2265,2267,2269,2271,2273,2275,2278],{"class":51,"line":52},[49,2260,357],{"class":242},[49,2262,252],{"class":121},[49,2264,620],{"class":229},[49,2266,258],{"class":121},[49,2268,271],{"class":242},[49,2270,252],{"class":121},[49,2272,629],{"class":229},[49,2274,258],{"class":121},[49,2276,2277],{"class":347},"2",[49,2279,279],{"class":121},[49,2281,2282,2284,2286,2289,2291,2293,2295,2298,2300,2302,2304,2306,2308,2311],{"class":51,"line":128},[49,2283,357],{"class":242},[49,2285,252],{"class":121},[49,2287,2288],{"class":229},"Line",[49,2290,258],{"class":121},[49,2292,261],{"class":242},[49,2294,252],{"class":121},[49,2296,2297],{"class":229},"LineThickness",[49,2299,258],{"class":121},[49,2301,271],{"class":242},[49,2303,252],{"class":121},[49,2305,1191],{"class":229},[49,2307,258],{"class":121},[49,2309,2310],{"class":347},"0.5",[49,2312,2313],{"class":121},")))\n",[49,2315,2316,2318,2320,2322,2324,2326,2328,2330,2332,2334],{"class":51,"line":135},[49,2317,357],{"class":242},[49,2319,252],{"class":121},[49,2321,620],{"class":229},[49,2323,258],{"class":121},[49,2325,271],{"class":242},[49,2327,252],{"class":121},[49,2329,629],{"class":229},[49,2331,258],{"class":121},[49,2333,2277],{"class":347},[49,2335,279],{"class":121},[19,2337,2338,2341,2342,2344,2345,252],{},[23,2339,2340],{},"A payment-info column under the header."," Duplicate the header row structure with \"Bill To\" and \"Payment Info\" inside two new ",[28,2343,1473],{}," cells. This is the only pattern that starts pushing the code toward a function extraction — two near-identical column layouts is fine, three is the point where you pull out ",[28,2346,2347],{},"func billTo(c *template.ColBuilder, ...)",[14,2349,2351],{"id":2350},"running-it","Running it",[40,2353,2355],{"className":42,"code":2354,"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# paste main.go\ngo run .\nopen invoice.pdf    # macOS; xdg-open on Linux, start on Windows\n",[28,2356,2357,2365,2372,2385,2393,2398,2408],{"__ignoreMap":45},[49,2358,2359,2362],{"class":51,"line":52},[49,2360,2361],{"class":55},"mkdir",[49,2363,2364],{"class":59}," invoice-demo\n",[49,2366,2367,2370],{"class":51,"line":128},[49,2368,2369],{"class":229},"cd",[49,2371,2364],{"class":59},[49,2373,2374,2376,2379,2382],{"class":51,"line":135},[49,2375,56],{"class":55},[49,2377,2378],{"class":59}," mod",[49,2380,2381],{"class":59}," init",[49,2383,2384],{"class":59}," example.com/invoice-demo\n",[49,2386,2387,2389,2391],{"class":51,"line":145},[49,2388,56],{"class":55},[49,2390,60],{"class":59},[49,2392,63],{"class":59},[49,2394,2395],{"class":51,"line":157},[49,2396,2397],{"class":2071},"# paste main.go\n",[49,2399,2400,2402,2405],{"class":51,"line":167},[49,2401,56],{"class":55},[49,2403,2404],{"class":59}," run",[49,2406,2407],{"class":59}," .\n",[49,2409,2410,2413,2416],{"class":51,"line":172},[49,2411,2412],{"class":55},"open",[49,2414,2415],{"class":59}," invoice.pdf",[49,2417,2418],{"class":2071},"    # macOS; xdg-open on Linux, start on Windows\n",[19,2420,2421,2422,2424],{},"The first ",[28,2423,34],{}," pulls about 3 MB of source (no compiled binaries). Subsequent runs are instant because the module is cached.",[19,2426,2427,2428,2430,2431,1333,2434,2437],{},"If ",[28,2429,1149],{}," produces nothing and exits cleanly, check whether the file was written somewhere unexpected — the program uses the current working directory as the output path. On a Docker build with a read-only filesystem, swap ",[28,2432,2433],{},"os.WriteFile",[28,2435,2436],{},"io.Copy(w, bytes.NewReader(b))"," into an HTTP response.",[14,2439,2441],{"id":2440},"where-this-pattern-breaks","Where this pattern breaks",[19,2443,2444],{},"The 50-line version scales up gracefully until one of four things happens.",[19,2446,2447,2450,2451,2454],{},[23,2448,2449],{},"The items list becomes data."," If the line items come from a database query or a JSON payload instead of a hardcoded slice, the table stays identical — you just construct ",[28,2452,2453],{},"[][]string"," from your data. That's not a break; it's the expected shape.",[19,2456,2457,2460,2461,2464,2465,2468,2469,2472],{},[23,2458,2459],{},"You want to reuse the layout."," The moment you're generating invoices in a loop, stop inlining the body into ",[28,2462,2463],{},"main",". Extract ",[28,2466,2467],{},"func renderInvoice(doc *template.Document, inv Invoice)",". The 50-line template stays recognizable; you just pass the ",[28,2470,2471],{},"doc"," and the data through.",[19,2474,2475,2478,2479,2484,2485,2488,2489,2493],{},[23,2476,2477],{},"The layout branches."," Some invoices have a purchase-order column, some don't. Some customers get a tax line, some don't. Once you have conditional sections, the Builder API starts feeling verbose and the ",[2480,2481,2483],"a",{"href":2482},"/docs/guide/json-templates","JSON schema entrypoint"," (",[28,2486,2487],{},"gpdf.NewDocumentFromJSON",") or ",[2480,2490,2492],{"href":2491},"/docs/guide/go-templates","Go templates entrypoint"," becomes a better fit — you declare the structure in a template file and feed it data.",[19,2495,2496,2499,2500,1832,2504,252],{},[23,2497,2498],{},"You need CJK text."," Japanese, Chinese, or Korean characters in the above code render as tofu boxes because the default font is Latin-only. You need one extra call at document construction to register a TTF. That's covered in ",[2480,2501,2503],{"href":2502},"/blog/tofu-boxes-japanese","Why does my PDF show tofu boxes for Japanese?",[2480,2505,2507],{"href":2506},"/blog/embed-japanese-font","How do I embed a Japanese font in gpdf?",[19,2509,2510],{},"None of these are \"rewrite from scratch\" moments. They're incremental. The 50-line version is the starting shape you carry forward.",[14,2512,2514],{"id":2513},"faq","FAQ",[19,2516,2517,2520],{},[23,2518,2519],{},"Can I use this for commercial invoices without attribution?","\nYes. gpdf is MIT-licensed. Build whatever you want on top, including closed-source commercial products. Attribution isn't required, though a GitHub star is nice.",[19,2522,2523,2530,2531,2534,2535,2537,2538,2541],{},[23,2524,2525,2526,2529],{},"Does it support writing to ",[28,2527,2528],{},"io.Writer"," directly, without the byte slice?","\nYes — ",[28,2532,2533],{},"doc.Render(w io.Writer) error",". The ",[28,2536,1950],{}," version above is a convenience for the common case where you want ",[28,2539,2540],{},"[]byte"," to attach to something else.",[19,2543,2544,2547,2548,2551],{},[23,2545,2546],{},"How fast is this actually?","\nThe 50-line program above generates its PDF in about 100 µs on an M1, dominated by the three-row table and the text layout. A single-page hello-world lands at ",[23,2549,2550],{},"13 µs",". For batch workloads — nightly statement runs, bulk invoicing — gpdf's per-document cost is low enough that the bottleneck becomes whatever is feeding it data.",[19,2553,2554,2557,2558,2561,2562,2565,2566,2569,2570,252],{},[23,2555,2556],{},"Can I generate an invoice PDF in Go without gpdf?","\nSure. ",[28,2559,2560],{},"jung-kurt/gofpdf"," works (it's archived but stable), ",[28,2563,2564],{},"signintech/gopdf"," works at a lower level, and ",[28,2567,2568],{},"johnfercher/maroto"," gives you a different layout abstraction. All of them end up more verbose than the 50 lines above for the same invoice. We wrote more about that in ",[2480,2571,2573],{"href":2572},"/blog/go-pdf-library-showdown-2026","the 2026 showdown",[19,2575,2576,2583,2584,2587],{},[23,2577,2578,2579,2582],{},"Why isn't there a first-party ",[28,2580,2581],{},"gpdf.Invoice"," helper?","\nBecause \"invoice\" means different things in different countries and every simplification picks a side. We'd rather give you a 50-line starting point you can adapt than a ",[28,2585,2586],{},"NewInvoice(companyName, lineItems)"," constructor that breaks the moment you need a Japanese 適格請求書 with a 登録番号 or a Brazilian NFe with DANFE metadata. The Builder API is the helper.",[19,2589,2590,2593,2594,2597,2598,252],{},[23,2591,2592],{},"Does the PDF validate against PDF/A or any archival standard?","\nThe default output is standard PDF 1.7. For PDF/A-2b (archival compliance) you add ",[28,2595,2596],{},"gpdf.WithPDFA(pdfa.Level2B)"," at document construction. That's a separate topic, covered in ",[2480,2599,2601],{"href":2600},"/docs/guide/pdf-a","Building PDF/A-2b in pure Go",[14,2603,2605],{"id":2604},"try-gpdf","Try gpdf",[19,2607,2608],{},"gpdf is a Go library for generating PDFs. MIT, zero dependencies, native CJK, 10–30× faster than alternatives on the workloads we benchmark.",[40,2610,2611],{"className":42,"code":43,"language":44,"meta":45,"style":45},[28,2612,2613],{"__ignoreMap":45},[49,2614,2615,2617,2619],{"class":51,"line":52},[49,2616,56],{"class":55},[49,2618,60],{"class":59},[49,2620,63],{"class":59},[19,2622,2623,2629,2630],{},[2480,2624,2628],{"href":2625,"rel":2626},"https://github.com/gpdf-dev/gpdf",[2627],"nofollow","⭐ Star on GitHub"," · ",[2480,2631,2634],{"href":2632,"rel":2633},"https://gpdf.dev/docs/quickstart",[2627],"Read the docs",[14,2636,2638],{"id":2637},"next-reads","Next reads",[81,2640,2641,2648,2654],{},[84,2642,2643,2647],{},[2480,2644,2646],{"href":2645},"/blog/why-gpdf-is-faster","Why gpdf is 10–30× faster than other Go PDF libraries"," — the benchmark numbers behind the \"few hundred microseconds\" claim.",[84,2649,2650,2653],{},[2480,2651,2652],{"href":2572},"Go PDF Library Showdown 2026"," — how this 50 lines compares to the same invoice in gofpdf, gopdf, and Maroto.",[84,2655,2656,2660],{},[2480,2657,2659],{"href":2658},"/blog/12-column-grid","How does the 12-column grid work in gpdf?"," — the layout model used in the header row above, in more depth.",[2662,2663,2664],"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":128,"depth":128,"links":2666},[2667,2668,2669,2670,2678,2679,2680,2681,2682,2683],{"id":16,"depth":128,"text":17},{"id":72,"depth":128,"text":73},{"id":109,"depth":128,"text":110},{"id":1156,"depth":128,"text":1157,"children":2671},[2672,2673,2674,2675,2676,2677],{"id":1161,"depth":135,"text":1162},{"id":1279,"depth":135,"text":1280},{"id":1351,"depth":135,"text":1352},{"id":1504,"depth":135,"text":1505},{"id":1766,"depth":135,"text":1767},{"id":1837,"depth":135,"text":1838},{"id":1972,"depth":128,"text":1973},{"id":2350,"depth":128,"text":2351},{"id":2440,"depth":128,"text":2441},{"id":2513,"depth":128,"text":2514},{"id":2604,"depth":128,"text":2605},{"id":2637,"depth":128,"text":2638},"2026-04-21","A complete, runnable invoice PDF in Go — 50 lines with gpdf, zero dependencies, no Chromium, no CGO. Here's the code and what every block does.",false,"md",{"name":2689,"totalTime":2690,"tools":2691,"steps":2693},"Generate an invoice PDF in Go with gpdf","PT10M",[2692],"Go 1.22+",[2694,2697,2700,2703,2706],{"name":2695,"text":2696},"Install gpdf","Run go get github.com/gpdf-dev/gpdf in your module. gpdf has no transitive dependencies, so go.sum gets one line and your binary gains no CGO surface.",{"name":2698,"text":2699},"Construct the document","Call gpdf.NewDocument with template.WithPageSize(document.A4). Then doc.AddPage() returns a PageBuilder you can stack rows onto.",{"name":2701,"text":2702},"Lay out the header with two 6-column cells","Use page.AutoRow with r.Col(6, ...) twice. Put the company block on the left and the INVOICE label plus number on the right with template.AlignRight().",{"name":2704,"text":2705},"Render the items table","Inside a 12-column cell, call c.Table with a header slice and a rows slice. Pass template.ColumnWidths and template.TableHeaderStyle to style it.",{"name":2707,"text":2708},"Generate and write the bytes","Call doc.Generate() for []byte, then os.WriteFile(\"invoice.pdf\", b, 0644). One file on disk, no temp directory, no external binary.",null,{},"/blog/invoice-pdf-go-under-50-lines",{"title":5,"description":2685},"blog/012.invoice-pdf-go-under-50-lines",[2715,2716],"tutorial","templates","h3puhmykSl8uTg-uXFzpwk9s5_QRIebe9A2-nL3WweQ",1779199011681]