feat: v1.5.0 - F011 模板格式提取式应用(从模板读取字体/边框/填充/对齐)
This commit is contained in:
@@ -171,15 +171,18 @@ class TemplateReader:
|
|||||||
'fontId': xf.get('fontId', '0'),
|
'fontId': xf.get('fontId', '0'),
|
||||||
'fillId': xf.get('fillId', '0'),
|
'fillId': xf.get('fillId', '0'),
|
||||||
'borderId': xf.get('borderId', '0'),
|
'borderId': xf.get('borderId', '0'),
|
||||||
|
'xfId': xf.get('xfId', '0'),
|
||||||
'applyFont': xf.get('applyFont', ''),
|
'applyFont': xf.get('applyFont', ''),
|
||||||
'applyFill': xf.get('applyFill', ''),
|
'applyFill': xf.get('applyFill', ''),
|
||||||
'applyBorder': xf.get('applyBorder', ''),
|
'applyBorder': xf.get('applyBorder', ''),
|
||||||
|
'applyAlignment': xf.get('applyAlignment', ''),
|
||||||
}
|
}
|
||||||
# 对齐方式
|
# 对齐方式
|
||||||
align = xf.find(_tag('alignment'))
|
align = xf.find(_tag('alignment'))
|
||||||
if align is not None:
|
if align is not None:
|
||||||
xf_info['hAlign'] = align.get('horizontal', '')
|
xf_info['hAlign'] = align.get('horizontal', '')
|
||||||
xf_info['vAlign'] = align.get('vertical', '')
|
xf_info['vAlign'] = align.get('vertical', '')
|
||||||
|
xf_info['wrapText'] = align.get('wrapText', '')
|
||||||
style.cell_xfs.append(xf_info)
|
style.cell_xfs.append(xf_info)
|
||||||
|
|
||||||
def _parse_sheet_dims(self, data: bytes, style: TemplateStyle):
|
def _parse_sheet_dims(self, data: bytes, style: TemplateStyle):
|
||||||
|
|||||||
@@ -258,93 +258,35 @@ class StyledXLSXWriter:
|
|||||||
return '\n'.join(parts)
|
return '\n'.join(parts)
|
||||||
|
|
||||||
def _styles_xml(self) -> str:
|
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
|
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:
|
if s and s.fonts:
|
||||||
f0 = s.fonts[0]
|
# ── 有模板:从模板提取实际值构建样式 ──────────────────
|
||||||
font_name = f0.name
|
fonts_xml = self._build_fonts_xml_from_template(s.fonts)
|
||||||
font_size = str(f0.size)
|
fills_xml = self._build_fills_xml_from_template(s.fills)
|
||||||
font_color = "FF" + f0.color if f0.color and not f0.color.startswith("FF") else f0.color
|
borders_xml = self._build_borders_xml_from_template(s.borders)
|
||||||
fonts_xml += (
|
cell_xfs_xml = self._build_cell_xfs_xml_from_template(
|
||||||
f'<font><sz val="{font_size}"/>'
|
s.cell_xfs, s.fonts, s.fills, s.borders
|
||||||
f'<name val="{font_name}"/>'
|
|
||||||
f'<color rgb="{font_color}"/>'
|
|
||||||
f'</font>'
|
|
||||||
)
|
)
|
||||||
# Font 1: bold (for package info in A1)
|
else:
|
||||||
fonts_xml += (
|
# ── 无模板:回退到硬编码默认样式(F011 fallback)──────
|
||||||
f'<font><sz val="{font_size}"/>'
|
fonts_xml = self._default_fonts_xml()
|
||||||
f'<b/>'
|
fills_xml = self._default_fills_xml()
|
||||||
f'<name val="{font_name}"/>'
|
borders_xml = self._default_borders_xml()
|
||||||
f'<color rgb="{font_color}"/>'
|
cell_xfs_xml = self._default_cell_xfs_xml()
|
||||||
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>'
|
|
||||||
|
|
||||||
parts = ['<?xml version="1.0" encoding="UTF-8" standalone="yes"?>']
|
parts = ['<?xml version="1.0" encoding="UTF-8" standalone="yes"?>']
|
||||||
parts.append(
|
parts.append(
|
||||||
@@ -357,6 +299,250 @@ class StyledXLSXWriter:
|
|||||||
parts.append('</styleSheet>')
|
parts.append('</styleSheet>')
|
||||||
return '\n'.join(parts)
|
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:
|
def _sheet_xml(self, data: dict[str, str]) -> str:
|
||||||
"""Build sheet1.xml with style indices applied."""
|
"""Build sheet1.xml with style indices applied."""
|
||||||
max_row = 0
|
max_row = 0
|
||||||
|
|||||||
Reference in New Issue
Block a user