Skip to content

Commit 452cfd5

Browse files
authored
Merge branch 'main' into fix/outlook-msg-ansi
2 parents b5b22cb + 83ce26d commit 452cfd5

29 files changed

Lines changed: 1314 additions & 72 deletions

.github/dependabot.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ updates:
44
directory: "/"
55
schedule:
66
interval: "weekly"
7+
cooldown:
8+
default-days: 7

.github/workflows/pre-commit.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ jobs:
55
pre-commit:
66
runs-on: ubuntu-latest
77
steps:
8-
- uses: actions/checkout@v5
8+
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
99
- name: Set up Python
10-
uses: actions/setup-python@v5
10+
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
1111
with:
1212
python-version: "3.x"
1313

.github/workflows/tests.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ jobs:
55
tests:
66
runs-on: ubuntu-latest
77
steps:
8-
- uses: actions/checkout@v5
9-
- uses: actions/setup-python@v5
8+
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
9+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
1010
with:
1111
python-version: |
1212
3.10

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,13 @@ Content Understanding is ideal when you need capabilities beyond what built-in o
188188
markitdown path-to-file.pdf --use-cu --cu-endpoint "<content_understanding_endpoint>"
189189
```
190190

191+
The endpoint can also be set once in the environment, so callers only need `--use-cu`:
192+
193+
```bash
194+
export MARKITDOWN_CU_ENDPOINT="<content_understanding_endpoint>"
195+
markitdown path-to-file.pdf --use-cu
196+
```
197+
191198
**Python API:**
192199

193200
```python
@@ -244,6 +251,13 @@ To use Microsoft Document Intelligence for conversion:
244251
markitdown path-to-file.pdf -o document.md -d -e "<document_intelligence_endpoint>"
245252
```
246253

254+
The endpoint can also be set once in the environment, so callers only need `-d`:
255+
256+
```bash
257+
export MARKITDOWN_DOCINTEL_ENDPOINT="<document_intelligence_endpoint>"
258+
markitdown path-to-file.pdf -o document.md -d
259+
```
260+
247261
More information about how to set up an Azure Document Intelligence Resource can be found [here](https://learn.microsoft.com/en-us/azure/ai-services/document-intelligence/how-to-guides/create-document-intelligence-resource?view=doc-intel-4.0.0)
248262

249263
### Python API

packages/markitdown/src/markitdown/__main__.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
#
33
# SPDX-License-Identifier: MIT
44
import argparse
5+
import os
56
import sys
67
import codecs
8+
import io
79
from typing import Any, Dict
810
from textwrap import dedent
911
from importlib.metadata import entry_points
@@ -98,13 +100,15 @@ def main():
98100
"-e",
99101
"--endpoint",
100102
type=str,
101-
help="Document Intelligence Endpoint. Required if using Document Intelligence.",
103+
default=os.environ.get("MARKITDOWN_DOCINTEL_ENDPOINT") or None,
104+
help="Document Intelligence Endpoint. Required if using Document Intelligence. Defaults to the MARKITDOWN_DOCINTEL_ENDPOINT environment variable.",
102105
)
103106

104107
parser.add_argument(
105108
"--cu-endpoint",
106109
type=str,
107-
help="Content Understanding Endpoint. Required if using --use-cu.",
110+
default=os.environ.get("MARKITDOWN_CU_ENDPOINT") or None,
111+
help="Content Understanding Endpoint. Required if using --use-cu. Defaults to the MARKITDOWN_CU_ENDPOINT environment variable.",
108112
)
109113

110114
parser.add_argument(
@@ -203,7 +207,8 @@ def main():
203207
if args.use_docintel:
204208
if args.endpoint is None:
205209
_exit_with_error(
206-
"Document Intelligence Endpoint is required when using Document Intelligence."
210+
"Document Intelligence Endpoint is required when using Document Intelligence. "
211+
"Pass -e/--endpoint or set MARKITDOWN_DOCINTEL_ENDPOINT."
207212
)
208213
elif args.filename is None:
209214
_exit_with_error("Filename is required when using Document Intelligence.")
@@ -215,9 +220,8 @@ def main():
215220
if args.cu_endpoint is None:
216221
_exit_with_error(
217222
"Content Understanding Endpoint (--cu-endpoint) is required when using --use-cu."
223+
"Pass --cu-endpoint or set MARKITDOWN_CU_ENDPOINT."
218224
)
219-
elif args.filename is None:
220-
_exit_with_error("Filename is required when using Content Understanding.")
221225

222226
cu_kwargs: Dict[str, Any] = {
223227
"cu_endpoint": args.cu_endpoint,
@@ -245,8 +249,9 @@ def main():
245249
markitdown = MarkItDown(enable_plugins=args.use_plugins)
246250

247251
if args.filename is None:
252+
# Windows pipe-backed stdin can report seekable() even though it cannot rewind.
248253
result = markitdown.convert_stream(
249-
sys.stdin.buffer,
254+
io.BytesIO(sys.stdin.buffer.read()),
250255
stream_info=stream_info,
251256
keep_data_uris=args.keep_data_uris,
252257
)

packages/markitdown/src/markitdown/_exceptions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,10 @@ def __init__(
7373
else:
7474
message += f" - {type(attempt.converter).__name__} threw {attempt.exc_info[0].__name__} with message: {attempt.exc_info[1]}\n"
7575

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+
7682
super().__init__(message)

packages/markitdown/src/markitdown/_markitdown.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import traceback
77
import io
88
from dataclasses import dataclass
9+
from email.message import Message
10+
from email.utils import collapse_rfc2231_value
911
from importlib.metadata import entry_points
1012
from typing import Any, List, Dict, Optional, Union, BinaryIO
1113
from pathlib import Path
@@ -51,6 +53,23 @@
5153
)
5254

5355

56+
def _get_content_disposition_filename(content_disposition: str) -> Optional[str]:
57+
message = Message()
58+
message["content-disposition"] = content_disposition
59+
60+
fallback_filename: Optional[str] = None
61+
extended_filename: Optional[str] = None
62+
for key, value in message.get_params(header="content-disposition", unquote=True):
63+
if key != "filename":
64+
continue
65+
if isinstance(value, tuple):
66+
extended_filename = collapse_rfc2231_value(value)
67+
elif fallback_filename is None:
68+
fallback_filename = value
69+
70+
return extended_filename or fallback_filename
71+
72+
5473
# Lower priority values are tried first.
5574
PRIORITY_SPECIFIC_FILE_FORMAT = (
5675
0.0 # e.g., .docx, .pdf, .xlsx, Or specific pages, e.g., wikipedia
@@ -512,9 +531,10 @@ def convert_response(
512531
filename: Optional[str] = None
513532
extension: Optional[str] = None
514533
if "content-disposition" in response.headers:
515-
m = re.search(r"filename=([^;]+)", response.headers["content-disposition"])
516-
if m:
517-
filename = m.group(1).strip("\"'")
534+
filename = _get_content_disposition_filename(
535+
response.headers["content-disposition"]
536+
)
537+
if filename is not None:
518538
_, _extension = os.path.splitext(filename)
519539
if len(_extension) > 0:
520540
extension = _extension
@@ -726,9 +746,9 @@ def _get_stream_info_guesses(
726746
# If it's text, also guess the charset
727747
charset = None
728748
if result.prediction.output.is_text:
729-
# Read the first 4k to guess the charset
749+
# Read the first 64k to guess the charset
730750
file_stream.seek(cur_pos)
731-
stream_page = file_stream.read(4096)
751+
stream_page = file_stream.read(65536)
732752
charset_result = charset_normalizer.from_bytes(stream_page).best()
733753

734754
if charset_result is not None:

packages/markitdown/src/markitdown/_uri_utils.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ def file_uri_to_path(file_uri: str) -> Tuple[str | None, str]:
1212
raise ValueError(f"Not a file URL: {file_uri}")
1313

1414
netloc = parsed.netloc if parsed.netloc else None
15-
path = os.path.abspath(url2pathname(parsed.path))
15+
path = url2pathname(parsed.path)
16+
if os.name == "nt" and path[:1] in "/\\" and path[2:3] == ":":
17+
path = path[1:]
18+
path = os.path.abspath(path)
1619
return netloc, path
1720

1821

packages/markitdown/src/markitdown/converter_utils/docx/math/latex_dict.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@
2626
"\u0307": "\\dot{{{0}}}",
2727
"\u0308": "\\ddot{{{0}}}",
2828
"\u0309": "\\ovhook{{{0}}}",
29-
"\u030a": "\\ocirc{{{0}}}}",
30-
"\u030c": "\\check{{{0}}}}",
29+
"\u030a": "\\ocirc{{{0}}}",
30+
"\u030c": "\\check{{{0}}}",
3131
"\u0310": "\\candra{{{0}}}",
3232
"\u0312": "\\oturnedcomma{{{0}}}",
3333
"\u0315": "\\ocommatopright{{{0}}}",
@@ -50,7 +50,7 @@
5050
"\u20e8": "\\threeunderdot{{{0}}}",
5151
"\u20ec": "\\underrightharpoondown{{{0}}}",
5252
"\u20ed": "\\underleftharpoondown{{{0}}}",
53-
"\u20ee": "\\underledtarrow{{{0}}}",
53+
"\u20ee": "\\underleftarrow{{{0}}}",
5454
"\u20ef": "\\underrightarrow{{{0}}}",
5555
# Over | group
5656
"\u23b4": "\\overbracket{{{0}}}",
@@ -180,6 +180,8 @@
180180
"\U0001d452": "e",
181181
"\U0001d453": "f",
182182
"\U0001d454": "g",
183+
# U+1D455 is reserved: the math italic small h is unified with U+210E
184+
"\u210e": "h",
183185
"\U0001d456": "i",
184186
"\U0001d457": "j",
185187
"\U0001d458": "k",

packages/markitdown/src/markitdown/converter_utils/docx/math/omml.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ def do_r(self, elm):
378378
@todo \text (latex pure text support)
379379
"""
380380
_str = []
381-
for s in elm.findtext("./{0}t".format(OMML_NS)):
381+
for s in elm.findtext("./{0}t".format(OMML_NS)) or "":
382382
# s = s if isinstance(s,unicode) else unicode(s,'utf-8')
383383
_str.append(self._t_dict.get(s, s))
384384
return escape_latex(BLANK.join(_str))

0 commit comments

Comments
 (0)