[{"data":1,"prerenderedAt":1847},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fautomated-map-layout-generation\u002Fadd-label-and-html-items-to-layout-pyqgis":3},{"id":4,"title":5,"body":6,"description":1836,"extension":1837,"meta":1838,"navigation":215,"path":1843,"seo":1844,"stem":1845,"__hash__":1846},"docs\u002Fspatial-data-processing-automation\u002Fautomated-map-layout-generation\u002Fadd-label-and-html-items-to-layout-pyqgis\u002Findex.md","Add Label and HTML Items to a Layout in PyQGIS",{"type":7,"value":8,"toc":1822},"minimark",[9,13,17,26,115,120,138,142,150,383,406,410,417,485,646,670,674,677,758,773,777,780,871,1171,1204,1208,1211,1401,1406,1410,1413,1649,1654,1658,1686,1690,1725,1729,1738,1742,1756,1766,1776,1785,1789,1818],[10,11,5],"h1",{"id":12},"add-label-and-html-items-to-a-layout-in-pyqgis",[14,15,16],"p",{},"A printed map is half text: a title, a subtitle, a source note, a date, a disclaimer, a paragraph explaining what the colours mean, sometimes a table of figures. QGIS layouts have two text items. The label item handles short text — titles, captions, notes — with fonts, alignment and embedded expressions. The HTML frame handles long or rich content — formatted paragraphs, lists, tables — that can flow across several frames and pages. Both can be created and filled entirely from Python.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fspatial-data-processing-automation\u002Fautomated-map-layout-generation\u002F","Automated Map Layout Generation",". It adds labels with text formats and alignment, embeds expressions for dates and data, uses HTML in labels for mixed formatting, adds HTML frames for long text and tables, and keeps text readable in exports.",[14,27,28],{},[29,30,35,39,43,50,59,69,75,81,85,89,94,99,103,106,109,112],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 262","img","Label items for short text with one format and expressions, compared with HTML frames for long styled content that flows across frames","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"Label or HTML frame",[40,41,42],"desc",{},"A label item suits short text: titles, subtitles, source notes and dates, with one text format, alignment and expressions, optionally rendered as simple HTML. An HTML frame suits long or structured content: paragraphs, lists and tables styled with CSS, which can flow from one frame to another across pages. Both evaluate QGIS expressions inside the text.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","262","#f6f3ea",[51,52,58],"text",{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"380","28","text-anchor:middle;font-size:14px;font-family:sans-serif;font-weight:bold","#17211d","middle","Short text or long content?",[44,60],{"x":61,"y":62,"width":63,"height":64,"rx":65,"fill":66,"stroke":67,"style":68},"24","56","340","190","8","#eef7f4","#0f766e","stroke-width:2",[51,70,74],{"x":71,"y":72,"style":73,"fill":67,"textAnchor":57},"194","98.78","text-anchor:middle;font-size:11.5px;font-family:sans-serif;font-weight:bold","QgsLayoutItemLabel",[51,76,80],{"x":71,"y":77,"style":78,"fill":79,"textAnchor":57},"126.78","text-anchor:middle;font-size:10.5px;font-family:sans-serif","#2f3b35","titles, captions, notes",[51,82,84],{"x":71,"y":83,"style":78,"fill":79,"textAnchor":57},"154.78","one text format",[51,86,88],{"x":71,"y":87,"style":78,"fill":79,"textAnchor":57},"182.78","expressions [% %]",[51,90,93],{"x":71,"y":91,"style":78,"fill":92,"textAnchor":57},"210.78","#59645f","simple HTML optional",[44,95],{"x":96,"y":62,"width":63,"height":64,"rx":65,"fill":97,"stroke":98,"style":68},"396","#eff3ff","#2563eb",[51,100,102],{"x":101,"y":72,"style":73,"fill":98,"textAnchor":57},"566","QgsLayoutItemHtml",[51,104,105],{"x":101,"y":77,"style":78,"fill":79,"textAnchor":57},"paragraphs, lists, tables",[51,107,108],{"x":101,"y":83,"style":78,"fill":79,"textAnchor":57},"CSS styling",[51,110,111],{"x":101,"y":87,"style":78,"fill":79,"textAnchor":57},"flows across frames",[51,113,114],{"x":101,"y":91,"style":78,"fill":92,"textAnchor":57},"multi-page",[116,117,119],"h2",{"id":118},"prerequisites","Prerequisites",[121,122,123,127,135],"ul",{},[124,125,126],"li",{},"QGIS 3.34 LTR or newer, or the QGIS 4 series.",[124,128,129,130,134],{},"A layout to add to, created in code or loaded from a template as in ",[21,131,133],{"href":132},"\u002Fspatial-data-processing-automation\u002Fautomated-map-layout-generation\u002Fload-layout-from-qpt-template-pyqgis\u002F","loading a layout from a QPT template",".",[124,136,137],{},"The fonts you intend to use installed on every machine that exports.",[116,139,141],{"id":140},"add-a-title-label","Add a title label",[14,143,144,145,149],{},"A label needs text, a text format and a position and size. ",[146,147,148],"code",{},"QgsTextFormat"," sets font, size, colour and optional buffer or background, exactly as for map labels.",[151,152,157],"pre",{"className":153,"code":154,"language":155,"meta":156,"style":156},"language-python shiki shiki-themes github-dark","from qgis.core import (QgsProject, QgsLayoutItemLabel, QgsTextFormat, QgsLayoutPoint,\n                       QgsLayoutSize, QgsUnitTypes)\nfrom qgis.PyQt.QtGui import QFont, QColor\nfrom qgis.PyQt.QtCore import Qt\n\nlayout = QgsProject.instance().layoutManager().layoutByName(\"District report\")\n\ntitle = QgsLayoutItemLabel(layout)\ntitle.setId(\"title\")\ntitle.setText(\"Green space provision by district\")\nfmt = QgsTextFormat()\nfmt.setFont(QFont(\"Noto Sans\", 20, QFont.Bold))\nfmt.setSize(20)\nfmt.setColor(QColor(\"#17211d\"))\ntitle.setTextFormat(fmt)\ntitle.setHAlign(Qt.AlignLeft)\ntitle.setVAlign(Qt.AlignVCenter)\ntitle.attemptMove(QgsLayoutPoint(12, 10, QgsUnitTypes.LayoutMillimeters))\ntitle.attemptResize(QgsLayoutSize(260, 14, QgsUnitTypes.LayoutMillimeters))\nlayout.addLayoutItem(title)\n","python","",[146,158,159,178,184,197,210,217,236,241,252,263,274,285,304,314,326,332,338,344,361,377],{"__ignoreMap":156},[160,161,164,168,172,175],"span",{"class":162,"line":163},"line",1,[160,165,167],{"class":166},"snl16","from",[160,169,171],{"class":170},"s95oV"," qgis.core ",[160,173,174],{"class":166},"import",[160,176,177],{"class":170}," (QgsProject, QgsLayoutItemLabel, QgsTextFormat, QgsLayoutPoint,\n",[160,179,181],{"class":162,"line":180},2,[160,182,183],{"class":170},"                       QgsLayoutSize, QgsUnitTypes)\n",[160,185,187,189,192,194],{"class":162,"line":186},3,[160,188,167],{"class":166},[160,190,191],{"class":170}," qgis.PyQt.QtGui ",[160,193,174],{"class":166},[160,195,196],{"class":170}," QFont, QColor\n",[160,198,200,202,205,207],{"class":162,"line":199},4,[160,201,167],{"class":166},[160,203,204],{"class":170}," qgis.PyQt.QtCore ",[160,206,174],{"class":166},[160,208,209],{"class":170}," Qt\n",[160,211,213],{"class":162,"line":212},5,[160,214,216],{"emptyLinePlaceholder":215},true,"\n",[160,218,220,223,226,229,233],{"class":162,"line":219},6,[160,221,222],{"class":170},"layout ",[160,224,225],{"class":166},"=",[160,227,228],{"class":170}," QgsProject.instance().layoutManager().layoutByName(",[160,230,232],{"class":231},"sU2Wk","\"District report\"",[160,234,235],{"class":170},")\n",[160,237,239],{"class":162,"line":238},7,[160,240,216],{"emptyLinePlaceholder":215},[160,242,244,247,249],{"class":162,"line":243},8,[160,245,246],{"class":170},"title ",[160,248,225],{"class":166},[160,250,251],{"class":170}," QgsLayoutItemLabel(layout)\n",[160,253,255,258,261],{"class":162,"line":254},9,[160,256,257],{"class":170},"title.setId(",[160,259,260],{"class":231},"\"title\"",[160,262,235],{"class":170},[160,264,266,269,272],{"class":162,"line":265},10,[160,267,268],{"class":170},"title.setText(",[160,270,271],{"class":231},"\"Green space provision by district\"",[160,273,235],{"class":170},[160,275,277,280,282],{"class":162,"line":276},11,[160,278,279],{"class":170},"fmt ",[160,281,225],{"class":166},[160,283,284],{"class":170}," QgsTextFormat()\n",[160,286,288,291,294,297,301],{"class":162,"line":287},12,[160,289,290],{"class":170},"fmt.setFont(QFont(",[160,292,293],{"class":231},"\"Noto Sans\"",[160,295,296],{"class":170},", ",[160,298,300],{"class":299},"sDLfK","20",[160,302,303],{"class":170},", QFont.Bold))\n",[160,305,307,310,312],{"class":162,"line":306},13,[160,308,309],{"class":170},"fmt.setSize(",[160,311,300],{"class":299},[160,313,235],{"class":170},[160,315,317,320,323],{"class":162,"line":316},14,[160,318,319],{"class":170},"fmt.setColor(QColor(",[160,321,322],{"class":231},"\"#17211d\"",[160,324,325],{"class":170},"))\n",[160,327,329],{"class":162,"line":328},15,[160,330,331],{"class":170},"title.setTextFormat(fmt)\n",[160,333,335],{"class":162,"line":334},16,[160,336,337],{"class":170},"title.setHAlign(Qt.AlignLeft)\n",[160,339,341],{"class":162,"line":340},17,[160,342,343],{"class":170},"title.setVAlign(Qt.AlignVCenter)\n",[160,345,347,350,353,355,358],{"class":162,"line":346},18,[160,348,349],{"class":170},"title.attemptMove(QgsLayoutPoint(",[160,351,352],{"class":299},"12",[160,354,296],{"class":170},[160,356,357],{"class":299},"10",[160,359,360],{"class":170},", QgsUnitTypes.LayoutMillimeters))\n",[160,362,364,367,370,372,375],{"class":162,"line":363},19,[160,365,366],{"class":170},"title.attemptResize(QgsLayoutSize(",[160,368,369],{"class":299},"260",[160,371,296],{"class":170},[160,373,374],{"class":299},"14",[160,376,360],{"class":170},[160,378,380],{"class":162,"line":379},20,[160,381,382],{"class":170},"layout.addLayoutItem(title)\n",[14,384,385,389,390,393,394,397,398,401,402,405],{},[386,387,388],"strong",{},"Breakdown:"," ",[146,391,392],{},"setTextFormat"," (QGIS 3.24 and newer) replaces the older ",[146,395,396],{},"setFont"," and ",[146,399,400],{},"setFontColor"," calls and gives labels the same text engine as map labelling — buffers, shadows and backgrounds included. Setting both the font's point size and the format's size keeps them consistent. An explicit id makes the item easy to find and update later. Fixed position and size in millimetres place the title precisely; ",[146,403,404],{},"adjustSizeToText()"," shrinks the frame to fit instead, which is handy for labels whose text length varies.",[116,407,409],{"id":408},"add-data-driven-text-with-expressions","Add data-driven text with expressions",[14,411,412,413,416],{},"Text inside ",[146,414,415],{},"[% %]"," is evaluated as a QGIS expression when the layout renders. That covers dates, counts, project metadata and values computed from layers.",[14,418,419],{},[29,420,423,426,429,432,447,450,455,460,463,467,472,480],{"viewBox":421,"role":32,"ariaLabel":422,"xmlns":34},"0 0 760 240","A label with expression brackets for the date, the project title and a feature count, each replaced by its value when the layout renders",[36,424,425],{},"Expressions inside label text",[40,427,428],{},"A source note label contains static text with expression brackets: the current date formatted as day month year, the project title from project variables, and a count of features from an aggregate over the parks layer. At render time each bracket is replaced by its value, so the note stays correct every time the layout is exported.",[44,430],{"x":46,"y":46,"width":47,"height":431,"fill":49},"240",[433,434,435],"defs",{},[436,437,443],"marker",{"id":438,"viewBox":439,"refX":65,"refY":440,"markerWidth":441,"markerHeight":441,"orient":442},"lblExprArrow","0 0 10 10","5","7","auto-start-reverse",[444,445],"path",{"d":446,"fill":79},"M0 0 L10 5 L0 10 z",[51,448,449],{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"Static text, live values",[44,451],{"x":61,"y":62,"width":452,"height":453,"rx":65,"fill":454,"stroke":92,"style":68},"712","46","#fffdf7",[51,456,459],{"x":53,"y":457,"style":458,"fill":79,"textAnchor":57},"82.6","text-anchor:middle;font-size:9.5px;font-family:monospace","Source: [% @project_title %] · [% aggregate('parks', 'count', \"id\") %] parks",[44,461],{"x":61,"y":462,"width":452,"height":453,"rx":65,"fill":454,"stroke":92,"style":68},"120",[51,464,466],{"x":53,"y":465,"style":458,"fill":79,"textAnchor":57},"146.6","Exported [% format_date(now(), 'd MMMM yyyy') %]",[162,468],{"x1":53,"y1":469,"x2":53,"y2":470,"stroke":79,"style":471},"172","192","stroke-width:1.8;marker-end:url(#lblExprArrow)",[44,473],{"x":474,"y":475,"width":476,"height":477,"rx":65,"fill":478,"stroke":479,"style":68},"160","196","440","36","#e8efe6","#15803d",[51,481,484],{"x":53,"y":482,"style":483,"fill":79,"textAnchor":57},"217.6","text-anchor:middle;font-size:10.0px;font-family:sans-serif","Source: City Parks 2026 · 214 parks · Exported 2 October 2026",[151,486,488],{"className":153,"code":487,"language":155,"meta":156,"style":156},"note = QgsLayoutItemLabel(layout)\nnote.setId(\"source_note\")\nnote.setText(\n    \"Source: [% @project_title %], green space survey 2026 · \"\n    \"[% aggregate(layer:='parks', aggregate:='count', expression:=\\\"park_id\\\") %] parks mapped · \"\n    \"Exported [% format_date(now(), 'd MMMM yyyy') %]\")\nsmall = QgsTextFormat()\nsmall.setFont(QFont(\"Noto Sans\", 7))\nsmall.setSize(7)\nsmall.setColor(QColor(\"#59645f\"))\nnote.setTextFormat(small)\nnote.attemptMove(QgsLayoutPoint(12, 198, QgsUnitTypes.LayoutMillimeters))\nnote.attemptResize(QgsLayoutSize(260, 6, QgsUnitTypes.LayoutMillimeters))\nlayout.addLayoutItem(note)\nnote.refreshExpressionContext()\nprint(note.currentText())\n",[146,489,490,499,509,514,519,541,554,563,576,585,595,600,614,628,633,638],{"__ignoreMap":156},[160,491,492,495,497],{"class":162,"line":163},[160,493,494],{"class":170},"note ",[160,496,225],{"class":166},[160,498,251],{"class":170},[160,500,501,504,507],{"class":162,"line":180},[160,502,503],{"class":170},"note.setId(",[160,505,506],{"class":231},"\"source_note\"",[160,508,235],{"class":170},[160,510,511],{"class":162,"line":186},[160,512,513],{"class":170},"note.setText(\n",[160,515,516],{"class":162,"line":199},[160,517,518],{"class":231},"    \"Source: [% @project_title %], green space survey 2026 · \"\n",[160,520,521,524,527,530,533,536,538],{"class":162,"line":212},[160,522,523],{"class":231},"    \"[",[160,525,526],{"class":299},"% a",[160,528,529],{"class":231},"ggregate(layer:='parks', aggregate:='count', expression:=",[160,531,532],{"class":299},"\\\"",[160,534,535],{"class":231},"park_id",[160,537,532],{"class":299},[160,539,540],{"class":231},") %] parks mapped · \"\n",[160,542,543,546,549,552],{"class":162,"line":219},[160,544,545],{"class":231},"    \"Exported [",[160,547,548],{"class":299},"% f",[160,550,551],{"class":231},"ormat_date(now(), 'd MMMM yyyy') %]\"",[160,553,235],{"class":170},[160,555,556,559,561],{"class":162,"line":238},[160,557,558],{"class":170},"small ",[160,560,225],{"class":166},[160,562,284],{"class":170},[160,564,565,568,570,572,574],{"class":162,"line":243},[160,566,567],{"class":170},"small.setFont(QFont(",[160,569,293],{"class":231},[160,571,296],{"class":170},[160,573,441],{"class":299},[160,575,325],{"class":170},[160,577,578,581,583],{"class":162,"line":254},[160,579,580],{"class":170},"small.setSize(",[160,582,441],{"class":299},[160,584,235],{"class":170},[160,586,587,590,593],{"class":162,"line":265},[160,588,589],{"class":170},"small.setColor(QColor(",[160,591,592],{"class":231},"\"#59645f\"",[160,594,325],{"class":170},[160,596,597],{"class":162,"line":276},[160,598,599],{"class":170},"note.setTextFormat(small)\n",[160,601,602,605,607,609,612],{"class":162,"line":287},[160,603,604],{"class":170},"note.attemptMove(QgsLayoutPoint(",[160,606,352],{"class":299},[160,608,296],{"class":170},[160,610,611],{"class":299},"198",[160,613,360],{"class":170},[160,615,616,619,621,623,626],{"class":162,"line":306},[160,617,618],{"class":170},"note.attemptResize(QgsLayoutSize(",[160,620,369],{"class":299},[160,622,296],{"class":170},[160,624,625],{"class":299},"6",[160,627,360],{"class":170},[160,629,630],{"class":162,"line":316},[160,631,632],{"class":170},"layout.addLayoutItem(note)\n",[160,634,635],{"class":162,"line":328},[160,636,637],{"class":170},"note.refreshExpressionContext()\n",[160,639,640,643],{"class":162,"line":334},[160,641,642],{"class":299},"print",[160,644,645],{"class":170},"(note.currentText())\n",[14,647,648,389,650,653,654,657,658,661,662,665,666,669],{},[386,649,388],{},[146,651,652],{},"@project_title"," comes from the project's metadata; ",[146,655,656],{},"aggregate"," counts features in another layer; ",[146,659,660],{},"format_date(now(), …)"," writes the export date. Because these are evaluated at render time, the note is correct whenever the layout is exported, with no code changes. Within an atlas, the expression context also includes ",[146,663,664],{},"@atlas_feature",", so captions can describe each page's feature. ",[146,667,668],{},"currentText()"," shows the evaluated string, which is the easiest way to test expressions from Python.",[116,671,673],{"id":672},"mix-formatting-with-html-labels","Mix formatting with HTML labels",[14,675,676],{},"A single label has one text format. For a word in bold, a coloured term or a line break, switch the label to HTML mode and use simple tags.",[151,678,680],{"className":153,"code":679,"language":155,"meta":156,"style":156},"legend_note = QgsLayoutItemLabel(layout)\nlegend_note.setMode(QgsLayoutItemLabel.ModeHtml)\nlegend_note.setText(\n    \"\u003Cp style='font-family:Noto Sans; font-size:8pt; color:#2f3b35'>\"\n    \"Districts below the target of \u003Cb>9 m² per resident\u003C\u002Fb> are shown in \"\n    \"\u003Cspan style='color:#b91c1c'>\u003Cb>red\u003C\u002Fb>\u003C\u002Fspan>.\u003Cbr>\"\n    \"Target: [% @green_target %] m² per resident.\u003C\u002Fp>\")\nlegend_note.attemptMove(QgsLayoutPoint(200, 150, QgsUnitTypes.LayoutMillimeters))\nlegend_note.attemptResize(QgsLayoutSize(80, 18, QgsUnitTypes.LayoutMillimeters))\nlayout.addLayoutItem(legend_note)\n",[146,681,682,691,696,701,706,711,716,723,738,753],{"__ignoreMap":156},[160,683,684,687,689],{"class":162,"line":163},[160,685,686],{"class":170},"legend_note ",[160,688,225],{"class":166},[160,690,251],{"class":170},[160,692,693],{"class":162,"line":180},[160,694,695],{"class":170},"legend_note.setMode(QgsLayoutItemLabel.ModeHtml)\n",[160,697,698],{"class":162,"line":186},[160,699,700],{"class":170},"legend_note.setText(\n",[160,702,703],{"class":162,"line":199},[160,704,705],{"class":231},"    \"\u003Cp style='font-family:Noto Sans; font-size:8pt; color:#2f3b35'>\"\n",[160,707,708],{"class":162,"line":212},[160,709,710],{"class":231},"    \"Districts below the target of \u003Cb>9 m² per resident\u003C\u002Fb> are shown in \"\n",[160,712,713],{"class":162,"line":219},[160,714,715],{"class":231},"    \"\u003Cspan style='color:#b91c1c'>\u003Cb>red\u003C\u002Fb>\u003C\u002Fspan>.\u003Cbr>\"\n",[160,717,718,721],{"class":162,"line":238},[160,719,720],{"class":231},"    \"Target: [% @green_target %] m² per resident.\u003C\u002Fp>\"",[160,722,235],{"class":170},[160,724,725,728,731,733,736],{"class":162,"line":243},[160,726,727],{"class":170},"legend_note.attemptMove(QgsLayoutPoint(",[160,729,730],{"class":299},"200",[160,732,296],{"class":170},[160,734,735],{"class":299},"150",[160,737,360],{"class":170},[160,739,740,743,746,748,751],{"class":162,"line":254},[160,741,742],{"class":170},"legend_note.attemptResize(QgsLayoutSize(",[160,744,745],{"class":299},"80",[160,747,296],{"class":170},[160,749,750],{"class":299},"18",[160,752,360],{"class":170},[160,754,755],{"class":162,"line":265},[160,756,757],{"class":170},"layout.addLayoutItem(legend_note)\n",[14,759,760,762,763,296,766,397,769,772],{},[386,761,388],{}," In HTML mode the label renders its text with a web engine, so inline CSS controls fonts, sizes and colours, and tags such as ",[146,764,765],{},"\u003Cb>",[146,767,768],{},"\u003Cbr>",[146,770,771],{},"\u003Cspan>"," work. Expressions still work inside the HTML. Keep HTML labels short and simple; complex markup belongs in an HTML frame. Because the text engine differs, an HTML label's fonts can look slightly different from a plain label's at the same size — compare them in an export rather than on screen.",[116,774,776],{"id":775},"add-an-html-frame-for-long-content","Add an HTML frame for long content",[14,778,779],{},"For a paragraph of explanation, a list of methods or a table of figures, an HTML frame is the right item. Its content can be set directly or loaded from a URL, styled with CSS, and can flow into further frames when it is longer than one box.",[14,781,782],{},[29,783,785,788,791,793,800,803,806,810,814,818,825,828,833,838,842,844,848,852,858,863,867],{"viewBox":421,"role":32,"ariaLabel":784,"xmlns":34},"One HTML document flowing from a frame on page one into a second frame on page two",[36,786,787],{},"Content flowing between frames",[40,789,790],{},"An HTML multiframe holds one document. Its first frame on page one shows the start of the content; when the content is longer than the frame, it continues in a second frame on page two, and the layout can add pages automatically until all content is shown. Frames can be resized independently.",[44,792],{"x":46,"y":46,"width":47,"height":431,"fill":49},[433,794,795],{},[436,796,798],{"id":797,"viewBox":439,"refX":65,"refY":440,"markerWidth":441,"markerHeight":441,"orient":442},"lblFlowArrow",[444,799],{"d":446,"fill":79},[51,801,802],{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"One document, several frames",[44,804],{"x":61,"y":62,"width":805,"height":735,"rx":65,"fill":454,"stroke":92,"style":68},"220",[51,807,102],{"x":808,"y":809,"style":73,"fill":56,"textAnchor":57},"134","108.78",[51,811,813],{"x":808,"y":812,"style":78,"fill":92,"textAnchor":57},"134.78","one HTML document",[51,815,817],{"x":808,"y":816,"style":78,"fill":92,"textAnchor":57},"160.78","CSS styled",[162,819],{"x1":820,"y1":821,"x2":822,"y2":823,"stroke":79,"style":824},"244","131","286","110","stroke-width:1.8;marker-end:url(#lblFlowArrow)",[162,826],{"x1":820,"y1":821,"x2":822,"y2":827,"stroke":79,"style":824},"170",[44,829],{"x":830,"y":831,"width":805,"height":832,"rx":65,"fill":66,"stroke":67,"style":68},"290","70","64",[51,834,837],{"x":835,"y":836,"style":73,"fill":67,"textAnchor":57},"400","95.78","frame 1, page 1",[51,839,841],{"x":835,"y":840,"style":78,"fill":92,"textAnchor":57},"115.78","start of content",[44,843],{"x":830,"y":735,"width":805,"height":832,"rx":65,"fill":97,"stroke":98,"style":68},[51,845,847],{"x":835,"y":846,"style":73,"fill":98,"textAnchor":57},"175.78","frame 2, page 2",[51,849,851],{"x":835,"y":850,"style":78,"fill":92,"textAnchor":57},"195.78","continues",[44,853],{"x":854,"y":855,"width":64,"height":855,"rx":65,"fill":856,"stroke":857,"style":68},"546","90","#fdf2e2","#b45309",[51,859,862],{"x":860,"y":861,"style":73,"fill":857,"textAnchor":57},"641","114.78","resize mode",[51,864,866],{"x":860,"y":865,"style":78,"fill":92,"textAnchor":57},"138.78","add pages",[51,868,870],{"x":860,"y":869,"style":78,"fill":92,"textAnchor":57},"162.78","until it fits",[151,872,874],{"className":153,"code":873,"language":155,"meta":156,"style":156},"from qgis.core import QgsLayoutItemHtml, QgsLayoutFrame\nfrom qgis.PyQt.QtCore import QRectF\n\nrows = \"\".join(\n    f\"\u003Ctr>\u003Ctd>{f['name']}\u003C\u002Ftd>\u003Ctd style='text-align:right'>{f['green_m2_per_res']:.1f}\u003C\u002Ftd>\u003C\u002Ftr>\"\n    for f in sorted(QgsProject.instance().mapLayersByName(\"districts\")[0].getFeatures(),\n                    key=lambda f: f[\"green_m2_per_res\"]))\nhtml_doc = f\"\"\"\n\u003Cstyle>\n  body {{ font-family: 'Noto Sans'; font-size: 8pt; color: #2f3b35; }}\n  table {{ border-collapse: collapse; width: 100%; }}\n  td, th {{ padding: 1mm 2mm; border-bottom: 0.2mm solid #d9d3c4; }}\n\u003C\u002Fstyle>\n\u003Ch3>Green space per resident\u003C\u002Fh3>\n\u003Ctable>\u003Ctr>\u003Cth>District\u003C\u002Fth>\u003Cth style='text-align:right'>m²\u003C\u002Fth>\u003C\u002Ftr>{rows}\u003C\u002Ftable>\n\"\"\"\n\nhtml = QgsLayoutItemHtml(layout)\nhtml.setContentMode(QgsLayoutItemHtml.ManualHtml)\nhtml.setHtml(html_doc)\nframe = QgsLayoutFrame(layout, html)\nframe.attemptSetSceneRect(QRectF(200, 20, 85, 120))\nhtml.addFrame(frame)\nhtml.setResizeMode(QgsLayoutItemHtml.ExtendToNextPage)\nhtml.loadHtml()\nlayout.addMultiFrame(html)\n",[146,875,876,887,898,902,915,958,986,1004,1017,1022,1036,1048,1060,1065,1070,1085,1089,1093,1103,1108,1113,1124,1147,1153,1159,1165],{"__ignoreMap":156},[160,877,878,880,882,884],{"class":162,"line":163},[160,879,167],{"class":166},[160,881,171],{"class":170},[160,883,174],{"class":166},[160,885,886],{"class":170}," QgsLayoutItemHtml, QgsLayoutFrame\n",[160,888,889,891,893,895],{"class":162,"line":180},[160,890,167],{"class":166},[160,892,204],{"class":170},[160,894,174],{"class":166},[160,896,897],{"class":170}," QRectF\n",[160,899,900],{"class":162,"line":186},[160,901,216],{"emptyLinePlaceholder":215},[160,903,904,907,909,912],{"class":162,"line":199},[160,905,906],{"class":170},"rows ",[160,908,225],{"class":166},[160,910,911],{"class":231}," \"\"",[160,913,914],{"class":170},".join(\n",[160,916,917,920,923,926,929,932,935,938,941,943,945,948,950,953,955],{"class":162,"line":212},[160,918,919],{"class":166},"    f",[160,921,922],{"class":231},"\"\u003Ctr>\u003Ctd>",[160,924,925],{"class":299},"{",[160,927,928],{"class":170},"f[",[160,930,931],{"class":231},"'name'",[160,933,934],{"class":170},"]",[160,936,937],{"class":299},"}",[160,939,940],{"class":231},"\u003C\u002Ftd>\u003Ctd style='text-align:right'>",[160,942,925],{"class":299},[160,944,928],{"class":170},[160,946,947],{"class":231},"'green_m2_per_res'",[160,949,934],{"class":170},[160,951,952],{"class":166},":.1f",[160,954,937],{"class":299},[160,956,957],{"class":231},"\u003C\u002Ftd>\u003C\u002Ftr>\"\n",[160,959,960,963,966,969,972,975,978,981,983],{"class":162,"line":219},[160,961,962],{"class":166},"    for",[160,964,965],{"class":170}," f ",[160,967,968],{"class":166},"in",[160,970,971],{"class":299}," sorted",[160,973,974],{"class":170},"(QgsProject.instance().mapLayersByName(",[160,976,977],{"class":231},"\"districts\"",[160,979,980],{"class":170},")[",[160,982,46],{"class":299},[160,984,985],{"class":170},"].getFeatures(),\n",[160,987,988,992,995,998,1001],{"class":162,"line":238},[160,989,991],{"class":990},"s9osk","                    key",[160,993,994],{"class":166},"=lambda",[160,996,997],{"class":170}," f: f[",[160,999,1000],{"class":231},"\"green_m2_per_res\"",[160,1002,1003],{"class":170},"]))\n",[160,1005,1006,1009,1011,1014],{"class":162,"line":243},[160,1007,1008],{"class":170},"html_doc ",[160,1010,225],{"class":166},[160,1012,1013],{"class":166}," f",[160,1015,1016],{"class":231},"\"\"\"\n",[160,1018,1019],{"class":162,"line":254},[160,1020,1021],{"class":231},"\u003Cstyle>\n",[160,1023,1024,1027,1030,1033],{"class":162,"line":265},[160,1025,1026],{"class":231},"  body ",[160,1028,1029],{"class":299},"{{",[160,1031,1032],{"class":231}," font-family: 'Noto Sans'; font-size: 8pt; color: #2f3b35; ",[160,1034,1035],{"class":299},"}}\n",[160,1037,1038,1041,1043,1046],{"class":162,"line":276},[160,1039,1040],{"class":231},"  table ",[160,1042,1029],{"class":299},[160,1044,1045],{"class":231}," border-collapse: collapse; width: 100%; ",[160,1047,1035],{"class":299},[160,1049,1050,1053,1055,1058],{"class":162,"line":287},[160,1051,1052],{"class":231},"  td, th ",[160,1054,1029],{"class":299},[160,1056,1057],{"class":231}," padding: 1mm 2mm; border-bottom: 0.2mm solid #d9d3c4; ",[160,1059,1035],{"class":299},[160,1061,1062],{"class":162,"line":306},[160,1063,1064],{"class":231},"\u003C\u002Fstyle>\n",[160,1066,1067],{"class":162,"line":316},[160,1068,1069],{"class":231},"\u003Ch3>Green space per resident\u003C\u002Fh3>\n",[160,1071,1072,1075,1077,1080,1082],{"class":162,"line":328},[160,1073,1074],{"class":231},"\u003Ctable>\u003Ctr>\u003Cth>District\u003C\u002Fth>\u003Cth style='text-align:right'>m²\u003C\u002Fth>\u003C\u002Ftr>",[160,1076,925],{"class":299},[160,1078,1079],{"class":170},"rows",[160,1081,937],{"class":299},[160,1083,1084],{"class":231},"\u003C\u002Ftable>\n",[160,1086,1087],{"class":162,"line":334},[160,1088,1016],{"class":231},[160,1090,1091],{"class":162,"line":340},[160,1092,216],{"emptyLinePlaceholder":215},[160,1094,1095,1098,1100],{"class":162,"line":346},[160,1096,1097],{"class":170},"html ",[160,1099,225],{"class":166},[160,1101,1102],{"class":170}," QgsLayoutItemHtml(layout)\n",[160,1104,1105],{"class":162,"line":363},[160,1106,1107],{"class":170},"html.setContentMode(QgsLayoutItemHtml.ManualHtml)\n",[160,1109,1110],{"class":162,"line":379},[160,1111,1112],{"class":170},"html.setHtml(html_doc)\n",[160,1114,1116,1119,1121],{"class":162,"line":1115},21,[160,1117,1118],{"class":170},"frame ",[160,1120,225],{"class":166},[160,1122,1123],{"class":170}," QgsLayoutFrame(layout, html)\n",[160,1125,1127,1130,1132,1134,1136,1138,1141,1143,1145],{"class":162,"line":1126},22,[160,1128,1129],{"class":170},"frame.attemptSetSceneRect(QRectF(",[160,1131,730],{"class":299},[160,1133,296],{"class":170},[160,1135,300],{"class":299},[160,1137,296],{"class":170},[160,1139,1140],{"class":299},"85",[160,1142,296],{"class":170},[160,1144,462],{"class":299},[160,1146,325],{"class":170},[160,1148,1150],{"class":162,"line":1149},23,[160,1151,1152],{"class":170},"html.addFrame(frame)\n",[160,1154,1156],{"class":162,"line":1155},24,[160,1157,1158],{"class":170},"html.setResizeMode(QgsLayoutItemHtml.ExtendToNextPage)\n",[160,1160,1162],{"class":162,"line":1161},25,[160,1163,1164],{"class":170},"html.loadHtml()\n",[160,1166,1168],{"class":162,"line":1167},26,[160,1169,1170],{"class":170},"layout.addMultiFrame(html)\n",[14,1172,1173,1175,1176,1178,1179,1182,1183,1186,1187,1190,1191,1194,1195,1198,1199,1203],{},[386,1174,388],{}," An HTML item is a multiframe: the content lives in ",[146,1177,102],{},", and one or more ",[146,1180,1181],{},"QgsLayoutFrame"," objects show parts of it on the page. ",[146,1184,1185],{},"ManualHtml"," content mode uses the string set with ",[146,1188,1189],{},"setHtml","; ",[146,1192,1193],{},"loadHtml()"," renders it. With ",[146,1196,1197],{},"ExtendToNextPage",", content that does not fit continues in a new frame on a new page, which is how a long table becomes a multi-page appendix. Generating the table rows from the layer means the table always matches the map. For tables of attributes specifically, the dedicated ",[21,1200,1202],{"href":1201},"\u002Fspatial-data-processing-automation\u002Fautomated-map-layout-generation\u002Fadd-attribute-table-to-layout-pyqgis\u002F","attribute table item"," is simpler; HTML frames win when you need custom formatting or content from several sources.",[116,1205,1207],{"id":1206},"update-text-across-many-layouts","Update text across many layouts",[14,1209,1210],{},"Projects accumulate layouts — one per district, per theme, per year — and text that should be identical drifts: a copyright year here, an old department name there. A small loop finds and fixes every label at once.",[151,1212,1214],{"className":153,"code":1213,"language":155,"meta":156,"style":156},"import re\n\nmanager = QgsProject.instance().layoutManager()\nchanged = 0\nfor lay in manager.printLayouts():\n    for item in lay.items():\n        if isinstance(item, QgsLayoutItemLabel):\n            old = item.text()\n            new = re.sub(r\"© \\d{4}\", \"© [% year(now()) %]\", old)\n            new = new.replace(\"Dept. of Planning\", \"Planning & Environment Office\")\n            if new != old:\n                item.setText(new)\n                changed += 1\nprint(changed, \"labels updated across\", len(manager.printLayouts()), \"layouts\")\n",[146,1215,1216,1223,1227,1237,1247,1260,1272,1283,1293,1329,1348,1362,1367,1378],{"__ignoreMap":156},[160,1217,1218,1220],{"class":162,"line":163},[160,1219,174],{"class":166},[160,1221,1222],{"class":170}," re\n",[160,1224,1225],{"class":162,"line":180},[160,1226,216],{"emptyLinePlaceholder":215},[160,1228,1229,1232,1234],{"class":162,"line":186},[160,1230,1231],{"class":170},"manager ",[160,1233,225],{"class":166},[160,1235,1236],{"class":170}," QgsProject.instance().layoutManager()\n",[160,1238,1239,1242,1244],{"class":162,"line":199},[160,1240,1241],{"class":170},"changed ",[160,1243,225],{"class":166},[160,1245,1246],{"class":299}," 0\n",[160,1248,1249,1252,1255,1257],{"class":162,"line":212},[160,1250,1251],{"class":166},"for",[160,1253,1254],{"class":170}," lay ",[160,1256,968],{"class":166},[160,1258,1259],{"class":170}," manager.printLayouts():\n",[160,1261,1262,1264,1267,1269],{"class":162,"line":219},[160,1263,962],{"class":166},[160,1265,1266],{"class":170}," item ",[160,1268,968],{"class":166},[160,1270,1271],{"class":170}," lay.items():\n",[160,1273,1274,1277,1280],{"class":162,"line":238},[160,1275,1276],{"class":166},"        if",[160,1278,1279],{"class":299}," isinstance",[160,1281,1282],{"class":170},"(item, QgsLayoutItemLabel):\n",[160,1284,1285,1288,1290],{"class":162,"line":243},[160,1286,1287],{"class":170},"            old ",[160,1289,225],{"class":166},[160,1291,1292],{"class":170}," item.text()\n",[160,1294,1295,1298,1300,1303,1306,1309,1313,1316,1319,1321,1323,1326],{"class":162,"line":254},[160,1296,1297],{"class":170},"            new ",[160,1299,225],{"class":166},[160,1301,1302],{"class":170}," re.sub(",[160,1304,1305],{"class":166},"r",[160,1307,1308],{"class":231},"\"",[160,1310,1312],{"class":1311},"sns5M","© ",[160,1314,1315],{"class":299},"\\d",[160,1317,1318],{"class":166},"{4}",[160,1320,1308],{"class":231},[160,1322,296],{"class":170},[160,1324,1325],{"class":231},"\"© [% year(now()) %]\"",[160,1327,1328],{"class":170},", old)\n",[160,1330,1331,1333,1335,1338,1341,1343,1346],{"class":162,"line":265},[160,1332,1297],{"class":170},[160,1334,225],{"class":166},[160,1336,1337],{"class":170}," new.replace(",[160,1339,1340],{"class":231},"\"Dept. of Planning\"",[160,1342,296],{"class":170},[160,1344,1345],{"class":231},"\"Planning & Environment Office\"",[160,1347,235],{"class":170},[160,1349,1350,1353,1356,1359],{"class":162,"line":276},[160,1351,1352],{"class":166},"            if",[160,1354,1355],{"class":170}," new ",[160,1357,1358],{"class":166},"!=",[160,1360,1361],{"class":170}," old:\n",[160,1363,1364],{"class":162,"line":287},[160,1365,1366],{"class":170},"                item.setText(new)\n",[160,1368,1369,1372,1375],{"class":162,"line":306},[160,1370,1371],{"class":170},"                changed ",[160,1373,1374],{"class":166},"+=",[160,1376,1377],{"class":299}," 1\n",[160,1379,1380,1382,1385,1388,1390,1393,1396,1399],{"class":162,"line":316},[160,1381,642],{"class":299},[160,1383,1384],{"class":170},"(changed, ",[160,1386,1387],{"class":231},"\"labels updated across\"",[160,1389,296],{"class":170},[160,1391,1392],{"class":299},"len",[160,1394,1395],{"class":170},"(manager.printLayouts()), ",[160,1397,1398],{"class":231},"\"layouts\"",[160,1400,235],{"class":170},[14,1402,1403,1405],{},[386,1404,388],{}," Iterating every print layout's items and filtering for labels reaches text in all of them, including layouts nobody has opened for months. Replacing a hard-coded copyright year with an expression means it never needs updating again — the general lesson being that anything that changes should be an expression or a variable, not literal text. Run such a sweep on a copy of the project first and save only when the count of changes matches what you expect.",[116,1407,1409],{"id":1408},"keep-text-consistent-and-readable","Keep text consistent and readable",[14,1411,1412],{},"A few practical rules prevent the commonest text problems in automated layouts.",[151,1414,1416],{"className":153,"code":1415,"language":155,"meta":156,"style":156},"def text_format(size, bold=False, colour=\"#2f3b35\", family=\"Noto Sans\"):\n    f = QgsTextFormat()\n    font = QFont(family, int(size))\n    font.setBold(bold)\n    f.setFont(font)\n    f.setSize(size)\n    f.setColor(QColor(colour))\n    return f\n\nSTYLES = {\"title\": text_format(20, True, \"#17211d\"),\n          \"subtitle\": text_format(12, False, \"#2f3b35\"),\n          \"note\": text_format(7, False, \"#59645f\")}\nfor item_id, style in ((\"title\", \"title\"), (\"source_note\", \"note\")):\n    item = layout.itemById(item_id)\n    if item is not None:\n        item.setTextFormat(STYLES[style])\n",[146,1417,1418,1453,1462,1478,1483,1488,1493,1498,1506,1510,1540,1559,1579,1610,1620,1639],{"__ignoreMap":156},[160,1419,1420,1423,1427,1430,1432,1435,1438,1440,1443,1446,1448,1450],{"class":162,"line":163},[160,1421,1422],{"class":166},"def",[160,1424,1426],{"class":1425},"svObZ"," text_format",[160,1428,1429],{"class":170},"(size, bold",[160,1431,225],{"class":166},[160,1433,1434],{"class":299},"False",[160,1436,1437],{"class":170},", colour",[160,1439,225],{"class":166},[160,1441,1442],{"class":231},"\"#2f3b35\"",[160,1444,1445],{"class":170},", family",[160,1447,225],{"class":166},[160,1449,293],{"class":231},[160,1451,1452],{"class":170},"):\n",[160,1454,1455,1458,1460],{"class":162,"line":180},[160,1456,1457],{"class":170},"    f ",[160,1459,225],{"class":166},[160,1461,284],{"class":170},[160,1463,1464,1467,1469,1472,1475],{"class":162,"line":186},[160,1465,1466],{"class":170},"    font ",[160,1468,225],{"class":166},[160,1470,1471],{"class":170}," QFont(family, ",[160,1473,1474],{"class":299},"int",[160,1476,1477],{"class":170},"(size))\n",[160,1479,1480],{"class":162,"line":199},[160,1481,1482],{"class":170},"    font.setBold(bold)\n",[160,1484,1485],{"class":162,"line":212},[160,1486,1487],{"class":170},"    f.setFont(font)\n",[160,1489,1490],{"class":162,"line":219},[160,1491,1492],{"class":170},"    f.setSize(size)\n",[160,1494,1495],{"class":162,"line":238},[160,1496,1497],{"class":170},"    f.setColor(QColor(colour))\n",[160,1499,1500,1503],{"class":162,"line":243},[160,1501,1502],{"class":166},"    return",[160,1504,1505],{"class":170}," f\n",[160,1507,1508],{"class":162,"line":254},[160,1509,216],{"emptyLinePlaceholder":215},[160,1511,1512,1515,1518,1521,1523,1526,1528,1530,1533,1535,1537],{"class":162,"line":265},[160,1513,1514],{"class":299},"STYLES",[160,1516,1517],{"class":166}," =",[160,1519,1520],{"class":170}," {",[160,1522,260],{"class":231},[160,1524,1525],{"class":170},": text_format(",[160,1527,300],{"class":299},[160,1529,296],{"class":170},[160,1531,1532],{"class":299},"True",[160,1534,296],{"class":170},[160,1536,322],{"class":231},[160,1538,1539],{"class":170},"),\n",[160,1541,1542,1545,1547,1549,1551,1553,1555,1557],{"class":162,"line":276},[160,1543,1544],{"class":231},"          \"subtitle\"",[160,1546,1525],{"class":170},[160,1548,352],{"class":299},[160,1550,296],{"class":170},[160,1552,1434],{"class":299},[160,1554,296],{"class":170},[160,1556,1442],{"class":231},[160,1558,1539],{"class":170},[160,1560,1561,1564,1566,1568,1570,1572,1574,1576],{"class":162,"line":287},[160,1562,1563],{"class":231},"          \"note\"",[160,1565,1525],{"class":170},[160,1567,441],{"class":299},[160,1569,296],{"class":170},[160,1571,1434],{"class":299},[160,1573,296],{"class":170},[160,1575,592],{"class":231},[160,1577,1578],{"class":170},")}\n",[160,1580,1581,1583,1586,1588,1591,1593,1595,1597,1600,1602,1604,1607],{"class":162,"line":306},[160,1582,1251],{"class":166},[160,1584,1585],{"class":170}," item_id, style ",[160,1587,968],{"class":166},[160,1589,1590],{"class":170}," ((",[160,1592,260],{"class":231},[160,1594,296],{"class":170},[160,1596,260],{"class":231},[160,1598,1599],{"class":170},"), (",[160,1601,506],{"class":231},[160,1603,296],{"class":170},[160,1605,1606],{"class":231},"\"note\"",[160,1608,1609],{"class":170},")):\n",[160,1611,1612,1615,1617],{"class":162,"line":316},[160,1613,1614],{"class":170},"    item ",[160,1616,225],{"class":166},[160,1618,1619],{"class":170}," layout.itemById(item_id)\n",[160,1621,1622,1625,1627,1630,1633,1636],{"class":162,"line":328},[160,1623,1624],{"class":166},"    if",[160,1626,1266],{"class":170},[160,1628,1629],{"class":166},"is",[160,1631,1632],{"class":166}," not",[160,1634,1635],{"class":299}," None",[160,1637,1638],{"class":170},":\n",[160,1640,1641,1644,1646],{"class":162,"line":334},[160,1642,1643],{"class":170},"        item.setTextFormat(",[160,1645,1514],{"class":299},[160,1647,1648],{"class":170},"[style])\n",[14,1650,1651,1653],{},[386,1652,388],{}," Defining text styles once and applying them by role keeps every layout in a series typographically consistent — the same title size and note colour on every page. Use sizes that survive printing: notes at 6–7 pt are the practical minimum, body text 8–10 pt. Check that the chosen font family exists where exports run; if it does not, Qt substitutes another font silently and layouts change. Embedding fonts in PDF exports is automatic for installed fonts.",[116,1655,1657],{"id":1656},"qgis-version-compatibility","QGIS version compatibility",[14,1659,1660,1662,1663,1665,1666,1668,1669,397,1671,1673,1674,296,1677,296,1680,296,1683,134],{},[146,1661,74],{},", label HTML mode, ",[146,1664,102],{}," and frames work on QGIS 3.34 LTR, 3.40 LTR and QGIS 4. ",[146,1667,392],{}," on labels requires QGIS 3.24 or newer; older code uses ",[146,1670,396],{},[146,1672,400],{},". On QGIS 4, enums are scoped: ",[146,1675,1676],{},"QgsLayoutItemLabel.Mode.ModeHtml",[146,1678,1679],{},"QgsLayoutItemHtml.ContentMode.ManualHtml",[146,1681,1682],{},"QgsLayoutMultiFrame.ResizeMode.ExtendToNextPage",[146,1684,1685],{},"Qt.AlignmentFlag.AlignLeft",[116,1687,1689],{"id":1688},"troubleshooting","Troubleshooting",[121,1691,1692,1700,1711,1719],{},[124,1693,1694,1697,1698,134],{},[386,1695,1696],{},"Expressions print literally."," A bracket is unbalanced or a function name is wrong; test with ",[146,1699,668],{},[124,1701,1702,389,1705,1707,1708,1710],{},[386,1703,1704],{},"HTML frame is empty.",[146,1706,1193],{}," was not called after ",[146,1709,1189],{},", or the content mode is still URL.",[124,1712,1713,1716,1717,134],{},[386,1714,1715],{},"Text is cut off."," The label frame is too small; enlarge it or call ",[146,1718,404],{},[124,1720,1721,1724],{},[386,1722,1723],{},"Fonts change on the server."," The font is not installed there; install it or choose a common family.",[116,1726,1728],{"id":1727},"conclusion","Conclusion",[14,1730,1731,1732,1734,1735,1737],{},"Use labels for short text with a ",[146,1733,148],{}," and explicit ids, embed ",[146,1736,415],{}," expressions for dates, metadata and counts, switch to HTML mode for small mixed formatting, use HTML frames for long text and custom tables that flow across pages, and define text styles once so every layout in a series matches.",[116,1739,1741],{"id":1740},"frequently-asked-questions","Frequently Asked Questions",[14,1743,1744,1747,1748,1751,1752,1755],{},[386,1745,1746],{},"Can a label show a value from the atlas feature?","\nYes: ",[146,1749,1750],{},"[% \"name\" %]"," or ",[146,1753,1754],{},"[% attribute(@atlas_feature, 'name') %]"," inside an atlas layout.",[14,1757,1758,1761,1762,1765],{},[386,1759,1760],{},"Can HTML frames load content from a web page?","\nYes, with ",[146,1763,1764],{},"QgsLayoutItemHtml.Url"," mode and a URL; local files work too.",[14,1767,1768,1771,1772,1775],{},[386,1769,1770],{},"How do I rotate a label?","\nUse ",[146,1773,1774],{},"setItemRotation(angle)"," on the item.",[14,1777,1778,1781,1782,1784],{},[386,1779,1780],{},"Can I use Markdown?","\nRecent QGIS releases support Markdown in labels as a text mode; check ",[146,1783,74],{}," modes on your version.",[116,1786,1788],{"id":1787},"related","Related",[121,1790,1791,1796,1801,1806,1812],{},[124,1792,1793,1795],{},[21,1794,24],{"href":23}," — the guide this recipe belongs to",[124,1797,1798],{},[21,1799,1800],{"href":132},"Load a Print Layout from a QPT Template in PyQGIS",[124,1802,1803],{},[21,1804,1805],{"href":1201},"Add an Attribute Table to a Layout in PyQGIS",[124,1807,1808],{},[21,1809,1811],{"href":1810},"\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series\u002Fatlas-expressions-and-dynamic-text-pyqgis\u002F","Atlas Expressions and Dynamic Text in PyQGIS",[124,1813,1814],{},[21,1815,1817],{"href":1816},"\u002Fpyqgis-cartography-visualization\u002Flabeling-and-annotations\u002Fuse-html-formatted-labels-pyqgis\u002F","Use HTML-Formatted Labels in PyQGIS",[1819,1820,1821],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}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 pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sns5M, html code.shiki .sns5M{--shiki-default:#DBEDFF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}",{"title":156,"searchDepth":180,"depth":180,"links":1823},[1824,1825,1826,1827,1828,1829,1830,1831,1832,1833,1834,1835],{"id":118,"depth":180,"text":119},{"id":140,"depth":180,"text":141},{"id":408,"depth":180,"text":409},{"id":672,"depth":180,"text":673},{"id":775,"depth":180,"text":776},{"id":1206,"depth":180,"text":1207},{"id":1408,"depth":180,"text":1409},{"id":1656,"depth":180,"text":1657},{"id":1688,"depth":180,"text":1689},{"id":1727,"depth":180,"text":1728},{"id":1740,"depth":180,"text":1741},{"id":1787,"depth":180,"text":1788},"Put text on print layouts from PyQGIS — titles and captions with QgsLayoutItemLabel, text formats and alignment, expressions for dates and data-driven text, HTML labels for mixed formatting, and HTML frames for long text and tables that flow across pages.","md",{"slug":1839,"type":1840,"breadcrumb":1841,"datePublished":1842,"dateModified":1842},"add-label-and-html-items-to-layout-pyqgis","article","Add Label and HTML Items to a Layout","2026-10-02","\u002Fspatial-data-processing-automation\u002Fautomated-map-layout-generation\u002Fadd-label-and-html-items-to-layout-pyqgis",{"title":5,"description":1836},"spatial-data-processing-automation\u002Fautomated-map-layout-generation\u002Fadd-label-and-html-items-to-layout-pyqgis\u002Findex","kzNxv6vcvW-Zn24hhkcuSno9dys47oxyP6a_9z1eZ7g",1790966260377]