# 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)