본문 바로가기
아래아한글 자동화/python+hwp 중급

[5/5, HwpEqn] 수식을 png이미지와 MathML, LaTeX로 내보내기

by 일코 2020. 12. 20.

지난 포스팅은...

 

[4/5, HwpEqn] 수식문자열로 수식 만드는 방법

지난 포스팅은... [3/5, HwpEqn] 컨트롤 탐색방법(feat. 파이썬) 지난 포스팅은... [2/5, HwpEqn] HWP문서 수식을 텍스트로 추출하는 방법 (1) 안녕하세요 ~ 저는 유튜브 회사원코딩 구독자입니다. 올려주신

www.martinii.fun


지난 포스팅에서는 (다소 간략히) 미리 저장해놓은 한/글 수식 문자열을 가지고 수식을 만드는 과정을 보여드렸다. 강조하지는 않았지만, 수식 문자열만 가지고 수식을 생성할 때 맞닥뜨리는, 저품질의 폰트 해결방법이 들어가 있으니, 필요할 때 참고하시기 바란다.

이번 포스팅에서는 한/글의 수식 전부를 이미지(배경투명 png나 흰배경의 gif포맷)로, 또는 mml(MathML) 서식의 텍스트파일로 저장하는 방법을 소개할 예정이다. XSLT를 활용한 레이텍(LaTeX) 저장방법도 소개하려 했으나, 테스트결과 변환 정확도가 높지 않았고, 무엇보다도 2018년도 공주대에서 열린 LaTeX워크샵에서 공개된 hmltotex 프로그램의 성능이 가장 뛰어나다고 하니 아래에 그 링크를 첨부하고, 이 포스팅에서 레이텍변환은 생략하기로 한다. (무엇보다 필자가 다루지 않는 분야인데, 워낙 깊은 뭔가 필요한 것 같아, 블로그의 범위를 벗어나는 느낌이었다ㄷㄷ)

 

La TeXWorkshop/2018

* 과정: 초급과정 * 주제: TeX 수식, 기초부터 활용까지 * 일시: 2018년 11월 17일 (토) 오후 1:00 -- 5:00 * 장소: 공주대학교 인문사회과학대학 107호 컴퓨터실 * ...

wiki.ktug.org

참고로 수식이 들어가 있는 한/글 문서를 MS워드파일로 저장하고 MS워드 안에서 수식을 선택한 후 레이텍버튼을 클릭하면 레이텍 문자열이 문서 안에 나타난다. 워드로 변환 후 LaTeX를 추출하는 방법이 가장 정확도가 높은 것으로 판단된다. 

워드 파일을 열고

 

수식을 클릭한 후,

 

바로 LaTeX 문법으로 표시된다. 온갖 XSLT를 테스트해보았지만, 워드변환이 짱!

 

아래는 각각 배경없는 PNG, 흰배경의 GIF 및 MML(MathML)서식으로 내보내는 방법들이다. 포스팅이 다소 장황하지만, 필요에 따라 발췌해서 사용해 주시길 바람.


 

1. 삽입된 수식 전부를 배경이 투명한 png로 저장하기

이 과정은, 복잡할 것 같지만 사실 간단한 아이디어 한 가지로 해결된다. 바로 "html로 저장하기"를 사용하는 것. 다소 실망스럽겠지만, 가장 간편한 방법이다. 기본적으로는 아래 두 줄의 코드를 실행하면 되는데,

hwp.SaveAs(Path=os.path.join(BASE_DIR, "html.html"), Format="HTML")  # html 서식으로 저장(이미지 자동생성됨)
os.remove(os.path.join(BASE_DIR, "html.html"))  # html파일만 삭제함(이미지는 그대로 둠)

 

한 가지 문제가 있다. hwp.SaveAs 메서드를 사용하는 경우, 파일명이 어떻든 이미지 이름이 PICXXXX.png 형태로 랜덤하게 저장되기 때문에, [여러 개의 한/글 파일을 다루는 경우] 어느 게 어느 파일에서 나온 이미지인지 구분할 수도 없을뿐더러 순서도 뒤섞일 수 있다. 몇 가지 양념을 좀 쳐야 하는데, 우선 코드를 먼저 보여드리겠다.

hwp.SaveAs(Path=os.path.join(BASE_DIR, f"{filename.split('.')[0]}"), Format="HTML+")
os.remove(os.path.join(BASE_DIR, f"{filename.split('.')[0]}"))
os.remove(os.path.join(BASE_DIR, f"{filename.split('.')[0]}_style.css"))

위의 코드와 아래 코드의 차이를 간략히 설명드리면,

1. Format을 "HTML"에서 "HTML+"로 변경했는데, 이러면 이미지파일의 이름이 저장하는 파일명_hd1.png 방식으로 저장된다.

2. CSS도 추가로 저장되는데, 이미지 말고는 필요없으므로 삭제한다.

3. f"{filename.split(".")[0]}"은 파이썬 내부에서 일종의 "렌더링"을 거친 다음에 실행되는데, 변환결과는 "수식예제"이다. 왜냐면,

filename이 "수식예제.hwp"이고(이전 포스팅의 코드 참조),
"."을 기준점으로 좌우로 잘라내서 ["수식예제", "hwp"] 라는 리스트를 생성,
뒤에 붙은 [0]이 "수식예제"를 가리키므로, 변환결과는 "수식예제"

그래서 위 코드를 실행하면, 문서 내의 모든 이미지가 배경없는 png로 저장되면서, 확장자가 없는 "수식예제."라는 html파일과, "수식예제_style.css"파일 두 개가 추가로 생성된다. 위 코드의 마지막 두 줄은 이 파일들을 삭제하는 라인이다.

 


 

2. 삽입된 수식 전부를 흰 배경의 gif파일로 저장하기

위 방법과 비슷하다. gif파일로 저장하려면 "XML"로 저장하면 된다.

hwp.SaveAs(os.path.join(BASE_DIR, f"{filename.split('.')[0]}.xml"), "XML")
os.remove(os.path.join(BASE_DIR, f"{filename.split('.')[0]}.xml"))
os.remove(os.path.join(BASE_DIR, f"{filename.split('.')[0]}.xsl"))

다만, 위 방법으로 저장하면 파일명이 EQUATION152875044.gif 방식으로 저장된다. 이걸 "수식예제1.png" 방식으로 저장하려면, 같이 저장되는 xsl이나 xml파일을 열어서 <SHAPEOBJECT InstId="xxxxxxxxxx"/>라는 태그의 "xxxxxxxxx"를 추출해서 rename에 활용하면 된다. 복잡할 것 같지만, os모듈을 사용하면 간단하다. 파일명 수정 코드를 추가하면 아래와 같다.

# XML파일 저장하기
hwp.SaveAs(os.path.join(BASE_DIR, f"{filename.split('.')[0]}.xml"), "XML")

# XML파일 안에서 <EQUATION><SHAPEOBJECT> 안의 InstId가 수식 아이디인 것을 이용,
with open(os.path.join(BASE_DIR, f"{filename.split('.')[0]}.xml"), encoding="UTF-8") as f:
    xml_string = f.read()  # XML문자열 불러와서

# 두 번의 리스트컴프리헨션을 통해 불필요한 문자열 제거
xml_list = [i for i in xml_string.split("<EQUATION><SHAPEOBJECT InstId=") if i.startswith('"')]
"""
['"1528745045"/></EQUATION><CHAR/></TEXT></P><P ParaShape="20" Style="0"><TEXT CharShape="7"/></P><P ParaShape="20" Style="0"><TEXT CharShape="7"><CHAR>&lt;스칼라삼중곱&gt;</CHAR></TEXT></P><P ParaShape="20" Style="0"><TEXT CharShape="8">',
 '"1528745046"/></EQUATION><CHAR/></TEXT></P><P ParaShape="20" Style="0"><TEXT CharShape="7"/></P><P ParaShape="20" Style="0"><TEXT CharShape="7"><CHAR>&lt;등가속도 운동의 이동거리&gt;</CHAR></TEXT></P><P ParaShape="20" Style="0"><TEXT CharShape="8">',
 '"1528745044"/></EQUATION><CHAR/></TEXT></P></SECTION></BODY></HWPML>']
"""
id_list = [i.split('/')[0].replace('"', '') for i in xml_list]
"""
['1528745045', '1528745046', '1528745044']
"""

os.chdir(BASE_DIR)
for i, j in enumerate(id_list):
    os.rename(f"EQUATION{j}.gif", f"{filename.split('.')[0]}{i}.gif")

os.remove(os.path.join(BASE_DIR, f"{filename.split('.')[0]}.xml"))
os.remove(os.path.join(BASE_DIR, f"{filename.split('.')[0]}.xsl"))

위 코드를 실행한 모습은 아래와 같다.

gif 포맷으로, 문서 안의 모든 수식 이미지 저장하기

 


 

3. 삽입된 수식 전부를 mml(MathML)로 저장하기

이 부분은 PyAutoGui(이하 pag) 모듈을 사용하여 자동화해보려고 한다.

과정은 아래와 같다.

1. 문서를 열고 차례대로 수식 앞으로 이동한다. (for문 및 hwp.SetPos 이용)
2. "속성편집Ctrl-N-K"을 입력한다(pag.hotkey 이용)
3. 차례로 "편집Alt-E" 및 "MathML로 저장하기Alt-S"를 입력한다. (pag.hotkey 이용)
4. 저장경로는 클립보드 조작 모듈(pyperclip)과 pag.hotkey("Ctrl", "V")를 사용하여 입력한다.
5. 모든 수식에 대해 위 과정을 반복한다.

위 과정을 코드로 옮기면 아래와 같다.

for index, location in enumerate(eqn_dict.keys()):
    hwp.SetPos(*location)
    pag.hotkey("Ctrl", "n", "k")  # 편집(E)
    sleep(0.1)
    pag.hotkey("Alt", "e")  # 편집(E)
    sleep(0.1)
    pag.hotkey("Alt", "s")  # MathML로 저장하기
    sleep(0.1)
    if index == 0:
        cb.copy(BASE_DIR.replace("/", "\\")+"\\")
        pag.hotkey("Ctrl", "v")
    cb.copy(index)
    pag.typewrite(cb.paste())
    pag.hotkey("Enter")
    sleep(0.1)
    pag.hotkey("Esc")
    sleep(0.1)
    pag.hotkey("Esc")
    sleep(0.1)

참고로 위 코드 첫 줄의 eqn_dict.keys()는 이전 포스팅에서 다루었다.

파일을 여는 단계부터 코드를 적어보면 아래와 같다.

import os
import pyautogui as pag
from tkinter import Tk
from tkinter.filedialog import askopenfilename
import win32com.client as win32
from time import sleep
import pyperclip as cb

# %%
# 예제파일 선택
root = Tk()
filename = askopenfilename()
root.destroy()
BASE_DIR = os.path.dirname(filename)
filename = os.path.basename(filename)

# 한/글 열기
hwp = win32.gencache.EnsureDispatch("HWPFrame.HwpObject")
hwp.RegisterModule("FilePathCheckDLL", "FilePathCheckerModule")
hwp.Open(os.path.join(BASE_DIR, filename))
hwp.XHwpWindows.Item(0).Visible = True
hwp.HAction.Run("FrameFullScreen")

eqn_dict = {}
ctrl = hwp.HeadCtrl
while ctrl != None:
    nextctrl = ctrl.Next
    if ctrl.CtrlID == "eqed":
        position = ctrl.GetAnchorPos(0)
        position = position.Item("List"), position.Item("Para"), position.Item("Pos")
        hwp.SetPos(*position)
        hwp.FindCtrl()
        Act = hwp.CreateAction("EquationModify")
        Set = Act.CreateSet()
        Pset = Set.CreateItemSet("EqEdit", "EqEdit")
        Act.GetDefault(Pset)
        eqn_dict[position] = Pset.Item("String")
    ctrl = nextctrl
hwp.Run("Cancel")

for index, location in enumerate(eqn_dict.keys()):
    hwp.SetPos(*location)
    pag.hotkey("Ctrl", "n", "k")  # 편집(E)
    sleep(0.1)
    pag.hotkey("Alt", "e")  # 편집(E)
    sleep(0.1)
    pag.hotkey("Alt", "s")  # MathML로 저장하기
    sleep(0.1)
    if index == 0:
        cb.copy(BASE_DIR.replace("/", "\\")+"\\")
        pag.hotkey("Ctrl", "v")
    cb.copy(index)
    pag.typewrite(cb.paste())
    pag.hotkey("Enter")
    sleep(0.1)
    pag.hotkey("Esc")
    sleep(0.1)
    pag.hotkey("Esc")
    sleep(0.1)
    

위 코드를 실행한 모습은 아래와 같다.

문서 내 모든 수식을 mml로 저장하는 pag스크립트 실행화면

문서 내 모든 수식을 mml로 저장하는 pag스크립트 실행화면(움짤)

저장된 mml파일을 하나 열어볼까? 아래 수식이미지에 대응하는 코드[0.mml]는 아래에 적어놓았다.

예제이미지

<math>
    <msup>
        <mi>a</mi>
        <mrow>
            <mi>m</mi>
        </mrow>
    </msup>
    <mo>&#x000F7;</mo>
    <msup>
        <mi>a</mi>
        <mrow>
            <mi>n</mi>
        </mrow>
    </msup>
    <mo>=</mo>
    <mrow>
        <mo>{</mo>
        <mtable>
            <mtr>
                <mtd>
                    <msup>
                        <mi>a</mi>
                        <mrow>
                            <mi>m</mi>
                            <mo>-</mo>
                            <mi>n</mi>
                        </mrow>
                    </msup>
                    <mo>&#x00020;</mo>
                    <mo>&#x00020;</mo>
                </mtd>
                <mtd>
                    <mi>(</mi>
                    <mi>m</mi>
                    <mo>&#x03009;</mo>
                    <mi>n</mi>
                    <mi>)</mi>
                </mtd>
            </mtr>
            <mtr>
                <mtd>
                    <mn>1</mn>
                </mtd>
                <mtd>
                    <mi>(</mi>
                    <mi>m</mi>
                    <mo>=</mo>
                    <mi>n</mi>
                    <mi>)</mi>
                </mtd>
            </mtr>
            <mtr>
                <mtd>
                    <mfrac>
                        <mrow>
                            <mn>1</mn>
                        </mrow>
                        <mrow>
                            <msup>
                                <mi>a</mi>
                                <mrow>
                                    <mi>n</mi>
                                    <mo>-</mo>
                                    <mi>m</mi>
                                </mrow>
                            </msup>
                        </mrow>
                    </mfrac>
                </mtd>
                <mtd>
                    <mi>(</mi>
                    <mi>m</mi>
                    <mo>&#x03008;</mo>
                    <mi>n</mi>
                    <mi>)</mi>
                </mtd>
            </mtr>
        </mtable>
    </mrow>
</math>

여기까지, 한/글 문서 내의 모든 수식을 아래 네 가지로 내보내는 방법에 대해 알아보았다.

0. 한/글 수식을 LaTeX로 내보내기(워드로 변환 후 LaTeX표시 사용 : 추후 워드 연동한 자동화 코드 포스팅 예정)

1. 한/글 수식을 PNG이미지로 내보내기(배경없음)

2. 한/글 수식을 GIF이미지로 내보내기(흰색배경)

3. 한/글 수식을 MML 서식으로 내보내기(MathML파일, PyAutoGUI 사용)

이번 포스팅은 여기서 마친다.

다소 마이너한 주제(?)를 다룬 것 같은데, 부디 많은 분들에게 도움이 되면 좋겠다.

다음 시리즈는, 누름틀을 활용한 청구서와 수료증 발송에 대해 알아보자.

댓글