markitdown-gc 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- markitdown/GUIDE.md +93 -0
- markitdown/__about__.py +4 -0
- markitdown/__init__.py +34 -0
- markitdown/__main__.py +439 -0
- markitdown/_base_converter.py +109 -0
- markitdown/_exceptions.py +82 -0
- markitdown/_hwp_media.py +132 -0
- markitdown/_markitdown.py +861 -0
- markitdown/_media.py +67 -0
- markitdown/_stream_info.py +32 -0
- markitdown/_uri_utils.py +75 -0
- markitdown/converter_utils/__init__.py +0 -0
- markitdown/converter_utils/_image.py +19 -0
- markitdown/converter_utils/_xlsx_images.py +114 -0
- markitdown/converter_utils/docx/__init__.py +0 -0
- markitdown/converter_utils/docx/_images.py +118 -0
- markitdown/converter_utils/docx/math/__init__.py +0 -0
- markitdown/converter_utils/docx/math/latex_dict.py +291 -0
- markitdown/converter_utils/docx/math/omml.py +417 -0
- markitdown/converter_utils/docx/pre_process.py +290 -0
- markitdown/converters/__init__.py +56 -0
- markitdown/converters/_audio_converter.py +106 -0
- markitdown/converters/_bing_serp_converter.py +120 -0
- markitdown/converters/_csv_converter.py +133 -0
- markitdown/converters/_cu_converter.py +570 -0
- markitdown/converters/_doc_intel_converter.py +258 -0
- markitdown/converters/_docx_converter.py +163 -0
- markitdown/converters/_epub_converter.py +185 -0
- markitdown/converters/_exiftool.py +57 -0
- markitdown/converters/_html_converter.py +112 -0
- markitdown/converters/_hwp_converter.py +105 -0
- markitdown/converters/_image_converter.py +139 -0
- markitdown/converters/_ipynb_converter.py +103 -0
- markitdown/converters/_llm_caption.py +50 -0
- markitdown/converters/_markdownify.py +178 -0
- markitdown/converters/_outlook_msg_converter.py +349 -0
- markitdown/converters/_pdf_converter.py +593 -0
- markitdown/converters/_plain_text_converter.py +66 -0
- markitdown/converters/_pptx_converter.py +387 -0
- markitdown/converters/_rss_converter.py +488 -0
- markitdown/converters/_transcribe_audio.py +60 -0
- markitdown/converters/_wikipedia_converter.py +99 -0
- markitdown/converters/_xlsx_converter.py +279 -0
- markitdown/converters/_youtube_converter.py +256 -0
- markitdown/converters/_zip_converter.py +130 -0
- markitdown/ocr_triage.py +943 -0
- markitdown/py.typed +0 -0
- markitdown_gc-0.2.0.dist-info/METADATA +137 -0
- markitdown_gc-0.2.0.dist-info/RECORD +53 -0
- markitdown_gc-0.2.0.dist-info/WHEEL +4 -0
- markitdown_gc-0.2.0.dist-info/entry_points.txt +2 -0
- markitdown_gc-0.2.0.dist-info/licenses/LICENSE +21 -0
- markitdown_gc-0.2.0.dist-info/licenses/ThirdPartyNotices.md +232 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from typing import Optional, List, Any
|
|
2
|
+
|
|
3
|
+
MISSING_DEPENDENCY_MESSAGE = """{converter} recognized the input as a potential {extension} file, but the dependencies needed to read {extension} files have not been installed. To resolve this error, include the optional dependency [{feature}] or [all] when installing MarkItDown. For example:
|
|
4
|
+
|
|
5
|
+
* pip install 'markitdown-gc[{feature}]'
|
|
6
|
+
* pip install 'markitdown-gc[all]'
|
|
7
|
+
* pip install 'markitdown-gc[{feature}, ...]'
|
|
8
|
+
* etc."""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MarkItDownException(Exception):
|
|
12
|
+
"""
|
|
13
|
+
Base exception class for MarkItDown.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MissingDependencyException(MarkItDownException):
|
|
20
|
+
"""
|
|
21
|
+
Converters shipped with MarkItDown may depend on optional
|
|
22
|
+
dependencies. This exception is thrown when a converter's
|
|
23
|
+
convert() method is called, but the required dependency is not
|
|
24
|
+
installed. This is not necessarily a fatal error, as the converter
|
|
25
|
+
will simply be skipped (an error will bubble up only if no other
|
|
26
|
+
suitable converter is found).
|
|
27
|
+
|
|
28
|
+
Error messages should clearly indicate which dependency is missing.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class UnsupportedFormatException(MarkItDownException):
|
|
35
|
+
"""
|
|
36
|
+
Thrown when no suitable converter was found for the given file.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class FailedConversionAttempt(object):
|
|
43
|
+
"""
|
|
44
|
+
Represents a single attempt to convert a file.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, converter: Any, exc_info: Optional[tuple] = None):
|
|
48
|
+
self.converter = converter
|
|
49
|
+
self.exc_info = exc_info
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class FileConversionException(MarkItDownException):
|
|
53
|
+
"""
|
|
54
|
+
Thrown when a suitable converter was found, but the conversion
|
|
55
|
+
process fails for any reason.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
message: Optional[str] = None,
|
|
61
|
+
attempts: Optional[List[FailedConversionAttempt]] = None,
|
|
62
|
+
):
|
|
63
|
+
self.attempts = attempts
|
|
64
|
+
|
|
65
|
+
if message is None:
|
|
66
|
+
if attempts is None:
|
|
67
|
+
message = "File conversion failed."
|
|
68
|
+
else:
|
|
69
|
+
message = f"File conversion failed after {len(attempts)} attempts:\n"
|
|
70
|
+
for attempt in attempts:
|
|
71
|
+
if attempt.exc_info is None:
|
|
72
|
+
message += f" - {type(attempt.converter).__name__} provided no execution info."
|
|
73
|
+
else:
|
|
74
|
+
message += f" - {type(attempt.converter).__name__} threw {attempt.exc_info[0].__name__} with message: {attempt.exc_info[1]}\n"
|
|
75
|
+
|
|
76
|
+
if isinstance(attempt.exc_info[1], UnicodeDecodeError):
|
|
77
|
+
message += (
|
|
78
|
+
"The charset may have been automatically guessed incorrectly. "
|
|
79
|
+
"If you didn't specify the charset, please retry with an explicitly defined one.\n"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
super().__init__(message)
|
markitdown/_hwp_media.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Borrow the HWP parser with guaranteed temporary-file cleanup."""
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import tempfile
|
|
7
|
+
import zipfile
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
|
|
10
|
+
from ._media import save_media, validate_members
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@contextmanager
|
|
14
|
+
def hwp_reader(data, suffix):
|
|
15
|
+
import hwp_hwpx_parser as parser
|
|
16
|
+
|
|
17
|
+
temporary = tempfile.NamedTemporaryFile(suffix=suffix, delete=False)
|
|
18
|
+
try:
|
|
19
|
+
with temporary:
|
|
20
|
+
temporary.write(data)
|
|
21
|
+
reader = parser.read(temporary.name)
|
|
22
|
+
try:
|
|
23
|
+
yield reader
|
|
24
|
+
finally:
|
|
25
|
+
reader.close()
|
|
26
|
+
finally:
|
|
27
|
+
os.unlink(temporary.name)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def hwp_images(data, suffix, reader):
|
|
31
|
+
"""Return image payloads with stable member/stream identities, including omissions."""
|
|
32
|
+
images = reader.get_images()
|
|
33
|
+
if suffix == ".hwpx":
|
|
34
|
+
with zipfile.ZipFile(io.BytesIO(data)) as archive:
|
|
35
|
+
validate_members(archive)
|
|
36
|
+
returned = {i.filename: i for i in images}
|
|
37
|
+
rows = []
|
|
38
|
+
for name in sorted(archive.namelist()):
|
|
39
|
+
if not name.startswith("BinData/") or name.endswith("/"):
|
|
40
|
+
continue
|
|
41
|
+
ext = name.rsplit(".", 1)[-1].lower()
|
|
42
|
+
from .ocr_triage import _IMAGE_EXTENSIONS
|
|
43
|
+
|
|
44
|
+
if ext not in _IMAGE_EXTENSIONS:
|
|
45
|
+
continue
|
|
46
|
+
image = returned.get(name.rsplit("/", 1)[-1])
|
|
47
|
+
rows.append((name, archive.read(name), ext, image is not None))
|
|
48
|
+
return rows
|
|
49
|
+
rows = [("BinData/" + i.filename, i.data, i.format.lower(), True) for i in images]
|
|
50
|
+
# The parser omits unknown encodings. Inspect stored OLE names as well.
|
|
51
|
+
import olefile
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
with olefile.OleFileIO(io.BytesIO(data)) as ole:
|
|
55
|
+
known = {r[0] for r in rows}
|
|
56
|
+
for path in sorted(ole.listdir()):
|
|
57
|
+
name = "/".join(path)
|
|
58
|
+
if len(path) == 2 and path[0] == "BinData" and name not in known:
|
|
59
|
+
rows.append(
|
|
60
|
+
(
|
|
61
|
+
name,
|
|
62
|
+
ole.openstream(path).read(),
|
|
63
|
+
path[-1].rsplit(".", 1)[-1].lower(),
|
|
64
|
+
False,
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
except (OSError, ValueError):
|
|
68
|
+
# Parser payloads remain usable when stored-stream inspection is unavailable.
|
|
69
|
+
pass
|
|
70
|
+
return sorted(rows, key=lambda row: row[0])
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def extract_hwp_markdown(markdown, rows, **options):
|
|
74
|
+
links = {}
|
|
75
|
+
for name, payload, ext, supported in rows:
|
|
76
|
+
if supported:
|
|
77
|
+
path = save_media(payload, ext, **options)
|
|
78
|
+
link = f""
|
|
79
|
+
else:
|
|
80
|
+
label = ext.upper() if re.fullmatch("[a-z0-9]{1,12}", ext) else "Image"
|
|
81
|
+
link = f"![{label} omitted]"
|
|
82
|
+
basename = name.rsplit("/", 1)[-1]
|
|
83
|
+
for identity in (basename, basename.rsplit(".", 1)[0]):
|
|
84
|
+
if identity in links and links[identity] != link:
|
|
85
|
+
raise ValueError("Unsafe or colliding image reference")
|
|
86
|
+
links[identity] = link
|
|
87
|
+
used = set()
|
|
88
|
+
|
|
89
|
+
def replace(match):
|
|
90
|
+
identity = match.group(1)
|
|
91
|
+
link = links.get(identity)
|
|
92
|
+
if link is None:
|
|
93
|
+
return "![Image omitted]"
|
|
94
|
+
used.add(link)
|
|
95
|
+
return link
|
|
96
|
+
|
|
97
|
+
markdown = re.sub(r"\[IMAGE: ([^\]\r\n]+)\]", replace, markdown)
|
|
98
|
+
# Orphaned/skipped media remain visible, never silently disappear.
|
|
99
|
+
for link in dict.fromkeys(links.values()):
|
|
100
|
+
if link not in used:
|
|
101
|
+
markdown += "\n\n" + link
|
|
102
|
+
return markdown
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def has_hwp_images(data, suffix):
|
|
106
|
+
"""Observe package structures, never search converted/document text."""
|
|
107
|
+
try:
|
|
108
|
+
if suffix == ".hwpx":
|
|
109
|
+
from defusedxml import ElementTree
|
|
110
|
+
|
|
111
|
+
with zipfile.ZipFile(io.BytesIO(data)) as archive:
|
|
112
|
+
if any(
|
|
113
|
+
n.startswith("BinData/") and not n.endswith("/")
|
|
114
|
+
for n in archive.namelist()
|
|
115
|
+
):
|
|
116
|
+
return True
|
|
117
|
+
for name in archive.namelist():
|
|
118
|
+
if re.fullmatch(r"Contents/section\d+\.xml", name):
|
|
119
|
+
root = ElementTree.fromstring(archive.read(name))
|
|
120
|
+
if any(
|
|
121
|
+
e.tag.rsplit("}", 1)[-1] in ("pic", "img")
|
|
122
|
+
for e in root.iter()
|
|
123
|
+
):
|
|
124
|
+
return True
|
|
125
|
+
else:
|
|
126
|
+
import olefile
|
|
127
|
+
|
|
128
|
+
with olefile.OleFileIO(io.BytesIO(data)) as ole:
|
|
129
|
+
return any(p[0] == "BinData" for p in ole.listdir())
|
|
130
|
+
except (OSError, ValueError, zipfile.BadZipFile):
|
|
131
|
+
pass
|
|
132
|
+
return False
|