Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 14 additions & 2 deletions packages/markitdown/src/markitdown/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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)
Expand Down
38 changes: 35 additions & 3 deletions packages/markitdown/src/markitdown/converters/_xlsx_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down
74 changes: 74 additions & 0 deletions packages/markitdown/tests/test_xlsx_converter.py
Original file line number Diff line number Diff line change
@@ -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