# Doc Number: Korea JTC1/SC2: k2653_282_word_to_png_transfer.py
# Date: 2026.06.11.
# Author: SHIN, SangHyun
import argparse
import os
import win32com.client
import fitz # pymupdf — pip install pymupdf
from PIL import Image, ImageChops # pillow — pip install pillow
# ── Word 상수 ─────────────────────────────────────────────────────────────────
WD_ACTIVE_END_PAGE_NUMBER = 3 # wdActiveEndPageNumber
WD_STAT_PAGES = 4 # wdStatisticPages
WD_PAGE_PT_MAX = 1584.0 # Word 허용 최대 페이지 크기(포인트)
WD_PAGE_PT_MIN = 7.2 # Word 허용 최소 페이지 크기(포인트)
# ── 기본값 (커맨드라인 인자로 덮어씀) ─────────────────────────────────────────
DPI = 400
# ─────────────────────────────────────────────────────────────────────────────
# 유틸리티
# ─────────────────────────────────────────────────────────────────────────────
def autocrop_white(img: Image.Image, dpi: int) -> Image.Image:
"""
흰색(255,255,255) 배경을 기준으로 이미지 여백 자동 제거 후,
상하좌우에 정확히 1mm의 흰색 여백 추가.
"""
bg = Image.new("RGB", img.size, (255, 255, 255))
diff = ImageChops.difference(img, bg)
bbox = diff.getbbox() # 비흰색 픽셀의 bounding box
if not bbox:
return img
cropped = img.crop(bbox)
# 1mm에 해당하는 픽셀 수 계산 (1 inch = 25.4 mm)
margin_px = int(round(dpi * 1.0 / 25.4))
new_w = cropped.width + 2 * margin_px
new_h = cropped.height + 2 * margin_px
# 흰색 배경에 크롭된 이미지를 중앙에 배치
padded = Image.new("RGB", (new_w, new_h), (255, 255, 255))
padded.paste(cropped, (margin_px, margin_px))
return padded
def get_total_pages(doc) -> int:
"""문서 전체 페이지 수."""
try:
return doc.ComputeStatistics(WD_STAT_PAGES)
except Exception:
try:
return doc.BuiltInDocumentProperties("Number of Pages").Value
except Exception:
return 1
def get_all_tables_by_page(doc) -> dict:
"""
문서의 모든 테이블을 {페이지번호: [테이블1-based인덱스, ...]} 로 반환.
테이블 첫 번째 셀의 페이지 기준.
"""
page_table_map = {}
for i in range(1, doc.Tables.Count + 1):
try:
page = doc.Tables(i).Cell(1, 1).Range.Information(WD_ACTIVE_END_PAGE_NUMBER)
page_table_map.setdefault(page, []).append(i)
except Exception as e:
print(f" [경고] 테이블 {i} 페이지 확인 실패: {e}")
return page_table_map
# ─────────────────────────────────────────────────────────────────────────────
# 핵심 변환 함수
# ─────────────────────────────────────────────────────────────────────────────
def table_to_png(word, table, output_png: str, temp_pdf: str) -> tuple[int, int]:
"""
Word 테이블 하나를 고해상도 PNG로 변환.
전략:
- 임시 문서 여백을 0으로 설정, 페이지 크기를 최대(1584pt)로 설정
→ 테이블이 가급적 적은 페이지로 분할됨
- PDF 내보내기 후 PyMuPDF로 래스터화
- 여러 페이지가 나오면 Pillow로 수직 스티칭
- 흰 여백 자동 크롭 → 테이블 콘텐츠만 남은 PNG 저장
반환: (width_px, height_px)
"""
new_doc = word.Documents.Add()
try:
ps = new_doc.PageSetup
# ① 원본 테이블의 편집용지 너비 읽기 (A4 ≈ 595pt, 가로 842pt 등)
# 콘텐츠 너비 = 페이지 너비 - 좌우 여백 (여백은 새 문서에서 0으로 설정하므로 그 값을 사용)
try:
src_ps = table.Range.PageSetup
page_width = src_ps.PageWidth # 예: A4 = 595.3pt
# 원본 여백을 제외한 실제 콘텐츠 너비
content_w = page_width - src_ps.LeftMargin - src_ps.RightMargin
content_w = max(min(content_w, WD_PAGE_PT_MAX), WD_PAGE_PT_MIN)
except Exception:
content_w = 451.0 # fallback: A4 기본 여백 제거 시 콘텐츠 너비 (~15.9cm)
# ② 여백을 먼저 0으로 설정 (PageHeight 설정 전 "여백 > 페이지" 오류 방지)
for attr in ("TopMargin", "BottomMargin", "LeftMargin", "RightMargin"):
try:
setattr(ps, attr, 0)
except Exception:
setattr(ps, attr, WD_PAGE_PT_MIN)
# ③ 페이지 너비 = 원본 콘텐츠 너비, 높이 = 최대 (페이지 분할 최소화)
ps.PageWidth = content_w
ps.PageHeight = WD_PAGE_PT_MAX
# ④ 테이블 복사 → 새 문서에 붙여넣기 (원본 서식 유지를 위해 PasteAndFormat 사용)
table.Range.Copy()
new_doc.Range(0, 0).PasteAndFormat(16) # 16 = wdFormatOriginalFormatting
# ⑤ 16열 한자 표인 경우 열 너비 균일화 및 줄바꿈 방지 설정
pasted_table = new_doc.Tables(1)
if pasted_table.Columns.Count == 16:
try:
pasted_table.AllowAutoFit = False
# ① 열 너비 균일화 설정
for c in range(1, 17):
if c % 2 != 0:
# 번호 열 (홀수 열): 조금 넓게 설정 (32.5 / 468.0 비율)
pasted_table.Columns(c).Width = content_w * (32.5 / 468.0)
else:
# 한자 열 (짝수 열): 조금 좁게 설정 (26.0 / 468.0 비율)
pasted_table.Columns(c).Width = content_w * (26.0 / 468.0)
# ② 단락 공백 제거 및 줄바꿈 규칙 강제 적용 (행간 어긋남 방지)
pasted_table.Range.ParagraphFormat.SpaceBefore = 0.0
pasted_table.Range.ParagraphFormat.SpaceAfter = 0.0
pasted_table.Range.ParagraphFormat.LineSpacingRule = 0 # wdLineSpaceSingle (단일 행간)
# ③ 모든 셀의 상하 수직 정렬을 가운데 정렬로 설정 (wdCellAlignVerticalCenter = 1)
pasted_table.Range.Cells.VerticalAlignment = 1
# ④ 행 높이(행간) 일치화 설정 (wdRowHeightExactly = 2)
pasted_table.Rows(1).HeightRule = 2
pasted_table.Rows(1).Height = 16.0
for r in range(2, pasted_table.Rows.Count + 1):
pasted_table.Rows(r).HeightRule = 2
pasted_table.Rows(r).Height = 30.5
# ⑤ 테두리 설정: 모든 테두리 제거 (음영이 연속적인 띠가 되도록 설정)
for b_idx in range(-8, 1):
try:
pasted_table.Borders(b_idx).LineStyle = 0
except Exception:
pass
except Exception as e:
print(f" [경고] 표 규격(열 너비/행 높이/정렬) 조정 실패 (병합된 셀이 있을 수 있음): {e}")
# ⑥ PDF 내보내기
new_doc.ExportAsFixedFormat(
OutputFileName = os.path.abspath(temp_pdf),
ExportFormat = 17, # wdExportFormatPDF
OpenAfterExport = False,
OptimizeFor = 0, # wdExportOptimizeForPrint (최고 화질)
BitmapMissingFonts = True,
UseISO19005_1 = False,
)
finally:
new_doc.Close(False)
# ⑤ PDF → 이미지 래스터화 (모든 페이지)
mat = fitz.Matrix(DPI / 72, DPI / 72) # 72dpi 기준 배율 변환
pdf_doc = fitz.open(temp_pdf)
page_imgs = []
for pi in range(len(pdf_doc)):
pix = pdf_doc[pi].get_pixmap(matrix=mat, alpha=False)
page_imgs.append(Image.frombytes("RGB", [pix.width, pix.height], pix.samples))
pdf_doc.close()
n_pages = len(page_imgs)
# ⑥ 수직 스티칭 (단일 페이지면 그대로)
if n_pages == 1:
stitched = page_imgs[0]
else:
total_w = max(im.width for im in page_imgs)
total_h = sum(im.height for im in page_imgs)
stitched = Image.new("RGB", (total_w, total_h), (255, 255, 255))
y_off = 0
for im in page_imgs:
stitched.paste(im, (0, y_off))
y_off += im.height
# ⑦ 흰 여백 자동 크롭 (테이블 외 여백 제거) 및 1mm 여백 추가
cropped = autocrop_white(stitched, DPI)
# ⑧ PNG 저장
cropped.save(output_png, "PNG")
# 임시 PDF 삭제
try:
os.remove(temp_pdf)
except Exception:
pass
suffix = f", {n_pages}장 스티칭" if n_pages > 1 else ""
print(f" ✅ {os.path.basename(output_png)}"
f" ({cropped.width}×{cropped.height}px, {DPI}dpi{suffix})")
return cropped.width, cropped.height
# ─────────────────────────────────────────────────────────────────────────────
# 메인 처리
# ─────────────────────────────────────────────────────────────────────────────
def export_all_tables(docx_path: str):
"""지정한 Word 파일의 모든 테이블을 고해상도 PNG로 변환."""
# 출력 폴더: Word 파일 옆 output_tables/<파일명>/ 자동 생성
doc_dir = os.path.dirname(os.path.abspath(docx_path))
doc_stem = os.path.splitext(os.path.basename(docx_path))[0]
output_dir = os.path.join(doc_dir, "output_tables", doc_stem)
os.makedirs(output_dir, exist_ok=True)
print(f"파일 : {os.path.basename(docx_path)}")
print(f"출력 : {output_dir}")
word = win32com.client.Dispatch("Word.Application")
word.Visible = False
doc = word.Documents.Open(os.path.abspath(docx_path))
saved = []
try:
total_pages = get_total_pages(doc)
total_tables = doc.Tables.Count
print(f"페이지: {total_pages}쪽 | 테이블: {total_tables}개\n")
if total_tables == 0:
print("테이블이 없습니다.")
return []
page_table_map = get_all_tables_by_page(doc)
for page_num in sorted(page_table_map.keys()):
indices = page_table_map[page_num]
print(f" [페이지 {page_num}] 테이블 {len(indices)}개")
for seq, tbl_idx in enumerate(indices, start=1):
print(f" → 테이블 {tbl_idx} 변환 중...")
table = doc.Tables(tbl_idx)
out_png = os.path.join(output_dir, f"page{page_num:03d}_table{seq:02d}.png")
temp_pdf = os.path.join(output_dir, f"_tmp_{page_num:03d}_{seq:02d}.pdf")
try:
table_to_png(word, table, out_png, temp_pdf)
saved.append(out_png)
except Exception as e:
print(f" [오류] {e}")
finally:
doc.Close(False)
word.Quit()
print(f"\n완료! PNG {len(saved)}개 → {output_dir}")
return saved
# ─────────────────────────────────────────────────────────────────────────────
# 진입점
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
import sys
try:
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
except Exception:
pass
parser = argparse.ArgumentParser(
description="Word 문서의 모든 테이블을 고해상도 PNG로 변환합니다."
)
parser.add_argument(
"docx",
metavar="Word파일경로",
help="변환할 .docx / .doc 파일의 경로"
)
parser.add_argument(
"--dpi",
type=int,
default=400,
metavar="N",
help="출력 해상도 dpi (기본값: 400)"
)
args = parser.parse_args()
# 파일 탐색 및 유효성 검사 (CWD 및 스크립트 디렉토리 탐색, 확장자 보정)
input_path = args.docx
resolved_path = None
search_dirs = [os.getcwd(), os.path.dirname(os.path.abspath(__file__))]
extensions = ["", ".docx", ".doc"]
for s_dir in search_dirs:
for ext in extensions:
# 입력한 파일명에 이미 확장자가 있는데 다른 확장자를 덧붙이는 것 방지
if ext != "" and input_path.lower().endswith((".docx", ".doc")):
continue
candidate = os.path.join(s_dir, input_path + ext)
if os.path.isfile(candidate):
resolved_path = os.path.abspath(candidate)
break
if resolved_path:
break
if not resolved_path:
print(f"[오류] 파일을 찾을 수 없습니다: {input_path}")
print(" → 현재 작업 디렉토리(CWD)와 스크립트 디렉토리를 모두 검색했습니다.")
print(' → 파일명에 공백이 있으면 큰따옴표로 감싸세요.')
print(f' 예) python word_to_png_transfer.py "파일명 포함 공백.docx"')
raise SystemExit(1)
if not resolved_path.lower().endswith((".docx", ".doc")):
print(f"[오류] Word 파일(.docx/.doc)이 아닙니다: {resolved_path}")
raise SystemExit(1)
docx_path = resolved_path
DPI = args.dpi
export_all_tables(docx_path)