Skip to content
Advertisement

How do I change Heading font face and size in python-docx?

I filed this as a python-docx issue: https://github.com/python-openxml/python-docx/issues/805 but was requested to open a discussion here.

https://python-docx.readthedocs.io/en/latest/user/styles-using.html implies that I should be able to change Heading font styles like this:

font = doc.styles['Heading 1'].font
font.name = 'Times New Roman'
font.size = docx.shared.Pt(16)

But that doesn’t work: the resulting document uses Calibri for all headings. (They’re also blue and Heading 1 has an underline, which I also need to eliminate somehow.)

It also doesn’t work to change the font on a specific heading, nor to delete the latent_styles for the headings.

Here’s a test program that tries all three methods, but the Headings 1 and 2 still come out as blue Calibri despite all attempts to change them to Times New Roman:

import docx

doc = docx.Document()

# Deleting heading latent styles seems to do nothing:
latent_styles = doc.styles.latent_styles
latent_styles['Heading 1'].delete()
latent_styles['Heading 2'].delete()

# Setting the Normal font works:
font = doc.styles['Normal'].font
font.name = 'Times New Roman'
font.size = docx.shared.Pt(12)

# Setting heading styles doesn't do anything:
# they all still end up as blue Calibri.
font = doc.styles['Heading 1'].font
font.name = 'Times New Roman'
font.size = docx.shared.Pt(16)

font = doc.styles['Heading 2'].font
font.name = 'Times New Roman'
font.size = docx.shared.Pt(14)

doc.add_heading("Heading 0", 0)
doc.add_paragraph('Here is a paragraph of text.')
doc.add_heading("Heading 1", 1)
doc.add_paragraph('Here is a paragraph of text.')
doc.add_heading("Heading 2", 2)
doc.add_paragraph('Here is a paragraph of text.')

# It also doesn't work to change the style of a specific heading:
heading = doc.add_heading("Another Heading 1", 1)
heading.style.font.name = "Times New Roman"
doc.add_paragraph('Here is a paragraph of text.')

doc.save('test.docx')

Can’t change “heading 1” font name using docx mentions this as a bug, and suggests creating a new style as a workaround. A simpler workaround is to create runs inside normal paragraph text, then style those runs. But it seems like it would be better to use standard elements like “Heading 1” if it was possible to style those headings as something other than blue Calibri … and the documentation implies this is possible.

Advertisement

Answer

As you observe, normally changing the typeface (font.name) for a style “just works”. For reasons I don’t fully understand, the Title and perhaps Heading 1, Heading 2, etc. styles are an exception. I expect this has to do with their font choices being specified by the theme. Perhaps it is related to their special role in forming a table-of-contents.

To start, a couple observations:

  • The style applied by document.add_heading("0th-level Heading", 0) is Title. This makes some sort of sense I suppose, in that the highest-level heading entitles the whole document. The styles Heading 1, Heading 2, etc. are applied when 1 and 2 are used in that function call, respectively.

  • If we apply the font-name “Times New Roman” to the Title style and then inspect the XML generated we see the following:

>>> heading = document.add_heading("Title", 0)
>>> title_style = heading.style
>>> title_style.font.name = "Times New Roman"
>>> title_style.element.xml
<w:style xmlns:w=... w:type="paragraph" w:styleId="Title">
  <w:name w:val="Title"/>
  <w:basedOn w:val="Normal"/>
  <w:next w:val="Normal"/>
  <w:link w:val="TitleChar"/>
  <w:uiPriority w:val="10"/>
  <w:qFormat/>
  <w:rsid w:val="00FC693F"/>
  <w:pPr>
    <w:pBdr>
      <w:bottom w:val="single" w:sz="8" w:space="4" w:color="4F81BD" w:themeColor="accent1"/>
    </w:pBdr>
    <w:spacing w:after="300" w:line="240" w:lineRule="auto"/>
    <w:contextualSpacing/>
  </w:pPr>
  <w:rPr>
    <w:rFonts w:asciiTheme="majorHAnsi" w:eastAsiaTheme="majorEastAsia"
              w:hAnsiTheme="majorHAnsi" w:cstheme="majorBidi"
              w:ascii="Times New Roman" w:hAnsi="Times New Roman"/>
    <w:color w:val="17365D" w:themeColor="text2" w:themeShade="BF"/>
    <w:spacing w:val="5"/>
    <w:kern w:val="28"/>
    <w:sz w:val="52"/>
    <w:szCs w:val="52"/>
  </w:rPr>
</w:style>
  • From this we can see a lot if interesting items, but our focus for the moment can be limited to the <w:rFonts> element:
>>> title_style.element.rPr.rFonts.xml
<w:rFonts w:asciiTheme="majorHAnsi" w:eastAsiaTheme="majorEastAsia"
          w:hAnsiTheme="majorHAnsi" w:cstheme="majorBidi"
          w:ascii="Times New Roman" w:hAnsi="Times New Roman"/>

We can see that “Times New Roman” has indeed by applied to two of the font settings, yet the title still appears in Calibri, which as it happens is what “majorHAnsi” maps to.

To jump to the solution, if we set the w:asciiTheme font-name to “Times New Roman”, the heading appears as desired:

from docx.oxml.ns import qn

rFonts = title_style.element.rPr.rFonts
rFonts.set(qn("w:asciiTheme"), "Times New Roman")

I expect the same sort of procedure will work on other heading styles.

Note that if you are generating a document from “scratch” rather than editing an existing one, it may be easier to start with a blank document that already has the styles you want:

document = Document("my-starting-document.docx")
User contributions licensed under: CC BY-SA
9 People found this is helpful
Advertisement