55from docx .enum .text import WD_LINE_SPACING
66from docx .enum .section import WD_SECTION
77from docx .enum .section import WD_ORIENT
8+ from docx .enum .style import WD_STYLE_TYPE
89from dataclasses import dataclass
10+ from pathlib import Path
911
10- @dataclass
11- class DocxOptions :
12- page_height :float = 210
13- page_width :float = 297
14- landscape :bool = False
15- left_margin :float = 25.4
16- right_margin :float = 25.4
17- top_margin :float = 25.4
18- bottom_margin :float = 25.4
19- heading_text :str | None = None
20- heading_font_name :str = "Aptos"
21- heading_font_size = 12
22- heading_bold = True
23- heading_italic = False
24- heading_underline = False
25- heading_color = (0x00 , 0x00 , 0x00 )
26- heading_space_before = 0
27- heading_space_after = 6
28-
29- def prepare_docx (options :DocxOptions ):
30-
31- # create new document
32- doc = Document ()
33-
34- # Page layout
35- section = doc .sections [0 ]
36- if options .landscape :
37- section .page_height = Mm (options .page_height )
38- section .page_width = Mm (options .page_width )
39- section .orientation = WD_ORIENT .LANDSCAPE
40-
41- else :
42- section .page_height = Mm (options .page_width )
43- section .page_width = Mm (options .page_height )
44- section .orientation = WD_ORIENT .PORTRAIT
12+
13+ class DocxFile :
14+ def __init__ (
15+ self ,
16+ page_height : float = 210 ,
17+ page_width : float = 297 ,
18+ landscape : bool = False ,
19+ left_margin : float = 25.4 ,
20+ right_margin : float = 25.4 ,
21+ top_margin : float = 25.4 ,
22+ bottom_margin : float = 25.4 ,
23+ heading_text : str | None = None ,
24+ heading_font_name : str = "Aptos" ,
25+ heading_font_size : int = 12 ,
26+ heading_bold : bool = True ,
27+ heading_italic : bool = False ,
28+ heading_underline : bool = False ,
29+ heading_color : [int , int , int ] = (0x00 , 0x00 , 0x00 ),
30+ heading_space_before : int = 0 ,
31+ heading_space_after : int = 6 ,
32+ ):
33+
34+ # create new document
35+ self .doc = Document ()
36+
37+ # Page layout
38+ section = self .doc .sections [0 ]
39+ if landscape :
40+ self .page_height = page_height
41+ self .page_width = page_width
42+ section .orientation = WD_ORIENT .LANDSCAPE
43+ else :
44+ self .page_height = page_width
45+ self .page_width = page_height
46+ section .orientation = WD_ORIENT .PORTRAIT
47+
48+ section .page_height = Mm (self .page_height )
49+ section .page_width = Mm (self .page_width )
50+
51+ # Page margins
52+ section .left_margin = Mm (left_margin )
53+ section .right_margin = Mm (right_margin )
54+ section .top_margin = Mm (top_margin )
55+ section .bottom_margin = Mm (bottom_margin )
56+ section .header_distance = Mm (12.7 )
57+ section .footer_distance = Mm (12.7 )
58+
59+ # Add section heading
60+ if heading_text :
61+ # Add heading
62+ self .doc .add_heading (heading_text )
63+
64+ # Set style of the header
65+ style = self .doc .styles ["Heading 1" ]
66+
67+ font = style .font
68+
69+ font .name = heading_font_name
70+ # Setting the style of the header requires this additional workaround
71+ # see: https://stackoverflow.com/a/60922725/1719931
72+ rFonts = style .element .rPr .rFonts
73+ rFonts .set (qn ("w:asciiTheme" ), heading_font_name )
74+
75+ font .size = Pt (heading_font_size )
76+ font .bold = heading_bold
77+ font .italic = heading_italic
78+ font .underline = heading_underline
79+
80+ font .color .rgb = RGBColor (* heading_color )
81+
82+ paragraph_format = style .paragraph_format
83+ paragraph_format .space_before = Pt (heading_space_before )
84+ paragraph_format .space_after = Pt (heading_space_after )
85+ paragraph_format .line_spacing_rule = WD_LINE_SPACING .SINGLE
4586
46- section .left_margin = Mm (options .left_margin )
47- section .right_margin = Mm (options .right_margin )
48- section .top_margin = Mm (options .top_margin )
49- section .bottom_margin = Mm (options .bottom_margin )
50- section .header_distance = Mm (12.7 )
51- section .footer_distance = Mm (12.7 )
52-
53- # Add section heading
54- if options .heading_text :
55-
56- # Add heading
57- doc .add_heading (options .heading_text )
87+ self .table = None
88+
89+ def add_table (
90+ self ,
91+ n_rows ,
92+ n_cols ,
93+ table_autofit : bool = True ,
94+ table_style : str = "Table Grid" ,
95+ table_font_name : str = "Aptos" ,
96+ table_font_size : int = 12 ,
97+ table_header_font_name : str = "Aptos" ,
98+ table_header_font_size : int = 12 ,
99+ table_header_font_bold : bool = True ,
100+ ):
101+
102+ # Create table
103+ self .table_autofit = table_autofit
104+ self .table = self .doc .add_table (n_rows , n_cols )
58105
59- # Set style of the header
60- style = doc .styles ["Heading 1" ]
106+ # Set table style
107+ # See: https://github.com/python-openxml/python-docx/issues/9
108+ self .table .style = table_style
61109
110+ # Create style for table header (first row)
111+ style = self .doc .styles .add_style ("CellHeader" , WD_STYLE_TYPE .PARAGRAPH )
112+ style .base_style = self .doc .styles ["Normal" ]
62113 font = style .font
114+ font .name = table_header_font_name
115+ font .size = Pt (table_header_font_size )
116+ font .bold = table_header_font_bold
63117
64- font .name = options .heading_font_name
65- # Setting the style of the header requires this additional workaround
66- # see: https://stackoverflow.com/a/60922725/1719931
67- rFonts = style .element .rPr .rFonts
68- rFonts .set (qn ("w:asciiTheme" ), options .heading_font_name )
118+ # Create style for table cells (second row onwards)
119+ style = self .doc .styles .add_style ("CellText" , WD_STYLE_TYPE .PARAGRAPH )
120+ style .base_style = self .doc .styles ["Normal" ]
121+ font = style .font
122+ font .name = table_font_name
123+ font .size = Pt (table_font_size )
69124
70- font .size = Pt (options .heading_font_size )
71- font .bold = options .heading_bold
72- font .italic = options .heading_italic
73- font .underline = options .heading_underline
125+ # Additional workaround to make autofit actually work
126+ # See: https://github.com/python-openxml/python-docx/issues/209#issuecomment-566128709
127+ def _table_autofit_hotfix (self ):
128+ for column in self .table .columns :
129+ for cell in column .cells :
130+ tc = cell ._tc
131+ tcPr = tc .get_or_add_tcPr ()
132+ tcW = tcPr .get_or_add_tcW ()
133+ tcW .type = "auto"
74134
75- font .color .rgb = RGBColor (* options .heading_color )
135+ def save (self , outfp : Path ):
136+
137+ if self .table :
138+ # Hotfix for table autofit
139+ if self .table_autofit :
140+ self ._table_autofit_hotfix ()
76141
77- paragraph_format = style .paragraph_format
78- paragraph_format .space_before = Pt (options .heading_space_before )
79- paragraph_format .space_after = Pt (options .heading_space_after )
80- paragraph_format .line_spacing_rule = WD_LINE_SPACING .SINGLE
81-
82- return doc
83-
84- # Additional workaround to make autofit actually work
85- # See: https://github.com/python-openxml/python-docx/issues/209#issuecomment-566128709
86- def table_autofit_hotfix (table ):
87- for column in table .columns :
88- for cell in column .cells :
89- tc = cell ._tc
90- tcPr = tc .get_or_add_tcPr ()
91- tcW = tcPr .get_or_add_tcW ()
92- tcW .type = 'auto'
93- return table
142+ # Set table text style for header (row 0)
143+ for cell in self .table .rows [0 ].cells :
144+ for paragraph in cell .paragraphs :
145+ paragraph .style = "CellHeader"
146+
147+ # Set table text style for data cells (rows >= 1)
148+ for row in self .table .rows [1 :]:
149+ for cell in row .cells :
150+ for paragraph in cell .paragraphs :
151+ paragraph .style = "CellText" # assuming style named "Cell Text"
152+
153+ # save the doc
154+ self .doc .save (outfp )
0 commit comments