TiH0 commited on
Commit
8923e89
·
verified ·
1 Parent(s): 4137799

Update meta.py

Browse files
Files changed (1) hide show
  1. meta.py +465 -424
meta.py CHANGED
@@ -1,3 +1,12 @@
 
 
 
 
 
 
 
 
 
1
  import re
2
  import os
3
  import html
@@ -18,6 +27,36 @@ import tempfile
18
  THEME_COLOR_HEX = "5FFFDF" # Hex version for XML elements
19
  THEME_COLOR = RGBColor.from_string(THEME_COLOR_HEX)
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  # Common paper sizes (width x height in inches)
22
  PAPER_SIZES = {
23
  'LETTER': (8.5, 11), # US Letter
@@ -262,30 +301,236 @@ def find_image_in_folder(filename, image_folder):
262
  return None
263
 
264
 
265
- def process_excel_to_word(excel_file_path, output_word_path, image_folder=None, display_name=None, use_two_columns=True,
266
- add_separator_line=True, balance_method="dynamic", theme_hex=None):
267
- """Main function to process Excel and create a Word document with TOC on the first page"""
268
- if theme_hex is None:
269
- theme_hex = THEME_COLOR_HEX
270
- theme_color = RGBColor.from_string(theme_hex)
271
-
272
- # Prepare image folder (extract if ZIP) - gracefully handle None
273
- actual_image_folder, is_temp, temp_dir_obj = prepare_image_folder(image_folder)
274
-
275
- # Map images from the prepared folder (returns empty dict if None)
276
- question_photos = map_images_from_excel(excel_file_path, actual_image_folder)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
 
278
- # ... rest of the function remains the same ...
279
- # The code will now handle missing images gracefully since question_photos will be empty
 
280
 
281
- # At the end, clean up temporary folder if it was created
282
- if is_temp and temp_dir_obj is not None:
283
- print(f"\n🧹 Cleaning up temporary folder...")
284
- try:
285
- temp_dir_obj.cleanup()
286
- print(f" ✓ Temporary files removed")
287
- except Exception as e:
288
- print(f" ⚠️ Could not clean up: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
 
290
 
291
  def preview_image_mapping(question_images):
@@ -609,7 +854,7 @@ def create_course_title(doc, course_number, course_title, theme_color=None, them
609
 
610
 
611
  def highlight_words_in_text(paragraph, text, highlight_words, theme_color, font_name='Inter Display Medium',
612
- font_size=10.5, bold=False):
613
  """
614
  Add text to paragraph with specific words/substrings highlighted in theme color.
615
  Highlights literal text matches (including special characters like parentheses, backslashes).
@@ -632,9 +877,6 @@ def highlight_words_in_text(paragraph, text, highlight_words, theme_color, font_
632
  run.font.bold = True
633
  return
634
 
635
- # Create pattern for matching (escape each string to treat as literal text)
636
- import re
637
- # Escape each word/phrase to match it literally, then join with OR
638
  escaped_words = [re.escape(word) for word in highlight_words]
639
  pattern = '(' + '|'.join(escaped_words) + ')'
640
 
@@ -658,13 +900,15 @@ def highlight_words_in_text(paragraph, text, highlight_words, theme_color, font_
658
 
659
  def format_question_block(doc, question_num, question_text, choices, correct_answers, source, comment=None,
660
  choice_commentaire=None, photo_q=None, photo_c=None, theme_color=None, theme_hex=None,
661
- highlight_words=None):
662
  if theme_color is None:
663
  theme_color = THEME_COLOR
664
  if theme_hex is None:
665
  theme_hex = THEME_COLOR_HEX
666
  if highlight_words is None:
667
  highlight_words = []
 
 
668
 
669
  """Format a single question block with reduced spacing and keep together formatting"""
670
 
@@ -804,47 +1048,103 @@ def add_page_numbers(doc, theme_hex=None):
804
  if theme_hex is None:
805
  theme_hex = THEME_COLOR_HEX
806
 
 
 
 
 
 
 
 
 
 
 
807
  def create_footer_content(footer_elem, theme_hex):
808
- """Helper function to create footer content with page number and TOC link"""
809
- # Add an empty line above the page number
 
 
810
  empty_para = footer_elem.paragraphs[0]
811
  empty_para.paragraph_format.space_before = Pt(0)
812
  empty_para.paragraph_format.space_after = Pt(0)
813
  empty_para.paragraph_format.line_spacing = 1.0
814
 
815
- # Add the page number paragraph
816
  paragraph = footer_elem.add_paragraph()
817
  paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
818
-
819
- # Set vertical alignment to center
820
  paragraph.paragraph_format.space_before = Pt(0)
821
  paragraph.paragraph_format.space_after = Pt(0)
822
 
823
- # Add page number in center
824
  run = paragraph.add_run()
825
-
826
- # Create the PAGE field
827
  fldChar1 = OxmlElement('w:fldChar')
828
  fldChar1.set(qn('w:fldCharType'), 'begin')
829
-
830
  instrText = OxmlElement('w:instrText')
831
  instrText.set(qn('xml:space'), 'preserve')
832
  instrText.text = "PAGE"
833
-
834
  fldChar2 = OxmlElement('w:fldChar')
835
  fldChar2.set(qn('w:fldCharType'), 'end')
836
-
837
  run._r.append(fldChar1)
838
  run._r.append(instrText)
839
  run._r.append(fldChar2)
840
-
841
  run.font.name = 'Montserrat'
842
  run.font.size = Pt(14)
843
  run.font.bold = True
844
  run.font.color.rgb = RGBColor.from_string(theme_hex)
845
 
846
- # ===== ADD TOC LINK IN TEXT BOX (BOTTOM RIGHT) =====
847
- # Create TOC link text box - absolutely positioned, does not affect page number centering
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
848
  toc_textbox_xml = f'''
849
  <w:r xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
850
  xmlns:v="urn:schemas-microsoft-com:vml"
@@ -885,161 +1185,38 @@ def add_page_numbers(doc, theme_hex=None):
885
  </w:pict>
886
  </w:r>
887
  '''
888
-
889
- toc_textbox_element = parse_xml(toc_textbox_xml)
890
- paragraph._p.append(toc_textbox_element)
891
 
892
  for section_idx, section in enumerate(doc.sections):
893
- # ===== HEADER (keep existing text like module name) =====
894
  header = section.header
895
  header.is_linked_to_previous = False
896
  section.header_distance = Cm(0.3)
897
-
898
- # If header is empty, add a blank paragraph
899
  if not header.paragraphs:
900
  header.add_paragraph()
901
 
902
- # ===== FOOTER FOR ODD/DEFAULT PAGES (page numbers + TOC link) =====
903
  footer = section.footer
904
  footer.is_linked_to_previous = False
905
- section.footer_distance = Cm(0.4) # Distance from bottom of page to footer
906
 
907
- # Clear existing text in footer
908
  if footer.paragraphs:
909
  footer.paragraphs[0].clear()
910
  else:
911
  footer.add_paragraph()
912
 
913
- # Skip page numbers for the first section (TOC)
914
  if section_idx == 0:
915
  continue
916
 
917
- # For the second section (first content page), restart numbering at 1
918
  if section_idx == 1:
919
  sectPr = section._sectPr
920
  pgNumType = sectPr.find(qn('w:pgNumType'))
921
  if pgNumType is None:
922
  pgNumType = OxmlElement('w:pgNumType')
923
  sectPr.append(pgNumType)
924
- pgNumType.set(qn('w:start'), '1') # Start at page 1
925
 
926
- # Create footer content for odd/default pages
927
  create_footer_content(footer, theme_hex)
928
 
929
- # ===== CREATE EVEN PAGE FOOTER =====
930
- try:
931
- # Check if even_page_footer property exists
932
- if hasattr(section, 'even_page_footer'):
933
- footer_even = section.even_page_footer
934
- footer_even.is_linked_to_previous = False
935
- if not footer_even.paragraphs:
936
- footer_even.add_paragraph()
937
- else:
938
- footer_even.paragraphs[0].clear()
939
- create_footer_content(footer_even, theme_hex)
940
- print("✓ Created even page footer using built-in property")
941
- else:
942
- # Manual method - create even footer via XML
943
- from docx.opc.packuri import PackURI
944
- from docx.opc.part import XmlPart
945
-
946
- # Build even footer XML with same structure as odd footer
947
- even_ftr_xml = f'''<w:ftr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
948
- xmlns:v="urn:schemas-microsoft-com:vml"
949
- xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
950
- <w:p>
951
- <w:pPr><w:spacing w:before="0" w:after="0"/></w:pPr>
952
- </w:p>
953
- <w:p>
954
- <w:pPr>
955
- <w:jc w:val="center"/>
956
- <w:spacing w:before="0" w:after="0"/>
957
- </w:pPr>
958
- <w:r>
959
- <w:fldChar w:fldCharType="begin"/>
960
- </w:r>
961
- <w:r>
962
- <w:instrText xml:space="preserve">PAGE</w:instrText>
963
- </w:r>
964
- <w:r>
965
- <w:fldChar w:fldCharType="end"/>
966
- </w:r>
967
- <w:r>
968
- <w:rPr>
969
- <w:rFonts w:ascii="Montserrat" w:hAnsi="Montserrat"/>
970
- <w:b/>
971
- <w:sz w:val="28"/>
972
- <w:color w:val="{theme_hex}"/>
973
- </w:rPr>
974
- </w:r>
975
- <w:r>
976
- <w:pict>
977
- <v:shape style="position:absolute;margin-left:0in;margin-top:0;width:60pt;height:20pt;z-index:1;mso-position-horizontal:right;mso-position-horizontal-relative:margin;mso-position-vertical-relative:line" fillcolor="#FFFFFF" filled="f" stroked="f">
978
- <v:textbox inset="5pt,0pt,5pt,0pt" style="mso-fit-shape-to-text:t">
979
- <w:txbxContent>
980
- <w:p>
981
- <w:pPr>
982
- <w:jc w:val="right"/>
983
- <w:spacing w:before="0" w:after="0"/>
984
- </w:pPr>
985
- <w:hyperlink w:anchor="TOC_BOOKMARK">
986
- <w:r>
987
- <w:rPr>
988
- <w:rFonts w:ascii="Aptos" w:hAnsi="Aptos"/>
989
- <w:sz w:val="28"/>
990
- <w:color w:val="{theme_hex}"/>
991
- </w:rPr>
992
- <w:t>↗️</w:t>
993
- </w:r>
994
- <w:r>
995
- <w:rPr>
996
- <w:rFonts w:ascii="Montserrat" w:hAnsi="Montserrat"/>
997
- <w:b/>
998
- <w:sz w:val="18"/>
999
- <w:color w:val="{theme_hex}"/>
1000
- <w:u w:val="single"/>
1001
- </w:rPr>
1002
- <w:t> SOM</w:t>
1003
- </w:r>
1004
- </w:hyperlink>
1005
- </w:p>
1006
- </w:txbxContent>
1007
- </v:textbox>
1008
- </v:shape>
1009
- </w:pict>
1010
- </w:r>
1011
- </w:p>
1012
- </w:ftr>'''
1013
-
1014
- # Create part
1015
- partname = PackURI(f'/word/footer_even_{id(section)}.xml')
1016
- element = parse_xml(even_ftr_xml)
1017
- content_type = 'application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml'
1018
- package = section.part.package
1019
- even_part = XmlPart(partname, content_type, element, package)
1020
-
1021
- # Create relationship
1022
- rId = section.part.relate_to(even_part,
1023
- 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer')
1024
-
1025
- # Add footer reference
1026
- sectPr = section._sectPr
1027
- # Remove any existing even footer references
1028
- for ref in list(sectPr.findall(qn('w:footerReference'))):
1029
- if ref.get(qn('w:type')) == 'even':
1030
- sectPr.remove(ref)
1031
-
1032
- ftr_ref = OxmlElement('w:footerReference')
1033
- ftr_ref.set(qn('w:type'), 'even')
1034
- ftr_ref.set(qn('r:id'), rId)
1035
- sectPr.append(ftr_ref)
1036
-
1037
- print("✓ Created even page footer via manual part creation")
1038
-
1039
- except Exception as e:
1040
- print(f"Warning: Could not create even page footer: {e}")
1041
- import traceback
1042
- traceback.print_exc()
1043
 
1044
 
1045
  def add_toc_bookmark(doc, toc_title_para):
@@ -1970,46 +2147,31 @@ def read_course_titles_from_module_sheet(excel_file_path, module_name):
1970
 
1971
 
1972
  def enable_odd_even_headers(doc):
1973
- """Enable different odd and even page headers/footers for the entire document"""
 
1974
  try:
1975
- # Access the document settings
1976
- settings = doc.settings
1977
- settings_element = settings.element
1978
-
1979
- # Add evenAndOddHeaders element if it doesn't exist
1980
  even_odd = settings_element.find(qn('w:evenAndOddHeaders'))
1981
- if even_odd is None:
1982
- even_odd = OxmlElement('w:evenAndOddHeaders')
1983
- # Insert at the beginning of settings
1984
- settings_element.insert(0, even_odd)
1985
- print("✓ Enabled odd/even page headers in document settings")
1986
  else:
1987
- print("✓ Odd/even page headers already enabled")
1988
  except Exception as e:
1989
- print(f"Warning: Could not enable odd/even headers: {e}")
1990
- # Try alternative method - modify the XML directly
1991
- try:
1992
- doc_element = doc.element
1993
- body = doc_element.body
1994
- # Find or create sectPr
1995
- sectPr = body.sectPr
1996
- if sectPr is not None:
1997
- print("✓ Document structure ready for odd/even headers")
1998
- except Exception as e2:
1999
- print(f"Warning: Alternative method also failed: {e2}")
2000
-
2001
-
2002
  def create_flexible_header(section, module_name, sheet_name, display_name=None, left_margin_inches=0,
2003
  right_margin_inches=0, theme_hex=None):
2004
- """Create flexible header text boxes that switch positions on odd/even pages"""
 
 
 
 
2005
  if theme_hex is None:
2006
  theme_hex = THEME_COLOR_HEX
2007
 
2008
  section.header_distance = Cm(0.6)
2009
 
2010
  module_name_str = str(module_name).upper()
2011
-
2012
- # Use display_name if provided, otherwise use sheet_name
2013
  if display_name:
2014
  sheet_name_str = str(display_name).upper()
2015
  else:
@@ -2018,175 +2180,77 @@ def create_flexible_header(section, module_name, sheet_name, display_name=None,
2018
  module_name_str = html.escape(module_name_str)
2019
  sheet_name_str = html.escape(sheet_name_str)
2020
 
2021
- # Calculate approximate widths based on text length
2022
- module_width = max(len(module_name_str) * 10 + 60, 100)
2023
- sheet_width = max(len(sheet_name_str) * 10 + 60, 100)
2024
-
2025
- def create_header_content(paragraph, left_text, left_width, right_text, right_width):
2026
- """Helper to create header content with two text boxes"""
2027
- paragraph.clear()
2028
-
2029
- left_xml = f'''<w:r xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
2030
- xmlns:v="urn:schemas-microsoft-com:vml"
2031
- xmlns:w10="urn:schemas-microsoft-com:office:word">
2032
- <w:pict>
2033
- <v:shape style="position:absolute;margin-left:{left_margin_inches}in;margin-top:0;width:{left_width}pt;height:25pt;z-index:1;mso-position-horizontal:left;mso-position-horizontal-relative:margin;mso-position-vertical-relative:line" fillcolor="#FFFFFF" filled="f" stroked="f">
2034
- <v:textbox inset="5pt,0pt,5pt,0pt" style="mso-fit-shape-to-text:t">
2035
- <w:txbxContent>
2036
- <w:p>
2037
- <w:pPr>
2038
- <w:jc w:val="left"/>
2039
- <w:spacing w:before="0" w:after="0"/>
2040
- </w:pPr>
2041
- <w:r>
2042
- <w:rPr>
2043
- <w:rFonts w:ascii="Montserrat" w:hAnsi="Montserrat"/>
2044
- <w:b/>
2045
- <w:sz w:val="26"/>
2046
- <w:color w:val="{theme_hex}"/>
2047
- </w:rPr>
2048
- <w:t>{left_text}</w:t>
2049
- </w:r>
2050
- </w:p>
2051
- </w:txbxContent>
2052
- </v:textbox>
2053
- </v:shape>
2054
- </w:pict>
2055
- </w:r>'''
2056
-
2057
- right_xml = f'''<w:r xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
2058
- xmlns:v="urn:schemas-microsoft-com:vml"
2059
- xmlns:w10="urn:schemas-microsoft-com:office:word">
2060
- <w:pict>
2061
- <v:shape style="position:absolute;margin-left:{right_margin_inches}in;margin-top:0;width:{right_width}pt;height:25pt;z-index:1;mso-position-horizontal:right;mso-position-horizontal-relative:margin;mso-position-vertical-relative:line" fillcolor="#FFFFFF" filled="f" stroked="f">
2062
- <v:textbox inset="5pt,0pt,5pt,0pt" style="mso-fit-shape-to-text:t">
2063
- <w:txbxContent>
2064
- <w:p>
2065
- <w:pPr>
2066
- <w:jc w:val="right"/>
2067
- <w:spacing w:before="0" w:after="0"/>
2068
- </w:pPr>
2069
- <w:r>
2070
- <w:rPr>
2071
- <w:rFonts w:ascii="Montserrat" w:hAnsi="Montserrat"/>
2072
- <w:b/>
2073
- <w:sz w:val="26"/>
2074
- <w:color w:val="{theme_hex}"/>
2075
- </w:rPr>
2076
- <w:t>{right_text}</w:t>
2077
- </w:r>
2078
- </w:p>
2079
- </w:txbxContent>
2080
- </v:textbox>
2081
- </v:shape>
2082
- </w:pict>
2083
- </w:r>'''
2084
-
2085
- paragraph._p.append(parse_xml(left_xml))
2086
- paragraph._p.append(parse_xml(right_xml))
2087
-
2088
- # ========== CREATE DEFAULT/ODD PAGES HEADER (Sheet Left, Module Right) ==========
2089
- header_odd = section.header
2090
- header_odd.is_linked_to_previous = False
2091
- if not header_odd.paragraphs:
2092
- header_odd.add_paragraph()
2093
-
2094
- create_header_content(header_odd.paragraphs[0], sheet_name_str, sheet_width, module_name_str, module_width)
2095
-
2096
- # ========== CREATE EVEN PAGES HEADER (Module Left, Sheet Right) ==========
2097
- try:
2098
- # Check if even_page_header property exists
2099
- if hasattr(section, 'even_page_header'):
2100
- header_even = section.even_page_header
2101
- header_even.is_linked_to_previous = False
2102
- if not header_even.paragraphs:
2103
- header_even.add_paragraph()
2104
- create_header_content(header_even.paragraphs[0], module_name_str, module_width, sheet_name_str, sheet_width)
2105
- print("✓ Created even page header using built-in property")
2106
- else:
2107
- # Manual method
2108
- from docx.opc.packuri import PackURI
2109
- from docx.opc.part import XmlPart
2110
-
2111
- # Build even header XML
2112
- even_hdr_xml = f'''<w:hdr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
2113
- xmlns:v="urn:schemas-microsoft-com:vml"
2114
- xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
2115
- <w:p>
2116
- <w:pPr><w:spacing w:before="0" w:after="0"/></w:pPr>
2117
- <w:r>
2118
- <w:pict>
2119
- <v:shape style="position:absolute;margin-left:{left_margin_inches}in;margin-top:0;width:{module_width}pt;height:25pt;z-index:1;mso-position-horizontal:left;mso-position-horizontal-relative:margin;mso-position-vertical-relative:line" fillcolor="#FFFFFF" filled="f" stroked="f">
2120
- <v:textbox inset="5pt,0pt,5pt,0pt" style="mso-fit-shape-to-text:t">
2121
- <w:txbxContent>
2122
- <w:p>
2123
- <w:pPr><w:jc w:val="left"/><w:spacing w:before="0" w:after="0"/></w:pPr>
2124
- <w:r>
2125
- <w:rPr>
2126
- <w:rFonts w:ascii="Montserrat" w:hAnsi="Montserrat"/>
2127
- <w:b/><w:sz w:val="26"/><w:color w:val="{theme_hex}"/>
2128
- </w:rPr>
2129
- <w:t>{module_name_str}</w:t>
2130
- </w:r>
2131
- </w:p>
2132
- </w:txbxContent>
2133
- </v:textbox>
2134
- </v:shape>
2135
- </w:pict>
2136
- </w:r>
2137
- <w:r>
2138
- <w:pict>
2139
- <v:shape style="position:absolute;margin-left:{right_margin_inches}in;margin-top:0;width:{sheet_width}pt;height:25pt;z-index:1;mso-position-horizontal:right;mso-position-horizontal-relative:margin;mso-position-vertical-relative:line" fillcolor="#FFFFFF" filled="f" stroked="f">
2140
- <v:textbox inset="5pt,0pt,5pt,0pt" style="mso-fit-shape-to-text:t">
2141
- <w:txbxContent>
2142
- <w:p>
2143
- <w:pPr><w:jc w:val="right"/><w:spacing w:before="0" w:after="0"/></w:pPr>
2144
- <w:r>
2145
- <w:rPr>
2146
- <w:rFonts w:ascii="Montserrat" w:hAnsi="Montserrat"/>
2147
- <w:b/><w:sz w:val="26"/><w:color w:val="{theme_hex}"/>
2148
- </w:rPr>
2149
- <w:t>{sheet_name_str}</w:t>
2150
- </w:r>
2151
- </w:p>
2152
- </w:txbxContent>
2153
- </v:textbox>
2154
- </v:shape>
2155
- </w:pict>
2156
- </w:r>
2157
- </w:p>
2158
- </w:hdr>'''
2159
-
2160
- # Create part
2161
- partname = PackURI(f'/word/header_even_{id(section)}.xml')
2162
- element = parse_xml(even_hdr_xml)
2163
- content_type = 'application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml'
2164
- package = section.part.package
2165
- even_part = XmlPart(partname, content_type, element, package)
2166
-
2167
- # Create relationship
2168
- rId = section.part.relate_to(even_part,
2169
- 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/header')
2170
-
2171
- # Add header reference
2172
- sectPr = section._sectPr
2173
- for ref in list(sectPr.findall(qn('w:headerReference'))):
2174
- if ref.get(qn('w:type')) == 'even':
2175
- sectPr.remove(ref)
2176
-
2177
- hdr_ref = OxmlElement('w:headerReference')
2178
- hdr_ref.set(qn('w:type'), 'even')
2179
- hdr_ref.set(qn('r:id'), rId)
2180
- sectPr.append(hdr_ref)
2181
-
2182
- print("✓ Created even page header via manual part creation")
2183
-
2184
- except Exception as e:
2185
- print(f"Warning: Could not create even header: {e}")
2186
- import traceback
2187
- traceback.print_exc()
2188
-
2189
 
 
 
2190
  def extract_display_name_from_excel(excel_file_path):
2191
  """Extract display name from Excel file - checks multiple locations"""
2192
  try:
@@ -2278,68 +2342,20 @@ def add_colored_column_separator(section, theme_hex=None):
2278
  # Add line to odd/default header
2279
  header = section.header
2280
  add_line_to_header(header, "columnSeparatorOdd")
2281
-
2282
- # Add line to even header
2283
- try:
2284
- # Check if even_page_header property exists
2285
- if hasattr(section, 'even_page_header'):
2286
- header_even = section.even_page_header
2287
- add_line_to_header(header_even, "columnSeparatorEven")
2288
- print("✓ Added column separator to even page header using built-in property")
2289
- else:
2290
- # Manual method - we need to add the line to the already-created even header
2291
- # Find the even header part
2292
- sectPr = section._sectPr
2293
- even_header_refs = [ref for ref in sectPr.findall(qn('w:headerReference'))
2294
- if ref.get(qn('w:type')) == 'even']
2295
-
2296
- if even_header_refs:
2297
- # Get the relationship ID
2298
- rId = even_header_refs[0].get(qn('r:id'))
2299
- # Get the header part
2300
- even_header_part = section.part.related_parts[rId]
2301
-
2302
- # Find the first paragraph in the even header
2303
- even_header_element = even_header_part.element
2304
- paras = even_header_element.findall(qn('w:p'))
2305
-
2306
- if paras:
2307
- # Add the line to the first paragraph
2308
- line_xml_content = f'''<w:r xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
2309
- xmlns:v="urn:schemas-microsoft-com:vml"
2310
- xmlns:o="urn:schemas-microsoft-com:office:office">
2311
- <w:pict>
2312
- <v:line id="columnSeparatorEven"
2313
- style="position:absolute;left:0;text-align:left;z-index:-1;
2314
- mso-position-horizontal:center;
2315
- mso-position-horizontal-relative:margin;
2316
- mso-position-vertical-relative:page"
2317
- from="0,0.49in" to="0,11.05in"
2318
- strokecolor="#{theme_hex}"
2319
- strokeweight="1.5pt">
2320
- <o:lock v:ext="edit" aspectratio="f"/>
2321
- </v:line>
2322
- </w:pict>
2323
- </w:r>'''
2324
- line_element = parse_xml(line_xml_content)
2325
- paras[0].append(line_element)
2326
- print("✓ Added column separator to even page header via manual part access")
2327
- else:
2328
- print("⚠ No even header reference found - skipping even page separator line")
2329
- except Exception as e:
2330
- print(f"Warning: Could not add separator line to even page header: {e}")
2331
- import traceback
2332
- traceback.print_exc()
2333
 
2334
 
2335
  def add_choice_commentaire_section(doc, choice_commentaire, photo_q_path, theme_color=None, theme_hex=None,
2336
- general_comment=None, question_num=None, highlight_words=None):
 
2337
  """Add a framed section with general comment, choice commentaires and optional photo Q
2338
  Split into 2/3 for comments and 1/3 for photo (or full width if no photo)
2339
  WITH DASHED BORDER AND SHADED BACKGROUND"""
2340
 
2341
  if highlight_words is None:
2342
  highlight_words = []
 
 
2343
 
2344
  if theme_color is None:
2345
  theme_color = THEME_COLOR
@@ -2520,11 +2536,13 @@ def add_choice_commentaire_section(doc, choice_commentaire, photo_q_path, theme_
2520
  letter_run.font.color.rgb = theme_color
2521
 
2522
  # Comment text
2523
- text_run = comment_para.add_run(comment_text)
2524
- text_run.font.name = 'Inter Display SemiBold'
2525
- text_run.font.size = Pt(8)
2526
-
2527
- # highlight_words_in_text(comment_para, comment_text, highlight_words, theme_color, font_name='Inter Display SemiBold', font_size=8)
 
 
2528
 
2529
  comment_index += 1
2530
 
@@ -2603,10 +2621,13 @@ def extract_embedded_images_info(excel_file_path):
2603
 
2604
 
2605
  def process_excel_to_word(excel_file_path, output_word_path, image_folder, display_name=None, use_two_columns=True,
2606
- add_separator_line=True, balance_method="dynamic", theme_hex=None, highlight_words=None):
 
2607
  """Main function to process Excel and create a Word document with TOC on the first page"""
2608
  if highlight_words is None:
2609
  highlight_words = []
 
 
2610
 
2611
  if theme_hex is None:
2612
  theme_hex = THEME_COLOR_HEX
@@ -2697,6 +2718,15 @@ def process_excel_to_word(excel_file_path, output_word_path, image_folder, displ
2697
  doc = Document()
2698
  enable_odd_even_headers(doc)
2699
 
 
 
 
 
 
 
 
 
 
2700
  core_props = doc.core_properties
2701
  core_props.author = "Natural Killer"
2702
  core_props.title = "Manhattan Project"
@@ -3071,7 +3101,8 @@ def process_excel_to_word(excel_file_path, output_word_path, image_folder, displ
3071
  q_data.get('photo_c', None), # NEW
3072
  theme_color,
3073
  theme_hex,
3074
- highlight_words
 
3075
  )
3076
 
3077
  course_question_count += 1
@@ -3267,6 +3298,16 @@ def process_excel_to_word(excel_file_path, output_word_path, image_folder, displ
3267
  body.insert(insert_index, new_p)
3268
  insert_index += 1 # Increment for next insertion
3269
 
 
 
 
 
 
 
 
 
 
 
3270
  # Add page numbers
3271
  add_page_numbers(doc, theme_hex)
3272
 
 
1
+ """
2
+ META.PY - Quiz Document Generator with Answer Tables Only
3
+ ==========================================================
4
+ This is the META version based on v4u.py with the following key difference:
5
+ - NO EMPTY TABLES after each course
6
+ - ONLY ANSWER TABLES at the end of each module
7
+ - All other features from v4u.py are preserved (images, highlighting, circled numbers, etc.)
8
+ """
9
+
10
  import re
11
  import os
12
  import html
 
27
  THEME_COLOR_HEX = "5FFFDF" # Hex version for XML elements
28
  THEME_COLOR = RGBColor.from_string(THEME_COLOR_HEX)
29
 
30
+ # ── Embedded logo (Cure logo, black on transparent PNG) ─────────────────────
31
+ _CURE_LOGO_B64 = "iVBORw0KGgoAAAANSUhEUgAAB9AAAAfQCAYAAACaOMR5AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAydpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDkuMS1jMDAzIDc5Ljk2OTBhODdmYywgMjAyNS8wMy8wNi0yMDo1MDoxNiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDI2LjExIChXaW5kb3dzKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo5MEUwQjIxODIwQzQxMUYxOUZCREI5QUIzODlFOTVDMCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo5MEUwQjIxOTIwQzQxMUYxOUZCREI5QUIzODlFOTVDMCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjkwRTBCMjE2MjBDNDExRjE5RkJEQjlBQjM4OUU5NUMwIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjkwRTBCMjE3MjBDNDExRjE5RkJEQjlBQjM4OUU5NUMwIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+1eXXUQAAbexJREFUeNrswQEBAAAAgJD+r+4ICgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgNm7/+u2rWxtwK+z/P/lV0HoCsJUYLqCYSoIXYHpCkxXIKcCKhXIU4HkCqRUIN0KpFvBfFB4lHAc/yAlEgRwnmetveR4Zo0yBwRB4sXeBwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA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AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADA/7cHhwQAAAAAgv6/9oUJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AU+KvmSQF6j1QAAAABJRU5ErkJggg=="
32
+
33
+ def get_themed_logo_b64(theme_hex):
34
+ """Recolor the black logo pixels to the theme color, return base64 PNG."""
35
+ import base64, io
36
+ try:
37
+ from PIL import Image
38
+ raw = base64.b64decode(_CURE_LOGO_B64)
39
+ img = Image.open(io.BytesIO(raw)).convert('RGBA')
40
+ r_t = int(theme_hex[0:2], 16)
41
+ g_t = int(theme_hex[2:4], 16)
42
+ b_t = int(theme_hex[4:6], 16)
43
+ w, h = img.size
44
+ pixels = img.load()
45
+ out = Image.new('RGBA', (w, h), (0, 0, 0, 0))
46
+ out_px = out.load()
47
+ for y in range(h):
48
+ for x in range(w):
49
+ r, g, b, a = pixels[x, y]
50
+ if a > 10:
51
+ out_px[x, y] = (r_t, g_t, b_t, a)
52
+ buf = io.BytesIO()
53
+ out.save(buf, format='PNG', optimize=True)
54
+ return base64.b64encode(buf.getvalue()).decode()
55
+ except Exception as e:
56
+ print(f"Logo recolor failed: {e}, using original")
57
+ return _CURE_LOGO_B64
58
+
59
+
60
  # Common paper sizes (width x height in inches)
61
  PAPER_SIZES = {
62
  'LETTER': (8.5, 11), # US Letter
 
301
  return None
302
 
303
 
304
+ def create_stats_table_in_toc(doc, body, insert_index, modules, questions_by_module,
305
+ modules_data, modules_course_order, theme_hex, theme_color):
306
+ """
307
+ Inserts a column-break then the Frequence & Traqueur stats table
308
+ directly into the body XML at insert_index.
309
+ Layout mirrors the reference PDF: module headers as full-width merged rows,
310
+ course rows with # | C1 | C2 | C3 columns (C1-C3 left blank for student fill-in).
311
+ """
312
+ W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'
313
+
314
+ def xml_para(text, bold=False, center=False, color_hex=None, font_size_half=None,
315
+ space_before=0, space_after=0, keep_with_next=False):
316
+ """Build a <w:p> element with a single run."""
317
+ p = body.makeelement(qn('w:p'), nsmap=body.nsmap)
318
+ pPr = OxmlElement('w:pPr')
319
+ sp = OxmlElement('w:spacing')
320
+ sp.set(qn('w:before'), str(space_before))
321
+ sp.set(qn('w:after'), str(space_after))
322
+ pPr.append(sp)
323
+ if center:
324
+ jc = OxmlElement('w:jc')
325
+ jc.set(qn('w:val'), 'center')
326
+ pPr.append(jc)
327
+ if keep_with_next:
328
+ kwn = OxmlElement('w:keepNext')
329
+ pPr.append(kwn)
330
+ p.append(pPr)
331
+ r = OxmlElement('w:r')
332
+ rPr = OxmlElement('w:rPr')
333
+ fnt = OxmlElement('w:rFonts')
334
+ fnt.set(qn('w:ascii'), 'Montserrat')
335
+ fnt.set(qn('w:hAnsi'), 'Montserrat')
336
+ rPr.append(fnt)
337
+ if bold:
338
+ rPr.append(OxmlElement('w:b'))
339
+ if color_hex:
340
+ col = OxmlElement('w:color')
341
+ col.set(qn('w:val'), color_hex)
342
+ rPr.append(col)
343
+ if font_size_half:
344
+ sz = OxmlElement('w:sz')
345
+ sz.set(qn('w:val'), str(font_size_half))
346
+ rPr.append(sz)
347
+ r.append(rPr)
348
+ t = OxmlElement('w:t')
349
+ t.set(qn('xml:space'), 'preserve')
350
+ t.text = text
351
+ r.append(t)
352
+ p.append(r)
353
+ return p
354
+
355
+ # ── 1. Column break to push into right column ───────────────────────────
356
+ col_break_p = body.makeelement(qn('w:p'), nsmap=body.nsmap)
357
+ col_break_pPr = OxmlElement('w:pPr')
358
+ sp0 = OxmlElement('w:spacing')
359
+ sp0.set(qn('w:before'), '0')
360
+ sp0.set(qn('w:after'), '0')
361
+ col_break_pPr.append(sp0)
362
+ col_break_p.append(col_break_pPr)
363
+ col_break_r = OxmlElement('w:r')
364
+ brk = OxmlElement('w:br')
365
+ brk.set(qn('w:type'), 'column')
366
+ col_break_r.append(brk)
367
+ col_break_p.append(col_break_r)
368
+ body.insert(insert_index, col_break_p)
369
+ insert_index += 1
370
+
371
+ # ── 2. Title "Fréquence & Traqueur" ────────────────────────────────────
372
+ title_p = xml_para('Fréquence & Traqueur', bold=True, center=True,
373
+ color_hex=theme_hex, font_size_half=28,
374
+ space_before=0, space_after=80)
375
+ body.insert(insert_index, title_p)
376
+ insert_index += 1
377
+
378
+ # ── 3. Build the stats table ────────────────────────────────────────────
379
+ tbl = OxmlElement('w:tbl')
380
+
381
+ # Table properties
382
+ tblPr = OxmlElement('w:tblPr')
383
+ tblW = OxmlElement('w:tblW')
384
+ tblW.set(qn('w:w'), '0')
385
+ tblW.set(qn('w:type'), 'auto')
386
+ tblPr.append(tblW)
387
+ # Table border (thin outer + inner)
388
+ tblBorders = OxmlElement('w:tblBorders')
389
+ for side in ['top', 'left', 'bottom', 'right', 'insideH', 'insideV']:
390
+ b = OxmlElement(f'w:{side}')
391
+ b.set(qn('w:val'), 'single')
392
+ b.set(qn('w:sz'), '4')
393
+ b.set(qn('w:space'), '0')
394
+ b.set(qn('w:color'), theme_hex)
395
+ tblBorders.append(b)
396
+ tblPr.append(tblBorders)
397
+ tbl.append(tblPr)
398
+
399
+ # Column widths: course name wide, # narrow, C1/C2/C3 narrow
400
+ tblGrid = OxmlElement('w:tblGrid')
401
+ for w in [2800, 500, 500, 500, 500]:
402
+ gc = OxmlElement('w:gridCol')
403
+ gc.set(qn('w:w'), str(w))
404
+ tblGrid.append(gc)
405
+ tbl.append(tblGrid)
406
+
407
+ def make_cell(text, bold=False, center=True, color_hex=None, font_size_half=18,
408
+ bg_color=None, merge_start=False, merge_cont=False, colspan=None):
409
+ tc = OxmlElement('w:tc')
410
+ tcPr = OxmlElement('w:tcPr')
411
+ if colspan:
412
+ span = OxmlElement('w:gridSpan')
413
+ span.set(qn('w:val'), str(colspan))
414
+ tcPr.append(span)
415
+ if merge_start:
416
+ vm = OxmlElement('w:vMerge')
417
+ vm.set(qn('w:val'), 'restart')
418
+ tcPr.append(vm)
419
+ if merge_cont:
420
+ vm = OxmlElement('w:vMerge')
421
+ tcPr.append(vm)
422
+ if bg_color:
423
+ shd = OxmlElement('w:shd')
424
+ shd.set(qn('w:val'), 'clear')
425
+ shd.set(qn('w:color'), 'auto')
426
+ shd.set(qn('w:fill'), bg_color)
427
+ tcPr.append(shd)
428
+ # Cell margins (tight)
429
+ tcMar = OxmlElement('w:tcMar')
430
+ for side, val in [('top', '40'), ('bottom', '40'), ('left', '80'), ('right', '80')]:
431
+ m = OxmlElement(f'w:{side}')
432
+ m.set(qn('w:w'), val)
433
+ m.set(qn('w:type'), 'dxa')
434
+ tcMar.append(m)
435
+ tcPr.append(tcMar)
436
+ tc.append(tcPr)
437
+ p = OxmlElement('w:p')
438
+ pPr = OxmlElement('w:pPr')
439
+ sp = OxmlElement('w:spacing')
440
+ sp.set(qn('w:before'), '0')
441
+ sp.set(qn('w:after'), '0')
442
+ pPr.append(sp)
443
+ if center:
444
+ jc = OxmlElement('w:jc')
445
+ jc.set(qn('w:val'), 'center')
446
+ pPr.append(jc)
447
+ p.append(pPr)
448
+ r = OxmlElement('w:r')
449
+ rPr = OxmlElement('w:rPr')
450
+ fnt = OxmlElement('w:rFonts')
451
+ fnt.set(qn('w:ascii'), 'Montserrat')
452
+ fnt.set(qn('w:hAnsi'), 'Montserrat')
453
+ rPr.append(fnt)
454
+ if bold:
455
+ rPr.append(OxmlElement('w:b'))
456
+ if color_hex:
457
+ col = OxmlElement('w:color')
458
+ col.set(qn('w:val'), color_hex)
459
+ rPr.append(col)
460
+ sz = OxmlElement('w:sz')
461
+ sz.set(qn('w:val'), str(font_size_half))
462
+ rPr.append(sz)
463
+ r.append(rPr)
464
+ t_el = OxmlElement('w:t')
465
+ t_el.set(qn('xml:space'), 'preserve')
466
+ t_el.text = str(text)
467
+ r.append(t_el)
468
+ p.append(r)
469
+ tc.append(p)
470
+ return tc
471
+
472
+ def make_row(cells, keep_with_next=False):
473
+ tr = OxmlElement('w:tr')
474
+ trPr = OxmlElement('w:trPr')
475
+ cant = OxmlElement('w:cantSplit')
476
+ trPr.append(cant)
477
+ if keep_with_next:
478
+ kwn = OxmlElement('w:trPr')
479
+ tr.append(trPr)
480
+ for c in cells:
481
+ tr.append(c)
482
+ return tr
483
+
484
+ # Header row: empty | # | C1 | C2 | C3
485
+ hdr_row = make_row([
486
+ make_cell('', bold=False, center=True, font_size_half=16),
487
+ make_cell('#', bold=True, center=True, color_hex=theme_hex, font_size_half=18),
488
+ make_cell('C1', bold=True, center=True, color_hex=theme_hex, font_size_half=18),
489
+ make_cell('C2', bold=True, center=True, color_hex=theme_hex, font_size_half=18),
490
+ make_cell('C3', bold=True, center=True, color_hex=theme_hex, font_size_half=18),
491
+ ])
492
+ tbl.append(hdr_row)
493
+
494
+ # Data rows per module then per course
495
+ for module_name in modules:
496
+ if module_name not in questions_by_module:
497
+ continue
498
 
499
+ course_order = modules_course_order.get(module_name,
500
+ sorted(questions_by_module[module_name].keys()))
501
+ cours_titles = modules_data.get(module_name, {})
502
 
503
+ # Module header row: merged across all 5 columns
504
+ mod_display = str(module_name).upper()
505
+ mod_tc = make_cell(mod_display, bold=True, center=True,
506
+ color_hex=theme_hex, font_size_half=20,
507
+ colspan=5)
508
+ mod_row = make_row([mod_tc])
509
+ tbl.append(mod_row)
510
+
511
+ # Course rows
512
+ for cours_num in course_order:
513
+ if cours_num not in questions_by_module[module_name]:
514
+ continue
515
+ course_title = cours_titles.get(cours_num, f"Course {cours_num}")
516
+ count = len(questions_by_module[module_name][cours_num])
517
+ row = make_row([
518
+ make_cell(str(course_title), bold=False, center=False,
519
+ font_size_half=16),
520
+ make_cell(str(count), bold=True, center=True,
521
+ color_hex=theme_hex, font_size_half=18),
522
+ make_cell('', center=True, font_size_half=16),
523
+ make_cell('', center=True, font_size_half=16),
524
+ make_cell('', center=True, font_size_half=16),
525
+ ])
526
+ tbl.append(row)
527
+
528
+ # Wrap table in a paragraph-like block and insert
529
+ tbl_wrap = body.makeelement(qn('w:tbl'), nsmap=body.nsmap)
530
+ body.insert(insert_index, tbl)
531
+ insert_index += 1
532
+
533
+ return insert_index
534
 
535
 
536
  def preview_image_mapping(question_images):
 
854
 
855
 
856
  def highlight_words_in_text(paragraph, text, highlight_words, theme_color, font_name='Inter Display Medium',
857
+ font_size=10.5, bold=False, word_boundary_mode=False):
858
  """
859
  Add text to paragraph with specific words/substrings highlighted in theme color.
860
  Highlights literal text matches (including special characters like parentheses, backslashes).
 
877
  run.font.bold = True
878
  return
879
 
 
 
 
880
  escaped_words = [re.escape(word) for word in highlight_words]
881
  pattern = '(' + '|'.join(escaped_words) + ')'
882
 
 
900
 
901
  def format_question_block(doc, question_num, question_text, choices, correct_answers, source, comment=None,
902
  choice_commentaire=None, photo_q=None, photo_c=None, theme_color=None, theme_hex=None,
903
+ highlight_words=None, highlight_comment_words=None):
904
  if theme_color is None:
905
  theme_color = THEME_COLOR
906
  if theme_hex is None:
907
  theme_hex = THEME_COLOR_HEX
908
  if highlight_words is None:
909
  highlight_words = []
910
+ if highlight_comment_words is None:
911
+ highlight_comment_words = []
912
 
913
  """Format a single question block with reduced spacing and keep together formatting"""
914
 
 
1048
  if theme_hex is None:
1049
  theme_hex = THEME_COLOR_HEX
1050
 
1051
+ # Pre-encode logo PNG once, recolor to theme color
1052
+ _logo_png_bytes = None
1053
+ try:
1054
+ import base64 as _b64
1055
+ _logo_png_bytes = _b64.b64decode(get_themed_logo_b64(theme_hex))
1056
+ print(f"\u2713 Logo PNG ready ({len(_logo_png_bytes)} bytes)")
1057
+ except Exception as _e:
1058
+ print(f"Logo setup failed: {_e}")
1059
+ _logo_png_bytes = None
1060
+
1061
  def create_footer_content(footer_elem, theme_hex):
1062
+ """Footer: original layout restored.
1063
+ Logo absolutely positioned bottom-left (VML, original size/position),
1064
+ wrapped in HYPERLINK field for clickability.
1065
+ Page number centered. SOM link right. All via absolute VML shapes."""
1066
  empty_para = footer_elem.paragraphs[0]
1067
  empty_para.paragraph_format.space_before = Pt(0)
1068
  empty_para.paragraph_format.space_after = Pt(0)
1069
  empty_para.paragraph_format.line_spacing = 1.0
1070
 
 
1071
  paragraph = footer_elem.add_paragraph()
1072
  paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
 
 
1073
  paragraph.paragraph_format.space_before = Pt(0)
1074
  paragraph.paragraph_format.space_after = Pt(0)
1075
 
1076
+ # Page number centered
1077
  run = paragraph.add_run()
 
 
1078
  fldChar1 = OxmlElement('w:fldChar')
1079
  fldChar1.set(qn('w:fldCharType'), 'begin')
 
1080
  instrText = OxmlElement('w:instrText')
1081
  instrText.set(qn('xml:space'), 'preserve')
1082
  instrText.text = "PAGE"
 
1083
  fldChar2 = OxmlElement('w:fldChar')
1084
  fldChar2.set(qn('w:fldCharType'), 'end')
 
1085
  run._r.append(fldChar1)
1086
  run._r.append(instrText)
1087
  run._r.append(fldChar2)
 
1088
  run.font.name = 'Montserrat'
1089
  run.font.size = Pt(14)
1090
  run.font.bold = True
1091
  run.font.color.rgb = RGBColor.from_string(theme_hex)
1092
 
1093
+ # Logo: HYPERLINK field wrapping a VML absolutely-positioned image
1094
+ # Original size (87.3pt x 31.5pt) and position (margin-top:-9.9pt) preserved exactly
1095
+ if _logo_png_bytes:
1096
+ try:
1097
+ from docx.opc.part import Part as _Part
1098
+ from docx.opc.packuri import PackURI as _PackURI
1099
+ _fp = footer_elem.part
1100
+ _img_part = _Part(
1101
+ _PackURI(f'/word/media/logo_footer_{id(_fp)}.png'),
1102
+ 'image/png', _logo_png_bytes
1103
+ )
1104
+ _r_id = _fp.relate_to(
1105
+ _img_part,
1106
+ 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image'
1107
+ )
1108
+ _ig_url = "https://www.instagram.com/cureology_/"
1109
+ _hl_rid = _fp.relate_to(
1110
+ _ig_url,
1111
+ 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink',
1112
+ is_external=True
1113
+ )
1114
+ logo_xml = f'''<w:r xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
1115
+ xmlns:v="urn:schemas-microsoft-com:vml"
1116
+ xmlns:o="urn:schemas-microsoft-com:office:office"
1117
+ xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
1118
+ <w:pict>
1119
+ <v:shape style="position:absolute;margin-left:0in;margin-top:-9.9pt;width:87.3pt;height:31.5pt;z-index:2;mso-position-horizontal:left;mso-position-horizontal-relative:margin;mso-position-vertical-relative:line"
1120
+ filled="f" stroked="f">
1121
+ <v:textbox inset="0,0,0,0">
1122
+ <w:txbxContent>
1123
+ <w:p>
1124
+ <w:pPr>
1125
+ <w:spacing w:before="0" w:after="0"/>
1126
+ </w:pPr>
1127
+ <w:hyperlink r:id="{_hl_rid}">
1128
+ <w:r>
1129
+ <w:pict>
1130
+ <v:shape style="width:87.3pt;height:31.5pt" stroked="f" id="logoCure">
1131
+ <v:imagedata r:id="{_r_id}" o:title="Cure"/>
1132
+ </v:shape>
1133
+ </w:pict>
1134
+ </w:r>
1135
+ </w:hyperlink>
1136
+ </w:p>
1137
+ </w:txbxContent>
1138
+ </v:textbox>
1139
+ </v:shape>
1140
+ </w:pict>
1141
+ </w:r>'''
1142
+ logo_element = parse_xml(logo_xml)
1143
+ paragraph._p.append(logo_element)
1144
+ print(f"\u2713 Logo with clickable textbox (img={_r_id}, hl={_hl_rid})")
1145
+ except Exception as _e:
1146
+ import traceback; traceback.print_exc()
1147
+ # SOM link right (absolutely positioned textbox)
1148
  toc_textbox_xml = f'''
1149
  <w:r xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
1150
  xmlns:v="urn:schemas-microsoft-com:vml"
 
1185
  </w:pict>
1186
  </w:r>
1187
  '''
1188
+ paragraph._p.append(parse_xml(toc_textbox_xml))
 
 
1189
 
1190
  for section_idx, section in enumerate(doc.sections):
 
1191
  header = section.header
1192
  header.is_linked_to_previous = False
1193
  section.header_distance = Cm(0.3)
 
 
1194
  if not header.paragraphs:
1195
  header.add_paragraph()
1196
 
 
1197
  footer = section.footer
1198
  footer.is_linked_to_previous = False
1199
+ section.footer_distance = Cm(0.4)
1200
 
 
1201
  if footer.paragraphs:
1202
  footer.paragraphs[0].clear()
1203
  else:
1204
  footer.add_paragraph()
1205
 
 
1206
  if section_idx == 0:
1207
  continue
1208
 
 
1209
  if section_idx == 1:
1210
  sectPr = section._sectPr
1211
  pgNumType = sectPr.find(qn('w:pgNumType'))
1212
  if pgNumType is None:
1213
  pgNumType = OxmlElement('w:pgNumType')
1214
  sectPr.append(pgNumType)
1215
+ pgNumType.set(qn('w:start'), '1')
1216
 
1217
+ # Single footer for all pages (no evenAndOddHeaders)
1218
  create_footer_content(footer, theme_hex)
1219
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1220
 
1221
 
1222
  def add_toc_bookmark(doc, toc_title_para):
 
2147
 
2148
 
2149
  def enable_odd_even_headers(doc):
2150
+ """Ensure evenAndOddHeaders is ABSENT. We use IF+MOD(PAGE,2) fields instead,
2151
+ which flip left/right text on every page with zero blank-page side effects."""
2152
  try:
2153
+ settings_element = doc.settings.element
 
 
 
 
2154
  even_odd = settings_element.find(qn('w:evenAndOddHeaders'))
2155
+ if even_odd is not None:
2156
+ settings_element.remove(even_odd)
2157
+ print("✓ evenAndOddHeaders removed - IF field flip active")
 
 
2158
  else:
2159
+ print("✓ evenAndOddHeaders absent - IF field flip active")
2160
  except Exception as e:
2161
+ print(f"Note: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
2162
  def create_flexible_header(section, module_name, sheet_name, display_name=None, left_margin_inches=0,
2163
  right_margin_inches=0, theme_hex=None):
2164
+ """Single header using IF+MOD(PAGE,2) fields to flip left/right text on each page.
2165
+ No evenAndOddHeaders needed. Works in Word, Adobe, ilovepdf with zero blank pages.
2166
+ Odd pages (PAGE mod 2 = 1): sheet_name LEFT, module_name RIGHT
2167
+ Even pages (PAGE mod 2 = 0): module_name LEFT, sheet_name RIGHT
2168
+ """
2169
  if theme_hex is None:
2170
  theme_hex = THEME_COLOR_HEX
2171
 
2172
  section.header_distance = Cm(0.6)
2173
 
2174
  module_name_str = str(module_name).upper()
 
 
2175
  if display_name:
2176
  sheet_name_str = str(display_name).upper()
2177
  else:
 
2180
  module_name_str = html.escape(module_name_str)
2181
  sheet_name_str = html.escape(sheet_name_str)
2182
 
2183
+ right_tab_twips = 11000
2184
+
2185
+ def make_if_mod_page_field(odd_text, even_text, color):
2186
+ W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'
2187
+ def run(instr=None, text=None, fld_type=None):
2188
+ r_xml = (
2189
+ f'<w:r xmlns:w="{W}">'
2190
+ f'<w:rPr><w:rFonts w:ascii="Montserrat" w:hAnsi="Montserrat"/>'
2191
+ f'<w:b/><w:sz w:val="26"/><w:color w:val="{color}"/></w:rPr>'
2192
+ )
2193
+ if fld_type:
2194
+ r_xml += f'<w:fldChar w:fldCharType="{fld_type}"/>'
2195
+ if instr is not None:
2196
+ r_xml += f'<w:instrText xml:space="preserve">{instr}</w:instrText>'
2197
+ if text is not None:
2198
+ r_xml += f'<w:t>{text}</w:t>'
2199
+ r_xml += '</w:r>'
2200
+ return parse_xml(r_xml)
2201
+ elements = []
2202
+ elements.append(run(fld_type='begin'))
2203
+ elements.append(run(instr=' IF '))
2204
+ elements.append(run(fld_type='begin'))
2205
+ elements.append(run(instr=' = MOD('))
2206
+ elements.append(run(fld_type='begin'))
2207
+ elements.append(run(instr=' PAGE '))
2208
+ elements.append(run(fld_type='separate'))
2209
+ elements.append(run(text='1'))
2210
+ elements.append(run(fld_type='end'))
2211
+ elements.append(run(instr=',2) '))
2212
+ elements.append(run(fld_type='separate'))
2213
+ elements.append(run(text='1'))
2214
+ elements.append(run(fld_type='end'))
2215
+ elements.append(run(instr=f' = 1 "{odd_text}" "{even_text}" '))
2216
+ elements.append(run(fld_type='separate'))
2217
+ elements.append(run(text=odd_text))
2218
+ elements.append(run(fld_type='end'))
2219
+ return elements
2220
+
2221
+ def build_flipping_header(header):
2222
+ header.is_linked_to_previous = False
2223
+ if not header.paragraphs:
2224
+ header.add_paragraph()
2225
+ para = header.paragraphs[0]
2226
+ para.clear()
2227
+ W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'
2228
+ pPr_xml = (
2229
+ f'<w:pPr xmlns:w="{W}">'
2230
+ '<w:spacing w:before="0" w:after="0"/>'
2231
+ '<w:tabs>'
2232
+ f'<w:tab w:val="right" w:pos="{right_tab_twips}"/>'
2233
+ '</w:tabs>'
2234
+ '</w:pPr>'
2235
+ )
2236
+ existing_pPr = para._p.find('{%s}pPr' % W)
2237
+ if existing_pPr is not None:
2238
+ para._p.remove(existing_pPr)
2239
+ para._p.insert(0, parse_xml(pPr_xml))
2240
+ for elem in make_if_mod_page_field(sheet_name_str, module_name_str, theme_hex):
2241
+ para._p.append(elem)
2242
+ tab_xml = (
2243
+ f'<w:r xmlns:w="{W}">'
2244
+ '<w:rPr><w:sz w:val="26"/></w:rPr>'
2245
+ '<w:tab/>'
2246
+ '</w:r>'
2247
+ )
2248
+ para._p.append(parse_xml(tab_xml))
2249
+ for elem in make_if_mod_page_field(module_name_str, sheet_name_str, theme_hex):
2250
+ para._p.append(elem)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2251
 
2252
+ build_flipping_header(section.header)
2253
+ print(f"Flipping header set: odd=[{sheet_name_str} | {module_name_str}] even=[{module_name_str} | {sheet_name_str}]")
2254
  def extract_display_name_from_excel(excel_file_path):
2255
  """Extract display name from Excel file - checks multiple locations"""
2256
  try:
 
2342
  # Add line to odd/default header
2343
  header = section.header
2344
  add_line_to_header(header, "columnSeparatorOdd")
2345
+ print("Added column separator")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2346
 
2347
 
2348
  def add_choice_commentaire_section(doc, choice_commentaire, photo_q_path, theme_color=None, theme_hex=None,
2349
+ general_comment=None, question_num=None, highlight_words=None,
2350
+ highlight_comment_words=None):
2351
  """Add a framed section with general comment, choice commentaires and optional photo Q
2352
  Split into 2/3 for comments and 1/3 for photo (or full width if no photo)
2353
  WITH DASHED BORDER AND SHADED BACKGROUND"""
2354
 
2355
  if highlight_words is None:
2356
  highlight_words = []
2357
+ if highlight_comment_words is None:
2358
+ highlight_comment_words = []
2359
 
2360
  if theme_color is None:
2361
  theme_color = THEME_COLOR
 
2536
  letter_run.font.color.rgb = theme_color
2537
 
2538
  # Comment text
2539
+ if highlight_comment_words:
2540
+ highlight_words_in_text(comment_para, comment_text, highlight_comment_words, theme_color,
2541
+ font_name='Inter Display SemiBold', font_size=8)
2542
+ else:
2543
+ text_run = comment_para.add_run(comment_text)
2544
+ text_run.font.name = 'Inter Display SemiBold'
2545
+ text_run.font.size = Pt(8)
2546
 
2547
  comment_index += 1
2548
 
 
2621
 
2622
 
2623
  def process_excel_to_word(excel_file_path, output_word_path, image_folder, display_name=None, use_two_columns=True,
2624
+ add_separator_line=True, balance_method="dynamic", theme_hex=None, highlight_words=None,
2625
+ highlight_comment_words=None):
2626
  """Main function to process Excel and create a Word document with TOC on the first page"""
2627
  if highlight_words is None:
2628
  highlight_words = []
2629
+ if highlight_comment_words is None:
2630
+ highlight_comment_words = []
2631
 
2632
  if theme_hex is None:
2633
  theme_hex = THEME_COLOR_HEX
 
2718
  doc = Document()
2719
  enable_odd_even_headers(doc)
2720
 
2721
+ # Force TOC section break to nextPage (prevents blank page with any PDF converter)
2722
+ toc_sectPr = doc.sections[0]._sectPr
2723
+ toc_type_elem = toc_sectPr.find(qn('w:type'))
2724
+ if toc_type_elem is None:
2725
+ toc_type_elem = OxmlElement('w:type')
2726
+ toc_sectPr.insert(0, toc_type_elem)
2727
+ toc_type_elem.set(qn('w:val'), 'nextPage')
2728
+ print("TOC section break set to nextPage")
2729
+
2730
  core_props = doc.core_properties
2731
  core_props.author = "Natural Killer"
2732
  core_props.title = "Manhattan Project"
 
3101
  q_data.get('photo_c', None), # NEW
3102
  theme_color,
3103
  theme_hex,
3104
+ highlight_words,
3105
+ highlight_comment_words
3106
  )
3107
 
3108
  course_question_count += 1
 
3298
  body.insert(insert_index, new_p)
3299
  insert_index += 1 # Increment for next insertion
3300
 
3301
+ # ── Insert Fréquence & Traqueur stats table into right TOC column ──────
3302
+ # Derive course order: preserve insertion order from questions_by_module
3303
+ _mcorder = {m: list(questions_by_module[m].keys()) for m in questions_by_module}
3304
+ insert_index = create_stats_table_in_toc(
3305
+ doc, body, insert_index,
3306
+ modules, questions_by_module,
3307
+ modules_data, _mcorder,
3308
+ theme_hex, theme_color
3309
+ )
3310
+
3311
  # Add page numbers
3312
  add_page_numbers(doc, theme_hex)
3313