Metadata-Version: 2.4
Name: undoc
Version: 0.25.0
Summary: High-performance Microsoft Office document extraction to Markdown
Author-email: iyulab <tech@iyulab.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/iyulab/undoc
Project-URL: Documentation, https://github.com/iyulab/undoc#readme
Project-URL: Repository, https://github.com/iyulab/undoc
Project-URL: Issues, https://github.com/iyulab/undoc/issues
Keywords: office,docx,xlsx,pptx,markdown,extraction
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX :: Linux
Classifier: Operating System :: MacOS :: MacOS X
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Text Processing :: Markup
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Requires-Dist: black>=23.0; extra == "dev"
Requires-Dist: mypy>=1.0; extra == "dev"
Dynamic: license-file

# undoc

High-performance Microsoft Office document extraction to Markdown.

## Installation

```bash
pip install undoc
```

## Usage

### Basic Usage

```python
from undoc import parse_file

# Parse a document
doc = parse_file("document.docx")

# Convert to Markdown
markdown = doc.to_markdown()
print(markdown)

# Convert to plain text
text = doc.to_text()

# Convert to JSON
json_data = doc.to_json()
```

### With Context Manager

```python
from undoc import parse_file

with parse_file("document.xlsx") as doc:
    print(doc.to_markdown(frontmatter=True))
    print(f"Sections: {doc.section_count}")
    print(f"Resources: {doc.resource_count}")
```

### Parse from Bytes

```python
from undoc import parse_bytes

with open("document.pptx", "rb") as f:
    data = f.read()

doc = parse_bytes(data)
markdown = doc.to_markdown()
```

### Extract Resources (Images)

```python
from undoc import parse_file

doc = parse_file("document.docx")

# Get all resource IDs
resource_ids = doc.get_resource_ids()

for rid in resource_ids:
    # Get resource metadata
    info = doc.get_resource_info(rid)
    # "primary": an image the document shows. "alternate" (a picture's SVG original) and
    # "layer" (an HD Photo effects layer) belong to the primary named by companion_of.
    if info["role"] != "primary":
        continue
    print(f"Resource: {info['filename']} ({info['mime_type']})")

    # Get resource binary data
    data = doc.get_resource_data(rid)

    # Save to file
    with open(info['filename'], 'wb') as f:
        f.write(data)
```

### Document Metadata

```python
from undoc import parse_file

doc = parse_file("document.docx")

print(f"Title: {doc.title}")
print(f"Author: {doc.author}")
print(f"Sections: {doc.section_count}")
print(f"Resources: {doc.resource_count}")
```

### Render a Slide

A slide of a `.pptx` renders to PNG from the parsed document, without reading the file again:

```python
from undoc import parse_file

with parse_file("deck.pptx") as doc:
    slide = doc.render_section(0, dpi=150)
    with open("slide1.png", "wb") as f:
        f.write(slide.png)
    if any(slide.gaps.values()):
        print("not painted:", slide.gaps)
```

What the renderer cannot paint yet — charts, embedded objects and other graphic frames, custom geometry,
pictures other than PNG and JPEG, text in a script that needs shaping — is left out and counted
in `gaps`; the rest of the slide is painted. No font is bundled. Text is drawn in the directories you name
(`font_dirs`), then the system's. A Linux container without fonts draws no text — install a font
package (Noto Sans CJK covers Latin and East Asian text) or pass a font directory — and
reports the runs as gaps. A section the document does not have raises `UndocError` with
`kind == ErrorKind.SECTION_OUT_OF_RANGE`; a document that is not a presentation,
`UNSUPPORTED_FORMAT`.

### Handling Failures

`UndocError.kind` says *why* a call failed, so you can react to the reason instead of
matching on message text:

```python
from undoc import ErrorKind, UndocError, parse_file

try:
    doc = parse_file(path)
    print(doc.to_markdown())
except UndocError as err:
    if err.kind is ErrorKind.ZIP_ARCHIVE:
        print("The file is damaged.")
    elif err.kind in (ErrorKind.UNKNOWN_FORMAT, ErrorKind.UNSUPPORTED_FORMAT):
        print("Not a supported Office document.")
    elif err.kind is ErrorKind.ENCRYPTED:
        print("The document is encrypted.")
    else:
        # Also the right branch for a reason this build has no name for.
        print(f"Extraction failed ({err.kind}): {err}")
```

The numbers behind `ErrorKind` are a stable ABI contract: a new reason takes the next free
number and existing ones are never renumbered. Always keep a final `else` — an
unrecognised value arrives as a plain `int` rather than an `ErrorKind`, so that a newer
native library stays usable. `kind` is `ErrorKind.OTHER` for failures raised by the wrapper
itself, and never `ErrorKind.NONE` (which means success).

## Supported Formats

- **DOCX** - Microsoft Word documents
- **XLSX** - Microsoft Excel spreadsheets
- **PPTX** - Microsoft PowerPoint presentations

## Features

- **RAG-Ready Output**: Structured Markdown optimized for RAG/LLM applications
- **High Performance**: Native Rust implementation via FFI
- **Asset Extraction**: Images and embedded resources
- **Metadata Preservation**: Document properties, styles, formatting
- **Cross-Platform**: Windows, Linux, macOS (Intel & ARM)

## API Reference

### Functions

- `parse_file(path)` - Parse document from file path
- `parse_bytes(data)` - Parse document from bytes
- `version()` - Get library version

### Undoc Class

#### Conversion Methods

- `to_markdown(frontmatter=False, escape_special=True, paragraph_spacing=True, refine=False, *, ...)` - Convert to Markdown (`escape_special=False` writes text without escaping Markdown special characters). Keyword-only, all defaulting to the library's own values: `image_path_prefix`, `include_images`, `table_fallback` (`"markdown"`, `"html"`), `max_heading_level` (1-6), `preserve_line_breaks`, `include_empty_paragraphs`, `list_marker`, `cleanup` (`None`, `"minimal"`, `"standard"`, `"aggressive"`), `revision_handling` (`"accept_all"`, `"reject_all"`, `"show_markup"`), `strip_redundant_emphasis_in_headings`, `emit_page_breaks`, `include_headers_footers`, `callout_blockquote`, `section_markers` (`"none"`, `"comment"`). A value the library cannot honour raises `UndocError` with `ErrorKind.INVALID_ARGUMENT`.
- `to_text()` - Convert to plain text
- `to_json(compact=False)` - Convert to JSON
- `plain_text()` - Get plain text (fast extraction)

#### Properties

- `title` - Document title
- `author` - Document author
- `section_count` - Number of sections
- `resource_count` - Number of resources

#### Tables

- `get_tables(tsv=False)` - Every table as CSV (RFC 4180), or tab-separated with `tsv=True`, in reading order: `{"section", "index", "text"}` — the section (a sheet, a slide, a document section), the table's place among that section's tables (from 1), and the text. A table nested in a cell is a table of its own, right after the one that holds it. A merged cell's text is in its top-left position and the positions it covers are empty, so every record has the same number of fields: `pandas.read_csv(io.StringIO(t["text"]))` reads one.

#### Resource Methods

- `get_resource_ids()` - List of resource IDs
- `get_resource_info(id)` - Resource metadata
- `get_resource_data(id)` - Resource binary data

#### Rendering

- `render_section(index, dpi=150, font_dirs=(), system_fonts=True)` - A slide as PNG, its size, and what was not painted (`RenderedSection`)

## Native library

The package ships the native library for your platform. To load another build of it — one
you compiled from the Rust crate, say — set `UNDOC_LIB_PATH` to that file. A path that names no file
is an error: importing raises `OSError` naming the path, rather than quietly loading the
packaged library instead.

## License

MIT License - see [LICENSE](../../LICENSE) for details.
