From 5cbd072b50dd9111b56dc0072dbbb2832a23054d Mon Sep 17 00:00:00 2001 From: Sidd Date: Tue, 29 Sep 2026 00:40:03 +0530 Subject: [PATCH] feat(xlsx): skip hidden sheets by default --- README.md | 11 +++ .../markitdown/src/markitdown/__main__.py | 16 +++- .../markitdown/converters/_xlsx_converter.py | 38 +++++++++- .../markitdown/tests/test_xlsx_converter.py | 74 +++++++++++++++++++ 4 files changed, 134 insertions(+), 5 deletions(-) create mode 100644 packages/markitdown/tests/test_xlsx_converter.py diff --git a/README.md b/README.md index fa33f6b343..6afb748839 100644 --- a/README.md +++ b/README.md @@ -271,6 +271,17 @@ result = md.convert("test.xlsx") print(result.markdown) ``` +Hidden and very hidden worksheets are excluded by default. To include them, pass +`include_hidden_sheets=True` to `convert`, or run the CLI with: + +```python +result = md.convert("test.xlsx", include_hidden_sheets=True) +``` + +```bash +markitdown test.xlsx --include-hidden-sheets +``` + Document Intelligence conversion in Python: ```python diff --git a/packages/markitdown/src/markitdown/__main__.py b/packages/markitdown/src/markitdown/__main__.py index 51b775ff74..dc5a80642d 100644 --- a/packages/markitdown/src/markitdown/__main__.py +++ b/packages/markitdown/src/markitdown/__main__.py @@ -142,6 +142,12 @@ def main(): help="Keep data URIs (like base64-encoded images) in the output. By default, data URIs are truncated.", ) + parser.add_argument( + "--include-hidden-sheets", + action="store_true", + help="Include hidden and very hidden sheets when converting XLSX files.", + ) + parser.add_argument("filename", nargs="?") args = parser.parse_args() @@ -248,16 +254,22 @@ def main(): else: markitdown = MarkItDown(enable_plugins=args.use_plugins) + conversion_options: Dict[str, Any] = { + "keep_data_uris": args.keep_data_uris, + } + if args.include_hidden_sheets: + conversion_options["include_hidden_sheets"] = True + if args.filename is None: # Windows pipe-backed stdin can report seekable() even though it cannot rewind. result = markitdown.convert_stream( io.BytesIO(sys.stdin.buffer.read()), stream_info=stream_info, - keep_data_uris=args.keep_data_uris, + **conversion_options, ) else: result = markitdown.convert( - args.filename, stream_info=stream_info, keep_data_uris=args.keep_data_uris + args.filename, stream_info=stream_info, **conversion_options ) _handle_output(args, result) diff --git a/packages/markitdown/src/markitdown/converters/_xlsx_converter.py b/packages/markitdown/src/markitdown/converters/_xlsx_converter.py index 9f794a3b77..dd1244b6fe 100644 --- a/packages/markitdown/src/markitdown/converters/_xlsx_converter.py +++ b/packages/markitdown/src/markitdown/converters/_xlsx_converter.py @@ -47,23 +47,49 @@ @contextmanager def _read_xlsx_sheets( file_stream: BinaryIO, + include_hidden_sheets: bool = False, ) -> Iterator[tuple[dict[str, Any], BinaryIO]]: start_pos = file_stream.tell() repaired_stream = None + + def read_sheets(stream: BinaryIO) -> dict[str, Any]: + sheet_names: Optional[list[str]] = None + if not include_hidden_sheets: + sheet_names = _get_visible_sheet_names(stream) + if not sheet_names: + return {} + return pd.read_excel(stream, sheet_name=sheet_names, engine="openpyxl") + try: try: - sheets = pd.read_excel(file_stream, sheet_name=None, engine="openpyxl") + sheets = read_sheets(file_stream) except TypeError as exc: if "showZeroes" not in str(exc): raise repaired_stream = _repair_sheetview_show_zeroes(file_stream, start_pos) - sheets = pd.read_excel(repaired_stream, sheet_name=None, engine="openpyxl") + sheets = read_sheets(repaired_stream) yield sheets, repaired_stream if repaired_stream is not None else file_stream finally: if repaired_stream is not None: repaired_stream.close() +def _get_visible_sheet_names(file_stream: BinaryIO) -> list[str]: + start_pos = file_stream.tell() + workbook = None + try: + workbook = openpyxl.load_workbook(file_stream, read_only=True) + return [ + name + for name in workbook.sheetnames + if workbook[name].sheet_state == "visible" + ] + finally: + if workbook is not None: + workbook.close() + file_stream.seek(start_pos) + + def _rename_show_zeroes_attribute(data: bytes) -> bytes: return _SHEET_VIEW_START_TAG.sub( lambda match: _SHOW_ZEROES_ATTRIBUTE.sub(rb"showZeros\1", match.group(0)), @@ -96,6 +122,9 @@ def _repair_sheetview_show_zeroes( class XlsxConverter(DocumentConverter): """ Converts XLSX files to Markdown, with each sheet presented as a separate Markdown table. + + Hidden and very hidden sheets are excluded by default. Pass + ``include_hidden_sheets=True`` to include them. """ def __init__(self): @@ -140,8 +169,11 @@ def convert( _xlsx_dependency_exc_info[2] ) + include_hidden_sheets = kwargs.pop("include_hidden_sheets", False) md_content = "" - with _read_xlsx_sheets(file_stream) as (sheets, workbook_stream): + with _read_xlsx_sheets( + file_stream, include_hidden_sheets=include_hidden_sheets + ) as (sheets, workbook_stream): images = None if type(self)._image_to_html is not XlsxConverter._image_to_html: from ..converter_utils._xlsx_images import _XlsxImages diff --git a/packages/markitdown/tests/test_xlsx_converter.py b/packages/markitdown/tests/test_xlsx_converter.py new file mode 100644 index 0000000000..a5b6f57c19 --- /dev/null +++ b/packages/markitdown/tests/test_xlsx_converter.py @@ -0,0 +1,74 @@ +import io +import sys + +from openpyxl import Workbook + +from markitdown import MarkItDown, StreamInfo +from markitdown.__main__ import main + + +def _workbook_with_hidden_sheets() -> bytes: + workbook = Workbook() + visible_sheet = workbook.active + visible_sheet.title = "Visible" + visible_sheet.append(["Visible value"]) + + hidden_sheet = workbook.create_sheet("Hidden") + hidden_sheet.append(["Hidden value"]) + hidden_sheet.sheet_state = "hidden" + + very_hidden_sheet = workbook.create_sheet("Very hidden") + very_hidden_sheet.append(["Very hidden value"]) + very_hidden_sheet.sheet_state = "veryHidden" + + stream = io.BytesIO() + workbook.save(stream) + workbook.close() + return stream.getvalue() + + +def _convert_with_hidden_sheets(**kwargs: bool) -> str: + return MarkItDown().convert( + io.BytesIO(_workbook_with_hidden_sheets()), + stream_info=StreamInfo(extension=".xlsx"), + **kwargs, + ).markdown + + +def test_hidden_sheets_are_excluded_by_default() -> None: + markdown = _convert_with_hidden_sheets() + + assert "## Visible" in markdown + assert "Visible value" in markdown + assert "## Hidden" not in markdown + assert "Hidden value" not in markdown + assert "## Very hidden" not in markdown + assert "Very hidden value" not in markdown + + +def test_hidden_sheets_can_be_included() -> None: + markdown = _convert_with_hidden_sheets(include_hidden_sheets=True) + + assert "## Visible" in markdown + assert "## Hidden" in markdown + assert "Hidden value" in markdown + assert "## Very hidden" in markdown + assert "Very hidden value" in markdown + + +def test_hidden_sheets_can_be_included_from_cli(tmp_path, monkeypatch, capsys) -> None: + workbook_path = tmp_path / "sheets.xlsx" + workbook_path.write_bytes(_workbook_with_hidden_sheets()) + monkeypatch.setattr( + sys, + "argv", + ["markitdown", "--include-hidden-sheets", str(workbook_path)], + ) + + main() + + markdown = capsys.readouterr().out + assert "## Hidden" in markdown + assert "Hidden value" in markdown + assert "## Very hidden" in markdown + assert "Very hidden value" in markdown