Skip to content

Commit 8357700

Browse files
feat: enhance Excel tools with comprehensive style handling and improved XML conversion
- Updated `excel-to-xml.py` to retrieve all cell styles, including merged cells, borders, background colors, and font colors. - Modified `create_xml_with_styles` to include all style information in the generated XML. - Enhanced `markdown-to-excel.py` to maintain column width settings when creating Excel files. - Improved `xml-to-excel.py` to set column widths and row heights based on XML input, ensuring better fidelity in Excel output.
1 parent 1e1a9a1 commit 8357700

File tree

3 files changed

+122
-63
lines changed

3 files changed

+122
-63
lines changed

tools/excel-to-xml.py

Lines changed: 90 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -8,99 +8,129 @@
88
from dify_plugin import Tool
99
from dify_plugin.entities.tool import ToolInvokeMessage
1010

11-
def get_important_cell_styles(cell) -> dict:
12-
"""デフォルトから変更のある重要なスタイル情報のみを取得"""
11+
def get_cell_styles(cell) -> dict:
12+
"""セルのすべてのスタイル情報を取得"""
1313
styles = {}
1414

1515
# セル結合情報の取得
16-
if cell.parent.merged_cells: # merged_cellsはワークシートの属性
16+
if cell.parent.merged_cells:
1717
for merged_range in cell.parent.merged_cells.ranges:
1818
if cell.coordinate in merged_range:
1919
styles['merge'] = {
20-
'start': merged_range.coord.split(':')[0], # 結合開始セル
21-
'end': merged_range.coord.split(':')[1] # 結合終了セル
20+
'start': merged_range.coord.split(':')[0],
21+
'end': merged_range.coord.split(':')[1]
2222
}
2323
break
2424

25-
# 罫線情報(デフォルトのNoneまたはthinは除外)
26-
borders = {}
27-
for side in ['top', 'bottom', 'left', 'right']:
28-
border = getattr(cell.border, side)
29-
if border.style and border.style not in ['thin', None]:
30-
borders[side] = border.style
31-
if borders:
32-
styles['borders'] = borders
25+
# 罫線情報(すべての罫線情報を取得)
26+
if cell.border:
27+
borders = {}
28+
for side in ['top', 'bottom', 'left', 'right']:
29+
border = getattr(cell.border, side)
30+
if border and border.style:
31+
borders[side] = border.style
32+
if borders:
33+
styles['borders'] = borders
3334

34-
# 背景色(デフォルトの白や透明以外で、かつ有効な値の場合のみ)
35-
if (cell.fill and cell.fill.start_color and
36-
cell.fill.start_color.rgb and
37-
cell.fill.start_color.rgb not in ['FFFFFFFF', '00000000'] and # 白と透明を除外
38-
isinstance(cell.fill.start_color.rgb, str)):
39-
styles['background'] = cell.fill.start_color.rgb
35+
# 背景色(パターンタイプと色情報を取得)
36+
if cell.fill:
37+
if hasattr(cell.fill, 'patternType') and cell.fill.patternType:
38+
if cell.fill.patternType == 'solid':
39+
if (cell.fill.start_color and
40+
cell.fill.start_color.rgb and
41+
isinstance(cell.fill.start_color.rgb, str)):
42+
# 透明の場合は除外
43+
if cell.fill.start_color.rgb != '00000000':
44+
styles['background'] = cell.fill.start_color.rgb
4045

41-
# 文字色(デフォルトの黒以外で、かつ有効な値の場合のみ
46+
# 文字色(すべての文字色を取得
4247
if (cell.font and cell.font.color and
4348
cell.font.color.rgb and
44-
cell.font.color.rgb != 'FF000000' and
4549
isinstance(cell.font.color.rgb, str)):
4650
styles['color'] = cell.font.color.rgb
4751

48-
return styles if styles else None
52+
return styles
4953

5054
def create_xml_with_styles(wb) -> str:
51-
"""ワークシートの内容と変更のあるスタイル情報のみをXMLに変換"""
55+
"""ワークシートの内容とすべてのスタイル情報をXMLに変換"""
5256
ws = wb.active
57+
58+
# 結合セルの範囲を保存
59+
merged_cells_ranges = ws.merged_cells.ranges.copy()
60+
61+
# 結合セルを一時的に解除
62+
for merged_cell_range in merged_cells_ranges:
63+
ws.unmerge_cells(str(merged_cell_range))
64+
5365
xml_parts = ['<?xml version="1.0" encoding="UTF-8"?>\n<workbook>']
66+
xml_parts.append(' <worksheet>')
67+
68+
# すべての列幅情報を追加
69+
for col_letter, col in ws.column_dimensions.items():
70+
if hasattr(col, 'width') and col.width is not None:
71+
width = float(col.width)
72+
xml_parts.append(f' <column letter="{col_letter}" width="{width:.2f}"/>')
73+
74+
# 結合セルを再設定
75+
for merged_cell_range in merged_cells_ranges:
76+
ws.merge_cells(str(merged_cell_range))
77+
78+
# すべての行高さ情報を追加
79+
for row_idx, row in ws.row_dimensions.items():
80+
if row.height:
81+
xml_parts.append(f' <row index="{row_idx}" height="{row.height:.2f}"/>')
5482

5583
# データ範囲を取得
5684
data_rows = list(ws.rows)
5785
if not data_rows:
5886
return "<workbook></workbook>"
5987

60-
xml_parts.append(' <worksheet>')
88+
# セルの値をXMLエスケープする関数
89+
def escape_xml(text):
90+
if not isinstance(text, str):
91+
text = str(text)
92+
return (text.replace('&', '&amp;')
93+
.replace('<', '&lt;')
94+
.replace('>', '&gt;')
95+
.replace('"', '&quot;')
96+
.replace("'", '&apos;'))
6197

62-
for row_idx, row in enumerate(data_rows, 1):
63-
has_content = False
64-
row_parts = []
98+
# すべてのセル情報を処理
99+
for row in data_rows:
100+
row_idx = row[0].row
101+
xml_parts.append(f' <row index="{row_idx}">')
65102

66-
for col_idx, cell in enumerate(row, 1):
67-
col_letter = get_column_letter(col_idx)
68-
value = cell.value if cell.value is not None else ""
69-
styles = get_important_cell_styles(cell)
103+
for cell in row:
104+
# 値の有無に関わらずすべてのセルを出力
105+
xml_parts.append(f' <cell ref="{cell.coordinate}">')
106+
107+
if cell.value is not None:
108+
value = escape_xml(cell.value)
109+
xml_parts.append(f' <value>{value}</value>')
70110

71-
# 値かスタイルがある場合のみ出力
72-
if value or styles:
73-
has_content = True
74-
cell_parts = [f' <cell ref="{col_letter}{row_idx}">']
75-
76-
if value:
77-
cell_parts.append(f' <value>{value}</value>')
78-
79-
if styles:
80-
for style_type, style_value in styles.items():
81-
if isinstance(style_value, dict):
82-
# 罫線情報の場合
83-
if style_value: # 空の辞書は出力しない
84-
cell_parts.append(f' <{style_type}>')
85-
for side, style in style_value.items():
86-
cell_parts.append(f' <border side="{side}" style="{style}"/>')
87-
cell_parts.append(f' </{style_type}>')
88-
else:
89-
# 背景色や文字色の場合(有効な値の場合のみ)
90-
cell_parts.append(f' <{style_type}>{style_value}</{style_type}>')
91-
92-
cell_parts.append(' </cell>')
93-
row_parts.extend(cell_parts)
111+
# スタイル情報を追加
112+
styles = get_cell_styles(cell)
113+
if styles:
114+
for style_name, style_value in styles.items():
115+
if style_name == 'merge':
116+
xml_parts.append(' <merge>')
117+
xml_parts.append(f' <start>{style_value["start"]}</start>')
118+
xml_parts.append(f' <end>{style_value["end"]}</end>')
119+
xml_parts.append(' </merge>')
120+
elif style_name == 'borders':
121+
xml_parts.append(' <borders>')
122+
for border_side, border_style in style_value.items():
123+
xml_parts.append(f' <border side="{border_side}" style="{border_style}"/>')
124+
xml_parts.append(' </borders>')
125+
else:
126+
xml_parts.append(f' <{style_name}>{style_value}</{style_name}>')
127+
128+
xml_parts.append(' </cell>')
94129

95-
# 行に内容がある場合のみ出力
96-
if has_content:
97-
xml_parts.append(f' <row index="{row_idx}">')
98-
xml_parts.extend(row_parts)
99-
xml_parts.append(' </row>')
130+
xml_parts.append(' </row>')
100131

101132
xml_parts.append(' </worksheet>')
102133
xml_parts.append('</workbook>')
103-
104134
return '\n'.join(xml_parts)
105135

106136
def get_url_from_file_data(file_data: Any) -> str:

tools/markdown-to-excel.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from pathlib import Path
77
from io import StringIO, BytesIO
88
import json
9+
import openpyxl
910

1011
from dify_plugin import Tool
1112
from dify_plugin.entities.tool import ToolInvokeMessage
@@ -60,6 +61,11 @@ def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessag
6061

6162
# メモリ上でExcelファイルを作成
6263
excel_buffer = BytesIO()
64+
wb = openpyxl.Workbook()
65+
# 列幅の設定を保持するためのオプションを追加
66+
wb.loaded_theme = True
67+
wb.iso_dates = False
68+
# その他の設定...
6369
df.to_excel(excel_buffer, index=False, engine='openpyxl')
6470
excel_data = excel_buffer.getvalue()
6571

tools/xml-to-excel.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,18 +60,41 @@ def apply_cell_styles(cell, style_elements):
6060

6161
def create_excel_from_xml(xml_text: str) -> BytesIO:
6262
"""XML形式のテキストからExcelファイルを生成"""
63-
# 新しいワークブックを作成
6463
wb = openpyxl.Workbook()
6564
ws = wb.active
6665

6766
try:
68-
# XMLをパース
6967
root = ET.fromstring(xml_text)
7068
worksheet = root.find('worksheet')
7169
if worksheet is None:
7270
raise ValueError("worksheetタグが見つかりません")
7371

74-
# 各行を処理
72+
# 列幅の設定
73+
for col_elem in worksheet.findall('column'):
74+
col_letter = col_elem.get('letter')
75+
width = col_elem.get('width')
76+
if col_letter and width:
77+
try:
78+
# 列幅を設定(直接ディメンションに設定)
79+
column_dimension = ws.column_dimensions[col_letter]
80+
column_dimension.width = float(width)
81+
print(f"Setting column {col_letter} width to {width}") # デバッグ用
82+
except Exception as e:
83+
print(f"Error setting column {col_letter} width: {str(e)}") # デバッグ用
84+
continue
85+
86+
# 行高さの設定
87+
for row_elem in worksheet.findall('row'):
88+
row_idx = row_elem.get('index')
89+
height = row_elem.get('height')
90+
if row_idx and height:
91+
try:
92+
row = ws.row_dimensions[int(row_idx)]
93+
row.height = float(height)
94+
except ValueError:
95+
continue # 無効な行インデックスはスキップ
96+
97+
# 各行のデータを処理
7598
for row_elem in worksheet.findall('row'):
7699
try:
77100
row_idx = int(row_elem.get('index'))

0 commit comments

Comments
 (0)