# Doc Number: Korea JTC1/SC2 k2677_882_export_vba_chunks_to_png.py
# Date: 2026.06.11.
# Author: SHIN, SangHyun
import win32com.client
import os
import sys
import time
import pythoncom
from pathlib import Path
from PIL import Image
# CopyPicture Appearance: xlScreen=1, xlPrinter=2 / Format: xlBitmap=2
_XL_SCREEN, _XL_PRINTER = 1, 2
_XL_BITMAP = 2
# 기호표 N열(0.13)과 동일한 캡처용 더미 열 너비 — PNG 바깥 테두리만 제거하고 데이터 오른쪽 선 유지
_SPACER_COL_WIDTH = 0.13
_MM_TO_PT = 72.0 / 25.4
def _create_excel_app(visible=True):
"""Excel 인스턴스 생성. 기존 프로세스에 붙을 때 Visible 설정이 실패할 수 있어 예외 무시."""
try:
excel = win32com.client.DispatchEx("Excel.Application")
except Exception:
excel = win32com.client.Dispatch("Excel.Application")
if visible:
try:
excel.Visible = True
except Exception:
pass
try:
excel.DisplayAlerts = False
except Exception:
pass
return excel
def export_vba_chunks(excel_path, output_dir):
print("==================================================")
print(" VBA 매크로 분할 로직(페이지 조각) 기반 PNG 추출")
print("==================================================")
if not os.path.exists(output_dir):
os.makedirs(output_dir)
print(f"저장 폴더 생성 완료: {output_dir}")
excel = _create_excel_app(visible=True)
try:
excel.EnableEvents = False
excel.ScreenUpdating = True
except Exception:
pass
excel.ErrorCheckingOptions.BackgroundChecking = False
excel.DisplayCommentIndicator = 0
try:
wb = excel.Workbooks.Open(excel_path)
print("\n[1] 기호테이블 조각 추출 시작...")
ws_symbol = wb.Sheets("기호표")
ws_symbol_name = wb.Sheets("기호명")
ws_symbol.Activate()
excel.ActiveWindow.Zoom = 150
ws_symbol_name.Activate()
excel.ActiveWindow.Zoom = 150
pages = [4, 2, 2, 2, 2, 2, 4, 4, 2]
i1 = 1
j1 = 1
img_idx = 1
for i in range(9):
i2 = i1 + 35
ws_symbol.Activate()
rng = ws_symbol.Range(ws_symbol.Cells(i1, 1), ws_symbol.Cells(i2 - 1, 13))
export_range_to_png(rng, ws_symbol, os.path.join(output_dir, f"01_기호_{img_idx:03d}_기호표_파트{i+1}.png"))
img_idx += 1
for j in range(1, pages[i] + 1):
j2 = j1 + 50
ws_symbol_name.Activate()
rng = ws_symbol_name.Range(ws_symbol_name.Cells(j1, 1), ws_symbol_name.Cells(j2 - 1, 5))
export_range_to_png(rng, ws_symbol_name, os.path.join(output_dir, f"01_기호_{img_idx:03d}_기호명.png"))
img_idx += 1
j1 = j1 + 51
i1 = i1 + 37
print("\n[2] 한글테이블 조각 추출 시작...")
ws_hangul = wb.Sheets("한글표")
ws_hangul.Activate()
excel.ActiveWindow.Zoom = 150
i1 = 1
for i in range(9):
i2 = i1 + 35
rng = ws_hangul.Range(ws_hangul.Cells(i1, 1), ws_hangul.Cells(i2 - 1, 19))
export_range_to_png(rng, ws_hangul, os.path.join(output_dir, f"02_한글_{i+1:03d}_한글표.png"))
i1 = i1 + 37
print("\n[3] 한자테이블 조각 추출 시작...")
ws_hanja = wb.Sheets("한자표")
ws_hanja.Activate()
excel.ActiveWindow.Zoom = 150
i1 = 1
for i in range(26):
i2 = i1 + 35
rng = ws_hanja.Range(ws_hanja.Cells(i1, 1), ws_hanja.Cells(i2 - 1, 25))
export_range_to_png(rng, ws_hanja, os.path.join(output_dir, f"03_한자_{i+1:03d}_한자표.png"))
i1 = i1 + 37
print("\n[4] 부속서 조각 추출 시작...")
ws_annex_a = wb.Sheets("부속서")
ws_annex_a.Activate()
excel.ActiveWindow.Zoom = 150
annex_a_ranges = [
(3, 6), (7, 43), (44, 49), (50, 75), (76, 90),
(91, 129), (130, 139), (140, 189), (190, 232), (233, 238),
(239, 288), (289, 317), (318, 334), (335, 384), (385, 392),
]
for i, (start_row, end_row) in enumerate(annex_a_ranges):
rng = ws_annex_a.Range(ws_annex_a.Cells(start_row, 1), ws_annex_a.Cells(end_row, 5))
# 부속서는 사용자가 원하는대로 값 있는 마지막 행까지만 엄격 trim
rng = _trim_range_to_last_value_row_strict(rng, keep_tail_rows=0)
export_range_to_png(
rng,
ws_annex_a,
os.path.join(output_dir, f"04_부속서_{i+1:03d}_부속서.png"),
)
# 부속서는 011 파일의 하단 여백을 기준으로 전체 하단 여백을 통일
_normalize_annex_bottom_margins(output_dir)
print("\n[5] 동형표 조각 추출 시작...")
ws_annex_b = wb.Sheets("동형표")
ws_annex_b.Activate()
excel.ActiveWindow.Zoom = 150
# 마지막 조각: B93:S100 — 부호 3개/4개 한자 표(테두리·더미열 조작 없음)
annex_b_chunks = [
{"rows": (1, 42), "cols": (1, 21), "keep_bottom_line": True},
{"rows": (47, 88), "cols": (1, 21), "keep_bottom_line": True},
{
"rows": (93, 100),
"cols": (2, 19),
"strip_borders": False,
"spacer": False,
"prepare": True,
# 3번 청크는 현재 결과 유지
"keep_bottom_line": None,
},
]
for i, chunk in enumerate(annex_b_chunks):
if chunk.get("prepare"):
_prepare_donghyeong_last_table(ws_annex_b)
r1, r2 = chunk["rows"]
c1, c2 = chunk["cols"]
rng = ws_annex_b.Range(
ws_annex_b.Cells(r1, c1), ws_annex_b.Cells(r2, c2)
)
export_range_to_png(
rng,
ws_annex_b,
os.path.join(output_dir, f"05_동형표_{i+1:03d}_동형표.png"),
use_spacer_column=chunk.get("spacer", True),
strip_outer_borders=chunk.get("strip_borders"),
keep_bottom_line=chunk.get("keep_bottom_line"),
)
print("\n모든 시트(부속서/동형표 포함) 조각(Chunk) 보내기 완벽 성공!")
finally:
excel.Quit()
_XL_EDGE_LEFT, _XL_EDGE_TOP, _XL_EDGE_BOTTOM, _XL_EDGE_RIGHT = 7, 8, 9, 10
_RANGE_EDGES = (_XL_EDGE_LEFT, _XL_EDGE_RIGHT, _XL_EDGE_TOP, _XL_EDGE_BOTTOM)
_XL_NONE = -4142
def _border_snapshot(border):
return {"LineStyle": border.LineStyle, "Weight": border.Weight, "Color": border.Color}
def _restore_border(border, snap):
if snap["LineStyle"] == _XL_NONE:
border.LineStyle = _XL_NONE
return
border.LineStyle = snap["LineStyle"]
try:
border.Weight = snap["Weight"]
except Exception:
pass
try:
border.Color = snap["Color"]
except Exception:
pass
def _strip_outer_borders_for_capture(rng, keep_bottom_line=False):
saved = []
edges = list(_RANGE_EDGES)
if keep_bottom_line and _XL_EDGE_BOTTOM in edges:
edges.remove(_XL_EDGE_BOTTOM)
for edge in edges:
b = rng.Borders(edge)
saved.append(("range", rng, edge, _border_snapshot(b)))
b.LineStyle = _XL_NONE
nrows, ncols = rng.Rows.Count, rng.Columns.Count
for c in range(1, ncols + 1):
row_edges = [(1, _XL_EDGE_TOP), (nrows, _XL_EDGE_BOTTOM)]
if keep_bottom_line:
row_edges = [(1, _XL_EDGE_TOP)]
for row, edge in row_edges:
cell = rng.Cells(row, c)
b = cell.Borders(edge)
saved.append(("cell", cell, edge, _border_snapshot(b)))
b.LineStyle = _XL_NONE
for r in range(1, nrows + 1):
for col, edge in ((1, _XL_EDGE_LEFT), (ncols, _XL_EDGE_RIGHT)):
cell = rng.Cells(r, col)
b = cell.Borders(edge)
saved.append(("cell", cell, edge, _border_snapshot(b)))
b.LineStyle = _XL_NONE
return saved
def _restore_outer_borders(saved):
for kind, target, edge, snap in saved:
_restore_border(target.Borders(edge), snap)
def _spacer_col_is_usable(ws, col, first_row, last_row):
"""데이터 오른쪽 열이 비어 있으면 삽입 없이 캡처 범위만 확장."""
rng = ws.Range(ws.Cells(first_row, col), ws.Cells(last_row, col))
v = rng.Value
if v is None:
return True
if not isinstance(v, tuple):
return v in (None, "")
for row in v:
if isinstance(row, tuple):
if any(cell not in (None, "") for cell in row):
return False
elif row not in (None, ""):
return False
return True
def _spacer_sides_for_sheet(ws_name):
"""더미 열: 기호명=양쪽, 기호표 등=오른쪽만, 부속서=없음."""
if ws_name == "부속서":
return "none"
if ws_name == "기호명":
return "both"
return "right"
def _strip_outer_borders_for_sheet(ws_name):
"""부속서는 상하좌우 테두리를 그대로 둔다."""
return ws_name != "부속서"
def _keep_bottom_line_for_sheet(ws_name):
"""요청 시트는 PNG 하단선이 반드시 보이도록 외곽선 제거 시 bottom은 유지."""
return ws_name in ("기호표", "한글표", "한자표", "기호명")
def _trim_range_to_last_value_row(rng, keep_tail_rows=0):
"""범위 하단의 빈 행을 줄인다.
값이 없는 행이라도 '하단 테두리'가 있으면 표의 마지막 선이므로 유지한다.
"""
ws = rng.Worksheet
r1 = rng.Row
r2 = rng.Row + rng.Rows.Count - 1
c1 = rng.Column
c2 = rng.Column + rng.Columns.Count - 1
last_val_row = r1
for r in range(r1, r2 + 1):
row_has_bottom_border = False
for c in range(c1, c2 + 1):
v = ws.Cells(r, c).Value
if v is not None and str(v).strip() != "":
last_val_row = r
break
try:
if ws.Cells(r, c).Borders(_XL_EDGE_BOTTOM).LineStyle != _XL_NONE:
row_has_bottom_border = True
except Exception:
pass
if row_has_bottom_border:
last_val_row = r
keep_until = min(r2, last_val_row + max(0, keep_tail_rows))
if keep_until < r2:
return ws.Range(ws.Cells(r1, c1), ws.Cells(keep_until, c2))
return rng
def _trim_range_to_last_value_row_strict(rng, keep_tail_rows=0):
"""값 있는 마지막 행까지만 유지(테두리-only 행 제외)."""
ws = rng.Worksheet
r1 = rng.Row
r2 = rng.Row + rng.Rows.Count - 1
c1 = rng.Column
c2 = rng.Column + rng.Columns.Count - 1
last_val_row = r1
for r in range(r1, r2 + 1):
for c in range(c1, c2 + 1):
v = ws.Cells(r, c).Value
if v is not None and str(v).strip() != "":
last_val_row = r
break
keep_until = min(r2, last_val_row + max(0, keep_tail_rows))
if keep_until < r2:
return ws.Range(ws.Cells(r1, c1), ws.Cells(keep_until, c2))
return rng
def _expand_range_with_bottom_spacer_row(rng, spacer_mm=1.0):
"""범위 하단에 더미 1행을 삽입하고(기본 1mm), 그 행까지 캡처 범위를 확장."""
ws = rng.Worksheet
r1 = rng.Row
r2 = rng.Row + rng.Rows.Count - 1
c1 = rng.Column
c2 = rng.Column + rng.Columns.Count - 1
spacer_row = r2 + 1
ws.Rows(spacer_row).Insert()
ws.Rows(spacer_row).RowHeight = max(1.0, spacer_mm * _MM_TO_PT)
expanded = ws.Range(ws.Cells(r1, c1), ws.Cells(spacer_row, c2))
return expanded, {"ws": ws, "spacer_row": spacer_row}
def _restore_bottom_spacer_row(state):
if not state:
return
try:
state["ws"].Rows(state["spacer_row"]).Delete()
except Exception:
pass
_XL_CENTER = -4108
def _prepare_donghyeong_last_table(ws):
"""동형표 마지막 표: '부호값이 4개인 한자' 제목을 M93:S93 병합으로 전부 노출."""
try:
title_val = None
for addr in ("M93", "P93", "N93"):
v = ws.Range(addr).Value
if v is not None and str(v).strip():
title_val = v
break
if title_val is None:
return
dst = ws.Range("M93:S93")
if dst.MergeCells:
dst.UnMerge()
dst.Merge()
dst.Value = title_val
dst.HorizontalAlignment = _XL_CENTER
dst.WrapText = False
except Exception:
pass
def _narrow_spacer_column(ws, col, first_row, last_row, insert_if_occupied=True):
inserted = False
if not _spacer_col_is_usable(ws, col, first_row, last_row):
if not insert_if_occupied:
raise ValueError(f"spacer column {col} is not empty")
ws.Columns(col).Insert()
inserted = True
width_before = ws.Columns(col).ColumnWidth
ws.Columns(col).ColumnWidth = _SPACER_COL_WIDTH
return {"col": col, "width_before": width_before, "inserted": inserted}
def _insert_left_spacer(ws, at_col):
ws.Columns(at_col).Insert()
width_before = ws.Columns(at_col).ColumnWidth
ws.Columns(at_col).ColumnWidth = _SPACER_COL_WIDTH
return {"col": at_col, "width_before": width_before, "inserted": True}
def _expand_range_with_spacers(rng, sides="right"):
"""캡처 범위 앞·뒤에 0.13 더미 열을 붙여 PNG 바깥 테두리만 제거한다.
- right: 데이터 오른쪽 열 1개 (기호표·한글표 등)
- both: 데이터 양옆 열 1개씩 (기호명 — 좌·우 세로선 유지)
"""
ws = rng.Worksheet
first_row = rng.Row
last_row = rng.Row + rng.Rows.Count - 1
first_col = rng.Column
last_col = rng.Column + rng.Columns.Count - 1
pads = []
capture_first = first_col
capture_last = last_col
if sides == "both":
if first_col > 1 and _spacer_col_is_usable(ws, first_col - 1, first_row, last_row):
pads.append(_narrow_spacer_column(ws, first_col - 1, first_row, last_row, insert_if_occupied=False))
capture_first = first_col - 1
else:
pads.append(_insert_left_spacer(ws, first_col))
capture_first = first_col
last_col += 1
if sides in ("right", "both"):
right_col = last_col + 1
pads.append(_narrow_spacer_column(ws, right_col, first_row, last_row, insert_if_occupied=True))
capture_last = right_col
capture_rng = ws.Range(
ws.Cells(first_row, capture_first),
ws.Cells(last_row, capture_last),
)
return capture_rng, {"ws": ws, "pads": pads}
def _restore_spacer_columns(state):
if not state:
return
ws = state["ws"]
for pad in sorted(state["pads"], key=lambda p: p["col"], reverse=True):
try:
if pad["inserted"]:
ws.Columns(pad["col"]).Delete()
else:
ws.Columns(pad["col"]).ColumnWidth = pad["width_before"]
except Exception:
pass
def _hide_chart_frame(chart):
for area_name in ("ChartArea", "PlotArea"):
try:
area = getattr(chart, area_name)
area.Format.Line.Visible = 0
except Exception:
try:
area = getattr(chart, area_name)
area.Border.LineStyle = _XL_NONE
except Exception:
pass
def _scroll_range_into_view(rng):
try:
rng.Application.Goto(rng, True)
time.sleep(0.08)
except Exception:
pass
def _purge_chart_objects(ws):
for _ in range(50):
try:
if ws.ChartObjects().Count == 0:
return
ws.ChartObjects(1).Delete()
except Exception:
return
def _chart_has_pasted_picture(chart):
try:
return chart.Shapes.Count > 0
except Exception:
return False
def _copy_range_picture(rng, appearance=_XL_PRINTER):
app = rng.Application
_scroll_range_into_view(rng)
was_updating = app.ScreenUpdating
app.ScreenUpdating = True
try:
rng.CopyPicture(Appearance=appearance, Format=_XL_BITMAP)
finally:
app.ScreenUpdating = was_updating
time.sleep(0.35)
pythoncom.PumpWaitingMessages()
def _paste_into_chart(chart, chart_obj, rng):
appearances = (_XL_PRINTER, _XL_PRINTER, _XL_SCREEN)
last_err = None
for i in range(5):
appearance = appearances[min(i, len(appearances) - 1)]
try:
try:
chart_obj.Activate()
except Exception:
pass
time.sleep(0.12)
chart.Paste()
time.sleep(0.15)
if _chart_has_pasted_picture(chart):
return
last_err = RuntimeError("차트에 붙은 그림 없음")
except Exception as e:
last_err = e
time.sleep(0.25)
try:
_copy_range_picture(rng, appearance)
except Exception:
pass
raise last_err
def _png_looks_valid(png_path, min_bytes=8000):
if not os.path.exists(png_path):
return False
return os.path.getsize(png_path) >= min_bytes
def _detect_bottom_blank_rows(img, white_threshold=245):
gray = img.convert("L")
w, h = gray.size
px = gray.load()
last_nonwhite = -1
for y in range(h - 1, -1, -1):
has_nonwhite = False
for x in range(w):
if px[x, y] < white_threshold:
has_nonwhite = True
break
if has_nonwhite:
last_nonwhite = y
break
if last_nonwhite < 0:
return h
return h - 1 - last_nonwhite
def _normalize_png_bottom_blank_rows(png_path, target_blank_rows):
with Image.open(png_path) as img:
img = img.convert("RGB")
w, h = img.size
current_blank = _detect_bottom_blank_rows(img)
if current_blank == target_blank_rows:
return
delta = target_blank_rows - current_blank
if delta < 0:
# 여백이 더 크면 하단을 잘라낸다.
new_h = max(1, h + delta)
out = img.crop((0, 0, w, new_h))
else:
# 여백이 더 작으면 흰색 행을 하단에 붙인다.
out = Image.new("RGB", (w, h + delta), "white")
out.paste(img, (0, 0))
out.save(png_path)
def _normalize_annex_bottom_margins(output_dir):
annex_files = [
os.path.join(output_dir, f"04_부속서_{i:03d}_부속서.png")
for i in range(1, 16)
]
annex_files = [p for p in annex_files if os.path.exists(p)]
if not annex_files:
return
ref_path = os.path.join(output_dir, "04_부속서_011_부속서.png")
if os.path.exists(ref_path):
with Image.open(ref_path) as ref_img:
target_blank = _detect_bottom_blank_rows(ref_img)
else:
blanks = []
for p in annex_files:
with Image.open(p) as img:
blanks.append(_detect_bottom_blank_rows(img))
blanks.sort()
target_blank = blanks[len(blanks) // 2]
for p in annex_files:
_normalize_png_bottom_blank_rows(p, target_blank)
def _export_range_once(
rng,
ws,
png_path,
use_spacer_column=True,
strip_outer_borders=None,
keep_bottom_line=None,
):
app = rng.Application
ws.Activate()
try:
app.ActiveWindow.Zoom = 150
except Exception:
pass
aw = app.ActiveWindow
grid_was_on = aw.DisplayGridlines
aw.DisplayGridlines = False
_purge_chart_objects(ws)
spacer_state = None
capture_rng = rng
if use_spacer_column:
sides = _spacer_sides_for_sheet(ws.Name)
if sides != "none":
capture_rng, spacer_state = _expand_range_with_spacers(rng, sides=sides)
if strip_outer_borders is None:
strip_outer_borders = _strip_outer_borders_for_sheet(ws.Name)
saved_borders = []
if keep_bottom_line is None:
keep_bottom_line = _keep_bottom_line_for_sheet(ws.Name)
if strip_outer_borders:
saved_borders = _strip_outer_borders_for_capture(
capture_rng,
keep_bottom_line=keep_bottom_line,
)
chart_obj = None
try:
copied = False
for appearance in (_XL_PRINTER, _XL_PRINTER, _XL_SCREEN):
for _ in range(3):
try:
_copy_range_picture(capture_rng, appearance)
copied = True
break
except Exception:
time.sleep(0.3)
if copied:
break
if not copied:
return False
# 차트 캔버스에 여유 패딩 추가 (하단 선 짤림 방지)
# CopyPicture 이미지가 rng 치수보다 미세하게 클 수 있어
# 캔버스가 정확히 같으면 하단/우측 테두리 선이 잘림
_H_PAD = 4 # pt (하단 선 짤림 방지 — 주요 원인)
_W_PAD = 2 # pt (우측 선 짤림 방지 — 보조)
chart_obj = ws.ChartObjects().Add(
capture_rng.Left, capture_rng.Top,
capture_rng.Width + _W_PAD,
capture_rng.Height + _H_PAD,
)
chart = chart_obj.Chart
_hide_chart_frame(chart)
try:
_paste_into_chart(chart, chart_obj, capture_rng)
except Exception:
return False
if os.path.exists(png_path):
os.remove(png_path)
chart.Export(png_path, "PNG")
return _png_looks_valid(png_path)
finally:
if chart_obj is not None:
try:
chart_obj.Delete()
except Exception:
pass
if saved_borders:
_restore_outer_borders(saved_borders)
_restore_spacer_columns(spacer_state)
aw.DisplayGridlines = grid_was_on
try:
app.CutCopyMode = False
except Exception:
pass
_purge_chart_objects(ws)
pythoncom.PumpWaitingMessages()
def export_range_to_png(
rng,
ws,
png_path,
use_spacer_column=True,
strip_outer_borders=None,
keep_bottom_line=None,
):
print(f" -> 추출 중: {os.path.basename(png_path)} ... ", end="", flush=True)
app = rng.Application
for attempt in range(3):
if _export_range_once(
rng,
ws,
png_path,
use_spacer_column=use_spacer_column,
strip_outer_borders=strip_outer_borders,
keep_bottom_line=keep_bottom_line,
):
print(f"완료 ({os.path.getsize(png_path):,} bytes)")
return
try:
app.CutCopyMode = False
except Exception:
pass
_purge_chart_objects(ws)
time.sleep(0.4 + attempt * 0.3)
print("실패 (하얀 공백 이미지 발생)")
def export_spacer_test(excel_path, output_dir, sheet_name, row_start, row_end, last_col, tag):
os.makedirs(output_dir, exist_ok=True)
excel = _create_excel_app(visible=True)
try:
wb = excel.Workbooks.Open(excel_path)
ws = wb.Sheets(sheet_name)
ws.Activate()
excel.ActiveWindow.Zoom = 150
rng = ws.Range(ws.Cells(row_start, 1), ws.Cells(row_end, last_col))
sides = _spacer_sides_for_sheet(sheet_name)
strip = _strip_outer_borders_for_sheet(sheet_name)
out = os.path.join(output_dir, f"TEST_{tag}_{sides}.png")
export_range_to_png(rng, ws, out, use_spacer_column=True)
print(f"시험({sheet_name}, spacer={sides}, strip={strip}): {out}")
wb.Close(False)
finally:
excel.Quit()
if __name__ == "__main__":
base_dir = Path(__file__).resolve().parent
excel_file = os.path.join(base_dir, "KSX1001_excel.xls")
output_folder = os.path.join(base_dir, "vba_chunk_images")
if not os.path.exists(excel_file):
print("엑셀 파일을 찾을 수 없습니다.")
elif len(sys.argv) > 1 and sys.argv[1] == "--test-giho":
export_spacer_test(excel_file, output_folder, "기호표", 1, 35, 13, "기호표")
elif len(sys.argv) > 1 and sys.argv[1] == "--test-both":
export_spacer_test(excel_file, output_folder, "기호명", 1, 49, 5, "기호명")
export_spacer_test(excel_file, output_folder, "부속서", 7, 43, 5, "부속서")
elif len(sys.argv) > 1 and sys.argv[1] == "--test-donghyeong":
os.makedirs(output_folder, exist_ok=True)
excel = _create_excel_app(visible=True)
try:
wb = excel.Workbooks.Open(excel_file)
ws = wb.Sheets("동형표")
ws.Activate()
excel.ActiveWindow.Zoom = 150
_prepare_donghyeong_last_table(ws)
rng = ws.Range(ws.Cells(93, 2), ws.Cells(100, 19))
out = os.path.join(output_folder, "TEST_동형표_마지막표.png")
export_range_to_png(
rng, ws, out, use_spacer_column=False, strip_outer_borders=False
)
print(f"시험 출력: {out}")
wb.Close(False)
finally:
excel.Quit()
else:
export_vba_chunks(excel_file, output_folder)