Skip to content

Commit 72c2934

Browse files
committed
fix: Handle UnicodeDecodeError in PlainTextConverter (microsoft#1505)
When charset detection samples only the first 4096 bytes and detects 'ascii', but the file contains UTF-8 characters beyond that point, decoding fails with UnicodeDecodeError. Added fallback to charset_normalizer when UnicodeDecodeError occurs, allowing proper handling of files with non-ASCII characters (Spanish, Korean, Japanese, Chinese, etc.) that appear after the 4096-byte sample. Cherry-picked from microsoft/markitdown PR microsoft#1540
1 parent 7fdaefb commit 72c2934

2 files changed

Lines changed: 55 additions & 2 deletions

File tree

packages/markitdown/src/markitdown/converters/_plain_text_converter.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,16 @@ def convert(
6363
stream_info: StreamInfo,
6464
**kwargs: Any, # Options to pass to the converter
6565
) -> DocumentConverterResult:
66+
file_bytes = file_stream.read()
67+
6668
if stream_info.charset:
67-
text_content = file_stream.read().decode(stream_info.charset)
69+
try:
70+
text_content = file_bytes.decode(stream_info.charset)
71+
except UnicodeDecodeError:
72+
# Charset detection from partial file content may be inaccurate.
73+
# Fall back to charset_normalizer for the full file content.
74+
text_content = str(from_bytes(file_bytes).best())
6875
else:
69-
text_content = str(from_bytes(file_stream.read()).best())
76+
text_content = str(from_bytes(file_bytes).best())
7077

7178
return DocumentConverterResult(markdown=text_content)

packages/markitdown/tests/test_module_misc.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,51 @@ def test_markitdown_llm_parameters() -> None:
456456
assert messages[0]["content"][0]["text"] == test_prompt
457457

458458

459+
def test_plaintext_charset_fallback() -> None:
460+
"""
461+
Test for GitHub issue #1505: PlainTextConverter throws UnicodeDecodeError
462+
when charset detection from partial file content is inaccurate.
463+
464+
When the first 4096 bytes are ASCII-only but later bytes contain UTF-8
465+
characters (e.g., accented or CJK characters), the charset may be incorrectly
466+
detected as 'ascii'. The converter should fall back to charset_normalizer
467+
when decoding fails.
468+
"""
469+
markitdown = MarkItDown()
470+
471+
test_cases = [
472+
(
473+
"Spanish",
474+
"Hola, señor! ¿Cómo está? Año nuevo, vida nueva.",
475+
["señor", "¿Cómo está?", "Año"],
476+
),
477+
("Korean", "안녕하세요! 한글 테스트입니다. 가나다라마바사", ["안녕하세요", "한글", "가나다라마바사"]),
478+
("Japanese", "こんにちは!日本語テストです。あいうえお", ["こんにちは", "日本語", "あいうえお"]),
479+
("Chinese", "你好!中文测试。这是一个测试文件。", ["你好", "中文测试", "测试文件"]),
480+
]
481+
482+
for lang, utf8_text, expected_substrings in test_cases:
483+
# Create a test file where:
484+
# - First 4100 bytes are ASCII (exceeds the 4096 byte sample for charset detection)
485+
# - Followed by UTF-8 encoded non-ASCII characters
486+
ascii_part = "A" * 4100
487+
test_content = ascii_part + utf8_text
488+
489+
# Use BytesIO to simulate a file stream
490+
file_stream = io.BytesIO(test_content.encode("utf-8"))
491+
492+
# Convert using stream with incorrect charset hint (simulating the bug)
493+
result = markitdown.convert_stream(
494+
file_stream, stream_info=StreamInfo(charset="ascii", extension=".txt")
495+
)
496+
497+
# Verify that the conversion succeeded and contains the UTF-8 characters
498+
for expected in expected_substrings:
499+
assert (
500+
expected in result.text_content
501+
), f"{lang}: Expected '{expected}' not found in result"
502+
503+
459504
@pytest.mark.skipif(
460505
skip_llm,
461506
reason="do not run llm tests without a key",
@@ -495,6 +540,7 @@ def test_markitdown_llm() -> None:
495540
test_exceptions,
496541
test_doc_rlink,
497542
test_markitdown_exiftool,
543+
test_plaintext_charset_fallback,
498544
test_markitdown_llm_parameters,
499545
test_markitdown_llm,
500546
]:

0 commit comments

Comments
 (0)