diff --git a/Code/src/template_reader.py b/Code/src/template_reader.py index 637fc9b..cd2ec6d 100644 --- a/Code/src/template_reader.py +++ b/Code/src/template_reader.py @@ -171,15 +171,18 @@ class TemplateReader: 'fontId': xf.get('fontId', '0'), 'fillId': xf.get('fillId', '0'), 'borderId': xf.get('borderId', '0'), + 'xfId': xf.get('xfId', '0'), 'applyFont': xf.get('applyFont', ''), 'applyFill': xf.get('applyFill', ''), 'applyBorder': xf.get('applyBorder', ''), + 'applyAlignment': xf.get('applyAlignment', ''), } # 对齐方式 align = xf.find(_tag('alignment')) if align is not None: xf_info['hAlign'] = align.get('horizontal', '') xf_info['vAlign'] = align.get('vertical', '') + xf_info['wrapText'] = align.get('wrapText', '') style.cell_xfs.append(xf_info) def _parse_sheet_dims(self, data: bytes, style: TemplateStyle): diff --git a/Code/src/xlsx_writer.py b/Code/src/xlsx_writer.py index 2391246..181cf87 100644 --- a/Code/src/xlsx_writer.py +++ b/Code/src/xlsx_writer.py @@ -258,93 +258,35 @@ class StyledXLSXWriter: return '\n'.join(parts) def _styles_xml(self) -> str: - """Build xl/styles.xml with fonts, fills, borders, and cellXfs.""" + """Build xl/styles.xml with fonts, fills, borders, and cellXfs. + + F011: 有模板时从模板的 fonts / fills / borders / cellXfs 中 + 提取实际样式定义;无模板时回退到硬编码默认样式。 + + 保留现有 4 个样式槽位(xf 0~3): + xf 0 = default (no style) + xf 1 = centered + thin border (pin cells) + xf 2 = bold + centered (A1 package info) + xf 3 = centered + border + light fill (header-like) + + 有模板时,四个 xf 的字体/边框/填充/对齐从模板读取。 + """ s = self._style - # ── Fonts ────────────────────────────────────────────────── - fonts_xml = '' - # Font 0: default (no bold) - font_name = "Calibri" - font_size = "11" - font_color = "FF000000" if s and s.fonts: - f0 = s.fonts[0] - font_name = f0.name - font_size = str(f0.size) - font_color = "FF" + f0.color if f0.color and not f0.color.startswith("FF") else f0.color - fonts_xml += ( - f'' - f'' - f'' - f'' - ) - # Font 1: bold (for package info in A1) - fonts_xml += ( - f'' - f'' - f'' - f'' - f'' - ) - fonts_xml += '' - - # ── Fills ────────────────────────────────────────────────── - fills_xml = '' - fills_xml += '' - # Fill 1: light gray for header-like cells - fills_xml += ( - '' - '' - '' - ) - fills_xml += '' - - # ── Borders ──────────────────────────────────────────────── - borders_xml = '' - # Border 0: none - borders_xml += ( - '' - '' - '' - ) - # Border 1: thin all sides - borders_xml += ( - '' - '' - '' - '' - '' - ) - borders_xml += '' - - # ── Cell XFs ─────────────────────────────────────────────── - # xf 0: default (no style) - # xf 1: centered with thin border (for pin cells) - # xf 2: bold + centered (for A1 package info) - # xf 3: centered + border + light fill (for header-like) - cell_xfs_xml = '' - cell_xfs_xml += ( - '' - ) - cell_xfs_xml += ( - '' - '' - '' - ) - cell_xfs_xml += ( - '' - '' - '' - ) - cell_xfs_xml += ( - '' - '' - '' - ) - cell_xfs_xml += '' + # ── 有模板:从模板提取实际值构建样式 ────────────────── + fonts_xml = self._build_fonts_xml_from_template(s.fonts) + fills_xml = self._build_fills_xml_from_template(s.fills) + borders_xml = self._build_borders_xml_from_template(s.borders) + cell_xfs_xml = self._build_cell_xfs_xml_from_template( + s.cell_xfs, s.fonts, s.fills, s.borders + ) + else: + # ── 无模板:回退到硬编码默认样式(F011 fallback)────── + fonts_xml = self._default_fonts_xml() + fills_xml = self._default_fills_xml() + borders_xml = self._default_borders_xml() + cell_xfs_xml = self._default_cell_xfs_xml() parts = [''] parts.append( @@ -357,6 +299,250 @@ class StyledXLSXWriter: parts.append('') return '\n'.join(parts) + # ── 模板样式构建(F011)───────────────────────────────────────── + + @staticmethod + def _build_fonts_xml_from_template(fonts: list) -> str: + """从模板字体列表构建 XML。""" + parts = [f''] + for f in fonts: + parts.append('') + parts.append(f'') + if f.bold: + parts.append('') + if f.italic: + parts.append('') + parts.append(f'') + color_val = f.color + if color_val and not color_val.startswith('FF'): + color_val = 'FF' + color_val + if color_val: + parts.append(f'') + parts.append('') + parts.append('') + return ''.join(parts) + + @staticmethod + def _build_fills_xml_from_template(fills: list) -> str: + """从模板填充列表构建 XML。""" + parts = [f''] + for fl in fills: + parts.append('') + parts.append(f'') + if fl.fg_color: + color_val = fl.fg_color + if not color_val.startswith('FF'): + color_val = 'FF' + color_val + parts.append(f'') + parts.append('') + parts.append('') + parts.append('') + return ''.join(parts) + + @staticmethod + def _build_borders_xml_from_template(borders: list) -> str: + """从模板边框列表构建 XML。""" + parts = [f''] + for b in borders: + parts.append('') + for side_name in ('left', 'right', 'top', 'bottom'): + style_val = getattr(b, side_name, 'none') + if style_val and style_val != 'none': + parts.append(f'<{side_name} style="{style_val}"/>') + else: + parts.append(f'<{side_name}/>') + parts.append('') + parts.append('') + parts.append('') + return ''.join(parts) + + @staticmethod + def _build_cell_xfs_xml_from_template( + cell_xfs: list, fonts: list, fills: list, borders: list + ) -> str: + """从模板 cellXfs 列表构建 XML。 + + 保留 4 个样式槽位(xf 0~3),每个从模板对应位置的 xf 提取: + - xf 0: default (模板的 xf 0) + - xf 1: pin cells — 使用模板中带边框的 xf(优先),否则 fallback + - xf 2: A1 bold — 使用模板中带 bold 字体的 xf(优先),否则 fallback + - xf 3: header — 使用模板中带填充的 xf(优先),否则 fallback + """ + def _xf_to_attrs(xf: dict) -> str: + """将 xf 信息字典转换为属性字符串。""" + attrs = [ + f'numFmtId="{xf.get("numFmtId", "0")}"', + f'fontId="{xf.get("fontId", "0")}"', + f'fillId="{xf.get("fillId", "0")}"', + f'borderId="{xf.get("borderId", "0")}"', + f'xfId="{xf.get("xfId", "0")}"', + ] + for attr_key in ('applyFont', 'applyFill', 'applyBorder', 'applyAlignment', + 'applyNumberFormat', 'applyProtection'): + val = xf.get(attr_key, '') + if val: + attrs.append(f'{attr_key}="{val}"') + return ' '.join(attrs) + + def _xf_to_alignment(xf: dict) -> str: + """从 xf 信息字典生成对齐 XML片段。""" + h_align = xf.get('hAlign', '') + v_align = xf.get('vAlign', '') + wrap_text = xf.get('wrapText', '') + if h_align or v_align or wrap_text: + align_attrs = [] + if h_align: + align_attrs.append(f'horizontal="{h_align}"') + if v_align: + align_attrs.append(f'vertical="{v_align}"') + if wrap_text: + align_attrs.append(f'wrapText="{wrap_text}"') + return f'' + return '' + + # 确保有足够的模板样式元素 + def _get_or_default(lst, idx, default_name): + if idx < len(lst): + return idx + return 0 + + # ── 确定 4 个 xf 的映射 ────────────────────────────────── + # xf 0: 模板的 xf 0(default) + xf0 = cell_xfs[0] if len(cell_xfs) > 0 else {} + fi0 = _get_or_default(fonts, int(xf0.get('fontId', '0')), 'font') + fl0 = _get_or_default(fills, int(xf0.get('fillId', '0')), 'fill') + bd0 = _get_or_default(borders, int(xf0.get('borderId', '0')), 'border') + + # xf 1: 寻找模板中有边框的 xf(borderId > 0 或 applyBorder 非空) + xf1 = xf0 + fi1, fl1, bd1 = fi0, fl0, bd0 + for xf in cell_xfs: + bid = int(xf.get('borderId', '0')) + ab = xf.get('applyBorder', '') + if bid > 0 or ab: + xf1 = xf + fi1 = _get_or_default(fonts, int(xf1.get('fontId', '0')), 'font') + fl1 = _get_or_default(fills, int(xf1.get('fillId', '0')), 'fill') + bd1 = _get_or_default(borders, int(xf1.get('borderId', '0')), 'border') + break + + # xf 2: 寻找模板中有 bold 字体的 xf + xf2 = xf0 + fi2, fl2, bd2 = fi0, fl0, bd0 + bold_font_id = None + for i, f in enumerate(fonts): + if f.bold: + bold_font_id = i + break + if bold_font_id is not None: + for xf in cell_xfs: + fid = int(xf.get('fontId', '0')) + if xf.get('applyFont', '') or fid == bold_font_id: + xf2 = xf + break + fi2 = bold_font_id + fl2 = _get_or_default(fills, int(xf2.get('fillId', '0')), 'fill') + bd2 = _get_or_default(borders, int(xf2.get('borderId', '0')), 'border') + + # xf 3: 寻找模板中有填充的 xf(fillId > 0 或 applyFill 非空) + xf3 = xf0 + fi3, fl3, bd3 = fi0, fl0, bd0 + for xf in cell_xfs: + fid = int(xf.get('fillId', '0')) + af = xf.get('applyFill', '') + if fid > 0 or af: + xf3 = xf + fi3 = _get_or_default(fonts, int(xf3.get('fontId', '0')), 'font') + fl3 = _get_or_default(fills, int(xf3.get('fillId', '0')), 'fill') + bd3 = _get_or_default(borders, int(xf3.get('borderId', '0')), 'border') + break + + # ── 构建 4 个 xf ────────────────────────────────────────── + parts = [''] + + # xf 0: default (no style) + parts.append(f'') + + # xf 1: pin cells (border + center align) + align1 = _xf_to_alignment(xf1) + parts.append( + f'' + f'{align1}' + f'' + ) + + # xf 2: A1 package info (bold + center align) + align2 = _xf_to_alignment(xf2) or '' + parts.append( + f'' + f'{align2}' + f'' + ) + + # xf 3: header-like (fill + border + center align) + align3 = _xf_to_alignment(xf3) or '' + parts.append( + f'' + f'{align3}' + f'' + ) + + parts.append('') + return ''.join(parts) + + # ── 默认硬编码样式(无模板时回退)──────────────────────────── + + @staticmethod + def _default_fonts_xml() -> str: + """生成默认硬编码字体:font 0 = Calibri 11pt, font 1 = Calibri 11pt bold。""" + return ( + '' + '' + '' + '' + ) + + @staticmethod + def _default_fills_xml() -> str: + """生成默认硬编码填充:fill 0 = none, fill 1 = light gray。""" + return ( + '' + '' + '' + '' + ) + + @staticmethod + def _default_borders_xml() -> str: + """生成默认硬编码边框:border 0 = none, border 1 = thin all sides。""" + return ( + '' + '' + '' + '' + '' + ) + + @staticmethod + def _default_cell_xfs_xml() -> str: + """生成默认硬编码 cellXfs: + xf 0 = default, xf 1 = centered+border, xf 2 = bold+centered, xf 3 = centered+border+fill。 + """ + return ( + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + ) + def _sheet_xml(self, data: dict[str, str]) -> str: """Build sheet1.xml with style indices applied.""" max_row = 0