431 lines
20 KiB
Python
431 lines
20 KiB
Python
"""
|
|
HWPX 파일 생성기
|
|
StyleAnalyzer 결과를 받아 스타일이 적용된 HWPX 파일 생성
|
|
"""
|
|
|
|
import os
|
|
import zipfile
|
|
import xml.etree.ElementTree as ET
|
|
from typing import List, Dict, Optional
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
from style_analyzer import StyleAnalyzer, StyledElement
|
|
from hwp_style_mapping import HwpStyleMapper, HwpStyle, ROLE_TO_STYLE_NAME
|
|
|
|
|
|
@dataclass
|
|
class HwpxConfig:
|
|
"""HWPX 생성 설정"""
|
|
paper_width: int = 59528 # A4 너비 (hwpunit, 1/7200 inch)
|
|
paper_height: int = 84188 # A4 높이
|
|
margin_left: int = 8504
|
|
margin_right: int = 8504
|
|
margin_top: int = 5668
|
|
margin_bottom: int = 4252
|
|
default_font: str = "함초롬바탕"
|
|
default_font_size: int = 1000 # 10pt (hwpunit)
|
|
|
|
|
|
class HwpxGenerator:
|
|
"""HWPX 파일 생성기"""
|
|
|
|
def __init__(self, config: Optional[HwpxConfig] = None):
|
|
self.config = config or HwpxConfig()
|
|
self.mapper = HwpStyleMapper()
|
|
self.used_styles: set = set()
|
|
|
|
def generate(self, elements: List[StyledElement], output_path: str) -> str:
|
|
"""
|
|
StyledElement 리스트로부터 HWPX 파일 생성
|
|
|
|
Args:
|
|
elements: StyleAnalyzer로 분류된 요소 리스트
|
|
output_path: 출력 파일 경로 (.hwpx)
|
|
|
|
Returns:
|
|
생성된 파일 경로
|
|
"""
|
|
# 사용된 스타일 수집
|
|
self.used_styles = {e.role for e in elements}
|
|
|
|
# 임시 디렉토리 생성
|
|
temp_dir = Path(output_path).with_suffix('.temp')
|
|
temp_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
try:
|
|
# HWPX 구조 생성
|
|
self._create_mimetype(temp_dir)
|
|
self._create_meta_inf(temp_dir)
|
|
self._create_version(temp_dir)
|
|
self._create_header(temp_dir)
|
|
self._create_content(temp_dir, elements)
|
|
self._create_settings(temp_dir)
|
|
|
|
# ZIP으로 압축
|
|
self._create_hwpx(temp_dir, output_path)
|
|
|
|
return output_path
|
|
|
|
finally:
|
|
# 임시 파일 정리
|
|
import shutil
|
|
if temp_dir.exists():
|
|
shutil.rmtree(temp_dir)
|
|
|
|
def _create_mimetype(self, temp_dir: Path):
|
|
"""mimetype 파일 생성"""
|
|
mimetype_path = temp_dir / "mimetype"
|
|
mimetype_path.write_text("application/hwp+zip")
|
|
|
|
def _create_meta_inf(self, temp_dir: Path):
|
|
"""META-INF/manifest.xml 생성"""
|
|
meta_dir = temp_dir / "META-INF"
|
|
meta_dir.mkdir(exist_ok=True)
|
|
|
|
manifest = """<?xml version="1.0" encoding="UTF-8"?>
|
|
<manifest:manifest xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0">
|
|
<manifest:file-entry manifest:full-path="/" manifest:media-type="application/hwp+zip"/>
|
|
<manifest:file-entry manifest:full-path="version.xml" manifest:media-type="application/xml"/>
|
|
<manifest:file-entry manifest:full-path="Contents/header.xml" manifest:media-type="application/xml"/>
|
|
<manifest:file-entry manifest:full-path="Contents/section0.xml" manifest:media-type="application/xml"/>
|
|
<manifest:file-entry manifest:full-path="settings.xml" manifest:media-type="application/xml"/>
|
|
</manifest:manifest>"""
|
|
|
|
(meta_dir / "manifest.xml").write_text(manifest, encoding='utf-8')
|
|
|
|
def _create_version(self, temp_dir: Path):
|
|
"""version.xml 생성"""
|
|
version = """<?xml version="1.0" encoding="UTF-8"?>
|
|
<hh:HWPMLVersion xmlns:hh="http://www.hancom.co.kr/hwpml/2011/head" version="1.1"/>"""
|
|
|
|
(temp_dir / "version.xml").write_text(version, encoding='utf-8')
|
|
|
|
def _create_header(self, temp_dir: Path):
|
|
"""Contents/header.xml 생성 (스타일 정의 포함)"""
|
|
contents_dir = temp_dir / "Contents"
|
|
contents_dir.mkdir(exist_ok=True)
|
|
|
|
# 스타일별 속성 생성
|
|
char_props_xml = self._generate_char_properties()
|
|
para_props_xml = self._generate_para_properties()
|
|
styles_xml = self._generate_styles_xml()
|
|
|
|
header = f"""<?xml version="1.0" encoding="UTF-8"?>
|
|
<hh:head xmlns:hh="http://www.hancom.co.kr/hwpml/2011/head"
|
|
xmlns:hc="http://www.hancom.co.kr/hwpml/2011/core"
|
|
xmlns:hp="http://www.hancom.co.kr/hwpml/2011/paragraph"
|
|
version="1.5" secCnt="1">
|
|
<hh:beginNum page="1" footnote="1" endnote="1" pic="1" tbl="1" equation="1"/>
|
|
<hh:refList>
|
|
<hh:fontfaces itemCnt="7">
|
|
<hh:fontface lang="HANGUL" fontCnt="2">
|
|
<hh:font id="0" face="맑은 고딕" type="TTF" isEmbedded="0"/>
|
|
<hh:font id="1" face="함초롬돋움" type="TTF" isEmbedded="0"/>
|
|
</hh:fontface>
|
|
<hh:fontface lang="LATIN" fontCnt="2">
|
|
<hh:font id="0" face="맑은 고딕" type="TTF" isEmbedded="0"/>
|
|
<hh:font id="1" face="함초롬돋움" type="TTF" isEmbedded="0"/>
|
|
</hh:fontface>
|
|
<hh:fontface lang="HANJA" fontCnt="2">
|
|
<hh:font id="0" face="맑은 고딕" type="TTF" isEmbedded="0"/>
|
|
<hh:font id="1" face="함초롬돋움" type="TTF" isEmbedded="0"/>
|
|
</hh:fontface>
|
|
<hh:fontface lang="JAPANESE" fontCnt="1">
|
|
<hh:font id="0" face="맑은 고딕" type="TTF" isEmbedded="0"/>
|
|
</hh:fontface>
|
|
<hh:fontface lang="OTHER" fontCnt="1">
|
|
<hh:font id="0" face="맑은 고딕" type="TTF" isEmbedded="0"/>
|
|
</hh:fontface>
|
|
<hh:fontface lang="SYMBOL" fontCnt="1">
|
|
<hh:font id="0" face="맑은 고딕" type="TTF" isEmbedded="0"/>
|
|
</hh:fontface>
|
|
<hh:fontface lang="USER" fontCnt="1">
|
|
<hh:font id="0" face="맑은 고딕" type="TTF" isEmbedded="0"/>
|
|
</hh:fontface>
|
|
</hh:fontfaces>
|
|
<hh:borderFills itemCnt="2">
|
|
<hh:borderFill id="1" threeD="0" shadow="0" centerLine="NONE">
|
|
<hh:slash type="NONE" Crooked="0" isCounter="0"/>
|
|
<hh:backSlash type="NONE" Crooked="0" isCounter="0"/>
|
|
<hh:leftBorder type="NONE" width="0.1 mm" color="#000000"/>
|
|
<hh:rightBorder type="NONE" width="0.1 mm" color="#000000"/>
|
|
<hh:topBorder type="NONE" width="0.1 mm" color="#000000"/>
|
|
<hh:bottomBorder type="NONE" width="0.1 mm" color="#000000"/>
|
|
</hh:borderFill>
|
|
<hh:borderFill id="2" threeD="0" shadow="0" centerLine="NONE">
|
|
<hh:slash type="NONE" Crooked="0" isCounter="0"/>
|
|
<hh:backSlash type="NONE" Crooked="0" isCounter="0"/>
|
|
<hh:leftBorder type="NONE" width="0.1 mm" color="#000000"/>
|
|
<hh:rightBorder type="NONE" width="0.1 mm" color="#000000"/>
|
|
<hh:topBorder type="NONE" width="0.1 mm" color="#000000"/>
|
|
<hh:bottomBorder type="NONE" width="0.1 mm" color="#000000"/>
|
|
<hc:fillBrush><hc:winBrush faceColor="none" hatchColor="#000000" alpha="0"/></hc:fillBrush>
|
|
</hh:borderFill>
|
|
</hh:borderFills>
|
|
{char_props_xml}
|
|
{para_props_xml}
|
|
{styles_xml}
|
|
</hh:refList>
|
|
<hh:compatibleDocument targetProgram="HWP201X"/>
|
|
<hh:docOption>
|
|
<hh:linkinfo path="" pageInherit="1" footnoteInherit="0"/>
|
|
</hh:docOption>
|
|
</hh:head>"""
|
|
|
|
(contents_dir / "header.xml").write_text(header, encoding='utf-8')
|
|
|
|
def _generate_char_properties(self) -> str:
|
|
"""글자 속성 XML 생성"""
|
|
lines = [f' <hh:charProperties itemCnt="{len(self.used_styles) + 1}">']
|
|
|
|
# 기본 글자 속성 (id=0)
|
|
lines.append(''' <hh:charPr id="0" height="1000" textColor="#000000" shadeColor="none" useFontSpace="0" useKerning="0" symMark="NONE" borderFillIDRef="1">
|
|
<hh:fontRef hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>
|
|
<hh:ratio hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/>
|
|
<hh:spacing hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>
|
|
<hh:relSz hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/>
|
|
<hh:offset hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>
|
|
<hh:underline type="NONE" shape="SOLID" color="#000000"/>
|
|
<hh:strikeout shape="NONE" color="#000000"/>
|
|
<hh:outline type="NONE"/>
|
|
<hh:shadow type="NONE" color="#B2B2B2" offsetX="10" offsetY="10"/>
|
|
</hh:charPr>''')
|
|
|
|
# 역할별 글자 속성
|
|
for idx, role in enumerate(sorted(self.used_styles), start=1):
|
|
style = self.mapper.get_style(role)
|
|
height = int(style.font_size * 100) # pt → hwpunit
|
|
color = style.font_color.lstrip('#')
|
|
font_id = "1" if style.font_bold else "0" # 굵게면 함초롬돋움
|
|
|
|
lines.append(f''' <hh:charPr id="{idx}" height="{height}" textColor="#{color}" shadeColor="none" useFontSpace="0" useKerning="0" symMark="NONE" borderFillIDRef="1">
|
|
<hh:fontRef hangul="{font_id}" latin="{font_id}" hanja="{font_id}" japanese="{font_id}" other="{font_id}" symbol="{font_id}" user="{font_id}"/>
|
|
<hh:ratio hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/>
|
|
<hh:spacing hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>
|
|
<hh:relSz hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/>
|
|
<hh:offset hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>
|
|
<hh:underline type="NONE" shape="SOLID" color="#000000"/>
|
|
<hh:strikeout shape="NONE" color="#000000"/>
|
|
<hh:outline type="NONE"/>
|
|
<hh:shadow type="NONE" color="#B2B2B2" offsetX="10" offsetY="10"/>
|
|
</hh:charPr>''')
|
|
|
|
lines.append(' </hh:charProperties>')
|
|
return '\n'.join(lines)
|
|
|
|
def _generate_para_properties(self) -> str:
|
|
"""문단 속성 XML 생성"""
|
|
lines = [f' <hh:paraProperties itemCnt="{len(self.used_styles) + 1}">']
|
|
|
|
# 기본 문단 속성 (id=0)
|
|
lines.append(''' <hh:paraPr id="0" tabPrIDRef="0" condense="0" fontLineHeight="0" snapToGrid="0" suppressLineNumbers="0" checked="0">
|
|
<hh:align horizontal="JUSTIFY" vertical="BASELINE"/>
|
|
<hh:heading type="NONE" idRef="0" level="0"/>
|
|
<hh:breakSetting breakLatinWord="KEEP_WORD" breakNonLatinWord="KEEP_WORD" widowOrphan="0" keepWithNext="0" keepLines="0" pageBreakBefore="0" lineWrap="BREAK"/>
|
|
<hh:autoSpacing eAsianEng="0" eAsianNum="0"/>
|
|
<hp:switch xmlns:hp="http://www.hancom.co.kr/hwpml/2011/paragraph">
|
|
<hp:case hp:required-namespace="http://www.hancom.co.kr/hwpml/2016/HwpUnitChar">
|
|
<hh:margin><hc:intent value="0" unit="HWPUNIT"/><hc:left value="0" unit="HWPUNIT"/><hc:right value="0" unit="HWPUNIT"/><hc:prev value="0" unit="HWPUNIT"/><hc:next value="0" unit="HWPUNIT"/></hh:margin>
|
|
<hh:lineSpacing type="PERCENT" value="160" unit="HWPUNIT"/>
|
|
</hp:case>
|
|
<hp:default>
|
|
<hh:margin><hc:intent value="0" unit="HWPUNIT"/><hc:left value="0" unit="HWPUNIT"/><hc:right value="0" unit="HWPUNIT"/><hc:prev value="0" unit="HWPUNIT"/><hc:next value="0" unit="HWPUNIT"/></hh:margin>
|
|
<hh:lineSpacing type="PERCENT" value="160" unit="HWPUNIT"/>
|
|
</hp:default>
|
|
</hp:switch>
|
|
<hh:border borderFillIDRef="1" offsetLeft="0" offsetRight="0" offsetTop="0" offsetBottom="0" connect="0" ignoreMargin="0"/>
|
|
</hh:paraPr>''')
|
|
|
|
# 역할별 문단 속성
|
|
align_map = {"left": "LEFT", "center": "CENTER", "right": "RIGHT", "justify": "JUSTIFY"}
|
|
|
|
for idx, role in enumerate(sorted(self.used_styles), start=1):
|
|
style = self.mapper.get_style(role)
|
|
align_val = align_map.get(style.align, "JUSTIFY")
|
|
line_spacing = int(style.line_spacing)
|
|
left_margin = int(style.indent_left * 100)
|
|
indent = int(style.indent_first * 100)
|
|
space_before = int(style.space_before * 100)
|
|
space_after = int(style.space_after * 100)
|
|
|
|
lines.append(f''' <hh:paraPr id="{idx}" tabPrIDRef="0" condense="0" fontLineHeight="0" snapToGrid="0" suppressLineNumbers="0" checked="0">
|
|
<hh:align horizontal="{align_val}" vertical="BASELINE"/>
|
|
<hh:heading type="NONE" idRef="0" level="0"/>
|
|
<hh:breakSetting breakLatinWord="KEEP_WORD" breakNonLatinWord="KEEP_WORD" widowOrphan="0" keepWithNext="0" keepLines="0" pageBreakBefore="0" lineWrap="BREAK"/>
|
|
<hh:autoSpacing eAsianEng="0" eAsianNum="0"/>
|
|
<hp:switch xmlns:hp="http://www.hancom.co.kr/hwpml/2011/paragraph">
|
|
<hp:case hp:required-namespace="http://www.hancom.co.kr/hwpml/2016/HwpUnitChar">
|
|
<hh:margin><hc:intent value="{indent}" unit="HWPUNIT"/><hc:left value="{left_margin}" unit="HWPUNIT"/><hc:right value="0" unit="HWPUNIT"/><hc:prev value="{space_before}" unit="HWPUNIT"/><hc:next value="{space_after}" unit="HWPUNIT"/></hh:margin>
|
|
<hh:lineSpacing type="PERCENT" value="{line_spacing}" unit="HWPUNIT"/>
|
|
</hp:case>
|
|
<hp:default>
|
|
<hh:margin><hc:intent value="{indent}" unit="HWPUNIT"/><hc:left value="{left_margin}" unit="HWPUNIT"/><hc:right value="0" unit="HWPUNIT"/><hc:prev value="{space_before}" unit="HWPUNIT"/><hc:next value="{space_after}" unit="HWPUNIT"/></hh:margin>
|
|
<hh:lineSpacing type="PERCENT" value="{line_spacing}" unit="HWPUNIT"/>
|
|
</hp:default>
|
|
</hp:switch>
|
|
<hh:border borderFillIDRef="1" offsetLeft="0" offsetRight="0" offsetTop="0" offsetBottom="0" connect="0" ignoreMargin="0"/>
|
|
</hh:paraPr>''')
|
|
|
|
lines.append(' </hh:paraProperties>')
|
|
return '\n'.join(lines)
|
|
|
|
def _generate_styles_xml(self) -> str:
|
|
"""스타일 정의 XML 생성 (charPrIDRef, paraPrIDRef 참조)"""
|
|
lines = [f' <hh:styles itemCnt="{len(self.used_styles) + 1}">']
|
|
|
|
# 기본 스타일 (id=0, 바탕글)
|
|
lines.append(' <hh:style id="0" type="PARA" name="바탕글" engName="Normal" paraPrIDRef="0" charPrIDRef="0" nextStyleIDRef="0" langID="1042" lockForm="0"/>')
|
|
|
|
# 역할별 스타일 (charPrIDRef, paraPrIDRef 참조)
|
|
for idx, role in enumerate(sorted(self.used_styles), start=1):
|
|
style = self.mapper.get_style(role)
|
|
style_name = style.name.replace('<', '<').replace('>', '>')
|
|
|
|
lines.append(f' <hh:style id="{idx}" type="PARA" name="{style_name}" engName="" paraPrIDRef="{idx}" charPrIDRef="{idx}" nextStyleIDRef="{idx}" langID="1042" lockForm="0"/>')
|
|
|
|
lines.append(' </hh:styles>')
|
|
return '\n'.join(lines)
|
|
|
|
def _create_content(self, temp_dir: Path, elements: List[StyledElement]):
|
|
"""Contents/section0.xml 생성 (본문 + 스타일 참조)"""
|
|
contents_dir = temp_dir / "Contents"
|
|
|
|
# 문단 XML 생성
|
|
paragraphs = []
|
|
current_table = None
|
|
|
|
# 역할 → 스타일 인덱스 매핑 생성
|
|
role_to_idx = {role: idx for idx, role in enumerate(sorted(self.used_styles), start=1)}
|
|
|
|
for elem in elements:
|
|
style = self.mapper.get_style(elem.role)
|
|
style_idx = role_to_idx.get(elem.role, 0)
|
|
|
|
# 테이블 요소는 특수 처리
|
|
if elem.role in ["TH", "TD", "TABLE_CAPTION", "TABLE", "FIGURE"]:
|
|
continue # 테이블/그림은 별도 처리 필요
|
|
|
|
# 일반 문단
|
|
para_xml = self._create_paragraph(elem.text, style, style_idx)
|
|
paragraphs.append(para_xml)
|
|
|
|
section = f"""<?xml version="1.0" encoding="UTF-8"?>
|
|
<hs:sec xmlns:hs="http://www.hancom.co.kr/hwpml/2011/section"
|
|
xmlns:hc="http://www.hancom.co.kr/hwpml/2011/core">
|
|
{"".join(paragraphs)}
|
|
</hs:sec>"""
|
|
|
|
(contents_dir / "section0.xml").write_text(section, encoding='utf-8')
|
|
|
|
def _create_paragraph(self, text: str, style: HwpStyle, style_idx: int) -> str:
|
|
"""단일 문단 XML 생성"""
|
|
text = self._escape_xml(text)
|
|
|
|
return f'''
|
|
<hp:p xmlns:hp="http://www.hancom.co.kr/hwpml/2011/paragraph"
|
|
paraPrIDRef="{style_idx}" styleIDRef="{style_idx}" pageBreak="0" columnBreak="0" merged="0">
|
|
<hp:run charPrIDRef="{style_idx}">
|
|
<hp:t>{text}</hp:t>
|
|
</hp:run>
|
|
</hp:p>'''
|
|
|
|
def _escape_xml(self, text: str) -> str:
|
|
"""XML 특수문자 이스케이프"""
|
|
return (text
|
|
.replace("&", "&")
|
|
.replace("<", "<")
|
|
.replace(">", ">")
|
|
.replace('"', """)
|
|
.replace("'", "'"))
|
|
|
|
def _create_settings(self, temp_dir: Path):
|
|
"""settings.xml 생성"""
|
|
settings = """<?xml version="1.0" encoding="UTF-8"?>
|
|
<hs:settings xmlns:hs="http://www.hancom.co.kr/hwpml/2011/settings">
|
|
<hs:viewSetting>
|
|
<hs:viewType val="printView"/>
|
|
<hs:zoom val="100"/>
|
|
</hs:viewSetting>
|
|
</hs:settings>"""
|
|
|
|
(temp_dir / "settings.xml").write_text(settings, encoding='utf-8')
|
|
|
|
def _create_hwpx(self, temp_dir: Path, output_path: str):
|
|
"""HWPX 파일 생성 (ZIP 압축)"""
|
|
with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zf:
|
|
# mimetype은 압축하지 않고 첫 번째로
|
|
mimetype_path = temp_dir / "mimetype"
|
|
zf.write(mimetype_path, "mimetype", compress_type=zipfile.ZIP_STORED)
|
|
|
|
# 나머지 파일들
|
|
for root, dirs, files in os.walk(temp_dir):
|
|
for file in files:
|
|
if file == "mimetype":
|
|
continue
|
|
file_path = Path(root) / file
|
|
arcname = file_path.relative_to(temp_dir)
|
|
zf.write(file_path, arcname)
|
|
|
|
|
|
def convert_html_to_hwpx(html: str, output_path: str) -> str:
|
|
"""
|
|
HTML → HWPX 변환 메인 함수
|
|
|
|
Args:
|
|
html: HTML 문자열
|
|
output_path: 출력 파일 경로
|
|
|
|
Returns:
|
|
생성된 파일 경로
|
|
"""
|
|
# 1. HTML 분석 → 역할 분류
|
|
analyzer = StyleAnalyzer()
|
|
elements = analyzer.analyze(html)
|
|
|
|
print(f"📊 분석 완료: {len(elements)}개 요소")
|
|
for role, count in analyzer.get_role_summary().items():
|
|
print(f" {role}: {count}")
|
|
|
|
# 2. HWPX 생성
|
|
generator = HwpxGenerator()
|
|
result_path = generator.generate(elements, output_path)
|
|
|
|
print(f"✅ 생성 완료: {result_path}")
|
|
return result_path
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# 테스트
|
|
test_html = """
|
|
<html>
|
|
<body>
|
|
<div class="box-cover">
|
|
<h1>건설·토목 측량 DX 실무지침</h1>
|
|
<h2>드론/UAV·GIS·지형/지반 모델 기반</h2>
|
|
<p>2024년 1월</p>
|
|
</div>
|
|
|
|
<h1>1. 개요</h1>
|
|
<p>본 보고서는 건설 및 토목 분야의 측량 디지털 전환에 대한 실무 지침을 제공합니다.</p>
|
|
|
|
<h2>1.1 배경</h2>
|
|
<p>최근 드론과 GIS 기술의 발전으로 측량 업무가 크게 변화하고 있습니다.</p>
|
|
|
|
<h3>1.1.1 기술 동향</h3>
|
|
<p>1) <strong>드론 측량의 발전</strong></p>
|
|
<p>드론을 활용한 측량은 기존 방식 대비 효율성이 크게 향상되었습니다.</p>
|
|
|
|
<p>(1) <strong>RTK 드론</strong></p>
|
|
<p>실시간 보정 기능을 갖춘 RTK 드론이 보급되고 있습니다.</p>
|
|
|
|
<ul>
|
|
<li>고정밀 GPS 수신기 내장</li>
|
|
<li>센티미터 단위 정확도</li>
|
|
</ul>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
output = "/home/claude/test_output.hwpx"
|
|
convert_html_to_hwpx(test_html, output) |