[{"data":1,"prerenderedAt":3082},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fcreate-a-pdf-report-from-excel-data-with-reportlab":3,"surround:\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fcreate-a-pdf-report-from-excel-data-with-reportlab":3051,"ancestors:\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fcreate-a-pdf-report-from-excel-data-with-reportlab":3060,"children:\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fcreate-a-pdf-report-from-excel-data-with-reportlab":3065,"related:\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fcreate-a-pdf-report-from-excel-data-with-reportlab":3066},{"id":4,"title":5,"body":6,"dateModified":3019,"datePublished":3019,"description":3020,"extension":3021,"faq":3022,"keyword":3031,"kind":3032,"meta":3033,"navigation":103,"path":3041,"seo":3042,"slug":3045,"stem":3046,"topics":3047,"type":3049,"__hash__":3050},"docs\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fcreate-a-pdf-report-from-excel-data-with-reportlab\u002Findex.md","Create a PDF Report from Excel Data with ReportLab",{"type":7,"value":8,"toc":3007},"minimark",[9,28,33,68,71,75,329,332,336,343,923,938,942,945,1759,1781,1912,2130,2134,2137,2547,2563,2567,2570,2785,2792,2883,2887,2930,2934,2943,2947,2953,2959,2965,2971,2975,3004],[10,11,12,13,17,18,21,22,27],"p",{},"Converting a workbook to PDF with LibreOffice or Excel reproduces the sheet as it is — gridlines, column widths and all. Sometimes the PDF should instead be a ",[14,15,16],"em",{},"document",": a title and a few sentences of commentary, a chart, then a well-set table that reads like a printed report rather than a screenshot of a spreadsheet. ReportLab builds exactly that from data, in pure Python, with no office software on the server. This page reads an Excel file of orders, summarises it with pandas, and lays out a report with ReportLab's high-level ",[14,19,20],{},"platypus"," layer: styled paragraphs, a native vector bar chart, a table with right-aligned formatted numbers, text that wraps inside its cell, zebra stripes, bold subtotal rows, a header row repeated on every page and a \"page X of Y\" footer. A final check reads the PDF back to confirm the layout. It belongs to ",[23,24,26],"a",{"href":25},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002F","Exporting Excel Reports to PDF",".",[29,30,32],"h2",{"id":31},"prerequisites","Prerequisites",[34,35,40],"pre",{"className":36,"code":37,"language":38,"meta":39,"style":39},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install reportlab pandas openpyxl pypdf\n","bash","",[41,42,43],"code",{"__ignoreMap":39},[44,45,48,52,56,59,62,65],"span",{"class":46,"line":47},"line",1,[44,49,51],{"class":50},"sMTad","pip",[44,53,55],{"class":54},"srMev"," install",[44,57,58],{"class":54}," reportlab",[44,60,61],{"class":54}," pandas",[44,63,64],{"class":54}," openpyxl",[44,66,67],{"class":54}," pypdf\n",[10,69,70],{},"ReportLab is the PDF engine; pandas reads and summarises the workbook; pypdf is used only for the checks. The parent guide shows the minimal version — a sheet dumped into a table. This page builds the version you would send to a client.",[29,72,74],{"id":73},"step-1-read-and-summarise-the-workbook","Step 1 — Read and summarise the workbook",[34,76,80],{"className":77,"code":78,"language":79,"meta":39,"style":39},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\norders = pd.read_excel(\"orders_q3.xlsx\", sheet_name=\"Orders\")\nby_region = orders.groupby(\"Region\", as_index=False).agg(Orders=(\"Order\", \"count\"), Revenue=(\"Amount\", \"sum\"))\nby_region = by_region.sort_values(\"Revenue\", ascending=False)\ntotal = orders[\"Amount\"].sum()\ntop = by_region.iloc[0]\nprint(len(orders), \"orders;\", f\"total {total:,.2f}; top region {top.Region} ({top.Revenue \u002F total:.0%})\")\n","python",[41,81,82,98,105,135,200,224,240,257],{"__ignoreMap":39},[44,83,84,88,92,95],{"class":46,"line":47},[44,85,87],{"class":86},"s-kum","import",[44,89,91],{"class":90},"skGVy"," pandas ",[44,93,94],{"class":86},"as",[44,96,97],{"class":90}," pd\n",[44,99,101],{"class":46,"line":100},2,[44,102,104],{"emptyLinePlaceholder":103},true,"\n",[44,106,108,111,114,117,120,123,127,129,132],{"class":46,"line":107},3,[44,109,110],{"class":90},"orders ",[44,112,113],{"class":86},"=",[44,115,116],{"class":90}," pd.read_excel(",[44,118,119],{"class":54},"\"orders_q3.xlsx\"",[44,121,122],{"class":90},", ",[44,124,126],{"class":125},"sa561","sheet_name",[44,128,113],{"class":86},[44,130,131],{"class":54},"\"Orders\"",[44,133,134],{"class":90},")\n",[44,136,138,141,143,146,149,151,154,156,160,163,166,168,171,174,176,179,182,185,187,189,192,194,197],{"class":46,"line":137},4,[44,139,140],{"class":90},"by_region ",[44,142,113],{"class":86},[44,144,145],{"class":90}," orders.groupby(",[44,147,148],{"class":54},"\"Region\"",[44,150,122],{"class":90},[44,152,153],{"class":125},"as_index",[44,155,113],{"class":86},[44,157,159],{"class":158},"sP0c6","False",[44,161,162],{"class":90},").agg(",[44,164,165],{"class":125},"Orders",[44,167,113],{"class":86},[44,169,170],{"class":90},"(",[44,172,173],{"class":54},"\"Order\"",[44,175,122],{"class":90},[44,177,178],{"class":54},"\"count\"",[44,180,181],{"class":90},"), ",[44,183,184],{"class":125},"Revenue",[44,186,113],{"class":86},[44,188,170],{"class":90},[44,190,191],{"class":54},"\"Amount\"",[44,193,122],{"class":90},[44,195,196],{"class":54},"\"sum\"",[44,198,199],{"class":90},"))\n",[44,201,203,205,207,210,213,215,218,220,222],{"class":46,"line":202},5,[44,204,140],{"class":90},[44,206,113],{"class":86},[44,208,209],{"class":90}," by_region.sort_values(",[44,211,212],{"class":54},"\"Revenue\"",[44,214,122],{"class":90},[44,216,217],{"class":125},"ascending",[44,219,113],{"class":86},[44,221,159],{"class":158},[44,223,134],{"class":90},[44,225,227,230,232,235,237],{"class":46,"line":226},6,[44,228,229],{"class":90},"total ",[44,231,113],{"class":86},[44,233,234],{"class":90}," orders[",[44,236,191],{"class":54},[44,238,239],{"class":90},"].sum()\n",[44,241,243,246,248,251,254],{"class":46,"line":242},7,[44,244,245],{"class":90},"top ",[44,247,113],{"class":86},[44,249,250],{"class":90}," by_region.iloc[",[44,252,253],{"class":158},"0",[44,255,256],{"class":90},"]\n",[44,258,260,263,265,268,271,274,276,279,282,286,289,292,295,298,300,303,305,308,310,313,316,319,322,324,327],{"class":46,"line":259},8,[44,261,262],{"class":158},"print",[44,264,170],{"class":90},[44,266,267],{"class":158},"len",[44,269,270],{"class":90},"(orders), ",[44,272,273],{"class":54},"\"orders;\"",[44,275,122],{"class":90},[44,277,278],{"class":86},"f",[44,280,281],{"class":54},"\"total ",[44,283,285],{"class":284},"sSjpA","{",[44,287,288],{"class":90},"total",[44,290,291],{"class":86},":,.2f",[44,293,294],{"class":284},"}",[44,296,297],{"class":54},"; top region ",[44,299,285],{"class":284},[44,301,302],{"class":90},"top.Region",[44,304,294],{"class":284},[44,306,307],{"class":54}," (",[44,309,285],{"class":284},[44,311,312],{"class":90},"top.Revenue ",[44,314,315],{"class":86},"\u002F",[44,317,318],{"class":90}," total",[44,320,321],{"class":86},":.0%",[44,323,294],{"class":284},[44,325,326],{"class":54},")\"",[44,328,134],{"class":90},[10,330,331],{},"All the numbers in the report come from here. Computing them in pandas, not in the layout code, keeps the two concerns apart: the summary can be unit-tested, and the layout code only formats what it is given. The commentary sentence in the next step is generated from the same figures, so the words and the numbers can never disagree.",[29,333,335],{"id":334},"step-2-title-commentary-and-a-vector-chart","Step 2 — Title, commentary and a vector chart",[10,337,338,339,342],{},"Platypus builds a document from a list of ",[14,340,341],{},"flowables"," — paragraphs, spacers, tables, drawings — which it flows onto pages. Start with a title, a sentence generated from the figures and a bar chart drawn with ReportLab's own graphics, so it stays sharp at any zoom and adds almost nothing to the file size:",[34,344,346],{"className":77,"code":345,"language":79,"meta":39,"style":39},"from reportlab.lib import colors\nfrom reportlab.lib.pagesizes import A4\nfrom reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle\nfrom reportlab.lib.units import mm\nfrom reportlab.graphics.shapes import Drawing\nfrom reportlab.graphics.charts.barcharts import VerticalBarChart\nfrom reportlab.platypus import Paragraph, Spacer\n\nstyles = getSampleStyleSheet()\nsmall = ParagraphStyle(\"small\", parent=styles[\"BodyText\"], fontSize=8, leading=10)\n\ndef revenue_chart(df, width=160 * mm, height=55 * mm):\n    d = Drawing(width, height)\n    chart = VerticalBarChart()\n    chart.x, chart.y, chart.width, chart.height = 30, 20, width - 50, height - 35\n    chart.data = [[round(v \u002F 1000, 1) for v in df[\"Revenue\"]]]\n    chart.categoryAxis.categoryNames = list(df[\"Region\"])\n    chart.valueAxis.valueMin = 0\n    chart.valueAxis.labelTextFormat = \"%d k\"\n    chart.bars[0].fillColor = colors.HexColor(\"#1f6feb\")\n    chart.barLabelFormat, chart.barLabels.nudge = \"%.1f k\", 7\n    d.add(chart)\n    return d\n\nstory = [\n    Paragraph(\"Orders report — Q3 2026\", styles[\"Title\"]),\n    Paragraph(f\"{len(orders)} orders worth {total:,.2f} were placed in the quarter. \"\n              f\"{top.Region} contributed the most, {top.Revenue:,.0f} or {top.Revenue \u002F total:.0%} of revenue.\", styles[\"BodyText\"]),\n    Spacer(1, 4 * mm),\n    revenue_chart(by_region),\n    Spacer(1, 6 * mm),\n]\nprint(len(story), \"flowables so far\")\n",[41,347,348,361,373,385,397,409,421,433,437,448,498,503,537,548,559,592,639,658,669,686,707,728,734,743,748,759,777,810,861,879,885,901,906],{"__ignoreMap":39},[44,349,350,353,356,358],{"class":46,"line":47},[44,351,352],{"class":86},"from",[44,354,355],{"class":90}," reportlab.lib ",[44,357,87],{"class":86},[44,359,360],{"class":90}," colors\n",[44,362,363,365,368,370],{"class":46,"line":100},[44,364,352],{"class":86},[44,366,367],{"class":90}," reportlab.lib.pagesizes ",[44,369,87],{"class":86},[44,371,372],{"class":90}," A4\n",[44,374,375,377,380,382],{"class":46,"line":107},[44,376,352],{"class":86},[44,378,379],{"class":90}," reportlab.lib.styles ",[44,381,87],{"class":86},[44,383,384],{"class":90}," getSampleStyleSheet, ParagraphStyle\n",[44,386,387,389,392,394],{"class":46,"line":137},[44,388,352],{"class":86},[44,390,391],{"class":90}," reportlab.lib.units ",[44,393,87],{"class":86},[44,395,396],{"class":90}," mm\n",[44,398,399,401,404,406],{"class":46,"line":202},[44,400,352],{"class":86},[44,402,403],{"class":90}," reportlab.graphics.shapes ",[44,405,87],{"class":86},[44,407,408],{"class":90}," Drawing\n",[44,410,411,413,416,418],{"class":46,"line":226},[44,412,352],{"class":86},[44,414,415],{"class":90}," reportlab.graphics.charts.barcharts ",[44,417,87],{"class":86},[44,419,420],{"class":90}," VerticalBarChart\n",[44,422,423,425,428,430],{"class":46,"line":242},[44,424,352],{"class":86},[44,426,427],{"class":90}," reportlab.platypus ",[44,429,87],{"class":86},[44,431,432],{"class":90}," Paragraph, Spacer\n",[44,434,435],{"class":46,"line":259},[44,436,104],{"emptyLinePlaceholder":103},[44,438,440,443,445],{"class":46,"line":439},9,[44,441,442],{"class":90},"styles ",[44,444,113],{"class":86},[44,446,447],{"class":90}," getSampleStyleSheet()\n",[44,449,451,454,456,459,462,464,467,469,472,475,478,481,483,486,488,491,493,496],{"class":46,"line":450},10,[44,452,453],{"class":90},"small ",[44,455,113],{"class":86},[44,457,458],{"class":90}," ParagraphStyle(",[44,460,461],{"class":54},"\"small\"",[44,463,122],{"class":90},[44,465,466],{"class":125},"parent",[44,468,113],{"class":86},[44,470,471],{"class":90},"styles[",[44,473,474],{"class":54},"\"BodyText\"",[44,476,477],{"class":90},"], ",[44,479,480],{"class":125},"fontSize",[44,482,113],{"class":86},[44,484,485],{"class":158},"8",[44,487,122],{"class":90},[44,489,490],{"class":125},"leading",[44,492,113],{"class":86},[44,494,495],{"class":158},"10",[44,497,134],{"class":90},[44,499,501],{"class":46,"line":500},11,[44,502,104],{"emptyLinePlaceholder":103},[44,504,506,509,513,516,518,521,524,527,529,532,534],{"class":46,"line":505},12,[44,507,508],{"class":86},"def",[44,510,512],{"class":511},"s_Opv"," revenue_chart",[44,514,515],{"class":90},"(df, width",[44,517,113],{"class":86},[44,519,520],{"class":158},"160",[44,522,523],{"class":86}," *",[44,525,526],{"class":90}," mm, height",[44,528,113],{"class":86},[44,530,531],{"class":158},"55",[44,533,523],{"class":86},[44,535,536],{"class":90}," mm):\n",[44,538,540,543,545],{"class":46,"line":539},13,[44,541,542],{"class":90},"    d ",[44,544,113],{"class":86},[44,546,547],{"class":90}," Drawing(width, height)\n",[44,549,551,554,556],{"class":46,"line":550},14,[44,552,553],{"class":90},"    chart ",[44,555,113],{"class":86},[44,557,558],{"class":90}," VerticalBarChart()\n",[44,560,562,565,567,570,572,575,578,581,584,587,589],{"class":46,"line":561},15,[44,563,564],{"class":90},"    chart.x, chart.y, chart.width, chart.height ",[44,566,113],{"class":86},[44,568,569],{"class":158}," 30",[44,571,122],{"class":90},[44,573,574],{"class":158},"20",[44,576,577],{"class":90},", width ",[44,579,580],{"class":86},"-",[44,582,583],{"class":158}," 50",[44,585,586],{"class":90},", height ",[44,588,580],{"class":86},[44,590,591],{"class":158}," 35\n",[44,593,595,598,600,603,606,609,611,614,616,619,622,625,628,631,634,636],{"class":46,"line":594},16,[44,596,597],{"class":90},"    chart.data ",[44,599,113],{"class":86},[44,601,602],{"class":90}," [[",[44,604,605],{"class":158},"round",[44,607,608],{"class":90},"(v ",[44,610,315],{"class":86},[44,612,613],{"class":158}," 1000",[44,615,122],{"class":90},[44,617,618],{"class":158},"1",[44,620,621],{"class":90},") ",[44,623,624],{"class":86},"for",[44,626,627],{"class":90}," v ",[44,629,630],{"class":86},"in",[44,632,633],{"class":90}," df[",[44,635,212],{"class":54},[44,637,638],{"class":90},"]]]\n",[44,640,642,645,647,650,653,655],{"class":46,"line":641},17,[44,643,644],{"class":90},"    chart.categoryAxis.categoryNames ",[44,646,113],{"class":86},[44,648,649],{"class":158}," list",[44,651,652],{"class":90},"(df[",[44,654,148],{"class":54},[44,656,657],{"class":90},"])\n",[44,659,661,664,666],{"class":46,"line":660},18,[44,662,663],{"class":90},"    chart.valueAxis.valueMin ",[44,665,113],{"class":86},[44,667,668],{"class":158}," 0\n",[44,670,672,675,677,680,683],{"class":46,"line":671},19,[44,673,674],{"class":90},"    chart.valueAxis.labelTextFormat ",[44,676,113],{"class":86},[44,678,679],{"class":54}," \"",[44,681,682],{"class":284},"%d",[44,684,685],{"class":54}," k\"\n",[44,687,689,692,694,697,699,702,705],{"class":46,"line":688},20,[44,690,691],{"class":90},"    chart.bars[",[44,693,253],{"class":158},[44,695,696],{"class":90},"].fillColor ",[44,698,113],{"class":86},[44,700,701],{"class":90}," colors.HexColor(",[44,703,704],{"class":54},"\"#1f6feb\"",[44,706,134],{"class":90},[44,708,710,713,715,717,720,723,725],{"class":46,"line":709},21,[44,711,712],{"class":90},"    chart.barLabelFormat, chart.barLabels.nudge ",[44,714,113],{"class":86},[44,716,679],{"class":54},[44,718,719],{"class":284},"%.1f",[44,721,722],{"class":54}," k\"",[44,724,122],{"class":90},[44,726,727],{"class":158},"7\n",[44,729,731],{"class":46,"line":730},22,[44,732,733],{"class":90},"    d.add(chart)\n",[44,735,737,740],{"class":46,"line":736},23,[44,738,739],{"class":86},"    return",[44,741,742],{"class":90}," d\n",[44,744,746],{"class":46,"line":745},24,[44,747,104],{"emptyLinePlaceholder":103},[44,749,751,754,756],{"class":46,"line":750},25,[44,752,753],{"class":90},"story ",[44,755,113],{"class":86},[44,757,758],{"class":90}," [\n",[44,760,762,765,768,771,774],{"class":46,"line":761},26,[44,763,764],{"class":90},"    Paragraph(",[44,766,767],{"class":54},"\"Orders report — Q3 2026\"",[44,769,770],{"class":90},", styles[",[44,772,773],{"class":54},"\"Title\"",[44,775,776],{"class":90},"]),\n",[44,778,780,782,784,787,789,791,794,796,799,801,803,805,807],{"class":46,"line":779},27,[44,781,764],{"class":90},[44,783,278],{"class":86},[44,785,786],{"class":54},"\"",[44,788,285],{"class":284},[44,790,267],{"class":158},[44,792,793],{"class":90},"(orders)",[44,795,294],{"class":284},[44,797,798],{"class":54}," orders worth ",[44,800,285],{"class":284},[44,802,288],{"class":90},[44,804,291],{"class":86},[44,806,294],{"class":284},[44,808,809],{"class":54}," were placed in the quarter. \"\n",[44,811,813,816,818,820,822,824,827,829,832,835,837,840,842,844,846,848,850,852,855,857,859],{"class":46,"line":812},28,[44,814,815],{"class":86},"              f",[44,817,786],{"class":54},[44,819,285],{"class":284},[44,821,302],{"class":90},[44,823,294],{"class":284},[44,825,826],{"class":54}," contributed the most, ",[44,828,285],{"class":284},[44,830,831],{"class":90},"top.Revenue",[44,833,834],{"class":86},":,.0f",[44,836,294],{"class":284},[44,838,839],{"class":54}," or ",[44,841,285],{"class":284},[44,843,312],{"class":90},[44,845,315],{"class":86},[44,847,318],{"class":90},[44,849,321],{"class":86},[44,851,294],{"class":284},[44,853,854],{"class":54}," of revenue.\"",[44,856,770],{"class":90},[44,858,474],{"class":54},[44,860,776],{"class":90},[44,862,864,867,869,871,874,876],{"class":46,"line":863},29,[44,865,866],{"class":90},"    Spacer(",[44,868,618],{"class":158},[44,870,122],{"class":90},[44,872,873],{"class":158},"4",[44,875,523],{"class":86},[44,877,878],{"class":90}," mm),\n",[44,880,882],{"class":46,"line":881},30,[44,883,884],{"class":90},"    revenue_chart(by_region),\n",[44,886,888,890,892,894,897,899],{"class":46,"line":887},31,[44,889,866],{"class":90},[44,891,618],{"class":158},[44,893,122],{"class":90},[44,895,896],{"class":158},"6",[44,898,523],{"class":86},[44,900,878],{"class":90},[44,902,904],{"class":46,"line":903},32,[44,905,256],{"class":90},[44,907,909,911,913,915,918,921],{"class":46,"line":908},33,[44,910,262],{"class":158},[44,912,170],{"class":90},[44,914,267],{"class":158},[44,916,917],{"class":90},"(story), ",[44,919,920],{"class":54},"\"flowables so far\"",[44,922,134],{"class":90},[10,924,925,928,929,932,933,937],{},[41,926,927],{},"VerticalBarChart"," takes its data as a list of series and draws axes, labels and bars as vector shapes. For charts ReportLab cannot draw — a scatter with a fitted line, say — render a matplotlib figure to an in-memory PNG or SVG and insert it as an ",[41,930,931],{},"Image","; ",[23,934,936],{"href":935},"\u002Fformatting-and-charting-excel-reports-with-python\u002Finserting-images-and-logos-into-excel\u002Fembed-a-matplotlib-chart-in-an-excel-report\u002F","Embed a matplotlib Chart in an Excel Report"," shows rendering a figure to an in-memory image, which works the same way for ReportLab.",[29,939,941],{"id":940},"step-3-a-table-that-reads-like-a-report","Step 3 — A table that reads like a report",[10,943,944],{},"The detail table is where most of the work is. Long product names must wrap rather than overflow, numbers must be formatted and right-aligned, each region needs a subtotal, and the header must repeat on every page:",[34,946,948],{"className":77,"code":947,"language":79,"meta":39,"style":39},"from reportlab.platypus import Table, TableStyle\n\ndef detail_table(df):\n    header = [\"Order\", \"Region\", \"Product\", \"Qty\", \"Amount\"]\n    data, subtotal_rows = [header], []\n    for region, grp in df.sort_values([\"Region\", \"Order\"]).groupby(\"Region\"):\n        for r in grp.itertuples(index=False):\n            data.append([r.Order, r.Region, Paragraph(r.Product, small), f\"{r.Qty:,}\", f\"{r.Amount:,.2f}\"])\n        data.append([\"\", f\"{region} subtotal\", \"\", f\"{grp['Qty'].sum():,}\", f\"{grp['Amount'].sum():,.2f}\"])\n        subtotal_rows.append(len(data) - 1)\n    data.append([\"\", \"Total\", \"\", f\"{df['Qty'].sum():,}\", f\"{df['Amount'].sum():,.2f}\"])\n    style = [\n        (\"BACKGROUND\", (0, 0), (-1, 0), colors.HexColor(\"#24292f\")),\n        (\"TEXTCOLOR\", (0, 0), (-1, 0), colors.white),\n        (\"FONTNAME\", (0, 0), (-1, 0), \"Helvetica-Bold\"),\n        (\"FONTSIZE\", (0, 0), (-1, -1), 8),\n        (\"ALIGN\", (3, 0), (-1, -1), \"RIGHT\"),\n        (\"VALIGN\", (0, 0), (-1, -1), \"MIDDLE\"),\n        (\"LINEBELOW\", (0, 0), (-1, 0), 0.8, colors.black),\n        (\"ROWBACKGROUNDS\", (0, 1), (-1, -2), [colors.white, colors.HexColor(\"#f6f8fa\")]),\n        (\"FONTNAME\", (0, -1), (-1, -1), \"Helvetica-Bold\"),\n        (\"LINEABOVE\", (0, -1), (-1, -1), 0.8, colors.black),\n    ]\n    for r in subtotal_rows:\n        style += [(\"FONTNAME\", (0, r), (-1, r), \"Helvetica-Bold\"), (\"BACKGROUND\", (0, r), (-1, r), colors.HexColor(\"#ddf4ff\"))]\n    widths = [22 * mm, 30 * mm, 70 * mm, 16 * mm, 26 * mm]\n    return Table(data, colWidths=widths, repeatRows=1, style=TableStyle(style))\n\nstory.append(detail_table(orders))\n",[41,949,950,961,965,975,1007,1017,1044,1066,1106,1180,1197,1256,1265,1302,1330,1363,1396,1431,1465,1498,1535,1569,1604,1609,1620,1672,1718,1750,1754],{"__ignoreMap":39},[44,951,952,954,956,958],{"class":46,"line":47},[44,953,352],{"class":86},[44,955,427],{"class":90},[44,957,87],{"class":86},[44,959,960],{"class":90}," Table, TableStyle\n",[44,962,963],{"class":46,"line":100},[44,964,104],{"emptyLinePlaceholder":103},[44,966,967,969,972],{"class":46,"line":107},[44,968,508],{"class":86},[44,970,971],{"class":511}," detail_table",[44,973,974],{"class":90},"(df):\n",[44,976,977,980,982,985,987,989,991,993,996,998,1001,1003,1005],{"class":46,"line":137},[44,978,979],{"class":90},"    header ",[44,981,113],{"class":86},[44,983,984],{"class":90}," [",[44,986,173],{"class":54},[44,988,122],{"class":90},[44,990,148],{"class":54},[44,992,122],{"class":90},[44,994,995],{"class":54},"\"Product\"",[44,997,122],{"class":90},[44,999,1000],{"class":54},"\"Qty\"",[44,1002,122],{"class":90},[44,1004,191],{"class":54},[44,1006,256],{"class":90},[44,1008,1009,1012,1014],{"class":46,"line":202},[44,1010,1011],{"class":90},"    data, subtotal_rows ",[44,1013,113],{"class":86},[44,1015,1016],{"class":90}," [header], []\n",[44,1018,1019,1022,1025,1027,1030,1032,1034,1036,1039,1041],{"class":46,"line":226},[44,1020,1021],{"class":86},"    for",[44,1023,1024],{"class":90}," region, grp ",[44,1026,630],{"class":86},[44,1028,1029],{"class":90}," df.sort_values([",[44,1031,148],{"class":54},[44,1033,122],{"class":90},[44,1035,173],{"class":54},[44,1037,1038],{"class":90},"]).groupby(",[44,1040,148],{"class":54},[44,1042,1043],{"class":90},"):\n",[44,1045,1046,1049,1052,1054,1057,1060,1062,1064],{"class":46,"line":242},[44,1047,1048],{"class":86},"        for",[44,1050,1051],{"class":90}," r ",[44,1053,630],{"class":86},[44,1055,1056],{"class":90}," grp.itertuples(",[44,1058,1059],{"class":125},"index",[44,1061,113],{"class":86},[44,1063,159],{"class":158},[44,1065,1043],{"class":90},[44,1067,1068,1071,1073,1075,1077,1080,1083,1085,1087,1089,1091,1093,1095,1098,1100,1102,1104],{"class":46,"line":259},[44,1069,1070],{"class":90},"            data.append([r.Order, r.Region, Paragraph(r.Product, small), ",[44,1072,278],{"class":86},[44,1074,786],{"class":54},[44,1076,285],{"class":284},[44,1078,1079],{"class":90},"r.Qty",[44,1081,1082],{"class":86},":,",[44,1084,294],{"class":284},[44,1086,786],{"class":54},[44,1088,122],{"class":90},[44,1090,278],{"class":86},[44,1092,786],{"class":54},[44,1094,285],{"class":284},[44,1096,1097],{"class":90},"r.Amount",[44,1099,291],{"class":86},[44,1101,294],{"class":284},[44,1103,786],{"class":54},[44,1105,657],{"class":90},[44,1107,1108,1111,1114,1116,1118,1120,1122,1125,1127,1130,1132,1134,1136,1138,1140,1142,1145,1148,1151,1153,1155,1157,1159,1161,1163,1165,1167,1170,1172,1174,1176,1178],{"class":46,"line":439},[44,1109,1110],{"class":90},"        data.append([",[44,1112,1113],{"class":54},"\"\"",[44,1115,122],{"class":90},[44,1117,278],{"class":86},[44,1119,786],{"class":54},[44,1121,285],{"class":284},[44,1123,1124],{"class":90},"region",[44,1126,294],{"class":284},[44,1128,1129],{"class":54}," subtotal\"",[44,1131,122],{"class":90},[44,1133,1113],{"class":54},[44,1135,122],{"class":90},[44,1137,278],{"class":86},[44,1139,786],{"class":54},[44,1141,285],{"class":284},[44,1143,1144],{"class":90},"grp[",[44,1146,1147],{"class":54},"'Qty'",[44,1149,1150],{"class":90},"].sum()",[44,1152,1082],{"class":86},[44,1154,294],{"class":284},[44,1156,786],{"class":54},[44,1158,122],{"class":90},[44,1160,278],{"class":86},[44,1162,786],{"class":54},[44,1164,285],{"class":284},[44,1166,1144],{"class":90},[44,1168,1169],{"class":54},"'Amount'",[44,1171,1150],{"class":90},[44,1173,291],{"class":86},[44,1175,294],{"class":284},[44,1177,786],{"class":54},[44,1179,657],{"class":90},[44,1181,1182,1185,1187,1190,1192,1195],{"class":46,"line":450},[44,1183,1184],{"class":90},"        subtotal_rows.append(",[44,1186,267],{"class":158},[44,1188,1189],{"class":90},"(data) ",[44,1191,580],{"class":86},[44,1193,1194],{"class":158}," 1",[44,1196,134],{"class":90},[44,1198,1199,1202,1204,1206,1209,1211,1213,1215,1217,1219,1221,1224,1226,1228,1230,1232,1234,1236,1238,1240,1242,1244,1246,1248,1250,1252,1254],{"class":46,"line":500},[44,1200,1201],{"class":90},"    data.append([",[44,1203,1113],{"class":54},[44,1205,122],{"class":90},[44,1207,1208],{"class":54},"\"Total\"",[44,1210,122],{"class":90},[44,1212,1113],{"class":54},[44,1214,122],{"class":90},[44,1216,278],{"class":86},[44,1218,786],{"class":54},[44,1220,285],{"class":284},[44,1222,1223],{"class":90},"df[",[44,1225,1147],{"class":54},[44,1227,1150],{"class":90},[44,1229,1082],{"class":86},[44,1231,294],{"class":284},[44,1233,786],{"class":54},[44,1235,122],{"class":90},[44,1237,278],{"class":86},[44,1239,786],{"class":54},[44,1241,285],{"class":284},[44,1243,1223],{"class":90},[44,1245,1169],{"class":54},[44,1247,1150],{"class":90},[44,1249,291],{"class":86},[44,1251,294],{"class":284},[44,1253,786],{"class":54},[44,1255,657],{"class":90},[44,1257,1258,1261,1263],{"class":46,"line":505},[44,1259,1260],{"class":90},"    style ",[44,1262,113],{"class":86},[44,1264,758],{"class":90},[44,1266,1267,1270,1273,1276,1278,1280,1282,1285,1287,1289,1291,1293,1296,1299],{"class":46,"line":539},[44,1268,1269],{"class":90},"        (",[44,1271,1272],{"class":54},"\"BACKGROUND\"",[44,1274,1275],{"class":90},", (",[44,1277,253],{"class":158},[44,1279,122],{"class":90},[44,1281,253],{"class":158},[44,1283,1284],{"class":90},"), (",[44,1286,580],{"class":86},[44,1288,618],{"class":158},[44,1290,122],{"class":90},[44,1292,253],{"class":158},[44,1294,1295],{"class":90},"), colors.HexColor(",[44,1297,1298],{"class":54},"\"#24292f\"",[44,1300,1301],{"class":90},")),\n",[44,1303,1304,1306,1309,1311,1313,1315,1317,1319,1321,1323,1325,1327],{"class":46,"line":550},[44,1305,1269],{"class":90},[44,1307,1308],{"class":54},"\"TEXTCOLOR\"",[44,1310,1275],{"class":90},[44,1312,253],{"class":158},[44,1314,122],{"class":90},[44,1316,253],{"class":158},[44,1318,1284],{"class":90},[44,1320,580],{"class":86},[44,1322,618],{"class":158},[44,1324,122],{"class":90},[44,1326,253],{"class":158},[44,1328,1329],{"class":90},"), colors.white),\n",[44,1331,1332,1334,1337,1339,1341,1343,1345,1347,1349,1351,1353,1355,1357,1360],{"class":46,"line":561},[44,1333,1269],{"class":90},[44,1335,1336],{"class":54},"\"FONTNAME\"",[44,1338,1275],{"class":90},[44,1340,253],{"class":158},[44,1342,122],{"class":90},[44,1344,253],{"class":158},[44,1346,1284],{"class":90},[44,1348,580],{"class":86},[44,1350,618],{"class":158},[44,1352,122],{"class":90},[44,1354,253],{"class":158},[44,1356,181],{"class":90},[44,1358,1359],{"class":54},"\"Helvetica-Bold\"",[44,1361,1362],{"class":90},"),\n",[44,1364,1365,1367,1370,1372,1374,1376,1378,1380,1382,1384,1386,1388,1390,1392,1394],{"class":46,"line":594},[44,1366,1269],{"class":90},[44,1368,1369],{"class":54},"\"FONTSIZE\"",[44,1371,1275],{"class":90},[44,1373,253],{"class":158},[44,1375,122],{"class":90},[44,1377,253],{"class":158},[44,1379,1284],{"class":90},[44,1381,580],{"class":86},[44,1383,618],{"class":158},[44,1385,122],{"class":90},[44,1387,580],{"class":86},[44,1389,618],{"class":158},[44,1391,181],{"class":90},[44,1393,485],{"class":158},[44,1395,1362],{"class":90},[44,1397,1398,1400,1403,1405,1408,1410,1412,1414,1416,1418,1420,1422,1424,1426,1429],{"class":46,"line":641},[44,1399,1269],{"class":90},[44,1401,1402],{"class":54},"\"ALIGN\"",[44,1404,1275],{"class":90},[44,1406,1407],{"class":158},"3",[44,1409,122],{"class":90},[44,1411,253],{"class":158},[44,1413,1284],{"class":90},[44,1415,580],{"class":86},[44,1417,618],{"class":158},[44,1419,122],{"class":90},[44,1421,580],{"class":86},[44,1423,618],{"class":158},[44,1425,181],{"class":90},[44,1427,1428],{"class":54},"\"RIGHT\"",[44,1430,1362],{"class":90},[44,1432,1433,1435,1438,1440,1442,1444,1446,1448,1450,1452,1454,1456,1458,1460,1463],{"class":46,"line":660},[44,1434,1269],{"class":90},[44,1436,1437],{"class":54},"\"VALIGN\"",[44,1439,1275],{"class":90},[44,1441,253],{"class":158},[44,1443,122],{"class":90},[44,1445,253],{"class":158},[44,1447,1284],{"class":90},[44,1449,580],{"class":86},[44,1451,618],{"class":158},[44,1453,122],{"class":90},[44,1455,580],{"class":86},[44,1457,618],{"class":158},[44,1459,181],{"class":90},[44,1461,1462],{"class":54},"\"MIDDLE\"",[44,1464,1362],{"class":90},[44,1466,1467,1469,1472,1474,1476,1478,1480,1482,1484,1486,1488,1490,1492,1495],{"class":46,"line":671},[44,1468,1269],{"class":90},[44,1470,1471],{"class":54},"\"LINEBELOW\"",[44,1473,1275],{"class":90},[44,1475,253],{"class":158},[44,1477,122],{"class":90},[44,1479,253],{"class":158},[44,1481,1284],{"class":90},[44,1483,580],{"class":86},[44,1485,618],{"class":158},[44,1487,122],{"class":90},[44,1489,253],{"class":158},[44,1491,181],{"class":90},[44,1493,1494],{"class":158},"0.8",[44,1496,1497],{"class":90},", colors.black),\n",[44,1499,1500,1502,1505,1507,1509,1511,1513,1515,1517,1519,1521,1523,1526,1529,1532],{"class":46,"line":688},[44,1501,1269],{"class":90},[44,1503,1504],{"class":54},"\"ROWBACKGROUNDS\"",[44,1506,1275],{"class":90},[44,1508,253],{"class":158},[44,1510,122],{"class":90},[44,1512,618],{"class":158},[44,1514,1284],{"class":90},[44,1516,580],{"class":86},[44,1518,618],{"class":158},[44,1520,122],{"class":90},[44,1522,580],{"class":86},[44,1524,1525],{"class":158},"2",[44,1527,1528],{"class":90},"), [colors.white, colors.HexColor(",[44,1530,1531],{"class":54},"\"#f6f8fa\"",[44,1533,1534],{"class":90},")]),\n",[44,1536,1537,1539,1541,1543,1545,1547,1549,1551,1553,1555,1557,1559,1561,1563,1565,1567],{"class":46,"line":709},[44,1538,1269],{"class":90},[44,1540,1336],{"class":54},[44,1542,1275],{"class":90},[44,1544,253],{"class":158},[44,1546,122],{"class":90},[44,1548,580],{"class":86},[44,1550,618],{"class":158},[44,1552,1284],{"class":90},[44,1554,580],{"class":86},[44,1556,618],{"class":158},[44,1558,122],{"class":90},[44,1560,580],{"class":86},[44,1562,618],{"class":158},[44,1564,181],{"class":90},[44,1566,1359],{"class":54},[44,1568,1362],{"class":90},[44,1570,1571,1573,1576,1578,1580,1582,1584,1586,1588,1590,1592,1594,1596,1598,1600,1602],{"class":46,"line":730},[44,1572,1269],{"class":90},[44,1574,1575],{"class":54},"\"LINEABOVE\"",[44,1577,1275],{"class":90},[44,1579,253],{"class":158},[44,1581,122],{"class":90},[44,1583,580],{"class":86},[44,1585,618],{"class":158},[44,1587,1284],{"class":90},[44,1589,580],{"class":86},[44,1591,618],{"class":158},[44,1593,122],{"class":90},[44,1595,580],{"class":86},[44,1597,618],{"class":158},[44,1599,181],{"class":90},[44,1601,1494],{"class":158},[44,1603,1497],{"class":90},[44,1605,1606],{"class":46,"line":736},[44,1607,1608],{"class":90},"    ]\n",[44,1610,1611,1613,1615,1617],{"class":46,"line":745},[44,1612,1021],{"class":86},[44,1614,1051],{"class":90},[44,1616,630],{"class":86},[44,1618,1619],{"class":90}," subtotal_rows:\n",[44,1621,1622,1625,1628,1631,1633,1635,1637,1640,1642,1644,1647,1649,1651,1653,1655,1657,1659,1661,1663,1666,1669],{"class":46,"line":750},[44,1623,1624],{"class":90},"        style ",[44,1626,1627],{"class":86},"+=",[44,1629,1630],{"class":90}," [(",[44,1632,1336],{"class":54},[44,1634,1275],{"class":90},[44,1636,253],{"class":158},[44,1638,1639],{"class":90},", r), (",[44,1641,580],{"class":86},[44,1643,618],{"class":158},[44,1645,1646],{"class":90},", r), ",[44,1648,1359],{"class":54},[44,1650,1284],{"class":90},[44,1652,1272],{"class":54},[44,1654,1275],{"class":90},[44,1656,253],{"class":158},[44,1658,1639],{"class":90},[44,1660,580],{"class":86},[44,1662,618],{"class":158},[44,1664,1665],{"class":90},", r), colors.HexColor(",[44,1667,1668],{"class":54},"\"#ddf4ff\"",[44,1670,1671],{"class":90},"))]\n",[44,1673,1674,1677,1679,1681,1684,1686,1689,1692,1694,1696,1699,1701,1703,1706,1708,1710,1713,1715],{"class":46,"line":761},[44,1675,1676],{"class":90},"    widths ",[44,1678,113],{"class":86},[44,1680,984],{"class":90},[44,1682,1683],{"class":158},"22",[44,1685,523],{"class":86},[44,1687,1688],{"class":90}," mm, ",[44,1690,1691],{"class":158},"30",[44,1693,523],{"class":86},[44,1695,1688],{"class":90},[44,1697,1698],{"class":158},"70",[44,1700,523],{"class":86},[44,1702,1688],{"class":90},[44,1704,1705],{"class":158},"16",[44,1707,523],{"class":86},[44,1709,1688],{"class":90},[44,1711,1712],{"class":158},"26",[44,1714,523],{"class":86},[44,1716,1717],{"class":90}," mm]\n",[44,1719,1720,1722,1725,1728,1730,1733,1736,1738,1740,1742,1745,1747],{"class":46,"line":779},[44,1721,739],{"class":86},[44,1723,1724],{"class":90}," Table(data, ",[44,1726,1727],{"class":125},"colWidths",[44,1729,113],{"class":86},[44,1731,1732],{"class":90},"widths, ",[44,1734,1735],{"class":125},"repeatRows",[44,1737,113],{"class":86},[44,1739,618],{"class":158},[44,1741,122],{"class":90},[44,1743,1744],{"class":125},"style",[44,1746,113],{"class":86},[44,1748,1749],{"class":90},"TableStyle(style))\n",[44,1751,1752],{"class":46,"line":812},[44,1753,104],{"emptyLinePlaceholder":103},[44,1755,1756],{"class":46,"line":863},[44,1757,1758],{"class":90},"story.append(detail_table(orders))\n",[10,1760,1761,1762,1765,1766,1768,1769,1772,1773,1776,1777,1780],{},"Wrapping a cell's text in a ",[41,1763,1764],{},"Paragraph"," is what makes it wrap: a plain string in a table cell is drawn on one line, however long. Fixed ",[41,1767,1727],{}," that add up to the page's usable width (A4 is 210 mm; with 20 mm margins that leaves 170 mm, and these columns use 164 mm) stop the table from running off the page. Numbers are formatted as strings before they go into the table, which is where thousands separators and two decimals are decided; ",[41,1770,1771],{},"ALIGN RIGHT"," on the numeric columns lines their decimal points up. ",[41,1774,1775],{},"repeatRows=1"," repeats the header row after every page break, and ",[41,1778,1779],{},"ROWBACKGROUNDS"," alternates the stripe colours, which the subtotal rows then override with their own background.",[1782,1783,1791,1792,1791,1796,1791,1800,1791,1807,1791,1817,1791,1824,1791,1829,1791,1833,1791,1838,1791,1843,1791,1848,1791,1852,1791,1856,1791,1859,1791,1864,1791,1869,1791,1873,1791,1877,1791,1880,1791,1885,1791,1890,1791,1894,1791,1898,1791,1901,1791,1904,1791,1908],"svg",{"viewBox":1784,"role":1785,"ariaLabelledBy":1786,"xmlns":1789,"style":1790},"84 0 592 471","img",[1787,1788],"rl-layout-t","rl-layout-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:592px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[1793,1794,1795],"title",{"id":1787},"The flowables that make up the report",[1797,1798,1799],"desc",{"id":1788},"The document is a list of flowables laid onto pages in order. A title paragraph, a generated commentary paragraph, a vector bar chart and a detail table whose header repeats on each page, with a page X of Y footer drawn on every page.",[1801,1802],"rect",{"x":1803,"y":253,"width":1804,"height":1805,"fill":1806},"84","592","471","#ffffff",[1801,1808],{"x":1809,"y":1810,"width":1811,"height":1812,"rx":1813,"fill":1814,"stroke":1815,"style":1816},"100.0","16.0","560.0","59.0","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[1818,1819,1823],"text",{"x":1820,"y":1821,"style":1822},"380.0","41.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Title",[1818,1825,1828],{"x":1820,"y":1826,"style":1827},"61.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","Paragraph, Title style",[46,1830],{"x1":1820,"y1":1831,"x2":1820,"y2":1832,"stroke":1815,"style":1816},"80.0","102.0",[1834,1835],"polygon",{"points":1836,"fill":1837},"380.0,106.0 375.0,97.0 385.0,97.0","#5b5cf0",[1801,1839],{"x":1809,"y":1840,"width":1811,"height":1812,"rx":1813,"fill":1841,"stroke":1842,"style":1816},"111.0","#e7ebef","var(--line,#cdd5e6)",[1818,1844,1847],{"x":1820,"y":1845,"style":1846},"136.5","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Commentary",[1818,1849,1851],{"x":1820,"y":1850,"style":1827},"156.0","generated from the figures",[46,1853],{"x1":1820,"y1":1854,"x2":1820,"y2":1855,"stroke":1815,"style":1816},"175.0","197.0",[1834,1857],{"points":1858,"fill":1837},"380.0,201.0 375.0,192.0 385.0,192.0",[1801,1860],{"x":1809,"y":1861,"width":1811,"height":1812,"rx":1813,"fill":1862,"stroke":1863,"style":1816},"206.0","#d9f4f1","var(--teal,#0f9488)",[1818,1865,1868],{"x":1820,"y":1866,"style":1867},"231.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Bar chart",[1818,1870,1872],{"x":1820,"y":1871,"style":1827},"251.0","VerticalBarChart, vector",[46,1874],{"x1":1820,"y1":1875,"x2":1820,"y2":1876,"stroke":1815,"style":1816},"270.0","292.0",[1834,1878],{"points":1879,"fill":1837},"380.0,296.0 375.0,287.0 385.0,287.0",[1801,1881],{"x":1809,"y":1882,"width":1811,"height":1812,"rx":1813,"fill":1883,"stroke":1884,"style":1816},"301.0","#fdefd8","var(--gold,#b4740a)",[1818,1886,1889],{"x":1820,"y":1887,"style":1888},"326.5","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Detail table",[1818,1891,1893],{"x":1820,"y":1892,"style":1827},"346.0","repeatRows=1, subtotals, wrapping",[46,1895],{"x1":1820,"y1":1896,"x2":1820,"y2":1897,"stroke":1815,"style":1816},"365.0","387.0",[1834,1899],{"points":1900,"fill":1837},"380.0,391.0 375.0,382.0 385.0,382.0",[1801,1902],{"x":1809,"y":1903,"width":1811,"height":1812,"rx":1813,"fill":1841,"stroke":1842,"style":1816},"396.0",[1818,1905,1907],{"x":1820,"y":1906,"style":1846},"421.5","Footer",[1818,1909,1911],{"x":1820,"y":1910,"style":1827},"441.0","page X of Y on every page",[1782,1913,1791,1919,1791,1922,1791,1925,1791,1930,1791,1936,1791,1940,1791,1946,1791,1950,1791,1954,1791,1958,1791,1962,1791,1966,1791,1970,1791,1974,1791,1978,1791,1982,1791,1985,1791,1988,1791,1992,1791,1994,1791,1997,1791,1999,1791,2002,1791,2004,1791,2007,1791,2009,1791,2012,1791,2015,1791,2018,1791,2020,1791,2024,1791,2026,1791,2029,1791,2031,1791,2034,1791,2036,1791,2038,1791,2040,1791,2043,1791,2046,1791,2049,1791,2051,1791,2054,1791,2056,1791,2058,1791,2060,1791,2064,1791,2066,1791,2069,1791,2071,1791,2074,1791,2077,1791,2080,1791,2082,1791,2085,1791,2087,1791,2089,1791,2091,1791,2094,1791,2096,1791,2099,1791,2101,1791,2104,1791,2107,1791,2111,1791,2113,1791,2115,1791,2119,1791,2121,1791,2123,1791,2126,1791,2128],{"viewBox":1914,"role":1785,"ariaLabelledBy":1915,"xmlns":1789,"style":1918},"52 0 655 196",[1916,1917],"rl-table-t","rl-table-d","width:100%;max-width:655px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[1793,1920,1921],{"id":1916},"How the detail table is set",[1797,1923,1924],{"id":1917},"The header row is dark with white bold text and repeats on every page. Product names wrap within their column, numbers are right-aligned with thousands separators, rows alternate white and light grey, and each region ends with a bold subtotal row on a blue background.",[1801,1926],{"x":1927,"y":253,"width":1928,"height":1929,"fill":1806},"52","655","196",[1801,1931],{"x":1932,"y":1810,"width":1933,"height":1934,"rx":253,"fill":1841,"stroke":1842,"style":1935},"68.8","32.0","24.0","stroke-width:1px",[1801,1937],{"x":1938,"y":1810,"width":1939,"height":1934,"rx":253,"fill":1841,"stroke":1842,"style":1935},"100.8","80.9",[1818,1941,1945],{"x":1942,"y":1943,"style":1944},"141.2","33.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[1801,1947],{"x":1948,"y":1810,"width":1949,"height":1934,"rx":253,"fill":1841,"stroke":1842,"style":1935},"181.7","104.1",[1818,1951,1953],{"x":1952,"y":1943,"style":1944},"233.8","B",[1801,1955],{"x":1956,"y":1810,"width":1957,"height":1934,"rx":253,"fill":1841,"stroke":1842,"style":1935},"285.8","277.6",[1818,1959,1961],{"x":1960,"y":1943,"style":1944},"424.6","C",[1801,1963],{"x":1964,"y":1810,"width":1965,"height":1934,"rx":253,"fill":1841,"stroke":1842,"style":1935},"563.4","46.0",[1818,1967,1969],{"x":1968,"y":1943,"style":1944},"586.4","D",[1801,1971],{"x":1972,"y":1810,"width":1973,"height":1934,"rx":253,"fill":1841,"stroke":1842,"style":1935},"609.4","81.8",[1818,1975,1977],{"x":1976,"y":1943,"style":1944},"650.3","E",[1801,1979],{"x":1932,"y":1980,"width":1933,"height":1981,"rx":253,"fill":1841,"stroke":1842,"style":1935},"40.0","28.0",[1818,1983,618],{"x":1984,"y":1812,"style":1944},"84.8",[1801,1986],{"x":1938,"y":1980,"width":1939,"height":1981,"rx":253,"fill":1814,"stroke":1815,"style":1987},"stroke-width:1.5px",[1818,1989,1991],{"x":1942,"y":1812,"style":1990},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Order",[1801,1993],{"x":1948,"y":1980,"width":1949,"height":1981,"rx":253,"fill":1814,"stroke":1815,"style":1987},[1818,1995,1996],{"x":1952,"y":1812,"style":1990},"Region",[1801,1998],{"x":1956,"y":1980,"width":1957,"height":1981,"rx":253,"fill":1814,"stroke":1815,"style":1987},[1818,2000,2001],{"x":1960,"y":1812,"style":1990},"Product",[1801,2003],{"x":1964,"y":1980,"width":1965,"height":1981,"rx":253,"fill":1814,"stroke":1815,"style":1987},[1818,2005,2006],{"x":1968,"y":1812,"style":1990},"Qty",[1801,2008],{"x":1972,"y":1980,"width":1973,"height":1981,"rx":253,"fill":1814,"stroke":1815,"style":1987},[1818,2010,2011],{"x":1976,"y":1812,"style":1990},"Amount",[1801,2013],{"x":1932,"y":2014,"width":1933,"height":1981,"rx":253,"fill":1841,"stroke":1842,"style":1935},"68.0",[1818,2016,1525],{"x":1984,"y":2017,"style":1944},"87.0",[1801,2019],{"x":1938,"y":2014,"width":1939,"height":1981,"rx":253,"fill":1806,"stroke":1842,"style":1935},[1818,2021,2023],{"x":1942,"y":2017,"style":2022},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","SO-3002",[1801,2025],{"x":1948,"y":2014,"width":1949,"height":1981,"rx":253,"fill":1806,"stroke":1842,"style":1935},[1818,2027,2028],{"x":1952,"y":2017,"style":2022},"East",[1801,2030],{"x":1956,"y":2014,"width":1957,"height":1981,"rx":253,"fill":1806,"stroke":1842,"style":1935},[1818,2032,2033],{"x":1960,"y":2017,"style":2022},"Safety gloves",[1801,2035],{"x":1964,"y":2014,"width":1965,"height":1981,"rx":253,"fill":1806,"stroke":1842,"style":1935},[1818,2037,1683],{"x":1968,"y":2017,"style":2022},[1801,2039],{"x":1972,"y":2014,"width":1973,"height":1981,"rx":253,"fill":1806,"stroke":1842,"style":1935},[1818,2041,2042],{"x":1976,"y":2017,"style":2022},"3,904.23",[1801,2044],{"x":1932,"y":2045,"width":1933,"height":1981,"rx":253,"fill":1841,"stroke":1842,"style":1935},"96.0",[1818,2047,1407],{"x":1984,"y":2048,"style":1944},"115.0",[1801,2050],{"x":1938,"y":2045,"width":1939,"height":1981,"rx":253,"fill":1806,"stroke":1842,"style":1935},[1818,2052,2053],{"x":1942,"y":2048,"style":2022},"SO-3110",[1801,2055],{"x":1948,"y":2045,"width":1949,"height":1981,"rx":253,"fill":1806,"stroke":1842,"style":1935},[1818,2057,2028],{"x":1952,"y":2048,"style":2022},[1801,2059],{"x":1956,"y":2045,"width":1957,"height":1981,"rx":253,"fill":1883,"stroke":1884,"style":1987},[1818,2061,2063],{"x":1960,"y":2048,"style":2062},"font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Bearings and bushes for conveyor rollers",[1801,2065],{"x":1964,"y":2045,"width":1965,"height":1981,"rx":253,"fill":1806,"stroke":1842,"style":1935},[1818,2067,2068],{"x":1968,"y":2048,"style":2022},"118",[1801,2070],{"x":1972,"y":2045,"width":1973,"height":1981,"rx":253,"fill":1806,"stroke":1842,"style":1935},[1818,2072,2073],{"x":1976,"y":2048,"style":2022},"2,288.54",[1801,2075],{"x":1932,"y":2076,"width":1933,"height":1981,"rx":253,"fill":1841,"stroke":1842,"style":1935},"124.0",[1818,2078,873],{"x":1984,"y":2079,"style":1944},"143.0",[1801,2081],{"x":1938,"y":2076,"width":1939,"height":1981,"rx":253,"fill":1806,"stroke":1842,"style":1935},[1818,2083,2084],{"x":1942,"y":2079,"style":2022},"SO-3114",[1801,2086],{"x":1948,"y":2076,"width":1949,"height":1981,"rx":253,"fill":1806,"stroke":1842,"style":1935},[1818,2088,2028],{"x":1952,"y":2079,"style":2022},[1801,2090],{"x":1956,"y":2076,"width":1957,"height":1981,"rx":253,"fill":1806,"stroke":1842,"style":1935},[1818,2092,2093],{"x":1960,"y":2079,"style":2022},"Hydraulic fittings",[1801,2095],{"x":1964,"y":2076,"width":1965,"height":1981,"rx":253,"fill":1806,"stroke":1842,"style":1935},[1818,2097,2098],{"x":1968,"y":2079,"style":2022},"34",[1801,2100],{"x":1972,"y":2076,"width":1973,"height":1981,"rx":253,"fill":1806,"stroke":1842,"style":1935},[1818,2102,2103],{"x":1976,"y":2079,"style":2022},"3,651.94",[1801,2105],{"x":1932,"y":2106,"width":1933,"height":1981,"rx":253,"fill":1841,"stroke":1842,"style":1935},"152.0",[1818,2108,2110],{"x":1984,"y":2109,"style":1944},"171.0","5",[1801,2112],{"x":1938,"y":2106,"width":1939,"height":1981,"rx":253,"fill":1862,"stroke":1863,"style":1987},[1801,2114],{"x":1948,"y":2106,"width":1949,"height":1981,"rx":253,"fill":1862,"stroke":1863,"style":1987},[1818,2116,2118],{"x":1952,"y":2109,"style":2117},"font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","East subtotal",[1801,2120],{"x":1956,"y":2106,"width":1957,"height":1981,"rx":253,"fill":1862,"stroke":1863,"style":1987},[1801,2122],{"x":1964,"y":2106,"width":1965,"height":1981,"rx":253,"fill":1862,"stroke":1863,"style":1987},[1818,2124,2125],{"x":1968,"y":2109,"style":2117},"…",[1801,2127],{"x":1972,"y":2106,"width":1973,"height":1981,"rx":253,"fill":1862,"stroke":1863,"style":1987},[1818,2129,2125],{"x":1976,"y":2109,"style":2117},[29,2131,2133],{"id":2132},"step-4-page-x-of-y-and-building-the-document","Step 4 — \"Page X of Y\" and building the document",[10,2135,2136],{},"ReportLab knows the current page number while drawing, but not the total until the document is finished. The usual solution is a canvas subclass that holds each page back until the end, then stamps \"page X of Y\" on all of them:",[34,2138,2140],{"className":77,"code":2139,"language":79,"meta":39,"style":39},"from reportlab.pdfgen import canvas\nfrom reportlab.platypus import SimpleDocTemplate\n\nclass NumberedCanvas(canvas.Canvas):\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self._pages = []\n\n    def showPage(self):\n        self._pages.append(dict(self.__dict__))\n        self._startPage()\n\n    def save(self):\n        for state in self._pages:\n            self.__dict__.update(state)\n            self.setFont(\"Helvetica\", 7)\n            self.drawRightString(190 * mm, 10 * mm, f\"Page {self._pageNumber} of {len(self._pages)}\")\n            self.drawString(20 * mm, 10 * mm, \"Orders report — Q3 2026 — generated from orders_q3.xlsx\")\n            super().showPage()\n        super().save()\n\ndoc = SimpleDocTemplate(\"orders_q3.pdf\", pagesize=A4, leftMargin=20 * mm, rightMargin=20 * mm,\n                        topMargin=18 * mm, bottomMargin=18 * mm, title=\"Orders report — Q3 2026\", author=\"Sales operations\")\ndoc.build(story, canvasmaker=NumberedCanvas)\n",[41,2141,2142,2154,2165,2169,2190,2213,2235,2248,2252,2262,2284,2291,2295,2304,2319,2331,2348,2402,2426,2434,2441,2445,2491,2534],{"__ignoreMap":39},[44,2143,2144,2146,2149,2151],{"class":46,"line":47},[44,2145,352],{"class":86},[44,2147,2148],{"class":90}," reportlab.pdfgen ",[44,2150,87],{"class":86},[44,2152,2153],{"class":90}," canvas\n",[44,2155,2156,2158,2160,2162],{"class":46,"line":100},[44,2157,352],{"class":86},[44,2159,427],{"class":90},[44,2161,87],{"class":86},[44,2163,2164],{"class":90}," SimpleDocTemplate\n",[44,2166,2167],{"class":46,"line":107},[44,2168,104],{"emptyLinePlaceholder":103},[44,2170,2171,2174,2177,2179,2183,2185,2188],{"class":46,"line":137},[44,2172,2173],{"class":86},"class",[44,2175,2176],{"class":50}," NumberedCanvas",[44,2178,170],{"class":90},[44,2180,2182],{"class":2181},"sHg5J","canvas",[44,2184,27],{"class":90},[44,2186,2187],{"class":2181},"Canvas",[44,2189,1043],{"class":90},[44,2191,2192,2195,2198,2201,2204,2207,2210],{"class":46,"line":202},[44,2193,2194],{"class":86},"    def",[44,2196,2197],{"class":158}," __init__",[44,2199,2200],{"class":90},"(self, ",[44,2202,2203],{"class":86},"*",[44,2205,2206],{"class":90},"args, ",[44,2208,2209],{"class":86},"**",[44,2211,2212],{"class":90},"kwargs):\n",[44,2214,2215,2218,2221,2224,2226,2228,2230,2232],{"class":46,"line":226},[44,2216,2217],{"class":158},"        super",[44,2219,2220],{"class":90},"().",[44,2222,2223],{"class":158},"__init__",[44,2225,170],{"class":90},[44,2227,2203],{"class":86},[44,2229,2206],{"class":90},[44,2231,2209],{"class":86},[44,2233,2234],{"class":90},"kwargs)\n",[44,2236,2237,2240,2243,2245],{"class":46,"line":242},[44,2238,2239],{"class":158},"        self",[44,2241,2242],{"class":90},"._pages ",[44,2244,113],{"class":86},[44,2246,2247],{"class":90}," []\n",[44,2249,2250],{"class":46,"line":259},[44,2251,104],{"emptyLinePlaceholder":103},[44,2253,2254,2256,2259],{"class":46,"line":439},[44,2255,2194],{"class":86},[44,2257,2258],{"class":511}," showPage",[44,2260,2261],{"class":90},"(self):\n",[44,2263,2264,2266,2269,2272,2274,2277,2279,2282],{"class":46,"line":450},[44,2265,2239],{"class":158},[44,2267,2268],{"class":90},"._pages.append(",[44,2270,2271],{"class":158},"dict",[44,2273,170],{"class":90},[44,2275,2276],{"class":158},"self",[44,2278,27],{"class":90},[44,2280,2281],{"class":158},"__dict__",[44,2283,199],{"class":90},[44,2285,2286,2288],{"class":46,"line":500},[44,2287,2239],{"class":158},[44,2289,2290],{"class":90},"._startPage()\n",[44,2292,2293],{"class":46,"line":505},[44,2294,104],{"emptyLinePlaceholder":103},[44,2296,2297,2299,2302],{"class":46,"line":539},[44,2298,2194],{"class":86},[44,2300,2301],{"class":511}," save",[44,2303,2261],{"class":90},[44,2305,2306,2308,2311,2313,2316],{"class":46,"line":550},[44,2307,1048],{"class":86},[44,2309,2310],{"class":90}," state ",[44,2312,630],{"class":86},[44,2314,2315],{"class":158}," self",[44,2317,2318],{"class":90},"._pages:\n",[44,2320,2321,2324,2326,2328],{"class":46,"line":561},[44,2322,2323],{"class":158},"            self",[44,2325,27],{"class":90},[44,2327,2281],{"class":158},[44,2329,2330],{"class":90},".update(state)\n",[44,2332,2333,2335,2338,2341,2343,2346],{"class":46,"line":594},[44,2334,2323],{"class":158},[44,2336,2337],{"class":90},".setFont(",[44,2339,2340],{"class":54},"\"Helvetica\"",[44,2342,122],{"class":90},[44,2344,2345],{"class":158},"7",[44,2347,134],{"class":90},[44,2349,2350,2352,2355,2358,2360,2362,2364,2366,2368,2370,2373,2375,2377,2380,2382,2385,2387,2389,2391,2393,2396,2398,2400],{"class":46,"line":641},[44,2351,2323],{"class":158},[44,2353,2354],{"class":90},".drawRightString(",[44,2356,2357],{"class":158},"190",[44,2359,523],{"class":86},[44,2361,1688],{"class":90},[44,2363,495],{"class":158},[44,2365,523],{"class":86},[44,2367,1688],{"class":90},[44,2369,278],{"class":86},[44,2371,2372],{"class":54},"\"Page ",[44,2374,285],{"class":284},[44,2376,2276],{"class":158},[44,2378,2379],{"class":90},"._pageNumber",[44,2381,294],{"class":284},[44,2383,2384],{"class":54}," of ",[44,2386,285],{"class":284},[44,2388,267],{"class":158},[44,2390,170],{"class":90},[44,2392,2276],{"class":158},[44,2394,2395],{"class":90},"._pages)",[44,2397,294],{"class":284},[44,2399,786],{"class":54},[44,2401,134],{"class":90},[44,2403,2404,2406,2409,2411,2413,2415,2417,2419,2421,2424],{"class":46,"line":660},[44,2405,2323],{"class":158},[44,2407,2408],{"class":90},".drawString(",[44,2410,574],{"class":158},[44,2412,523],{"class":86},[44,2414,1688],{"class":90},[44,2416,495],{"class":158},[44,2418,523],{"class":86},[44,2420,1688],{"class":90},[44,2422,2423],{"class":54},"\"Orders report — Q3 2026 — generated from orders_q3.xlsx\"",[44,2425,134],{"class":90},[44,2427,2428,2431],{"class":46,"line":671},[44,2429,2430],{"class":158},"            super",[44,2432,2433],{"class":90},"().showPage()\n",[44,2435,2436,2438],{"class":46,"line":688},[44,2437,2217],{"class":158},[44,2439,2440],{"class":90},"().save()\n",[44,2442,2443],{"class":46,"line":709},[44,2444,104],{"emptyLinePlaceholder":103},[44,2446,2447,2450,2452,2455,2458,2460,2463,2465,2468,2471,2473,2475,2477,2479,2482,2484,2486,2488],{"class":46,"line":730},[44,2448,2449],{"class":90},"doc ",[44,2451,113],{"class":86},[44,2453,2454],{"class":90}," SimpleDocTemplate(",[44,2456,2457],{"class":54},"\"orders_q3.pdf\"",[44,2459,122],{"class":90},[44,2461,2462],{"class":125},"pagesize",[44,2464,113],{"class":86},[44,2466,2467],{"class":90},"A4, ",[44,2469,2470],{"class":125},"leftMargin",[44,2472,113],{"class":86},[44,2474,574],{"class":158},[44,2476,523],{"class":86},[44,2478,1688],{"class":90},[44,2480,2481],{"class":125},"rightMargin",[44,2483,113],{"class":86},[44,2485,574],{"class":158},[44,2487,523],{"class":86},[44,2489,2490],{"class":90}," mm,\n",[44,2492,2493,2496,2498,2501,2503,2505,2508,2510,2512,2514,2516,2518,2520,2522,2524,2527,2529,2532],{"class":46,"line":736},[44,2494,2495],{"class":125},"                        topMargin",[44,2497,113],{"class":86},[44,2499,2500],{"class":158},"18",[44,2502,523],{"class":86},[44,2504,1688],{"class":90},[44,2506,2507],{"class":125},"bottomMargin",[44,2509,113],{"class":86},[44,2511,2500],{"class":158},[44,2513,523],{"class":86},[44,2515,1688],{"class":90},[44,2517,1793],{"class":125},[44,2519,113],{"class":86},[44,2521,767],{"class":54},[44,2523,122],{"class":90},[44,2525,2526],{"class":125},"author",[44,2528,113],{"class":86},[44,2530,2531],{"class":54},"\"Sales operations\"",[44,2533,134],{"class":90},[44,2535,2536,2539,2542,2544],{"class":46,"line":745},[44,2537,2538],{"class":90},"doc.build(story, ",[44,2540,2541],{"class":125},"canvasmaker",[44,2543,113],{"class":86},[44,2545,2546],{"class":90},"NumberedCanvas)\n",[10,2548,2549,2552,2553,2556,2557,2559,2560,2562],{},[41,2550,2551],{},"showPage"," normally writes a page out immediately; the subclass saves the page's state instead, and ",[41,2554,2555],{},"save"," replays every page with the footer added now that the total is known. Passing ",[41,2558,1793],{}," and ",[41,2561,2526],{}," to the document template sets the PDF's metadata, which is what a PDF viewer shows as the window title and what a document library indexes. The same footer approach works for a confidentiality notice or a run timestamp.",[29,2564,2566],{"id":2565},"step-5-check-the-pdf","Step 5 — Check the PDF",[10,2568,2569],{},"Read the PDF back and check the things a person would look for — page count, the header on every page, the footer numbering and the total:",[34,2571,2573],{"className":77,"code":2572,"language":79,"meta":39,"style":39},"import re\nfrom pypdf import PdfReader\n\nreader = PdfReader(\"orders_q3.pdf\")\npages = [p.extract_text() for p in reader.pages]\nprint(\"pages:\", len(pages), \"| title:\", reader.metadata.title)\nprint(\"header on every page:\", all(\"Order\\nRegion\\nProduct\\nQty\\nAmount\" in t for t in pages))\nprint(\"footers:\", [re.search(r\"Page \\d+ of \\d+\", t).group() for t in pages])\nprint(\"total in PDF matches workbook:\", f\"{total:,.2f}\" in pages[-1])\n",[41,2574,2575,2582,2594,2598,2612,2632,2654,2706,2749],{"__ignoreMap":39},[44,2576,2577,2579],{"class":46,"line":47},[44,2578,87],{"class":86},[44,2580,2581],{"class":90}," re\n",[44,2583,2584,2586,2589,2591],{"class":46,"line":100},[44,2585,352],{"class":86},[44,2587,2588],{"class":90}," pypdf ",[44,2590,87],{"class":86},[44,2592,2593],{"class":90}," PdfReader\n",[44,2595,2596],{"class":46,"line":107},[44,2597,104],{"emptyLinePlaceholder":103},[44,2599,2600,2603,2605,2608,2610],{"class":46,"line":137},[44,2601,2602],{"class":90},"reader ",[44,2604,113],{"class":86},[44,2606,2607],{"class":90}," PdfReader(",[44,2609,2457],{"class":54},[44,2611,134],{"class":90},[44,2613,2614,2617,2619,2622,2624,2627,2629],{"class":46,"line":202},[44,2615,2616],{"class":90},"pages ",[44,2618,113],{"class":86},[44,2620,2621],{"class":90}," [p.extract_text() ",[44,2623,624],{"class":86},[44,2625,2626],{"class":90}," p ",[44,2628,630],{"class":86},[44,2630,2631],{"class":90}," reader.pages]\n",[44,2633,2634,2636,2638,2641,2643,2645,2648,2651],{"class":46,"line":226},[44,2635,262],{"class":158},[44,2637,170],{"class":90},[44,2639,2640],{"class":54},"\"pages:\"",[44,2642,122],{"class":90},[44,2644,267],{"class":158},[44,2646,2647],{"class":90},"(pages), ",[44,2649,2650],{"class":54},"\"| title:\"",[44,2652,2653],{"class":90},", reader.metadata.title)\n",[44,2655,2656,2658,2660,2663,2665,2668,2670,2673,2676,2678,2680,2682,2684,2686,2688,2691,2694,2697,2699,2701,2703],{"class":46,"line":242},[44,2657,262],{"class":158},[44,2659,170],{"class":90},[44,2661,2662],{"class":54},"\"header on every page:\"",[44,2664,122],{"class":90},[44,2666,2667],{"class":158},"all",[44,2669,170],{"class":90},[44,2671,2672],{"class":54},"\"Order",[44,2674,2675],{"class":284},"\\n",[44,2677,1996],{"class":54},[44,2679,2675],{"class":284},[44,2681,2001],{"class":54},[44,2683,2675],{"class":284},[44,2685,2006],{"class":54},[44,2687,2675],{"class":284},[44,2689,2690],{"class":54},"Amount\"",[44,2692,2693],{"class":86}," in",[44,2695,2696],{"class":90}," t ",[44,2698,624],{"class":86},[44,2700,2696],{"class":90},[44,2702,630],{"class":86},[44,2704,2705],{"class":90}," pages))\n",[44,2707,2708,2710,2712,2715,2718,2721,2723,2726,2729,2731,2733,2735,2737,2740,2742,2744,2746],{"class":46,"line":259},[44,2709,262],{"class":158},[44,2711,170],{"class":90},[44,2713,2714],{"class":54},"\"footers:\"",[44,2716,2717],{"class":90},", [re.search(",[44,2719,2720],{"class":86},"r",[44,2722,2372],{"class":54},[44,2724,2725],{"class":158},"\\d",[44,2727,2728],{"class":86},"+",[44,2730,2384],{"class":54},[44,2732,2725],{"class":158},[44,2734,2728],{"class":86},[44,2736,786],{"class":54},[44,2738,2739],{"class":90},", t).group() ",[44,2741,624],{"class":86},[44,2743,2696],{"class":90},[44,2745,630],{"class":86},[44,2747,2748],{"class":90}," pages])\n",[44,2750,2751,2753,2755,2758,2760,2762,2764,2766,2768,2770,2772,2774,2776,2779,2781,2783],{"class":46,"line":439},[44,2752,262],{"class":158},[44,2754,170],{"class":90},[44,2756,2757],{"class":54},"\"total in PDF matches workbook:\"",[44,2759,122],{"class":90},[44,2761,278],{"class":86},[44,2763,786],{"class":54},[44,2765,285],{"class":284},[44,2767,288],{"class":90},[44,2769,291],{"class":86},[44,2771,294],{"class":284},[44,2773,786],{"class":54},[44,2775,2693],{"class":86},[44,2777,2778],{"class":90}," pages[",[44,2780,580],{"class":86},[44,2782,618],{"class":158},[44,2784,657],{"class":90},[10,2786,2787,2788,27],{},"pypdf extracts each table cell on its own line, which is why the header check looks for the five column names separated by line breaks. The report runs to five pages; every page carries the repeated header row; the footers count through to the last page; and the grand total printed on the last page matches the figure computed from the workbook. Checks like these, run in the job before the PDF is sent, catch a broken layout — a table that lost its header, a total that disagrees with the source — before a reader does. Sending the result is covered in ",[23,2789,2791],{"href":2790},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-excel-report-to-multiple-recipients-python\u002F","Send an Excel Report to Multiple Recipients in Python",[1782,2793,1791,2799,1791,2802,1791,2805,1791,2809,1791,2815,1791,2826,1791,2832,1791,2837,1791,2840,1791,2844,1791,2848,1791,2852,1791,2855,1791,2862,1791,2867,1791,2871,1791,2874,1791,2877,1791,2880],{"viewBox":2794,"role":1785,"ariaLabelledBy":2795,"xmlns":1789,"style":2798},"4 0 752 233",[2796,2797],"rl-vs-t","rl-vs-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[1793,2800,2801],{"id":2796},"Convert the workbook or build the PDF from data",[1797,2803,2804],{"id":2797},"Converting with LibreOffice or Excel reproduces the sheet exactly and needs office software installed. Building with ReportLab needs only Python, gives full control of the document's design and wording, but every element must be laid out in code.",[1801,2806],{"x":873,"y":253,"width":2807,"height":2808,"fill":1806},"752","233",[1801,2810],{"x":2811,"y":1810,"width":2812,"height":2813,"rx":2814,"fill":1883,"stroke":1884,"style":1816},"20.0","351.0","201.0","14",[2816,2817,2819],"g",{"transform":2818},"translate(182.5,29.0) scale(1.083)",[2820,2821],"path",{"d":2822,"fill":2823,"stroke":2824,"style":2825},"M6 3h9l4 4v14H6z M15 3v4h4 M9.5 11.5l4.5 6 M14 11.5l-4.5 6","none","var(--gold-ink,#7a4e06)","stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[1818,2827,2831],{"x":2828,"y":2829,"style":2830},"195.5","78.0","font-size:14px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Convert the workbook",[46,2833],{"x1":2834,"y1":2835,"x2":2836,"y2":2835,"stroke":1884,"style":1935},"36.0","91.0","355.0",[1818,2838,2839],{"x":2828,"y":1840,"style":2022},"looks like the sheet",[1818,2841,2843],{"x":2828,"y":2842,"style":2022},"137.0","needs LibreOffice or Excel",[1818,2845,2847],{"x":2828,"y":2846,"style":2022},"163.0","layout set in Excel",[1818,2849,2851],{"x":2828,"y":2850,"style":2022},"189.0","little code",[1801,2853],{"x":2854,"y":1810,"width":2812,"height":2813,"rx":2814,"fill":1862,"stroke":1863,"style":1816},"389.0",[2816,2856,2858],{"transform":2857},"translate(551.5,29.0) scale(1.083)",[2820,2859],{"d":2860,"fill":2823,"stroke":2861,"style":2825},"M6 3h9l4 4v14H6z M15 3v4h4 M9 12h6 M9 15.5h6 M9 19h4","var(--teal-ink,#0b6157)",[1818,2863,2866],{"x":2864,"y":2829,"style":2865},"564.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Build with ReportLab",[46,2868],{"x1":2869,"y1":2835,"x2":2870,"y2":2835,"stroke":1863,"style":1935},"405.0","724.0",[1818,2872,2873],{"x":2864,"y":1840,"style":2022},"designed document",[1818,2875,2876],{"x":2864,"y":2842,"style":2022},"pure Python",[1818,2878,2879],{"x":2864,"y":2846,"style":2022},"commentary, charts, tables",[1818,2881,2882],{"x":2864,"y":2850,"style":2022},"more layout code",[29,2884,2886],{"id":2885},"common-pitfalls","Common pitfalls",[2888,2889,2890,2901,2910,2918,2924],"ul",{},[2891,2892,2893,2897,2898,2900],"li",{},[2894,2895,2896],"strong",{},"Long text in plain table cells."," Wrap it in a ",[41,2899,1764],{}," or it will not wrap.",[2891,2902,2903,2906,2907,2909],{},[2894,2904,2905],{},"No column widths."," Set ",[41,2908,1727],{}," that fit the page's usable width.",[2891,2911,2912,2915,2916,27],{},[2894,2913,2914],{},"Header missing after page one."," Pass ",[41,2917,1775],{},[2891,2919,2920,2923],{},[2894,2921,2922],{},"Numbers left-aligned."," Format as strings and align numeric columns right.",[2891,2925,2926,2929],{},[2894,2927,2928],{},"\"Page X\" without \"of Y\"."," Use a canvas subclass that stamps footers on save.",[29,2931,2933],{"id":2932},"conclusion","Conclusion",[10,2935,2936,2937,2939,2940,2942],{},"To create a designed PDF report from Excel data, read and summarise the workbook with pandas, then build a platypus story: a title, commentary generated from the figures, a vector chart, and a table with ",[41,2938,1764],{}," cells for wrapping text, right-aligned formatted numbers, zebra stripes, bold subtotal rows and ",[41,2941,1775],{},". Build it with a canvas subclass for \"page X of Y\" footers and check the result with pypdf before it is sent.",[29,2944,2946],{"id":2945},"frequently-asked-questions","Frequently asked questions",[10,2948,2949,2952],{},[2894,2950,2951],{},"How do I make text wrap in a ReportLab table cell?","\nPut the text in a Paragraph object rather than a plain string; Paragraphs wrap to the column width, plain strings do not.",[10,2954,2955,2958],{},[2894,2956,2957],{},"How do I repeat a table's header row on every page in ReportLab?","\nCreate the Table with repeatRows=1, or a higher number for multi-row headers, and add it to a platypus document.",[10,2960,2961,2964],{},[2894,2962,2963],{},"How do I add page X of Y to a ReportLab PDF?","\nUse a canvas subclass that stores each page in showPage and draws the footer for every stored page in save, when the total is known.",[10,2966,2967,2970],{},[2894,2968,2969],{},"Can ReportLab draw charts?","\nYes. reportlab.graphics.charts includes bar, line and pie charts drawn as vector graphics; for other chart types insert a matplotlib image.",[29,2972,2974],{"id":2973},"related","Related",[2888,2976,2977,2983,2990,2997],{},[2891,2978,2979,2980,2982],{},"Up one level: ",[23,2981,26],{"href":25}," — PDFs from Excel in Python.",[2891,2984,2985,2989],{},[23,2986,2988],{"href":2987},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-one-pdf-per-excel-row\u002F","Generate One PDF per Excel Row"," — many small documents instead of one report.",[2891,2991,2992,2996],{},[23,2993,2995],{"href":2994},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fconvert-excel-file-to-pdf-with-python\u002F","Convert an Excel File to PDF with Python"," — reproducing the sheet instead.",[2891,2998,2999,3003],{},[23,3000,3002],{"href":3001},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fadd-bookmarks-and-a-cover-page-to-an-excel-pdf-export\u002F","Add Bookmarks and a Cover Page to an Excel PDF Export"," — navigation in longer PDFs.",[1744,3005,3006],{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}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 .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sHg5J, html code.shiki .sHg5J{--shiki-default:#6F42C1;--shiki-dark:#91CBFF}",{"title":39,"searchDepth":100,"depth":100,"links":3008},[3009,3010,3011,3012,3013,3014,3015,3016,3017,3018],{"id":31,"depth":100,"text":32},{"id":73,"depth":100,"text":74},{"id":334,"depth":100,"text":335},{"id":940,"depth":100,"text":941},{"id":2132,"depth":100,"text":2133},{"id":2565,"depth":100,"text":2566},{"id":2885,"depth":100,"text":2886},{"id":2932,"depth":100,"text":2933},{"id":2945,"depth":100,"text":2946},{"id":2973,"depth":100,"text":2974},"2026-10-02","Turn Excel data into a designed PDF report with ReportLab, without Excel or LibreOffice: read and summarise the workbook with pandas, lay out a title, summary text and a native vector chart, build a table with formatted numbers, wrapped text, zebra stripes, bold subtotals and a header row that repeats on every page, add a \"page X of Y\" footer, and check the result with pypdf.","md",[3023,3025,3027,3029],{"q":2951,"a":3024},"Put the text in a Paragraph object rather than a plain string; Paragraphs wrap to the column width, plain strings do not.",{"q":2957,"a":3026},"Create the Table with repeatRows=1, or a higher number for multi-row headers, and add it to a platypus document.",{"q":2963,"a":3028},"Use a canvas subclass that stores each page in showPage and draws the footer for every stored page in save, when the total is known.",{"q":2969,"a":3030},"Yes. reportlab.graphics.charts includes bar, line and pie charts drawn as vector graphics; for other chart types insert a matplotlib image.","create a pdf report from excel data with reportlab",null,{"breadcrumb":3034},[3035,3037,3040],{"name":3036,"item":315},"Home",{"name":3038,"item":3039},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":26,"item":25},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fcreate-a-pdf-report-from-excel-data-with-reportlab",{"title":3043,"description":3044},"Create a PDF Report from Excel Data with ReportLab in Python","A full PDF report from Excel data with ReportLab platypus: summary paragraphs, a vector bar chart, formatted and wrapped table cells, repeating headers, subtotal rows, page X of Y footers and pypdf checks.","create-a-pdf-report-from-excel-data-with-reportlab","automating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fcreate-a-pdf-report-from-excel-data-with-reportlab\u002Findex",[3048],"pdf-export","how-to","l4JRtaQw3z591wbE2T2M8k2NN5c29IlRd0Xkbvaj7JY",[3052,3056],{"title":3053,"path":3054,"stem":3055,"children":-1},"Convert Only Selected Sheets of a Workbook to PDF","\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fconvert-only-selected-sheets-of-a-workbook-to-pdf","automating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fconvert-only-selected-sheets-of-a-workbook-to-pdf\u002Findex",{"title":3057,"path":3058,"stem":3059,"children":-1},"Export an Excel Report to PDF\u002FA for Archiving","\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fexport-an-excel-report-to-pdf-a-for-archiving","automating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fexport-an-excel-report-to-pdf-a-for-archiving\u002Findex",[3061,3063],{"path":3062,"title":26},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf",{"path":3064,"title":3038},"\u002Fautomating-reporting-workflows",[],[3067,3070,3072,3074,3077,3080],{"path":3068,"title":3069,"kind":3049},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Ffill-pdf-forms-from-excel-rows-with-python","Fill PDF Forms from Excel Rows with Python",{"path":3071,"title":2988,"kind":3049},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-one-pdf-per-excel-row",{"path":3073,"title":3002,"kind":3049},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fadd-bookmarks-and-a-cover-page-to-an-excel-pdf-export",{"path":3075,"title":3076,"kind":3049},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fadd-headers-footers-and-page-numbers-to-an-excel-pdf","Add Headers, Footers and Page Numbers to an Excel PDF",{"path":3078,"title":3079,"kind":3049},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fcheck-a-pdf-export-for-cut-off-columns","Check a PDF Export for Cut-Off Columns",{"path":3081,"title":2995,"kind":3049},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fconvert-excel-file-to-pdf-with-python",1790972136764]