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
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,46 @@ MarkItDown currently supports the conversion from:
- EPubs
- ... and more!

## CAD-exported PDFs

PDF text extraction does not preserve drawing geometry, dimensions, or spatial
relationships. A CAD drawing can therefore produce incomplete text or empty
output without a parsing error; OCR does not guarantee faithful reconstruction.

When a known CAD exporter is detected, successful conversion emits a Python
`UserWarning` (stderr in the CLI) about potential information loss. The Markdown
content remains unchanged. This warning is best-effort: missing PDF dependencies
or uninspectable metadata do not block otherwise successful conversion.

For workflows that must reject known CAD exporters, install the PDF dependencies
and opt into the metadata policy:

```bash
pip install 'markitdown[pdf]'
markitdown drawing.pdf --reject-cad-pdfs
```

```python
from markitdown import MarkItDown

result = MarkItDown().convert("drawing.pdf", reject_cad_pdfs=True)
```

This raises `UnsupportedFormatException` when a PDF's Creator or Producer metadata
names AutoCAD, Revit, SolidWorks, or MicroStation, before any plugin or cloud
converter runs. The check is local, also applies to stdin, and rejection occurs
before the CLI writes output. A rejected CAD PDF inside a ZIP, including nested
ZIPs, fails conversion instead of silently omitting that member. Existing partial
extraction remains available by
omitting the option.

This is an exporter policy, not a drawing classifier: it also rejects text-heavy
PDFs from those exporters. Missing, changed, or unrecognized metadata can evade
detection, and passing the check does not establish conversion fidelity. Malformed
PDFs that cannot be inspected fail rather than bypassing the requested check.
For ZIP input, the existing archive behavior still skips unsupported members.
Inspect the original rendered PDF when drawing content matters.

## Why Markdown?

Markdown is extremely close to plain text, with minimal markup or formatting, but still
Expand Down
16 changes: 15 additions & 1 deletion packages/markitdown/src/markitdown/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,12 @@ def main():
)

parser.add_argument("filename", nargs="?")
parser.add_argument(
"--reject-cad-pdfs",
action="store_true",
help="Reject PDFs identifying a known CAD exporter in Creator/Producer metadata. "
"Requires PDF dependencies; missing metadata cannot establish conversion fidelity.",
)
args = parser.parse_args()

# Parse the extension hint
Expand Down Expand Up @@ -248,16 +254,24 @@ def main():
else:
markitdown = MarkItDown(enable_plugins=args.use_plugins)

conversion_kwargs: Dict[str, Any] = {}
if args.reject_cad_pdfs:
conversion_kwargs["reject_cad_pdfs"] = 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_kwargs,
)
else:
result = markitdown.convert(
args.filename, stream_info=stream_info, keep_data_uris=args.keep_data_uris
args.filename,
stream_info=stream_info,
keep_data_uris=args.keep_data_uris,
**conversion_kwargs,
)

_handle_output(args, result)
Expand Down
4 changes: 4 additions & 0 deletions packages/markitdown/src/markitdown/_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ class UnsupportedFormatException(MarkItDownException):
pass


class CadPdfRejectedException(UnsupportedFormatException):
"""A requested CAD rejection policy must survive converter/archive fallbacks."""


class FailedConversionAttempt(object):
"""
Represents a single attempt to convert a file.
Expand Down
27 changes: 27 additions & 0 deletions packages/markitdown/src/markitdown/_markitdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@

from ._exceptions import (
FileConversionException,
CadPdfRejectedException,
UnsupportedFormatException,
FailedConversionAttempt,
)
Expand Down Expand Up @@ -602,6 +603,21 @@ def convert_response(
def _convert(
self, *, file_stream: BinaryIO, stream_info_guesses: List[StreamInfo], **kwargs
) -> DocumentConverterResult:
# Apply the requested policy before plugins, cloud services, and fallbacks.
cad_exporter = None
if any(
PdfConverter().accepts(file_stream, info) for info in stream_info_guesses
):
if kwargs.get("reject_cad_pdfs"):
PdfConverter.reject_cad_pdf(file_stream)
else:
# A best-effort warning must not change successful custom/cloud
# conversion when PDF dependencies or inspectable metadata are absent.
try:
cad_exporter = PdfConverter.cad_pdf_exporter(file_stream)
except Exception:
pass

res: Union[None, DocumentConverterResult] = None

# Keep track of which converters throw exceptions
Expand Down Expand Up @@ -668,6 +684,8 @@ def _convert(
if _accepts:
try:
res = converter.convert(file_stream, stream_info, **_kwargs)
except CadPdfRejectedException:
raise
except Exception:
failed_attempts.append(
FailedConversionAttempt(
Expand All @@ -678,6 +696,15 @@ def _convert(
file_stream.seek(cur_pos)

if res is not None:
if cad_exporter is not None:
warn(
"CAD PDF conversion may omit drawing geometry, dimensions, "
"and spatial relationships. It is not lossless. Use "
"reject_cad_pdfs=True / --reject-cad-pdfs to reject known "
"CAD exporters instead of accepting partial extraction.",
UserWarning,
stacklevel=2,
)
# Normalize the content
res.text_content = "\n".join(
[line.rstrip() for line in re.split(r"\r?\n", res.text_content)]
Expand Down
48 changes: 47 additions & 1 deletion packages/markitdown/src/markitdown/converters/_pdf_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@

from .._base_converter import DocumentConverter, DocumentConverterResult
from .._stream_info import StreamInfo
from .._exceptions import MissingDependencyException, MISSING_DEPENDENCY_MESSAGE
from .._exceptions import (
MissingDependencyException,
MISSING_DEPENDENCY_MESSAGE,
CadPdfRejectedException,
)

# Pattern for MasterFormat-style partial numbering (e.g., ".1", ".2", ".10")
PARTIAL_NUMBERING_PATTERN = re.compile(r"^\.\d+$")
Expand Down Expand Up @@ -62,6 +66,9 @@ def _merge_partial_numbering_lines(text: str) -> str:
try:
import pdfminer
import pdfminer.high_level
from pdfminer.pdfdocument import PDFDocument
from pdfminer.pdfparser import PDFParser
from pdfminer.utils import decode_text
import pdfplumber
except ImportError:
_dependency_exc_info = sys.exc_info()
Expand Down Expand Up @@ -499,6 +506,45 @@ class PdfConverter(DocumentConverter):
Falls back to pdfminer if pdfplumber is missing or fails.
"""

@staticmethod
def cad_pdf_exporter(file_stream: BinaryIO) -> str | None:
"""Identify a known exporter, without assessing drawing fidelity."""
if _dependency_exc_info is not None:
raise MissingDependencyException(
"CAD PDF detection requires the PDF dependencies. "
"Install 'markitdown[pdf]'."
) from _dependency_exc_info[1]

position = file_stream.tell()
try:
document = PDFDocument(PDFParser(file_stream))
for metadata in document.info:
for field, value in metadata.items():
# Revit exports may use /creator instead of /Creator.
if field.casefold() not in ("creator", "producer"):
continue
if isinstance(value, bytes):
value = decode_text(value)
if isinstance(value, str) and re.search(
r"\b(?:AutoCAD|Revit|SolidWorks|MicroStation)\b", value, re.I
):
return value
return None
finally:
file_stream.seek(position)

@staticmethod
def reject_cad_pdf(file_stream: BinaryIO) -> None:
"""Reject known CAD exporters; metadata is not a fidelity assessment."""
if PdfConverter.cad_pdf_exporter(file_stream) is not None:
raise CadPdfRejectedException(
"CAD-generated PDF rejected: Creator or Producer metadata "
"identifies a known CAD exporter. Text extraction cannot "
"preserve drawing geometry, dimensions, or spatial relationships. "
"Inspect the original PDF. Omit reject_cad_pdfs / "
"--reject-cad-pdfs only if partial text extraction is acceptable."
)

def accepts(
self,
file_stream: BinaryIO,
Expand Down
10 changes: 9 additions & 1 deletion packages/markitdown/src/markitdown/converters/_zip_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@

from .._base_converter import DocumentConverter, DocumentConverterResult
from .._stream_info import StreamInfo
from .._exceptions import UnsupportedFormatException, FileConversionException
from .._exceptions import (
UnsupportedFormatException,
FileConversionException,
CadPdfRejectedException,
)

# Break otherwise circular import for type hinting
if TYPE_CHECKING:
Expand Down Expand Up @@ -122,9 +126,13 @@ def convert(
if result is not None:
md_content += f"## File: {name}\n\n"
md_content += result.markdown + "\n\n"
except CadPdfRejectedException:
raise
except UnsupportedFormatException:
pass
except FileConversionException:
if kwargs.get("reject_cad_pdfs"):
raise
pass

return DocumentConverterResult(markdown=md_content.strip())
Loading