Spaces:
Sleeping
Sleeping
Update meta.py
Browse files
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
|
| 266 |
-
|
| 267 |
-
"""
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
|
| 278 |
-
|
| 279 |
-
|
|
|
|
| 280 |
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
"""
|
| 809 |
-
|
|
|
|
|
|
|
| 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 |
-
#
|
| 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 |
-
#
|
| 847 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)
|
| 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')
|
| 925 |
|
| 926 |
-
#
|
| 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 |
-
"""
|
|
|
|
| 1974 |
try:
|
| 1975 |
-
|
| 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 |
-
|
| 1983 |
-
|
| 1984 |
-
settings_element.insert(0, even_odd)
|
| 1985 |
-
print("✓ Enabled odd/even page headers in document settings")
|
| 1986 |
else:
|
| 1987 |
-
print("✓
|
| 1988 |
except Exception as e:
|
| 1989 |
-
print(f"
|
| 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 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 2022 |
-
|
| 2023 |
-
|
| 2024 |
-
|
| 2025 |
-
|
| 2026 |
-
|
| 2027 |
-
|
| 2028 |
-
|
| 2029 |
-
|
| 2030 |
-
|
| 2031 |
-
|
| 2032 |
-
|
| 2033 |
-
|
| 2034 |
-
|
| 2035 |
-
|
| 2036 |
-
|
| 2037 |
-
|
| 2038 |
-
|
| 2039 |
-
|
| 2040 |
-
|
| 2041 |
-
|
| 2042 |
-
|
| 2043 |
-
|
| 2044 |
-
|
| 2045 |
-
|
| 2046 |
-
|
| 2047 |
-
|
| 2048 |
-
|
| 2049 |
-
|
| 2050 |
-
|
| 2051 |
-
|
| 2052 |
-
|
| 2053 |
-
|
| 2054 |
-
|
| 2055 |
-
|
| 2056 |
-
|
| 2057 |
-
|
| 2058 |
-
|
| 2059 |
-
|
| 2060 |
-
|
| 2061 |
-
|
| 2062 |
-
|
| 2063 |
-
|
| 2064 |
-
|
| 2065 |
-
|
| 2066 |
-
|
| 2067 |
-
|
| 2068 |
-
|
| 2069 |
-
|
| 2070 |
-
|
| 2071 |
-
|
| 2072 |
-
|
| 2073 |
-
|
| 2074 |
-
|
| 2075 |
-
|
| 2076 |
-
|
| 2077 |
-
|
| 2078 |
-
|
| 2079 |
-
|
| 2080 |
-
|
| 2081 |
-
|
| 2082 |
-
</w:
|
| 2083 |
-
|
| 2084 |
-
|
| 2085 |
-
|
| 2086 |
-
|
| 2087 |
-
|
| 2088 |
-
|
| 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 |
-
|
| 2524 |
-
|
| 2525 |
-
|
| 2526 |
-
|
| 2527 |
-
|
|
|
|
|
|
|
| 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 |
|