[{"data":1,"prerenderedAt":2437},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fembed-a-chart-image-in-the-body-of-a-report-email":3,"surround:\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fembed-a-chart-image-in-the-body-of-a-report-email":2408,"ancestors:\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fembed-a-chart-image-in-the-body-of-a-report-email":2416,"children:\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fembed-a-chart-image-in-the-body-of-a-report-email":2421,"related:\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fembed-a-chart-image-in-the-body-of-a-report-email":2422},{"id":4,"title":5,"body":6,"dateModified":2375,"datePublished":2375,"description":2376,"extension":2377,"faq":2378,"keyword":2387,"kind":2388,"meta":2389,"navigation":147,"path":2397,"seo":2398,"slug":2401,"stem":2402,"topics":2403,"type":2406,"__hash__":2407},"docs\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fembed-a-chart-image-in-the-body-of-a-report-email\u002Findex.md","Embed a Chart Image in the Body of a Report Email",{"type":7,"value":8,"toc":2363},"minimark",[9,38,43,74,88,92,95,406,417,569,573,576,822,836,840,862,1085,1091,1251,1255,1262,1835,1867,1871,1877,2154,2161,2242,2246,2287,2291,2300,2304,2310,2316,2322,2328,2332,2359],[10,11,12,13,17,18,22,23,26,27,32,33,37],"p",{},"Most recipients of a report email read the body on a phone and never open the attached workbook. If the one chart that matters is in the body, they see it; if it is only in the attachment, most of them do not. Getting a chart into the body takes two things: an image of the chart that looks sharp on a high-resolution phone screen, and an email that carries it as an ",[14,15,16],"em",{},"inline"," part referenced by Content-ID, because most mail clients block remote images and many ignore ",[19,20,21],"code",{},"data:"," URIs. This page focuses on the image — including how to render the chart ",[14,24,25],{},"from the workbook itself",", so the email shows exactly what the attachment contains — then embeds one or several images and tests that every reference resolves. The overall message structure (plain text, HTML, attachments) is covered in ",[28,29,31],"a",{"href":30},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Femail-an-excel-report-with-an-html-summary-body\u002F","Email an Excel Report with an HTML Summary Body",". It belongs to ",[28,34,36],{"href":35},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002F","Emailing Excel Reports with smtplib",".",[39,40,42],"h2",{"id":41},"prerequisites","Prerequisites",[44,45,50],"pre",{"className":46,"code":47,"language":48,"meta":49,"style":49},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pypdfium2 pillow matplotlib\n","bash","",[19,51,52],{"__ignoreMap":49},[53,54,57,61,65,68,71],"span",{"class":55,"line":56},"line",1,[53,58,60],{"class":59},"sMTad","pip",[53,62,64],{"class":63},"srMev"," install",[53,66,67],{"class":63}," pypdfium2",[53,69,70],{"class":63}," pillow",[53,72,73],{"class":63}," matplotlib\n",[10,75,76,79,80,83,84,87],{},[19,77,78],{},"report.xlsx"," is the report being sent: a Data sheet with revenue by region and a Chart sheet holding a column chart of it. LibreOffice (",[19,81,82],{},"soffice",") renders the workbook for the first method; ",[19,85,86],{},"pypdfium2"," turns the PDF page into pixels and Pillow crops it. The second method needs only matplotlib.",[39,89,91],{"id":90},"step-1-render-the-workbooks-own-chart-to-png","Step 1 — Render the workbook's own chart to PNG",[10,93,94],{},"If the email shows a chart drawn separately with matplotlib, it can drift from the chart in the attached workbook — a different colour, a different sort order, a different number. Rendering the workbook's chart avoids that. Convert the chart sheet to PDF with LibreOffice, render the page at a high resolution, and crop away the white margins:",[44,96,100],{"className":97,"code":98,"language":99,"meta":49,"style":49},"language-python shiki shiki-themes github-light github-dark-high-contrast","import subprocess\nimport pypdfium2 as pdfium\nfrom PIL import Image, ImageChops\n\nsubprocess.run([\"soffice\", \"--headless\", \"--convert-to\", \"pdf\", \"report.xlsx\"], check=True, capture_output=True)\n\ndef render_chart(pdf_path, page=0, scale=2.5, margin=10):\n    pdf = pdfium.PdfDocument(pdf_path)\n    img = pdf[page].render(scale=scale).to_pil().convert(\"RGB\")\n    pdf.close()\n    bbox = ImageChops.difference(img, Image.new(\"RGB\", img.size, \"white\")).getbbox()\n    left, top, right, bottom = bbox\n    return img.crop((left - margin, top - margin, right + margin, bottom + margin))\n\nchart = render_chart(\"report.pdf\")\nchart.save(\"chart.png\", optimize=True)\nprint(\"chart.png\", chart.size)\n","python",[19,101,102,112,126,142,149,204,209,246,257,281,287,309,320,351,356,372,392],{"__ignoreMap":49},[53,103,104,108],{"class":55,"line":56},[53,105,107],{"class":106},"s-kum","import",[53,109,111],{"class":110},"skGVy"," subprocess\n",[53,113,115,117,120,123],{"class":55,"line":114},2,[53,116,107],{"class":106},[53,118,119],{"class":110}," pypdfium2 ",[53,121,122],{"class":106},"as",[53,124,125],{"class":110}," pdfium\n",[53,127,129,132,136,139],{"class":55,"line":128},3,[53,130,131],{"class":106},"from",[53,133,135],{"class":134},"sP0c6"," PIL",[53,137,138],{"class":106}," import",[53,140,141],{"class":110}," Image, ImageChops\n",[53,143,145],{"class":55,"line":144},4,[53,146,148],{"emptyLinePlaceholder":147},true,"\n",[53,150,152,155,158,161,164,166,169,171,174,176,179,182,186,189,192,194,197,199,201],{"class":55,"line":151},5,[53,153,154],{"class":110},"subprocess.run([",[53,156,157],{"class":63},"\"soffice\"",[53,159,160],{"class":110},", ",[53,162,163],{"class":63},"\"--headless\"",[53,165,160],{"class":110},[53,167,168],{"class":63},"\"--convert-to\"",[53,170,160],{"class":110},[53,172,173],{"class":63},"\"pdf\"",[53,175,160],{"class":110},[53,177,178],{"class":63},"\"report.xlsx\"",[53,180,181],{"class":110},"], ",[53,183,185],{"class":184},"sa561","check",[53,187,188],{"class":106},"=",[53,190,191],{"class":134},"True",[53,193,160],{"class":110},[53,195,196],{"class":184},"capture_output",[53,198,188],{"class":106},[53,200,191],{"class":134},[53,202,203],{"class":110},")\n",[53,205,207],{"class":55,"line":206},6,[53,208,148],{"emptyLinePlaceholder":147},[53,210,212,215,219,222,224,227,230,232,235,238,240,243],{"class":55,"line":211},7,[53,213,214],{"class":106},"def",[53,216,218],{"class":217},"s_Opv"," render_chart",[53,220,221],{"class":110},"(pdf_path, page",[53,223,188],{"class":106},[53,225,226],{"class":134},"0",[53,228,229],{"class":110},", scale",[53,231,188],{"class":106},[53,233,234],{"class":134},"2.5",[53,236,237],{"class":110},", margin",[53,239,188],{"class":106},[53,241,242],{"class":134},"10",[53,244,245],{"class":110},"):\n",[53,247,249,252,254],{"class":55,"line":248},8,[53,250,251],{"class":110},"    pdf ",[53,253,188],{"class":106},[53,255,256],{"class":110}," pdfium.PdfDocument(pdf_path)\n",[53,258,260,263,265,268,271,273,276,279],{"class":55,"line":259},9,[53,261,262],{"class":110},"    img ",[53,264,188],{"class":106},[53,266,267],{"class":110}," pdf[page].render(",[53,269,270],{"class":184},"scale",[53,272,188],{"class":106},[53,274,275],{"class":110},"scale).to_pil().convert(",[53,277,278],{"class":63},"\"RGB\"",[53,280,203],{"class":110},[53,282,284],{"class":55,"line":283},10,[53,285,286],{"class":110},"    pdf.close()\n",[53,288,290,293,295,298,300,303,306],{"class":55,"line":289},11,[53,291,292],{"class":110},"    bbox ",[53,294,188],{"class":106},[53,296,297],{"class":110}," ImageChops.difference(img, Image.new(",[53,299,278],{"class":63},[53,301,302],{"class":110},", img.size, ",[53,304,305],{"class":63},"\"white\"",[53,307,308],{"class":110},")).getbbox()\n",[53,310,312,315,317],{"class":55,"line":311},12,[53,313,314],{"class":110},"    left, top, right, bottom ",[53,316,188],{"class":106},[53,318,319],{"class":110}," bbox\n",[53,321,323,326,329,332,335,337,340,343,346,348],{"class":55,"line":322},13,[53,324,325],{"class":106},"    return",[53,327,328],{"class":110}," img.crop((left ",[53,330,331],{"class":106},"-",[53,333,334],{"class":110}," margin, top ",[53,336,331],{"class":106},[53,338,339],{"class":110}," margin, right ",[53,341,342],{"class":106},"+",[53,344,345],{"class":110}," margin, bottom ",[53,347,342],{"class":106},[53,349,350],{"class":110}," margin))\n",[53,352,354],{"class":55,"line":353},14,[53,355,148],{"emptyLinePlaceholder":147},[53,357,359,362,364,367,370],{"class":55,"line":358},15,[53,360,361],{"class":110},"chart ",[53,363,188],{"class":106},[53,365,366],{"class":110}," render_chart(",[53,368,369],{"class":63},"\"report.pdf\"",[53,371,203],{"class":110},[53,373,375,378,381,383,386,388,390],{"class":55,"line":374},16,[53,376,377],{"class":110},"chart.save(",[53,379,380],{"class":63},"\"chart.png\"",[53,382,160],{"class":110},[53,384,385],{"class":184},"optimize",[53,387,188],{"class":106},[53,389,191],{"class":134},[53,391,203],{"class":110},[53,393,395,398,401,403],{"class":55,"line":394},17,[53,396,397],{"class":134},"print",[53,399,400],{"class":110},"(",[53,402,380],{"class":63},[53,404,405],{"class":110},", chart.size)\n",[10,407,408,409,412,413,37],{},"LibreOffice prints the Chart sheet first because it is the first sheet in the workbook; the chart is the only thing on it, so cropping to the non-white area leaves exactly the chart. PDF pages are measured in points, 72 to the inch; ",[19,410,411],{},"scale=2.5"," renders at 180 dots per inch, so the chart — 640 pixels wide in the workbook, which is 480 points on the page — comes out about 1,200 pixels wide, twice the 600-pixel width it will be displayed at. That doubling is what keeps text crisp on high-density phone and laptop screens. If the chart sits on a sheet with a table, give it a sheet of its own for rendering, or convert just that sheet as shown in ",[28,414,416],{"href":415},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fconvert-only-selected-sheets-of-a-workbook-to-pdf\u002F","Convert Only Selected Sheets of a Workbook to PDF",[418,419,427,428,427,432,427,436,427,443,427,453,427,464,427,471,427,476,427,480,427,485,427,490,427,495,427,502,427,507,427,510,427,513,427,517,427,520,427,525,427,532,427,538,427,542,427,546,427,549,427,554,427,561,427,566],"svg",{"viewBox":420,"role":421,"ariaLabelledBy":422,"xmlns":425,"style":426},"4 0 752 142","img",[423,424],"em-render-t","em-render-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[429,430,431],"title",{"id":423},"From workbook chart to email image",[433,434,435],"desc",{"id":424},"LibreOffice converts the chart sheet to PDF, pypdfium2 renders the page at twice the display resolution, Pillow crops the white margins, and the PNG is attached inline and referenced by Content-ID from the HTML.",[437,438],"rect",{"x":439,"y":226,"width":440,"height":441,"fill":442},"4","752","142","#ffffff",[437,444],{"x":445,"y":446,"width":447,"height":448,"rx":449,"fill":450,"stroke":451,"style":452},"20.0","16.0","153.0","110.5","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[454,455,457],"g",{"transform":456},"translate(84.5,29.0) scale(1.000)",[458,459],"path",{"d":460,"fill":461,"stroke":462,"style":463},"M4 20h16 M7 17v-5 M12 17V7 M17 17v-8","none","var(--brand-strong,#4338ca)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[465,466,470],"text",{"x":467,"y":468,"style":469},"96.5","75.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Chart sheet",[465,472,475],{"x":467,"y":473,"style":474},"95.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","the workbook's own",[465,477,479],{"x":467,"y":478,"style":474},"112.5","chart",[55,481],{"x1":482,"y1":483,"x2":484,"y2":483,"stroke":451,"style":452},"178.0","71.2","200.0",[486,487],"polygon",{"points":488,"fill":489},"204.0,71.2 195.0,76.2 195.0,66.2","#5b5cf0",[437,491],{"x":492,"y":446,"width":447,"height":448,"rx":449,"fill":493,"stroke":494,"style":452},"209.0","#e7ebef","var(--line,#cdd5e6)",[454,496,498],{"transform":497},"translate(273.5,29.0) scale(1.000)",[458,499],{"d":500,"fill":461,"stroke":501,"style":463},"M6 3h9l4 4v14H6z M15 3v4h4 M9 12h6 M9 15.5h6 M9 19h4","var(--text,#172033)",[465,503,506],{"x":504,"y":468,"style":505},"285.5","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","PDF",[465,508,509],{"x":504,"y":473,"style":474},"soffice --convert-to",[465,511,512],{"x":504,"y":478,"style":474},"pdf",[55,514],{"x1":515,"y1":483,"x2":516,"y2":483,"stroke":451,"style":452},"367.0","389.0",[486,518],{"points":519,"fill":489},"393.0,71.2 384.0,76.2 384.0,66.2",[437,521],{"x":522,"y":446,"width":447,"height":448,"rx":449,"fill":523,"stroke":524,"style":452},"398.0","#d9f4f1","var(--teal,#0f9488)",[454,526,528],{"transform":527},"translate(462.5,37.8) scale(1.000)",[458,529],{"d":530,"fill":461,"stroke":531,"style":463},"M4 5h16v14H4z M4 16l5-5 4 4 3-3 4 4 M15 9h.01","var(--teal-ink,#0b6157)",[465,533,537],{"x":534,"y":535,"style":536},"474.5","84.2","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","PNG at 2x",[465,539,541],{"x":534,"y":540,"style":474},"103.8","pypdfium2 + crop",[55,543],{"x1":544,"y1":483,"x2":545,"y2":483,"stroke":451,"style":452},"556.0","578.0",[486,547],{"points":548,"fill":489},"582.0,71.2 573.0,76.2 573.0,66.2",[437,550],{"x":551,"y":446,"width":447,"height":448,"rx":449,"fill":552,"stroke":553,"style":452},"587.0","#fdefd8","var(--gold,#b4740a)",[454,555,557],{"transform":556},"translate(651.5,37.8) scale(1.000)",[458,558],{"d":559,"fill":461,"stroke":560,"style":463},"M3 6h18v12H3z M3 7l9 6 9-6","var(--gold-ink,#7a4e06)",[465,562,565],{"x":563,"y":535,"style":564},"663.5","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Inline part",[465,567,568],{"x":563,"y":540,"style":474},"cid: reference",[39,570,572],{"id":571},"step-2-or-draw-it-with-matplotlib","Step 2 — Or draw it with matplotlib",[10,574,575],{},"When there is no workbook chart — or you want a chart designed for the email, such as the quarter's trend, which the workbook does not chart — matplotlib produces one directly. Set the size in inches and the DPI so the pixel width is twice the display width:",[44,577,579],{"className":97,"code":578,"language":99,"meta":49,"style":49},"import matplotlib\nmatplotlib.use(\"Agg\")\nimport matplotlib.pyplot as plt\n\nmonths, revenue = [\"Jul\", \"Aug\", \"Sep\"], [1290, 1356, 1437]\nfig, ax = plt.subplots(figsize=(6, 3.2), dpi=200)          # 1200 x 640 px, shown at 600 x 320\nax.plot(months, revenue, color=\"#4338CA\", marker=\"o\", linewidth=2.5)\nax.set_title(\"Total revenue by month (k€)\")\nax.spines[[\"top\", \"right\"]].set_visible(False)\nfig.tight_layout()\nfig.savefig(\"chart_mpl.png\", facecolor=\"white\")\nplt.close(fig)\nwith Image.open(\"chart_mpl.png\") as im:\n    print(\"chart_mpl.png\", im.size)\n",[19,580,581,587,597,609,613,655,698,732,742,763,768,787,792,810],{"__ignoreMap":49},[53,582,583,585],{"class":55,"line":56},[53,584,107],{"class":106},[53,586,73],{"class":110},[53,588,589,592,595],{"class":55,"line":114},[53,590,591],{"class":110},"matplotlib.use(",[53,593,594],{"class":63},"\"Agg\"",[53,596,203],{"class":110},[53,598,599,601,604,606],{"class":55,"line":128},[53,600,107],{"class":106},[53,602,603],{"class":110}," matplotlib.pyplot ",[53,605,122],{"class":106},[53,607,608],{"class":110}," plt\n",[53,610,611],{"class":55,"line":144},[53,612,148],{"emptyLinePlaceholder":147},[53,614,615,618,620,623,626,628,631,633,636,639,642,644,647,649,652],{"class":55,"line":151},[53,616,617],{"class":110},"months, revenue ",[53,619,188],{"class":106},[53,621,622],{"class":110}," [",[53,624,625],{"class":63},"\"Jul\"",[53,627,160],{"class":110},[53,629,630],{"class":63},"\"Aug\"",[53,632,160],{"class":110},[53,634,635],{"class":63},"\"Sep\"",[53,637,638],{"class":110},"], [",[53,640,641],{"class":134},"1290",[53,643,160],{"class":110},[53,645,646],{"class":134},"1356",[53,648,160],{"class":110},[53,650,651],{"class":134},"1437",[53,653,654],{"class":110},"]\n",[53,656,657,660,662,665,668,670,672,675,677,680,683,686,688,691,694],{"class":55,"line":206},[53,658,659],{"class":110},"fig, ax ",[53,661,188],{"class":106},[53,663,664],{"class":110}," plt.subplots(",[53,666,667],{"class":184},"figsize",[53,669,188],{"class":106},[53,671,400],{"class":110},[53,673,674],{"class":134},"6",[53,676,160],{"class":110},[53,678,679],{"class":134},"3.2",[53,681,682],{"class":110},"), ",[53,684,685],{"class":184},"dpi",[53,687,188],{"class":106},[53,689,690],{"class":134},"200",[53,692,693],{"class":110},")          ",[53,695,697],{"class":696},"s-wDw","# 1200 x 640 px, shown at 600 x 320\n",[53,699,700,703,706,708,711,713,716,718,721,723,726,728,730],{"class":55,"line":211},[53,701,702],{"class":110},"ax.plot(months, revenue, ",[53,704,705],{"class":184},"color",[53,707,188],{"class":106},[53,709,710],{"class":63},"\"#4338CA\"",[53,712,160],{"class":110},[53,714,715],{"class":184},"marker",[53,717,188],{"class":106},[53,719,720],{"class":63},"\"o\"",[53,722,160],{"class":110},[53,724,725],{"class":184},"linewidth",[53,727,188],{"class":106},[53,729,234],{"class":134},[53,731,203],{"class":110},[53,733,734,737,740],{"class":55,"line":248},[53,735,736],{"class":110},"ax.set_title(",[53,738,739],{"class":63},"\"Total revenue by month (k€)\"",[53,741,203],{"class":110},[53,743,744,747,750,752,755,758,761],{"class":55,"line":259},[53,745,746],{"class":110},"ax.spines[[",[53,748,749],{"class":63},"\"top\"",[53,751,160],{"class":110},[53,753,754],{"class":63},"\"right\"",[53,756,757],{"class":110},"]].set_visible(",[53,759,760],{"class":134},"False",[53,762,203],{"class":110},[53,764,765],{"class":55,"line":283},[53,766,767],{"class":110},"fig.tight_layout()\n",[53,769,770,773,776,778,781,783,785],{"class":55,"line":289},[53,771,772],{"class":110},"fig.savefig(",[53,774,775],{"class":63},"\"chart_mpl.png\"",[53,777,160],{"class":110},[53,779,780],{"class":184},"facecolor",[53,782,188],{"class":106},[53,784,305],{"class":63},[53,786,203],{"class":110},[53,788,789],{"class":55,"line":311},[53,790,791],{"class":110},"plt.close(fig)\n",[53,793,794,797,800,802,805,807],{"class":55,"line":322},[53,795,796],{"class":106},"with",[53,798,799],{"class":110}," Image.open(",[53,801,775],{"class":63},[53,803,804],{"class":110},") ",[53,806,122],{"class":106},[53,808,809],{"class":110}," im:\n",[53,811,812,815,817,819],{"class":55,"line":353},[53,813,814],{"class":134},"    print",[53,816,400],{"class":110},[53,818,775],{"class":63},[53,820,821],{"class":110},", im.size)\n",[10,823,824,827,828,831,832,37],{},[19,825,826],{},"facecolor=\"white\""," matters: a transparent background looks fine in a light mail client but turns into dark text on a dark background in mail apps' dark mode, where the axis labels can vanish. A solid white background keeps the chart legible in both. ",[19,829,830],{},"matplotlib.use(\"Agg\")"," lets the script run on a server with no display. More on matplotlib output for reports is in ",[28,833,835],{"href":834},"\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",[39,837,839],{"id":838},"step-3-size-the-image-for-mail-clients","Step 3 — Size the image for mail clients",[10,841,842,843,846,847,850,851,853,854,857,858,861],{},"Mail clients do not agree on CSS, but nearly all honour the ",[19,844,845],{},"width"," and ",[19,848,849],{},"height"," attributes of an ",[19,852,421],{}," tag. Set them to the ",[14,855,856],{},"display"," size — half the pixel size of a 2x image — and add ",[19,859,860],{},"max-width:100%"," so the image shrinks on narrow screens:",[44,863,865],{"className":97,"code":864,"language":99,"meta":49,"style":49},"from pathlib import Path\n\ndef img_tag(path, src, alt, display_width=600):\n    with Image.open(path) as im:\n        w, h = im.size\n    display_height = round(h * display_width \u002F w)\n    return (f'\u003Cimg src=\"{src}\" alt=\"{alt}\" width=\"{display_width}\" height=\"{display_height}\" '\n            f'style=\"display:block;max-width:100%;height:auto;border:0\">')\n\nsize_kb = Path(\"chart.png\").stat().st_size \u002F 1024\nprint(img_tag(\"chart.png\", \"cid:revenue\", \"Revenue by region: North 410k, South 380k, East 295k, West 352k\"))\nprint(f\"chart.png is {size_kb:.0f} KB\")\n",[19,866,867,879,883,900,912,922,947,1003,1013,1017,1037,1059],{"__ignoreMap":49},[53,868,869,871,874,876],{"class":55,"line":56},[53,870,131],{"class":106},[53,872,873],{"class":110}," pathlib ",[53,875,107],{"class":106},[53,877,878],{"class":110}," Path\n",[53,880,881],{"class":55,"line":114},[53,882,148],{"emptyLinePlaceholder":147},[53,884,885,887,890,893,895,898],{"class":55,"line":128},[53,886,214],{"class":106},[53,888,889],{"class":217}," img_tag",[53,891,892],{"class":110},"(path, src, alt, display_width",[53,894,188],{"class":106},[53,896,897],{"class":134},"600",[53,899,245],{"class":110},[53,901,902,905,908,910],{"class":55,"line":144},[53,903,904],{"class":106},"    with",[53,906,907],{"class":110}," Image.open(path) ",[53,909,122],{"class":106},[53,911,809],{"class":110},[53,913,914,917,919],{"class":55,"line":151},[53,915,916],{"class":110},"        w, h ",[53,918,188],{"class":106},[53,920,921],{"class":110}," im.size\n",[53,923,924,927,929,932,935,938,941,944],{"class":55,"line":206},[53,925,926],{"class":110},"    display_height ",[53,928,188],{"class":106},[53,930,931],{"class":134}," round",[53,933,934],{"class":110},"(h ",[53,936,937],{"class":106},"*",[53,939,940],{"class":110}," display_width ",[53,942,943],{"class":106},"\u002F",[53,945,946],{"class":110}," w)\n",[53,948,949,951,954,957,960,964,967,970,973,975,978,980,983,985,988,990,993,995,998,1000],{"class":55,"line":211},[53,950,325],{"class":106},[53,952,953],{"class":110}," (",[53,955,956],{"class":106},"f",[53,958,959],{"class":63},"'\u003Cimg src=\"",[53,961,963],{"class":962},"sSjpA","{",[53,965,966],{"class":110},"src",[53,968,969],{"class":962},"}",[53,971,972],{"class":63},"\" alt=\"",[53,974,963],{"class":962},[53,976,977],{"class":110},"alt",[53,979,969],{"class":962},[53,981,982],{"class":63},"\" width=\"",[53,984,963],{"class":962},[53,986,987],{"class":110},"display_width",[53,989,969],{"class":962},[53,991,992],{"class":63},"\" height=\"",[53,994,963],{"class":962},[53,996,997],{"class":110},"display_height",[53,999,969],{"class":962},[53,1001,1002],{"class":63},"\" '\n",[53,1004,1005,1008,1011],{"class":55,"line":248},[53,1006,1007],{"class":106},"            f",[53,1009,1010],{"class":63},"'style=\"display:block;max-width:100%;height:auto;border:0\">'",[53,1012,203],{"class":110},[53,1014,1015],{"class":55,"line":259},[53,1016,148],{"emptyLinePlaceholder":147},[53,1018,1019,1022,1024,1027,1029,1032,1034],{"class":55,"line":283},[53,1020,1021],{"class":110},"size_kb ",[53,1023,188],{"class":106},[53,1025,1026],{"class":110}," Path(",[53,1028,380],{"class":63},[53,1030,1031],{"class":110},").stat().st_size ",[53,1033,943],{"class":106},[53,1035,1036],{"class":134}," 1024\n",[53,1038,1039,1041,1044,1046,1048,1051,1053,1056],{"class":55,"line":289},[53,1040,397],{"class":134},[53,1042,1043],{"class":110},"(img_tag(",[53,1045,380],{"class":63},[53,1047,160],{"class":110},[53,1049,1050],{"class":63},"\"cid:revenue\"",[53,1052,160],{"class":110},[53,1054,1055],{"class":63},"\"Revenue by region: North 410k, South 380k, East 295k, West 352k\"",[53,1057,1058],{"class":110},"))\n",[53,1060,1061,1063,1065,1067,1070,1072,1075,1078,1080,1083],{"class":55,"line":311},[53,1062,397],{"class":134},[53,1064,400],{"class":110},[53,1066,956],{"class":106},[53,1068,1069],{"class":63},"\"chart.png is ",[53,1071,963],{"class":962},[53,1073,1074],{"class":110},"size_kb",[53,1076,1077],{"class":106},":.0f",[53,1079,969],{"class":962},[53,1081,1082],{"class":63}," KB\"",[53,1084,203],{"class":110},[10,1086,1087,1088,1090],{},"A 600-pixel display width matches the width of most email layouts. The ",[19,1089,977],{}," text matters more than usual here: clients that block images until the reader allows them show the alt text in its place, so put the conclusion in it — the actual figures — rather than \"chart\". Keep each image well under 200 KB; flat charts compress very well as PNG (the rendered chart here is a few tens of kilobytes), and a heavy body is slow to load on mobile data.",[418,1092,427,1097,427,1100,427,1103,427,1106,427,1111,427,1118,427,1124,427,1130,427,1136,427,1142,427,1146,427,1151,427,1155,427,1161,427,1164,427,1170,427,1175,427,1179,427,1183,427,1190,427,1193,427,1198,427,1201,427,1206,427,1211,427,1217,427,1222,427,1226,427,1230,427,1235,427,1238,427,1243,427,1246],{"viewBox":1093,"role":421,"ariaLabelledBy":1094,"xmlns":425,"style":426},"4 0 752 207",[1095,1096],"em-ways-t","em-ways-d",[429,1098,1099],{"id":1095},"Three ways to put an image in an email",[433,1101,1102],{"id":1096},"An inline part referenced by Content-ID is shown by almost every client without a download prompt. A remote image URL is blocked by default in many clients and lets the sender track opens. A data URI inside the HTML is ignored or stripped by several major clients.",[437,1104],{"x":439,"y":226,"width":440,"height":1105,"fill":442},"207",[437,1107],{"x":445,"y":446,"width":1108,"height":1109,"rx":1110,"fill":523,"stroke":524,"style":452},"228.0","175.0","14",[454,1112,1114],{"transform":1113},"translate(121.0,29.0) scale(1.083)",[458,1115],{"d":1116,"fill":461,"stroke":531,"style":1117},"M5 12.5l4.5 4.5L19 7","stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[465,1119,1123],{"x":1120,"y":1121,"style":1122},"134.0","78.0","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","inline part, cid:",[55,1125],{"x1":1126,"y1":1127,"x2":1128,"y2":1127,"stroke":524,"style":1129},"36.0","91.0","232.0","stroke-width:1px",[465,1131,1135],{"x":1132,"y":1133,"style":1134},"60.0","111.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:start","shown by nearly all clients",[454,1137,1139],{"transform":1138},"translate(38.5,99.5) scale(0.625)",[458,1140],{"d":1116,"fill":461,"stroke":524,"style":1141},"stroke-width:2.88px;stroke-linecap:round;stroke-linejoin:round",[465,1143,1145],{"x":1132,"y":1144,"style":1134},"137.0","no external request",[454,1147,1149],{"transform":1148},"translate(38.5,125.5) scale(0.625)",[458,1150],{"d":1116,"fill":461,"stroke":524,"style":1141},[465,1152,1154],{"x":1132,"y":1153,"style":1134},"163.0","adds to message size",[454,1156,1158],{"transform":1157},"translate(38.5,151.5) scale(0.625)",[458,1159],{"d":1160,"fill":461,"stroke":553,"style":1141},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4",[437,1162],{"x":1163,"y":446,"width":1108,"height":1109,"rx":1110,"fill":552,"stroke":553,"style":452},"266.0",[454,1165,1167],{"transform":1166},"translate(367.0,29.0) scale(1.083)",[458,1168],{"d":1169,"fill":461,"stroke":560,"style":1117},"M12 3a9 9 0 1 0 .01 0z M3 12h18 M12 3c3.2 3 3.2 15 0 18 M12 3c-3.2 3-3.2 15 0 18",[465,1171,1174],{"x":1172,"y":1121,"style":1173},"380.0","font-size:14px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","remote URL",[55,1176],{"x1":1177,"y1":1127,"x2":1178,"y2":1127,"stroke":553,"style":1129},"282.0","478.0",[465,1180,1182],{"x":1181,"y":1133,"style":1134},"306.0","blocked until allowed",[454,1184,1186],{"transform":1185},"translate(284.5,99.5) scale(0.625)",[458,1187],{"d":1188,"fill":461,"stroke":1189,"style":1141},"M6 6l12 12 M18 6L6 18","var(--accent,#f43f8f)",[465,1191,1192],{"x":1181,"y":1144,"style":1134},"needs a public host",[454,1194,1196],{"transform":1195},"translate(284.5,125.5) scale(0.625)",[458,1197],{"d":1188,"fill":461,"stroke":1189,"style":1141},[465,1199,1200],{"x":1181,"y":1153,"style":1134},"small message",[454,1202,1204],{"transform":1203},"translate(284.5,151.5) scale(0.625)",[458,1205],{"d":1116,"fill":461,"stroke":524,"style":1141},[437,1207],{"x":1208,"y":446,"width":1108,"height":1109,"rx":1110,"fill":1209,"stroke":1210,"style":452},"512.0","#fce9e9","var(--danger,#dc2626)",[454,1212,1214],{"transform":1213},"translate(613.0,29.0) scale(1.083)",[458,1215],{"d":1188,"fill":461,"stroke":1216,"style":1117},"var(--accent-ink,#be185d)",[465,1218,1221],{"x":1219,"y":1121,"style":1220},"626.0","font-size:14px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","data: URI",[55,1223],{"x1":1224,"y1":1127,"x2":1225,"y2":1127,"stroke":1210,"style":1129},"528.0","724.0",[465,1227,1229],{"x":1228,"y":1133,"style":1134},"552.0","ignored by several clients",[454,1231,1233],{"transform":1232},"translate(530.5,99.5) scale(0.625)",[458,1234],{"d":1188,"fill":461,"stroke":1189,"style":1141},[465,1236,1237],{"x":1228,"y":1144,"style":1134},"bloats the HTML part",[454,1239,1241],{"transform":1240},"translate(530.5,125.5) scale(0.625)",[458,1242],{"d":1188,"fill":461,"stroke":1189,"style":1141},[465,1244,1245],{"x":1228,"y":1153,"style":1134},"may be flagged as spam",[454,1247,1249],{"transform":1248},"translate(530.5,151.5) scale(0.625)",[458,1250],{"d":1188,"fill":461,"stroke":1189,"style":1141},[39,1252,1254],{"id":1253},"step-4-embed-several-images-by-content-id","Step 4 — Embed several images by Content-ID",[10,1256,1257,1258,1261],{},"A report body often has two or three charts. A small helper takes HTML containing ",[19,1259,1260],{},"{cid:name}"," placeholders and a dictionary of image files, generates a unique Content-ID for each, and attaches each image to the HTML part:",[44,1263,1265],{"className":97,"code":1264,"language":99,"meta":49,"style":49},"from email.message import EmailMessage\nfrom email.utils import make_msgid\n\ndef build_email(subject, text, html_template, images, attachments=()):\n    msg = EmailMessage()\n    msg[\"Subject\"] = subject\n    msg[\"From\"] = \"reports@example.com\"\n    msg[\"To\"] = \"team@example.com\"\n    msg.set_content(text)\n\n    cids = {name: make_msgid(domain=\"reports.example.com\") for name in images}\n    html = html_template\n    for name, cid in cids.items():\n        html = html.replace(f\"{{cid:{name}}}\", \"cid:\" + cid[1:-1])\n    msg.add_alternative(html, subtype=\"html\")\n    html_part = msg.get_payload()[1]\n    for name, path in images.items():\n        html_part.add_related(Path(path).read_bytes(), maintype=\"image\", subtype=\"png\",\n                              cid=cids[name], filename=Path(path).name, disposition=\"inline\")\n    for path in attachments:\n        msg.add_attachment(Path(path).read_bytes(), maintype=\"application\",\n                           subtype=\"vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n                           filename=Path(path).name)\n    return msg\n\nhtml = f\"\"\"\u003Cp>September revenue was \u003Cb>1,437k€\u003C\u002Fb>, up 6% on August.\u003C\u002Fp>\n{img_tag(\"chart.png\", \"{cid:revenue}\", \"Revenue by region: North 410k, South 380k, East 295k, West 352k\")}\n\u003Cp>Trend over the quarter:\u003C\u002Fp>\n{img_tag(\"chart_mpl.png\", \"{cid:trend}\", \"Total revenue: July 1,290k, August 1,356k, September 1,437k\")}\"\"\"\n\nmsg = build_email(\"Revenue report — September 2026\",\n                  \"September revenue was 1,437k€, up 6% on August. Charts are in the HTML version; the workbook is attached.\",\n                  html, {\"revenue\": \"chart.png\", \"trend\": \"chart_mpl.png\"}, attachments=[\"report.xlsx\"])\nPath(\"report_email.eml\").write_bytes(msg.as_bytes())\nprint([p.get_content_type() for p in msg.walk()])\n",[19,1266,1267,1279,1291,1295,1310,1320,1336,1350,1364,1369,1373,1405,1415,1428,1483,1498,1512,1524,1550,1579,1592,1607,1620,1631,1639,1644,1658,1692,1698,1732,1737,1753,1767,1805,1817],{"__ignoreMap":49},[53,1268,1269,1271,1274,1276],{"class":55,"line":56},[53,1270,131],{"class":106},[53,1272,1273],{"class":110}," email.message ",[53,1275,107],{"class":106},[53,1277,1278],{"class":110}," EmailMessage\n",[53,1280,1281,1283,1286,1288],{"class":55,"line":114},[53,1282,131],{"class":106},[53,1284,1285],{"class":110}," email.utils ",[53,1287,107],{"class":106},[53,1289,1290],{"class":110}," make_msgid\n",[53,1292,1293],{"class":55,"line":128},[53,1294,148],{"emptyLinePlaceholder":147},[53,1296,1297,1299,1302,1305,1307],{"class":55,"line":144},[53,1298,214],{"class":106},[53,1300,1301],{"class":217}," build_email",[53,1303,1304],{"class":110},"(subject, text, html_template, images, attachments",[53,1306,188],{"class":106},[53,1308,1309],{"class":110},"()):\n",[53,1311,1312,1315,1317],{"class":55,"line":151},[53,1313,1314],{"class":110},"    msg ",[53,1316,188],{"class":106},[53,1318,1319],{"class":110}," EmailMessage()\n",[53,1321,1322,1325,1328,1331,1333],{"class":55,"line":206},[53,1323,1324],{"class":110},"    msg[",[53,1326,1327],{"class":63},"\"Subject\"",[53,1329,1330],{"class":110},"] ",[53,1332,188],{"class":106},[53,1334,1335],{"class":110}," subject\n",[53,1337,1338,1340,1343,1345,1347],{"class":55,"line":211},[53,1339,1324],{"class":110},[53,1341,1342],{"class":63},"\"From\"",[53,1344,1330],{"class":110},[53,1346,188],{"class":106},[53,1348,1349],{"class":63}," \"reports@example.com\"\n",[53,1351,1352,1354,1357,1359,1361],{"class":55,"line":248},[53,1353,1324],{"class":110},[53,1355,1356],{"class":63},"\"To\"",[53,1358,1330],{"class":110},[53,1360,188],{"class":106},[53,1362,1363],{"class":63}," \"team@example.com\"\n",[53,1365,1366],{"class":55,"line":259},[53,1367,1368],{"class":110},"    msg.set_content(text)\n",[53,1370,1371],{"class":55,"line":283},[53,1372,148],{"emptyLinePlaceholder":147},[53,1374,1375,1378,1380,1383,1386,1388,1391,1393,1396,1399,1402],{"class":55,"line":289},[53,1376,1377],{"class":110},"    cids ",[53,1379,188],{"class":106},[53,1381,1382],{"class":110}," {name: make_msgid(",[53,1384,1385],{"class":184},"domain",[53,1387,188],{"class":106},[53,1389,1390],{"class":63},"\"reports.example.com\"",[53,1392,804],{"class":110},[53,1394,1395],{"class":106},"for",[53,1397,1398],{"class":110}," name ",[53,1400,1401],{"class":106},"in",[53,1403,1404],{"class":110}," images}\n",[53,1406,1407,1410,1412],{"class":55,"line":311},[53,1408,1409],{"class":110},"    html ",[53,1411,188],{"class":106},[53,1413,1414],{"class":110}," html_template\n",[53,1416,1417,1420,1423,1425],{"class":55,"line":322},[53,1418,1419],{"class":106},"    for",[53,1421,1422],{"class":110}," name, cid ",[53,1424,1401],{"class":106},[53,1426,1427],{"class":110}," cids.items():\n",[53,1429,1430,1433,1435,1438,1440,1443,1446,1449,1451,1454,1457,1459,1461,1464,1467,1470,1473,1476,1478,1480],{"class":55,"line":353},[53,1431,1432],{"class":110},"        html ",[53,1434,188],{"class":106},[53,1436,1437],{"class":110}," html.replace(",[53,1439,956],{"class":106},[53,1441,1442],{"class":63},"\"",[53,1444,1445],{"class":962},"{{",[53,1447,1448],{"class":63},"cid:",[53,1450,963],{"class":962},[53,1452,1453],{"class":110},"name",[53,1455,1456],{"class":962},"}}}",[53,1458,1442],{"class":63},[53,1460,160],{"class":110},[53,1462,1463],{"class":63},"\"cid:\"",[53,1465,1466],{"class":106}," +",[53,1468,1469],{"class":110}," cid[",[53,1471,1472],{"class":134},"1",[53,1474,1475],{"class":110},":",[53,1477,331],{"class":106},[53,1479,1472],{"class":134},[53,1481,1482],{"class":110},"])\n",[53,1484,1485,1488,1491,1493,1496],{"class":55,"line":358},[53,1486,1487],{"class":110},"    msg.add_alternative(html, ",[53,1489,1490],{"class":184},"subtype",[53,1492,188],{"class":106},[53,1494,1495],{"class":63},"\"html\"",[53,1497,203],{"class":110},[53,1499,1500,1503,1505,1508,1510],{"class":55,"line":374},[53,1501,1502],{"class":110},"    html_part ",[53,1504,188],{"class":106},[53,1506,1507],{"class":110}," msg.get_payload()[",[53,1509,1472],{"class":134},[53,1511,654],{"class":110},[53,1513,1514,1516,1519,1521],{"class":55,"line":394},[53,1515,1419],{"class":106},[53,1517,1518],{"class":110}," name, path ",[53,1520,1401],{"class":106},[53,1522,1523],{"class":110}," images.items():\n",[53,1525,1527,1530,1533,1535,1538,1540,1542,1544,1547],{"class":55,"line":1526},18,[53,1528,1529],{"class":110},"        html_part.add_related(Path(path).read_bytes(), ",[53,1531,1532],{"class":184},"maintype",[53,1534,188],{"class":106},[53,1536,1537],{"class":63},"\"image\"",[53,1539,160],{"class":110},[53,1541,1490],{"class":184},[53,1543,188],{"class":106},[53,1545,1546],{"class":63},"\"png\"",[53,1548,1549],{"class":110},",\n",[53,1551,1553,1556,1558,1561,1564,1566,1569,1572,1574,1577],{"class":55,"line":1552},19,[53,1554,1555],{"class":184},"                              cid",[53,1557,188],{"class":106},[53,1559,1560],{"class":110},"cids[name], ",[53,1562,1563],{"class":184},"filename",[53,1565,188],{"class":106},[53,1567,1568],{"class":110},"Path(path).name, ",[53,1570,1571],{"class":184},"disposition",[53,1573,188],{"class":106},[53,1575,1576],{"class":63},"\"inline\"",[53,1578,203],{"class":110},[53,1580,1582,1584,1587,1589],{"class":55,"line":1581},20,[53,1583,1419],{"class":106},[53,1585,1586],{"class":110}," path ",[53,1588,1401],{"class":106},[53,1590,1591],{"class":110}," attachments:\n",[53,1593,1595,1598,1600,1602,1605],{"class":55,"line":1594},21,[53,1596,1597],{"class":110},"        msg.add_attachment(Path(path).read_bytes(), ",[53,1599,1532],{"class":184},[53,1601,188],{"class":106},[53,1603,1604],{"class":63},"\"application\"",[53,1606,1549],{"class":110},[53,1608,1610,1613,1615,1618],{"class":55,"line":1609},22,[53,1611,1612],{"class":184},"                           subtype",[53,1614,188],{"class":106},[53,1616,1617],{"class":63},"\"vnd.openxmlformats-officedocument.spreadsheetml.sheet\"",[53,1619,1549],{"class":110},[53,1621,1623,1626,1628],{"class":55,"line":1622},23,[53,1624,1625],{"class":184},"                           filename",[53,1627,188],{"class":106},[53,1629,1630],{"class":110},"Path(path).name)\n",[53,1632,1634,1636],{"class":55,"line":1633},24,[53,1635,325],{"class":106},[53,1637,1638],{"class":110}," msg\n",[53,1640,1642],{"class":55,"line":1641},25,[53,1643,148],{"emptyLinePlaceholder":147},[53,1645,1647,1650,1652,1655],{"class":55,"line":1646},26,[53,1648,1649],{"class":110},"html ",[53,1651,188],{"class":106},[53,1653,1654],{"class":106}," f",[53,1656,1657],{"class":63},"\"\"\"\u003Cp>September revenue was \u003Cb>1,437k€\u003C\u002Fb>, up 6% on August.\u003C\u002Fp>\n",[53,1659,1661,1663,1666,1668,1670,1672,1675,1677,1680,1682,1684,1686,1689],{"class":55,"line":1660},27,[53,1662,963],{"class":962},[53,1664,1665],{"class":110},"img_tag(",[53,1667,380],{"class":63},[53,1669,160],{"class":110},[53,1671,1442],{"class":63},[53,1673,1674],{"class":962},"{cid",[53,1676,1475],{"class":106},[53,1678,1679],{"class":962},"revenue}",[53,1681,1442],{"class":63},[53,1683,160],{"class":110},[53,1685,1055],{"class":63},[53,1687,1688],{"class":110},")",[53,1690,1691],{"class":962},"}\n",[53,1693,1695],{"class":55,"line":1694},28,[53,1696,1697],{"class":63},"\u003Cp>Trend over the quarter:\u003C\u002Fp>\n",[53,1699,1701,1703,1705,1707,1709,1711,1713,1715,1718,1720,1722,1725,1727,1729],{"class":55,"line":1700},29,[53,1702,963],{"class":962},[53,1704,1665],{"class":110},[53,1706,775],{"class":63},[53,1708,160],{"class":110},[53,1710,1442],{"class":63},[53,1712,1674],{"class":962},[53,1714,1475],{"class":106},[53,1716,1717],{"class":962},"trend}",[53,1719,1442],{"class":63},[53,1721,160],{"class":110},[53,1723,1724],{"class":63},"\"Total revenue: July 1,290k, August 1,356k, September 1,437k\"",[53,1726,1688],{"class":110},[53,1728,969],{"class":962},[53,1730,1731],{"class":63},"\"\"\"\n",[53,1733,1735],{"class":55,"line":1734},30,[53,1736,148],{"emptyLinePlaceholder":147},[53,1738,1740,1743,1745,1748,1751],{"class":55,"line":1739},31,[53,1741,1742],{"class":110},"msg ",[53,1744,188],{"class":106},[53,1746,1747],{"class":110}," build_email(",[53,1749,1750],{"class":63},"\"Revenue report — September 2026\"",[53,1752,1549],{"class":110},[53,1754,1756,1759,1762,1765],{"class":55,"line":1755},32,[53,1757,1758],{"class":63},"                  \"September revenue was 1,437k€, up 6",[53,1760,1761],{"class":962},"% o",[53,1763,1764],{"class":63},"n August. Charts are in the HTML version; the workbook is attached.\"",[53,1766,1549],{"class":110},[53,1768,1770,1773,1776,1779,1781,1783,1786,1788,1790,1793,1796,1798,1801,1803],{"class":55,"line":1769},33,[53,1771,1772],{"class":110},"                  html, {",[53,1774,1775],{"class":63},"\"revenue\"",[53,1777,1778],{"class":110},": ",[53,1780,380],{"class":63},[53,1782,160],{"class":110},[53,1784,1785],{"class":63},"\"trend\"",[53,1787,1778],{"class":110},[53,1789,775],{"class":63},[53,1791,1792],{"class":110},"}, ",[53,1794,1795],{"class":184},"attachments",[53,1797,188],{"class":106},[53,1799,1800],{"class":110},"[",[53,1802,178],{"class":63},[53,1804,1482],{"class":110},[53,1806,1808,1811,1814],{"class":55,"line":1807},34,[53,1809,1810],{"class":110},"Path(",[53,1812,1813],{"class":63},"\"report_email.eml\"",[53,1815,1816],{"class":110},").write_bytes(msg.as_bytes())\n",[53,1818,1820,1822,1825,1827,1830,1832],{"class":55,"line":1819},35,[53,1821,397],{"class":134},[53,1823,1824],{"class":110},"([p.get_content_type() ",[53,1826,1395],{"class":106},[53,1828,1829],{"class":110}," p ",[53,1831,1401],{"class":106},[53,1833,1834],{"class":110}," msg.walk()])\n",[10,1836,1837,1840,1841,1844,1845,1847,1848,1851,1852,1855,1856,1859,1860,1862,1863,1866],{},[19,1838,1839],{},"make_msgid()"," returns an identifier in angle brackets for the ",[19,1842,1843],{},"Content-ID"," header; the ",[19,1846,966],{}," attribute uses the same identifier without them, as ",[19,1849,1850],{},"cid:...",". Each image is attached to the HTML part with ",[19,1853,1854],{},"add_related",", which turns that part into a ",[19,1857,1858],{},"multipart\u002Frelated"," container — the place where ",[19,1861,1448],{}," references are looked up. The workbook is added to the message itself, as an ordinary attachment. Saving the message as an ",[19,1864,1865],{},".eml"," file lets you open it in a desktop mail client to see it exactly as recipients will before sending anything.",[39,1868,1870],{"id":1869},"step-5-test-that-every-image-reference-resolves","Step 5 — Test that every image reference resolves",[10,1872,1873,1874,1876],{},"The classic failure is an image that shows as a broken icon because its ",[19,1875,1448],{}," does not match any part. Parse the finished message and compare:",[44,1878,1880],{"className":97,"code":1879,"language":99,"meta":49,"style":49},"import re\nfrom email import policy\nfrom email.parser import BytesParser\n\nparsed = BytesParser(policy=policy.default).parsebytes(Path(\"report_email.eml\").read_bytes())\nhtml_body = parsed.get_body(preferencelist=(\"html\",)).get_content()\nreferenced = set(re.findall(r'src=\"cid:([^\"]+)\"', html_body))\navailable = {p[\"Content-ID\"].strip(\"\u003C>\") for p in parsed.walk() if p[\"Content-ID\"]}\n\nprint(\"referenced:\", len(referenced), \"| available:\", len(available))\nprint(\"unresolved:\", referenced - available or \"none\", \"| unused parts:\", available - referenced or \"none\")\nprint(\"message size:\", round(len(Path(\"report_email.eml\").read_bytes()) \u002F 1024), \"KB\")\n",[19,1881,1882,1889,1901,1913,1917,1940,1962,2002,2043,2047,2074,2116],{"__ignoreMap":49},[53,1883,1884,1886],{"class":55,"line":56},[53,1885,107],{"class":106},[53,1887,1888],{"class":110}," re\n",[53,1890,1891,1893,1896,1898],{"class":55,"line":114},[53,1892,131],{"class":106},[53,1894,1895],{"class":110}," email ",[53,1897,107],{"class":106},[53,1899,1900],{"class":110}," policy\n",[53,1902,1903,1905,1908,1910],{"class":55,"line":128},[53,1904,131],{"class":106},[53,1906,1907],{"class":110}," email.parser ",[53,1909,107],{"class":106},[53,1911,1912],{"class":110}," BytesParser\n",[53,1914,1915],{"class":55,"line":144},[53,1916,148],{"emptyLinePlaceholder":147},[53,1918,1919,1922,1924,1927,1930,1932,1935,1937],{"class":55,"line":151},[53,1920,1921],{"class":110},"parsed ",[53,1923,188],{"class":106},[53,1925,1926],{"class":110}," BytesParser(",[53,1928,1929],{"class":184},"policy",[53,1931,188],{"class":106},[53,1933,1934],{"class":110},"policy.default).parsebytes(Path(",[53,1936,1813],{"class":63},[53,1938,1939],{"class":110},").read_bytes())\n",[53,1941,1942,1945,1947,1950,1953,1955,1957,1959],{"class":55,"line":206},[53,1943,1944],{"class":110},"html_body ",[53,1946,188],{"class":106},[53,1948,1949],{"class":110}," parsed.get_body(",[53,1951,1952],{"class":184},"preferencelist",[53,1954,188],{"class":106},[53,1956,400],{"class":110},[53,1958,1495],{"class":63},[53,1960,1961],{"class":110},",)).get_content()\n",[53,1963,1964,1967,1969,1972,1975,1978,1981,1984,1987,1989,1992,1994,1996,1999],{"class":55,"line":211},[53,1965,1966],{"class":110},"referenced ",[53,1968,188],{"class":106},[53,1970,1971],{"class":134}," set",[53,1973,1974],{"class":110},"(re.findall(",[53,1976,1977],{"class":106},"r",[53,1979,1980],{"class":63},"'src=\"cid:",[53,1982,1983],{"class":134},"([",[53,1985,1986],{"class":106},"^",[53,1988,1442],{"class":962},[53,1990,1991],{"class":134},"]",[53,1993,342],{"class":106},[53,1995,1688],{"class":134},[53,1997,1998],{"class":63},"\"'",[53,2000,2001],{"class":110},", html_body))\n",[53,2003,2004,2007,2009,2012,2015,2018,2021,2023,2025,2027,2029,2032,2035,2038,2040],{"class":55,"line":248},[53,2005,2006],{"class":110},"available ",[53,2008,188],{"class":106},[53,2010,2011],{"class":110}," {p[",[53,2013,2014],{"class":63},"\"Content-ID\"",[53,2016,2017],{"class":110},"].strip(",[53,2019,2020],{"class":63},"\"\u003C>\"",[53,2022,804],{"class":110},[53,2024,1395],{"class":106},[53,2026,1829],{"class":110},[53,2028,1401],{"class":106},[53,2030,2031],{"class":110}," parsed.walk() ",[53,2033,2034],{"class":106},"if",[53,2036,2037],{"class":110}," p[",[53,2039,2014],{"class":63},[53,2041,2042],{"class":110},"]}\n",[53,2044,2045],{"class":55,"line":259},[53,2046,148],{"emptyLinePlaceholder":147},[53,2048,2049,2051,2053,2056,2058,2061,2064,2067,2069,2071],{"class":55,"line":283},[53,2050,397],{"class":134},[53,2052,400],{"class":110},[53,2054,2055],{"class":63},"\"referenced:\"",[53,2057,160],{"class":110},[53,2059,2060],{"class":134},"len",[53,2062,2063],{"class":110},"(referenced), ",[53,2065,2066],{"class":63},"\"| available:\"",[53,2068,160],{"class":110},[53,2070,2060],{"class":134},[53,2072,2073],{"class":110},"(available))\n",[53,2075,2076,2078,2080,2083,2086,2088,2091,2094,2097,2099,2102,2105,2107,2110,2112,2114],{"class":55,"line":289},[53,2077,397],{"class":134},[53,2079,400],{"class":110},[53,2081,2082],{"class":63},"\"unresolved:\"",[53,2084,2085],{"class":110},", referenced ",[53,2087,331],{"class":106},[53,2089,2090],{"class":110}," available ",[53,2092,2093],{"class":106},"or",[53,2095,2096],{"class":63}," \"none\"",[53,2098,160],{"class":110},[53,2100,2101],{"class":63},"\"| unused parts:\"",[53,2103,2104],{"class":110},", available ",[53,2106,331],{"class":106},[53,2108,2109],{"class":110}," referenced ",[53,2111,2093],{"class":106},[53,2113,2096],{"class":63},[53,2115,203],{"class":110},[53,2117,2118,2120,2122,2125,2127,2130,2132,2134,2137,2139,2142,2144,2147,2149,2152],{"class":55,"line":311},[53,2119,397],{"class":134},[53,2121,400],{"class":110},[53,2123,2124],{"class":63},"\"message size:\"",[53,2126,160],{"class":110},[53,2128,2129],{"class":134},"round",[53,2131,400],{"class":110},[53,2133,2060],{"class":134},[53,2135,2136],{"class":110},"(Path(",[53,2138,1813],{"class":63},[53,2140,2141],{"class":110},").read_bytes()) ",[53,2143,943],{"class":106},[53,2145,2146],{"class":134}," 1024",[53,2148,682],{"class":110},[53,2150,2151],{"class":63},"\"KB\"",[53,2153,203],{"class":110},[10,2155,2156,2157,37],{},"Both images are referenced and both are present, with no unused image parts — an unused inline part shows up in some clients as a stray attachment. The size line is the one to watch as charts are added: base64 encoding inflates every image by about a third, and messages over a few megabytes start to be rejected. Run this check in the tests of the report job; for an end-to-end check that also exercises sending, use ",[28,2158,2160],{"href":2159},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Ftest-report-emails-with-a-local-smtp-server\u002F","Test Report Emails with a Local SMTP Server",[418,2162,427,2168,427,2171,427,2174,427,2179,427,2184,427,2188,427,2192,427,2196,427,2199,427,2201,427,2205,427,2209,427,2212,427,2215,427,2218,427,2221,427,2225,427,2229,427,2232,427,2235,427,2239],{"viewBox":2163,"role":421,"ariaLabelledBy":2164,"xmlns":425,"style":2167},"84 0 592 376",[2165,2166],"em-structure-t","em-structure-d","width:100%;max-width:592px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[429,2169,2170],{"id":2165},"Where the chart lives inside the message",[433,2172,2173],{"id":2166},"The message is multipart mixed. Inside it, a multipart alternative holds the plain text and a multipart related part, which contains the HTML and the two inline PNG images. The workbook is a separate attachment at the top level.",[437,2175],{"x":2176,"y":226,"width":2177,"height":2178,"fill":442},"84","592","376",[437,2180],{"x":2181,"y":446,"width":2182,"height":2183,"rx":449,"fill":493,"stroke":494,"style":452},"100.0","560.0","59.0",[465,2185,2187],{"x":1172,"y":2186,"style":505},"41.5","multipart\u002Fmixed",[465,2189,2191],{"x":1172,"y":2190,"style":474},"61.0","the whole message",[55,2193],{"x1":1172,"y1":2194,"x2":1172,"y2":2195,"stroke":451,"style":452},"80.0","102.0",[486,2197],{"points":2198,"fill":489},"380.0,106.0 375.0,97.0 385.0,97.0",[437,2200],{"x":2181,"y":1133,"width":2182,"height":2183,"rx":449,"fill":450,"stroke":451,"style":452},[465,2202,2204],{"x":1172,"y":2203,"style":469},"136.5","multipart\u002Falternative",[465,2206,2208],{"x":1172,"y":2207,"style":474},"156.0","text\u002Fplain · or · HTML",[55,2210],{"x1":1172,"y1":1109,"x2":1172,"y2":2211,"stroke":451,"style":452},"197.0",[486,2213],{"points":2214,"fill":489},"380.0,201.0 375.0,192.0 385.0,192.0",[437,2216],{"x":2181,"y":2217,"width":2182,"height":2183,"rx":449,"fill":523,"stroke":524,"style":452},"206.0",[465,2219,1858],{"x":1172,"y":2220,"style":536},"231.5",[465,2222,2224],{"x":1172,"y":2223,"style":474},"251.0","text\u002Fhtml + image\u002Fpng × 2 (cid)",[55,2226],{"x1":1172,"y1":2227,"x2":1172,"y2":2228,"stroke":451,"style":452},"270.0","292.0",[486,2230],{"points":2231,"fill":489},"380.0,296.0 375.0,287.0 385.0,287.0",[437,2233],{"x":2181,"y":2234,"width":2182,"height":2183,"rx":449,"fill":552,"stroke":553,"style":452},"301.0",[465,2236,2238],{"x":1172,"y":2237,"style":564},"326.5","attachment",[465,2240,78],{"x":1172,"y":2241,"style":474},"346.0",[39,2243,2245],{"id":2244},"common-pitfalls","Common pitfalls",[2247,2248,2249,2257,2263,2272,2281],"ul",{},[2250,2251,2252,2256],"li",{},[2253,2254,2255],"strong",{},"Images at 1x."," They look blurry on phones; render at twice the display width.",[2250,2258,2259,2262],{},[2253,2260,2261],{},"Transparent backgrounds."," Dark mode turns them unreadable; use white.",[2250,2264,2265,2271],{},[2253,2266,2267,2268,2270],{},"No ",[19,2269,845],{}," attribute."," Some clients show a 1200-pixel image at full size.",[2250,2273,2274,2277,2278,2280],{},[2253,2275,2276],{},"Mismatched Content-IDs."," Test that every ",[19,2279,1448],{}," resolves.",[2250,2282,2283,2286],{},[2253,2284,2285],{},"Generic alt text."," Put the key figures in it for clients that block images.",[39,2288,2290],{"id":2289},"conclusion","Conclusion",[10,2292,2293,2294,2296,2297,2299],{},"To show a report chart in the email body, render it to a PNG at twice its display size — from the workbook's own chart via LibreOffice, pypdfium2 and a crop, or drawn with matplotlib on a white background — then embed it as an inline part related to the HTML and reference it with ",[19,2295,1448],{},". Give the ",[19,2298,421],{}," tag width, height and meaningful alt text, keep images small, and parse the finished message in a test to confirm every reference resolves.",[39,2301,2303],{"id":2302},"frequently-asked-questions","Frequently asked questions",[10,2305,2306,2309],{},[2253,2307,2308],{},"How do I put an image in the body of an email with Python?","\nCreate an EmailMessage, add the HTML with add_alternative, then call add_related on the HTML part with the image bytes and a Content-ID from make_msgid. Reference it in the HTML as src=\"cid:\" followed by the ID without angle brackets.",[10,2311,2312,2315],{},[2253,2313,2314],{},"How do I turn an Excel chart into an image?","\nConvert the workbook or the chart's sheet to PDF with LibreOffice, render the page with pypdfium2 at a scale of about 2.5, and crop the white margins with Pillow.",[10,2317,2318,2321],{},[2253,2319,2320],{},"Why is my email chart blurry on a phone?","\nThe image was rendered at the size it is displayed. Render it at twice the width and set the img width attribute to the display width.",[10,2323,2324,2327],{},[2253,2325,2326],{},"Why does my embedded image show as a broken icon?","\nThe cid in the HTML does not match the Content-ID of any image part, or the image was attached to the message instead of the HTML part. Parse the message and compare the two sets.",[39,2329,2331],{"id":2330},"related","Related",[2247,2333,2334,2340,2345,2352],{},[2250,2335,2336,2337,2339],{},"Up one level: ",[28,2338,36],{"href":35}," — sending reports by email.",[2250,2341,2342,2344],{},[28,2343,31],{"href":30}," — the full message structure.",[2250,2346,2347,2351],{},[28,2348,2350],{"href":2349},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fzip-large-excel-attachments-before-emailing\u002F","Zip Large Excel Attachments Before Emailing"," — keeping messages under size limits.",[2250,2353,2354,2358],{},[28,2355,2357],{"href":2356},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-an-excel-report-with-the-sendgrid-api\u002F","Send an Excel Report with the SendGrid API"," — the same email through an API.",[2360,2361,2362],"style",{},"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 .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":49,"searchDepth":114,"depth":114,"links":2364},[2365,2366,2367,2368,2369,2370,2371,2372,2373,2374],{"id":41,"depth":114,"text":42},{"id":90,"depth":114,"text":91},{"id":571,"depth":114,"text":572},{"id":838,"depth":114,"text":839},{"id":1253,"depth":114,"text":1254},{"id":1869,"depth":114,"text":1870},{"id":2244,"depth":114,"text":2245},{"id":2289,"depth":114,"text":2290},{"id":2302,"depth":114,"text":2303},{"id":2330,"depth":114,"text":2331},"2026-10-02","Show the report chart right in the email instead of hiding it in an attachment: render the workbook's own Excel chart to a sharp PNG through LibreOffice and pypdfium2 (or draw it with matplotlib), size it for phones and high-DPI screens, keep it light and dark-mode safe, embed several images by Content-ID, and test that every image reference resolves.","md",[2379,2381,2383,2385],{"q":2308,"a":2380},"Create an EmailMessage, add the HTML with add_alternative, then call add_related on the HTML part with the image bytes and a Content-ID from make_msgid. Reference it in the HTML as src=\"cid:\" followed by the ID without angle brackets.",{"q":2314,"a":2382},"Convert the workbook or the chart's sheet to PDF with LibreOffice, render the page with pypdfium2 at a scale of about 2.5, and crop the white margins with Pillow.",{"q":2320,"a":2384},"The image was rendered at the size it is displayed. Render it at twice the width and set the img width attribute to the display width.",{"q":2326,"a":2386},"The cid in the HTML does not match the Content-ID of any image part, or the image was attached to the message instead of the HTML part. Parse the message and compare the two sets.","embed a chart image in the body of a report email",null,{"breadcrumb":2390},[2391,2393,2396],{"name":2392,"item":943},"Home",{"name":2394,"item":2395},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":36,"item":35},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fembed-a-chart-image-in-the-body-of-a-report-email",{"title":2399,"description":2400},"Embed an Excel Chart Image in a Report Email Body with Python","Put the Excel report chart inside the email body: render the workbook chart to PNG with LibreOffice and pypdfium2, 2x sizing, small files, several CID images, and a test that each cid: reference resolves.","embed-a-chart-image-in-the-body-of-a-report-email","automating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fembed-a-chart-image-in-the-body-of-a-report-email\u002Findex",[2404,2405],"email-delivery","charts-and-images","how-to","kkmNcH4ybHYWznSKf8rYsZi10L0awYtsF1XmOdwhPAw",[2409,2412],{"title":31,"path":2410,"stem":2411,"children":-1},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Femail-an-excel-report-with-an-html-summary-body","automating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Femail-an-excel-report-with-an-html-summary-body\u002Findex",{"title":2413,"path":2414,"stem":2415,"children":-1},"Personalise Report Emails with Jinja2 Templates","\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fpersonalise-report-emails-with-jinja2-templates","automating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fpersonalise-report-emails-with-jinja2-templates\u002Findex",[2417,2419],{"path":2418,"title":36},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib",{"path":2420,"title":2394},"\u002Fautomating-reporting-workflows",[],[2423,2426,2429,2432,2435,2436],{"path":2424,"title":2425,"kind":2406},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-credit-control-dunning-letter-run","Build a Credit Control Dunning Letter Run",{"path":2427,"title":2428,"kind":2406},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fattach-multiple-excel-files-to-one-email-in-python","Attach Multiple Excel Files to One Email in Python",{"path":2430,"title":2431,"kind":2406},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fdownload-excel-attachments-from-an-email-inbox","Download Excel Attachments from an Email Inbox with Python",{"path":2433,"title":2434,"kind":2406},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Femail-a-download-link-instead-of-an-excel-attachment","Email a Download Link Instead of an Excel Attachment",{"path":2410,"title":31,"kind":2406},{"path":2414,"title":2413,"kind":2406},1790972129677]