Spaces:
Running
PII Scanner: HWP (Hancom) file support via pyhwp + olefile fallback
Browse filesAdds .hwp parsing so HWP v5 documents can be uploaded and analyzed
the same way as .txt/.pdf/.docx.
hwp_extract.py (new module)
- extract_hwp_text(raw_bytes) is the public entry point.
- Primary path: invoke pyhwp's `hwp5txt` CLI via subprocess.
Pure-Python and works on any platform once pyhwp is pip-installed.
- Fallback path: parse the file ourselves with olefile + zlib +
struct, walking the HWP v5 record stream and decoding the
HWPTAG_PARA_TEXT records (UTF-16-LE plus inline / 8-wchar
extended control chars). This keeps things working even if
pyhwp isn't installed or its CLI is not on PATH.
requirements*.txt
- pyhwp>=0.1b15 and olefile>=0.46 added to both full and lite
requirement lists. Both are pure Python so they install cleanly
on Python 3.11โ3.14.
app.py / app_lite.py
- extract_text() now dispatches '.hwp' to hwp_extract.extract_hwp_text.
templates/index.html, README.md
- Drag area and README list .hwp as a supported format and call out
the pyhwp + olefile pipeline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- README.md +2 -0
- app.py +4 -0
- app_lite.py +4 -0
- hwp_extract.py +223 -0
- requirements.txt +3 -0
- requirements_lite.txt +3 -0
- templates/index.html +1 -1
|
@@ -16,6 +16,8 @@
|
|
| 16 |
์ถ๊ฐ ๊ธฐ๋ฅ:
|
| 17 |
|
| 18 |
- **์ฌ์ฉ์ ์ ์ ๋ฃฐ** (`custom_patterns.yaml`) โ ์ ๊ท์ยทํค์๋ ์ธ์๊ธฐ ์์ ๋กญ๊ฒ ์ถ๊ฐ
|
|
|
|
|
|
|
| 19 |
- **ํค / ์ํธ๋ฌธ ์ง๋ ฌํ** โ Public/Secret/Relin Key ์ Encryption Parameters, Ciphertext ๋ฅผ base64 ๋ก ๋ณด๊ณ /๋ณต์ฌ/๋ค์ด๋ก๋
|
| 20 |
- **RRN ๊ฒ์ ๋ฌถ์ ๋ค์ด๋ก๋** โ ๋ชจ๋ ํ๋ณด ct + ์๋ฒ ๊ฒฐ๊ณผ ct + ๋ฉํ๋ฅผ JSON ํ ํ์ผ๋ก ์ ์ฅ
|
| 21 |
|
|
|
|
| 16 |
์ถ๊ฐ ๊ธฐ๋ฅ:
|
| 17 |
|
| 18 |
- **์ฌ์ฉ์ ์ ์ ๋ฃฐ** (`custom_patterns.yaml`) โ ์ ๊ท์ยทํค์๋ ์ธ์๊ธฐ ์์ ๋กญ๊ฒ ์ถ๊ฐ
|
| 19 |
+
- **์ธ์ด ์ ํ** โ PII Scanner ์์ ์๋(ํ+์) / ํ๊ตญ์ด / English ํ ๊ธ
|
| 20 |
+
- **์ง์ ํฌ๋งท** โ `.txt .log .csv .json .md .yaml .xml .html .pdf .docx .hwp` (HWP ๋ [pyhwp](https://pypi.org/project/pyhwp/) ์ `hwp5txt` CLI ๋ฅผ ์ฐ์ ์ฌ์ฉ, ๋ฏธ์ค์น ์ `olefile` ๊ธฐ๋ฐ ์์ฒด ํ์๋ก ํด๋ฐฑ)
|
| 21 |
- **ํค / ์ํธ๋ฌธ ์ง๋ ฌํ** โ Public/Secret/Relin Key ์ Encryption Parameters, Ciphertext ๋ฅผ base64 ๋ก ๋ณด๊ณ /๋ณต์ฌ/๋ค์ด๋ก๋
|
| 22 |
- **RRN ๊ฒ์ ๋ฌถ์ ๋ค์ด๋ก๋** โ ๋ชจ๋ ํ๋ณด ct + ์๋ฒ ๊ฒฐ๊ณผ ct + ๋ฉํ๋ฅผ JSON ํ ํ์ผ๋ก ์ ์ฅ
|
| 23 |
|
|
@@ -198,6 +198,10 @@ def extract_text(filename: str, raw: bytes) -> str:
|
|
| 198 |
parts.append(cell.text)
|
| 199 |
return "\n".join(parts)
|
| 200 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
return _smart_decode(raw)
|
| 202 |
|
| 203 |
|
|
|
|
| 198 |
parts.append(cell.text)
|
| 199 |
return "\n".join(parts)
|
| 200 |
|
| 201 |
+
if ext == ".hwp":
|
| 202 |
+
from hwp_extract import extract_hwp_text # lazy import
|
| 203 |
+
return extract_hwp_text(raw)
|
| 204 |
+
|
| 205 |
return _smart_decode(raw)
|
| 206 |
|
| 207 |
|
|
@@ -347,6 +347,10 @@ def extract_text(filename: str, raw: bytes) -> str:
|
|
| 347 |
parts.append(cell.text)
|
| 348 |
return "\n".join(parts)
|
| 349 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 350 |
return _smart_decode(raw)
|
| 351 |
|
| 352 |
|
|
|
|
| 347 |
parts.append(cell.text)
|
| 348 |
return "\n".join(parts)
|
| 349 |
|
| 350 |
+
if ext == ".hwp":
|
| 351 |
+
from hwp_extract import extract_hwp_text
|
| 352 |
+
return extract_hwp_text(raw)
|
| 353 |
+
|
| 354 |
return _smart_decode(raw)
|
| 355 |
|
| 356 |
|
|
@@ -0,0 +1,223 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
HWP (Hancom Office) ํ์ผ์์ ํ๋ฌธ ํ
์คํธ๋ฅผ ์ถ์ถํ๋ ํฌํผ.
|
| 3 |
+
|
| 4 |
+
์ ๋ต:
|
| 5 |
+
1) pyhwp ํจํค์ง์ `hwp5txt` CLI ๊ฐ PATH ์ ์์ผ๋ฉด ๊ทธ๊ฒ์ ์ฌ์ฉ (๊ฐ์ฅ ์ ํ).
|
| 6 |
+
โ pip install pyhwp
|
| 7 |
+
2) ์ฌ์ฉ ๋ถ๊ฐ ์ olefile + ์์ฒด ๋ฐ์ด๋๋ฆฌ ํ์๋ก ํด๋ฐฑ.
|
| 8 |
+
HWP v5 = OLE Compound Document โ BodyText/Section* ์คํธ๋ฆผ zlib ํด์
|
| 9 |
+
โ HWP ๋ ์ฝ๋ ์คํธ๋ฆผ ์ํฌ โ HWPTAG_PARA_TEXT ์ UTF-16-LE ํ
์คํธ ๋์ฝ๋.
|
| 10 |
+
|
| 11 |
+
ํธ์ถ: text = extract_hwp_text(raw_bytes)
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import io
|
| 17 |
+
import logging
|
| 18 |
+
import os
|
| 19 |
+
import shutil
|
| 20 |
+
import struct
|
| 21 |
+
import subprocess
|
| 22 |
+
import tempfile
|
| 23 |
+
import zlib
|
| 24 |
+
from typing import Optional
|
| 25 |
+
|
| 26 |
+
log = logging.getLogger("hwp")
|
| 27 |
+
|
| 28 |
+
# ---------------------------------------------------------------------------
|
| 29 |
+
# Public entry point
|
| 30 |
+
# ---------------------------------------------------------------------------
|
| 31 |
+
|
| 32 |
+
def extract_hwp_text(raw: bytes) -> str:
|
| 33 |
+
text = _try_pyhwp_cli(raw)
|
| 34 |
+
if text is not None:
|
| 35 |
+
return text
|
| 36 |
+
log.info("pyhwp(hwp5txt) ๋ฏธ์ฌ์ฉ/์คํจ โ ์์ฒด olefile ํ์๋ก ํด๋ฐฑ")
|
| 37 |
+
return _extract_manual(raw)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
# ---------------------------------------------------------------------------
|
| 41 |
+
# 1) pyhwp ์ CLI (hwp5txt) ์ฌ์ฉ
|
| 42 |
+
# ---------------------------------------------------------------------------
|
| 43 |
+
|
| 44 |
+
_HWP5TXT_PATH: Optional[str] = None
|
| 45 |
+
_HWP5TXT_CHECKED = False
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def _hwp5txt_path() -> Optional[str]:
|
| 49 |
+
global _HWP5TXT_PATH, _HWP5TXT_CHECKED
|
| 50 |
+
if _HWP5TXT_CHECKED:
|
| 51 |
+
return _HWP5TXT_PATH
|
| 52 |
+
_HWP5TXT_CHECKED = True
|
| 53 |
+
p = shutil.which("hwp5txt") or shutil.which("hwp5txt.exe")
|
| 54 |
+
if p:
|
| 55 |
+
log.info("hwp5txt found at: %s", p)
|
| 56 |
+
_HWP5TXT_PATH = p
|
| 57 |
+
return p
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def _try_pyhwp_cli(raw: bytes) -> Optional[str]:
|
| 61 |
+
exe = _hwp5txt_path()
|
| 62 |
+
if not exe:
|
| 63 |
+
return None
|
| 64 |
+
fd, tmp = tempfile.mkstemp(suffix=".hwp")
|
| 65 |
+
try:
|
| 66 |
+
os.write(fd, raw)
|
| 67 |
+
os.close(fd)
|
| 68 |
+
try:
|
| 69 |
+
result = subprocess.run(
|
| 70 |
+
[exe, tmp],
|
| 71 |
+
capture_output=True,
|
| 72 |
+
timeout=30,
|
| 73 |
+
)
|
| 74 |
+
except Exception as e: # noqa: BLE001
|
| 75 |
+
log.warning("hwp5txt ์คํ ์คํจ: %s", e)
|
| 76 |
+
return None
|
| 77 |
+
if result.returncode != 0:
|
| 78 |
+
log.warning(
|
| 79 |
+
"hwp5txt ๋น0 ์ข
๋ฃ(%s): %s",
|
| 80 |
+
result.returncode,
|
| 81 |
+
(result.stderr or b"").decode("utf-8", errors="replace")[:200],
|
| 82 |
+
)
|
| 83 |
+
return None
|
| 84 |
+
return result.stdout.decode("utf-8", errors="replace")
|
| 85 |
+
finally:
|
| 86 |
+
try:
|
| 87 |
+
os.unlink(tmp)
|
| 88 |
+
except OSError:
|
| 89 |
+
pass
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
# ---------------------------------------------------------------------------
|
| 93 |
+
# 2) olefile + ์์ฒด ๋ฐ์ด๋๋ฆฌ ํ์
|
| 94 |
+
# ---------------------------------------------------------------------------
|
| 95 |
+
|
| 96 |
+
# HWP v5 paragraph text record tag
|
| 97 |
+
HWPTAG_BEGIN = 0x10
|
| 98 |
+
HWPTAG_PARA_TEXT = HWPTAG_BEGIN + 0x32 # = 0x42
|
| 99 |
+
|
| 100 |
+
# 1-wchar inline ์ ์ด๋ฌธ์ (ํ์ฅ ์ ์ด๊ตฌ์กฐ๊ฐ ์๋)
|
| 101 |
+
_INLINE_CTRL = {0x00, 0x0A, 0x0D, 0x18, 0x1E, 0x1F}
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def _extract_manual(raw: bytes) -> str:
|
| 105 |
+
try:
|
| 106 |
+
import olefile # type: ignore
|
| 107 |
+
except ImportError:
|
| 108 |
+
log.error("olefile ๋ฏธ์ค์น โ HWP ํ์ฑ ๋ถ๊ฐ. pip install olefile")
|
| 109 |
+
return ""
|
| 110 |
+
|
| 111 |
+
if not olefile.isOleFile(io.BytesIO(raw)):
|
| 112 |
+
log.warning("HWP ํ์ผ์ด OLE ํ์์ด ์๋ (HWP v3 ๋ฑ ๋ฏธ์ง์)")
|
| 113 |
+
return ""
|
| 114 |
+
|
| 115 |
+
ole = olefile.OleFileIO(io.BytesIO(raw))
|
| 116 |
+
try:
|
| 117 |
+
compressed = _read_compression_flag(ole)
|
| 118 |
+
section_paths = sorted(
|
| 119 |
+
[
|
| 120 |
+
e for e in ole.listdir()
|
| 121 |
+
if len(e) == 2 and e[0] == "BodyText" and e[1].startswith("Section")
|
| 122 |
+
],
|
| 123 |
+
key=lambda e: _section_index(e[1]),
|
| 124 |
+
)
|
| 125 |
+
parts = []
|
| 126 |
+
for entry in section_paths:
|
| 127 |
+
try:
|
| 128 |
+
data = ole.openstream(entry).read()
|
| 129 |
+
except Exception as e: # noqa: BLE001
|
| 130 |
+
log.warning("HWP ์น์
์ฝ๊ธฐ ์คํจ (%s): %s", entry, e)
|
| 131 |
+
continue
|
| 132 |
+
if compressed:
|
| 133 |
+
data = _zlib_decompress(data)
|
| 134 |
+
if data is None:
|
| 135 |
+
log.warning("HWP ์น์
zlib ํด์ ์คํจ (%s)", entry)
|
| 136 |
+
continue
|
| 137 |
+
parts.append(_walk_records(data))
|
| 138 |
+
return "\n".join(parts).strip()
|
| 139 |
+
finally:
|
| 140 |
+
ole.close()
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def _section_index(name: str) -> int:
|
| 144 |
+
suffix = name.replace("Section", "")
|
| 145 |
+
return int(suffix) if suffix.isdigit() else 0
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def _read_compression_flag(ole) -> bool:
|
| 149 |
+
"""FileHeader ์คํธ๋ฆผ์ 36๋ฒ์งธ ๋ฐ์ดํธ bit0 = ์์ถ ์ฌ๋ถ.
|
| 150 |
+
๋ชป ์ฝ์ผ๋ฉด ์์ถ์ผ๋ก ๊ฐ์ (๋๋ถ๋ถ ์์ถ๋จ)."""
|
| 151 |
+
try:
|
| 152 |
+
header = ole.openstream("FileHeader").read()
|
| 153 |
+
if len(header) > 36:
|
| 154 |
+
return bool(header[36] & 0x01)
|
| 155 |
+
except Exception:
|
| 156 |
+
pass
|
| 157 |
+
return True
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def _zlib_decompress(data: bytes) -> Optional[bytes]:
|
| 161 |
+
# HWP ๋ raw deflate ์ฌ์ฉ (zlib ํค๏ฟฝ๏ฟฝ๏ฟฝ ์์, wbits=-15)
|
| 162 |
+
for wbits in (-15, 15):
|
| 163 |
+
try:
|
| 164 |
+
return zlib.decompress(data, wbits)
|
| 165 |
+
except zlib.error:
|
| 166 |
+
continue
|
| 167 |
+
return None
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def _walk_records(data: bytes) -> str:
|
| 171 |
+
"""HWP v5 record stream ์ ์ํฌํ๋ฉฐ PARA_TEXT ๋ง ๋ชจ์๋ค."""
|
| 172 |
+
pos = 0
|
| 173 |
+
out = []
|
| 174 |
+
n = len(data)
|
| 175 |
+
while pos + 4 <= n:
|
| 176 |
+
header = struct.unpack_from("<I", data, pos)[0]
|
| 177 |
+
pos += 4
|
| 178 |
+
tag = header & 0x3FF
|
| 179 |
+
size = (header >> 20) & 0xFFF
|
| 180 |
+
if size == 0xFFF:
|
| 181 |
+
if pos + 4 > n:
|
| 182 |
+
break
|
| 183 |
+
size = struct.unpack_from("<I", data, pos)[0]
|
| 184 |
+
pos += 4
|
| 185 |
+
if pos + size > n:
|
| 186 |
+
break
|
| 187 |
+
record = data[pos : pos + size]
|
| 188 |
+
pos += size
|
| 189 |
+
if tag == HWPTAG_PARA_TEXT:
|
| 190 |
+
t = _decode_para_text(record)
|
| 191 |
+
if t:
|
| 192 |
+
out.append(t)
|
| 193 |
+
return "\n".join(out)
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def _decode_para_text(data: bytes) -> str:
|
| 197 |
+
"""UTF-16-LE ๋ณธ๋ฌธ + HWP ์ ์ด๋ฌธ์ (1-wchar inline / 8-wchar extended)."""
|
| 198 |
+
pos = 0
|
| 199 |
+
n = len(data)
|
| 200 |
+
out = []
|
| 201 |
+
while pos + 2 <= n:
|
| 202 |
+
wc = data[pos] | (data[pos + 1] << 8)
|
| 203 |
+
pos += 2
|
| 204 |
+
if wc < 0x20:
|
| 205 |
+
if wc in _INLINE_CTRL:
|
| 206 |
+
if wc == 0x0A or wc == 0x0D:
|
| 207 |
+
out.append("\n")
|
| 208 |
+
# else: NUL / 0x18 (hyphen marker) / 0x1E / 0x1F โ skip
|
| 209 |
+
else:
|
| 210 |
+
# Extended control: 8 wchar ์ด๊ธธ์ด โ ์ถ๊ฐ๋ก 7 wchar(14 byte) ๊ฑด๋๋
|
| 211 |
+
pos += 14
|
| 212 |
+
else:
|
| 213 |
+
if 0xD800 <= wc <= 0xDBFF and pos + 2 <= n:
|
| 214 |
+
wc2 = data[pos] | (data[pos + 1] << 8)
|
| 215 |
+
pos += 2
|
| 216 |
+
if 0xDC00 <= wc2 <= 0xDFFF:
|
| 217 |
+
cp = 0x10000 + ((wc - 0xD800) << 10) + (wc2 - 0xDC00)
|
| 218 |
+
out.append(chr(cp))
|
| 219 |
+
else:
|
| 220 |
+
out.append(chr(wc))
|
| 221 |
+
else:
|
| 222 |
+
out.append(chr(wc))
|
| 223 |
+
return "".join(out)
|
|
@@ -6,3 +6,6 @@ pypdf==4.3.1
|
|
| 6 |
python-docx==1.1.2
|
| 7 |
spacy==3.7.5
|
| 8 |
numpy<2.0
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
python-docx==1.1.2
|
| 7 |
spacy==3.7.5
|
| 8 |
numpy<2.0
|
| 9 |
+
# HWP (Hancom) ํ์ผ ์ง์
|
| 10 |
+
pyhwp>=0.1b15
|
| 11 |
+
olefile>=0.46
|
|
@@ -3,3 +3,6 @@ Flask==3.0.3
|
|
| 3 |
PyYAML==6.0.2
|
| 4 |
pypdf==4.3.1
|
| 5 |
python-docx==1.1.2
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
PyYAML==6.0.2
|
| 4 |
pypdf==4.3.1
|
| 5 |
python-docx==1.1.2
|
| 6 |
+
# HWP (Hancom) ํ์ผ ์ง์
|
| 7 |
+
pyhwp>=0.1b15
|
| 8 |
+
olefile>=0.46
|
|
@@ -136,7 +136,7 @@
|
|
| 136 |
<div id="drop">
|
| 137 |
<div><b>์ฌ๊ธฐ์ ํ์ผ์ ๋๋กญ</b> ํ๊ฑฐ๋ ํด๋ฆญํด์ ์ ํ</div>
|
| 138 |
<div style="color:var(--muted); font-size:12px; margin-top:6px;">
|
| 139 |
-
.txt, .log, .csv, .json, .md, .pdf, .docx ์ง์ (์ต๋ 32MB)
|
| 140 |
</div>
|
| 141 |
<input id="file" type="file" />
|
| 142 |
</div>
|
|
|
|
| 136 |
<div id="drop">
|
| 137 |
<div><b>์ฌ๊ธฐ์ ํ์ผ์ ๋๋กญ</b> ํ๊ฑฐ๋ ํด๋ฆญํด์ ์ ํ</div>
|
| 138 |
<div style="color:var(--muted); font-size:12px; margin-top:6px;">
|
| 139 |
+
.txt, .log, .csv, .json, .md, .pdf, .docx, <b>.hwp</b> ์ง์ (์ต๋ 32MB)
|
| 140 |
</div>
|
| 141 |
<input id="file" type="file" />
|
| 142 |
</div>
|