feat: v1.5.0 - F011 模板格式提取式应用(从模板读取字体/边框/填充/对齐)
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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 = '<fonts count="2">'
|
||||
# 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'<font><sz val="{font_size}"/>'
|
||||
f'<name val="{font_name}"/>'
|
||||
f'<color rgb="{font_color}"/>'
|
||||
f'</font>'
|
||||
# ── 有模板:从模板提取实际值构建样式 ──────────────────
|
||||
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
|
||||
)
|
||||
# Font 1: bold (for package info in A1)
|
||||
fonts_xml += (
|
||||
f'<font><sz val="{font_size}"/>'
|
||||
f'<b/>'
|
||||
f'<name val="{font_name}"/>'
|
||||
f'<color rgb="{font_color}"/>'
|
||||
f'</font>'
|
||||
)
|
||||
fonts_xml += '</fonts>'
|
||||
|
||||
# ── Fills ──────────────────────────────────────────────────
|
||||
fills_xml = '<fills count="2">'
|
||||
fills_xml += '<fill><patternFill patternType="none"/></fill>'
|
||||
# Fill 1: light gray for header-like cells
|
||||
fills_xml += (
|
||||
'<fill><patternFill patternType="solid">'
|
||||
'<fgColor rgb="FFF0F0F0"/>'
|
||||
'</patternFill></fill>'
|
||||
)
|
||||
fills_xml += '</fills>'
|
||||
|
||||
# ── Borders ────────────────────────────────────────────────
|
||||
borders_xml = '<borders count="2">'
|
||||
# Border 0: none
|
||||
borders_xml += (
|
||||
'<border>'
|
||||
'<left/><right/><top/><bottom/><diagonal/>'
|
||||
'</border>'
|
||||
)
|
||||
# Border 1: thin all sides
|
||||
borders_xml += (
|
||||
'<border>'
|
||||
'<left style="thin"/><right style="thin"/>'
|
||||
'<top style="thin"/><bottom style="thin"/>'
|
||||
'<diagonal/>'
|
||||
'</border>'
|
||||
)
|
||||
borders_xml += '</borders>'
|
||||
|
||||
# ── 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 = '<cellXfs count="4">'
|
||||
cell_xfs_xml += (
|
||||
'<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>'
|
||||
)
|
||||
cell_xfs_xml += (
|
||||
'<xf numFmtId="0" fontId="0" fillId="0" borderId="1" xfId="0" '
|
||||
'applyBorder="1">'
|
||||
'<alignment horizontal="center" vertical="center"/>'
|
||||
'</xf>'
|
||||
)
|
||||
cell_xfs_xml += (
|
||||
'<xf numFmtId="0" fontId="1" fillId="0" borderId="0" xfId="0" '
|
||||
'applyFont="1">'
|
||||
'<alignment horizontal="center" vertical="center"/>'
|
||||
'</xf>'
|
||||
)
|
||||
cell_xfs_xml += (
|
||||
'<xf numFmtId="0" fontId="0" fillId="1" borderId="1" xfId="0" '
|
||||
'applyFill="1" applyBorder="1">'
|
||||
'<alignment horizontal="center" vertical="center"/>'
|
||||
'</xf>'
|
||||
)
|
||||
cell_xfs_xml += '</cellXfs>'
|
||||
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 = ['<?xml version="1.0" encoding="UTF-8" standalone="yes"?>']
|
||||
parts.append(
|
||||
@@ -357,6 +299,250 @@ class StyledXLSXWriter:
|
||||
parts.append('</styleSheet>')
|
||||
return '\n'.join(parts)
|
||||
|
||||
# ── 模板样式构建(F011)─────────────────────────────────────────
|
||||
|
||||
@staticmethod
|
||||
def _build_fonts_xml_from_template(fonts: list) -> str:
|
||||
"""从模板字体列表构建 <fonts> XML。"""
|
||||
parts = [f'<fonts count="{len(fonts)}">']
|
||||
for f in fonts:
|
||||
parts.append('<font>')
|
||||
parts.append(f'<sz val="{f.size}"/>')
|
||||
if f.bold:
|
||||
parts.append('<b/>')
|
||||
if f.italic:
|
||||
parts.append('<i/>')
|
||||
parts.append(f'<name val="{f.name}"/>')
|
||||
color_val = f.color
|
||||
if color_val and not color_val.startswith('FF'):
|
||||
color_val = 'FF' + color_val
|
||||
if color_val:
|
||||
parts.append(f'<color rgb="{color_val}"/>')
|
||||
parts.append('</font>')
|
||||
parts.append('</fonts>')
|
||||
return ''.join(parts)
|
||||
|
||||
@staticmethod
|
||||
def _build_fills_xml_from_template(fills: list) -> str:
|
||||
"""从模板填充列表构建 <fills> XML。"""
|
||||
parts = [f'<fills count="{len(fills)}">']
|
||||
for fl in fills:
|
||||
parts.append('<fill>')
|
||||
parts.append(f'<patternFill patternType="{fl.pattern_type}">')
|
||||
if fl.fg_color:
|
||||
color_val = fl.fg_color
|
||||
if not color_val.startswith('FF'):
|
||||
color_val = 'FF' + color_val
|
||||
parts.append(f'<fgColor rgb="{color_val}"/>')
|
||||
parts.append('</patternFill>')
|
||||
parts.append('</fill>')
|
||||
parts.append('</fills>')
|
||||
return ''.join(parts)
|
||||
|
||||
@staticmethod
|
||||
def _build_borders_xml_from_template(borders: list) -> str:
|
||||
"""从模板边框列表构建 <borders> XML。"""
|
||||
parts = [f'<borders count="{len(borders)}">']
|
||||
for b in borders:
|
||||
parts.append('<border>')
|
||||
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('<diagonal/>')
|
||||
parts.append('</border>')
|
||||
parts.append('</borders>')
|
||||
return ''.join(parts)
|
||||
|
||||
@staticmethod
|
||||
def _build_cell_xfs_xml_from_template(
|
||||
cell_xfs: list, fonts: list, fills: list, borders: list
|
||||
) -> str:
|
||||
"""从模板 cellXfs 列表构建 <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'<alignment {" ".join(align_attrs)}/>'
|
||||
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 = ['<cellXfs count="4">']
|
||||
|
||||
# xf 0: default (no style)
|
||||
parts.append(f'<xf numFmtId="0" fontId="{fi0}" fillId="{fl0}" borderId="{bd0}" xfId="0"/>')
|
||||
|
||||
# xf 1: pin cells (border + center align)
|
||||
align1 = _xf_to_alignment(xf1)
|
||||
parts.append(
|
||||
f'<xf numFmtId="0" fontId="{fi1}" fillId="{fl1}" borderId="{bd1}" xfId="0" applyBorder="1">'
|
||||
f'{align1}'
|
||||
f'</xf>'
|
||||
)
|
||||
|
||||
# xf 2: A1 package info (bold + center align)
|
||||
align2 = _xf_to_alignment(xf2) or '<alignment horizontal="center" vertical="center"/>'
|
||||
parts.append(
|
||||
f'<xf numFmtId="0" fontId="{fi2}" fillId="{fl2}" borderId="{bd2}" xfId="0" applyFont="1">'
|
||||
f'{align2}'
|
||||
f'</xf>'
|
||||
)
|
||||
|
||||
# xf 3: header-like (fill + border + center align)
|
||||
align3 = _xf_to_alignment(xf3) or '<alignment horizontal="center" vertical="center"/>'
|
||||
parts.append(
|
||||
f'<xf numFmtId="0" fontId="{fi3}" fillId="{fl3}" borderId="{bd3}" xfId="0" applyFill="1" applyBorder="1">'
|
||||
f'{align3}'
|
||||
f'</xf>'
|
||||
)
|
||||
|
||||
parts.append('</cellXfs>')
|
||||
return ''.join(parts)
|
||||
|
||||
# ── 默认硬编码样式(无模板时回退)────────────────────────────
|
||||
|
||||
@staticmethod
|
||||
def _default_fonts_xml() -> str:
|
||||
"""生成默认硬编码字体:font 0 = Calibri 11pt, font 1 = Calibri 11pt bold。"""
|
||||
return (
|
||||
'<fonts count="2">'
|
||||
'<font><sz val="11"/><name val="Calibri"/><color rgb="FF000000"/></font>'
|
||||
'<font><sz val="11"/><b/><name val="Calibri"/><color rgb="FF000000"/></font>'
|
||||
'</fonts>'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _default_fills_xml() -> str:
|
||||
"""生成默认硬编码填充:fill 0 = none, fill 1 = light gray。"""
|
||||
return (
|
||||
'<fills count="2">'
|
||||
'<fill><patternFill patternType="none"/></fill>'
|
||||
'<fill><patternFill patternType="solid"><fgColor rgb="FFF0F0F0"/></patternFill></fill>'
|
||||
'</fills>'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _default_borders_xml() -> str:
|
||||
"""生成默认硬编码边框:border 0 = none, border 1 = thin all sides。"""
|
||||
return (
|
||||
'<borders count="2">'
|
||||
'<border><left/><right/><top/><bottom/><diagonal/></border>'
|
||||
'<border><left style="thin"/><right style="thin"/>'
|
||||
'<top style="thin"/><bottom style="thin"/><diagonal/></border>'
|
||||
'</borders>'
|
||||
)
|
||||
|
||||
@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 (
|
||||
'<cellXfs count="4">'
|
||||
'<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>'
|
||||
'<xf numFmtId="0" fontId="0" fillId="0" borderId="1" xfId="0" applyBorder="1">'
|
||||
'<alignment horizontal="center" vertical="center"/>'
|
||||
'</xf>'
|
||||
'<xf numFmtId="0" fontId="1" fillId="0" borderId="0" xfId="0" applyFont="1">'
|
||||
'<alignment horizontal="center" vertical="center"/>'
|
||||
'</xf>'
|
||||
'<xf numFmtId="0" fontId="0" fillId="1" borderId="1" xfId="0" applyFill="1" applyBorder="1">'
|
||||
'<alignment horizontal="center" vertical="center"/>'
|
||||
'</xf>'
|
||||
'</cellXfs>'
|
||||
)
|
||||
|
||||
def _sheet_xml(self, data: dict[str, str]) -> str:
|
||||
"""Build sheet1.xml with style indices applied."""
|
||||
max_row = 0
|
||||
|
||||
Reference in New Issue
Block a user