Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d971de8
feat(image-ocr-support): Add .jpg, .jpeg, and .png support to documen…
joshjyu Aug 19, 2026
84ac5be
feat(image-ocr-support): Add image parsing functionality to DocumentI…
joshjyu Aug 19, 2026
1710626
feat(image-ocr-support): Document image support
joshjyu Aug 19, 2026
2a9fbfd
refactor(sdk): use shared MIME normalizer in document upload filter
joshjyu Aug 19, 2026
cff891b
document: add image support in upload UI and messages
joshjyu Aug 19, 2026
0bcc81c
fix(sdk): accept DOCX and image uploads in document upload filter
joshjyu Aug 19, 2026
bdff5d1
fix(document): omit page headers in image markdown and html output
byannayang-y-y Aug 21, 2026
2407735
feat(document): recover www and bare-domain links from OCR text
byannayang-y-y Aug 21, 2026
fcaa10f
docs: add document and image extraction to README
byannayang-y-y Aug 21, 2026
8127dd7
fix(document): degrade gracefully when OCR cannot read an image
byannayang-y-y Aug 21, 2026
5271add
fix(storage): return a readable 400 when a document upload is rejected
byannayang-y-y Aug 21, 2026
91eadcc
fix(document): stop extracting email domains as bare-domain links
joshjyu Aug 26, 2026
7c8032b
Merge: remote-tracking branch 'upstream/develop' into feature/image-o…
joshjyu Aug 26, 2026
2ae77fb
fix(document): bound OCR preprocessing memory by pixel count
joshjyu Aug 26, 2026
453e694
Clarify document extraction and parsing features
jessebaugh Aug 26, 2026
c6dcbed
fix(document): build image temp path from string literals only
joshjyu Aug 26, 2026
c8c3236
fix(document): make the OCR pixel guard fail closed
joshjyu Aug 28, 2026
8579ea0
fix(document): stop truncating multi-label domains in link extraction
joshjyu Aug 28, 2026
fb2f23c
fix(document): escape OCR text in HTML output
joshjyu Aug 28, 2026
e8209b8
fix(document): bound image size before script detection
joshjyu Aug 28, 2026
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: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,13 +126,14 @@ It supports extraction, crawling, scraping, and search — designed to scale fro
4. **[Search](https://docs.maxun.dev/robot/search/search-introduction)** – Run automated web searches to discover or scrape results, with support for time-based filters.
5. **[SDK](https://docs.maxun.dev/category/sdk)** – A complete developer toolkit for scraping, extraction, scheduling, and end-to-end data automation.
6. **[CLI](https://docs.maxun.dev/category/cli)** – Create robots, trigger runs, and retrieve extracted data from your terminal.
7. **[Document Extraction & Parsing](https://docs.maxun.dev/)** – Extract structured data from documents and images (PDF, DOCX, XLSX, CSV, JPG, PNG) with OCR, or convert them into clean Markdown, HTML, links, or a summary.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated


## How Does It Work?

Maxun robots are automated tools that help you collect data from websites without writing any code. Think of them as your personal web assistants that can navigate websites, extract information, and organize data just like you would manually - but faster and more efficiently.

There are four types of robots, each designed for a different job.
There are five types of robots, each designed for a different job.

### 1. Extract
Extract emulates real user behavior and captures structured data.
Expand Down Expand Up @@ -162,6 +163,13 @@ Run automated web searches to discover or scrape results, with support for time-

Learn more <a href="https://docs.maxun.dev/robot/search/search-introduction">here</a>.

### 5. Document Extraction & Parsing
Upload a document or image — PDF, DOCX, XLSX, CSV, JPG, or PNG — and Maxun reads the text out of it, using OCR for scanned pages and photos. Convert it into clean Markdown, HTML, a list of links, or a summary, or use AI-powered extraction to pull specific structured fields.

**Use cases:** digitize scanned receipts and invoices, pull fields from a photo of a paper form, turn a screenshot of a table into structured data, or convert a scanned document into clean Markdown for an AI workflow.

Learn more <a href="https://docs.maxun.dev/">here</a>.

## Quick Start

### Getting Started
Expand Down Expand Up @@ -190,6 +198,7 @@ Maxun can run locally with or without Docker
- ✨ **Turn Websites to Spreadsheets** – Direct data export to Google Sheets & Airtable
- ✨ **Adapt To Website Layout Changes** – Auto-recovery from site updates
- ✨ **Extract Behind Login** – Handle authentication seamlessly
- ✨ **Extract From Documents & Images** – OCR PDFs, DOCX, XLSX, CSV, JPG, and PNG into structured data, Markdown, HTML, or links
- ✨ **Integrations** – Connect with your favorite tools
- ✨ **MCP Support** – Model Context Protocol integration
- ✨ **LLM-Ready Data** – Clean Markdown for AI applications
Expand Down
10 changes: 7 additions & 3 deletions server/src/api/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1485,14 +1485,18 @@ const documentUpload = multer({
fileFilter: (_req, file, cb) => {
const allowedMimeTypes = [
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'text/csv',
'application/csv',
'image/jpeg',
'image/jpg',
'image/png',
];
if (allowedMimeTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Only PDF, XLSX, and CSV files are allowed'));
cb(new Error('Only PDF, DOCX, XLSX, CSV, JPG, and PNG files are allowed'));
}
},
});
Expand All @@ -1509,7 +1513,7 @@ router.post("/sdk/robots/document", requireAPIKey, documentUpload.single('file')
if (!user) return res.status(401).json({ error: 'Unauthorized' });

const file = (req as any).file as Express.Multer.File | undefined;
if (!file) return res.status(400).json({ error: 'A PDF file is required' });
if (!file) return res.status(400).json({ error: 'A PDF, DOCX, XLSX, CSV, JPG, or PNG file is required' });

const prompt: string = (req.body.prompt || '').trim();
if (!prompt) return res.status(400).json({ error: 'prompt is required' });
Expand Down Expand Up @@ -1583,7 +1587,7 @@ router.post("/sdk/robots/document-parse", requireAPIKey, documentUpload.single('
if (!user) return res.status(401).json({ error: 'Unauthorized' });

const file = (req as any).file as Express.Multer.File | undefined;
if (!file) return res.status(400).json({ error: 'A PDF file is required' });
if (!file) return res.status(400).json({ error: 'A PDF, DOCX, XLSX, CSV, JPG, or PNG file is required' });

const rawFormats = req.body['outputFormats[]'] ?? req.body.outputFormats ?? req.body.formats;
const requestedFormats: string[] = Array.isArray(rawFormats)
Expand Down
44 changes: 29 additions & 15 deletions server/src/routes/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,27 @@ const documentUpload = multer({
if (normalizeDocumentMimeType(file.mimetype, file.originalname)) {
cb(null, true);
} else {
cb(new Error('Only PDF, DOCX, XLSX, and CSV files are allowed'));
cb(new Error('Only PDF, DOCX, XLSX, CSV, JPG, and PNG files are allowed'));
}
},
});

const uploadDocument = (req: any, res: any, next: any) => {
Comment thread
joshjyu marked this conversation as resolved.
documentUpload.single('file')(req, res, (err: any) => {
if (err) {
if (err instanceof multer.MulterError && err.code === 'LIMIT_FILE_SIZE') {
const maxMb = Math.round(MAX_FILE_SIZE_BYTES / (1024 * 1024));
return res.status(400).json({ error: `File is too large. The maximum size is ${maxMb} MB.` });
}
return res.status(400).json({ error: err.message || 'Invalid file upload.' });
}
next();
});
};

//HELPER FUNCTION


// const normalizeRobotUrl = (rawUrl: string): string => {
// let normalizedUrl: URL;
// try {
Expand Down Expand Up @@ -2117,21 +2131,21 @@ router.post('/recordings/search', requireSignIn, async (req: AuthenticatedReques

/**
* POST endpoint for creating a document extraction robot (doc-extract).
* Accepts a PDF or DOCX upload and an extraction prompt. Uses the configured LLM to generate
* an extraction schema and stores the document in MinIO.
* Accepts a PDF, DOCX, XLSX, CSV, JPG, or PNG upload and an extraction prompt.
* Uses the configured LLM to generate an extraction schema and stores the document in MinIO.
*/
router.post(
'/recordings/document',
requireSignIn,
documentUpload.single('file'),
uploadDocument,
async (req: AuthenticatedRequest, res) => {
try {
if (!req.user) return res.status(401).json({ error: 'Unauthorized' });

const file = (req as any).file as Express.Multer.File | undefined;
if (!file) return res.status(400).json({ error: 'A PDF or DOCX file is required.' });
if (!file) return res.status(400).json({ error: 'A PDF, DOCX, XLSX, CSV, JPG, or PNG file is required.' });
const documentMimeType = normalizeDocumentMimeType(file.mimetype, file.originalname);
if (!documentMimeType) return res.status(400).json({ error: 'Only PDF and DOCX files are allowed.' });
if (!documentMimeType) return res.status(400).json({ error: 'Only PDF, DOCX, XLSX, CSV, JPG, or PNG files are allowed.' });

const { prompt, name, llmProvider, llmModel, llmApiKey, llmBaseUrl } = req.body;
if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
Expand Down Expand Up @@ -2191,21 +2205,21 @@ router.post(

/**
* POST endpoint for creating a document parse robot (doc-parse).
* Accepts a PDF or DOCX upload and output format list. Parses the document immediately and
* stores both the document and parsed output in MinIO / database.
* Accepts a PDF, DOCX, XLSX, CSV, JPG, or PNG upload and output format list.
* Parses the document immediately and stores both the document and parsed output in MinIO / database.
*/
router.post(
'/recordings/document-parse',
requireSignIn,
documentUpload.single('file'),
uploadDocument,
async (req: AuthenticatedRequest, res) => {
try {
if (!req.user) return res.status(401).json({ error: 'Unauthorized' });

const file = (req as any).file as Express.Multer.File | undefined;
if (!file) return res.status(400).json({ error: 'A PDF or DOCX file is required.' });
if (!file) return res.status(400).json({ error: 'A PDF, DOCX, XLSX, CSV, JPG, or PNG file is required.' });
const documentMimeType = normalizeDocumentMimeType(file.mimetype, file.originalname);
if (!documentMimeType) return res.status(400).json({ error: 'Only PDF and DOCX files are allowed.' });
if (!documentMimeType) return res.status(400).json({ error: 'Only PDF, DOCX, XLSX, CSV, JPG, or PNG files are allowed.' });

const { name, formats, llmProvider, llmModel, llmApiKey, llmBaseUrl } = req.body;

Expand All @@ -2218,7 +2232,7 @@ router.post(
: DOC_PARSE_OUTPUT_FORMAT_OPTIONS.filter((f) => f !== 'summary');

// Summaries need a working LLM. Ollama runs locally and needs no key, but the
// hosted providers do — fail early rather than parsing the PDF and then dying.
// hosted providers do — fail early rather than parsing the document and then dying.
const summaryProvider = (llmProvider || 'ollama') as 'anthropic' | 'openai' | 'ollama';
if (outputFormats.includes('summary') && summaryProvider !== 'ollama') {
const envKey = summaryProvider === 'anthropic'
Expand Down Expand Up @@ -2386,15 +2400,15 @@ router.post('/runs/document-parse-run/:id', requireSignIn, async (req: Authentic
router.put(
'/recordings/:id/document',
requireSignIn,
documentUpload.single('file'),
uploadDocument,
async (req: AuthenticatedRequest, res) => {
try {
if (!req.user) return res.status(401).json({ error: 'Unauthorized' });

const file = (req as any).file as Express.Multer.File | undefined;
if (!file) return res.status(400).json({ error: 'A PDF or DOCX file is required.' });
if (!file) return res.status(400).json({ error: 'A PDF, DOCX, XLSX, CSV, JPG, or PNG file is required.' });
const documentMimeType = normalizeDocumentMimeType(file.mimetype, file.originalname);
if (!documentMimeType) return res.status(400).json({ error: 'Only PDF and DOCX files are allowed.' });
if (!documentMimeType) return res.status(400).json({ error: 'Only PDF, DOCX, XLSX, CSV, JPG, or PNG files are allowed.' });

const robot = await Robot.findOne({ where: { 'recording_meta.id': req.params.id } });
if (!robot) return res.status(404).json({ error: 'Robot not found.' });
Expand Down
20 changes: 18 additions & 2 deletions server/src/utils/document/documentFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,54 @@ export const PDF_MIME_TYPE = 'application/pdf';
export const DOCX_MIME_TYPE = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
export const XLSX_MIME_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
export const CSV_MIME_TYPE = 'text/csv';
export const JPEG_MIME_TYPE = 'image/jpeg';
export const PNG_MIME_TYPE = 'image/png';

const ALTERNATE_CSV_MIME_TYPES = new Set<string>(['application/csv', 'text/x-csv']);
const ALTERNATE_JPEG_MIME_TYPES = new Set<string>(['image/jpg']);

const SUPPORTED_DOCUMENT_MIME_TYPES = new Set<string>([
PDF_MIME_TYPE,
DOCX_MIME_TYPE,
XLSX_MIME_TYPE,
CSV_MIME_TYPE,
JPEG_MIME_TYPE,
PNG_MIME_TYPE
]);

export const isSupportedDocumentMimeType = (mimeType: string | undefined): boolean =>
Boolean(mimeType && (SUPPORTED_DOCUMENT_MIME_TYPES.has(mimeType) || ALTERNATE_CSV_MIME_TYPES.has(mimeType)));
Boolean(mimeType && (
SUPPORTED_DOCUMENT_MIME_TYPES.has(mimeType)
|| ALTERNATE_CSV_MIME_TYPES.has(mimeType)
|| ALTERNATE_JPEG_MIME_TYPES.has(mimeType)));

export const isImageMimeType = (mimeType: string | undefined): boolean =>
mimeType === JPEG_MIME_TYPE || mimeType === PNG_MIME_TYPE;

export const normalizeDocumentMimeType = (
mimeType: string | undefined,
originalFileName?: string
): string | null => {
if (mimeType && ALTERNATE_CSV_MIME_TYPES.has(mimeType)) return CSV_MIME_TYPE;
if (mimeType && ALTERNATE_JPEG_MIME_TYPES.has(mimeType)) return JPEG_MIME_TYPE;
if (isSupportedDocumentMimeType(mimeType)) return mimeType as string;

const extension = path.extname(originalFileName || '').toLowerCase();
if (extension === '.pdf') return PDF_MIME_TYPE;
if (extension === '.docx') return DOCX_MIME_TYPE;
if (extension === '.xlsx') return XLSX_MIME_TYPE;
if (extension === '.csv') return CSV_MIME_TYPE;
if (extension === '.jpg' || extension === '.jpeg') return JPEG_MIME_TYPE;
if (extension === '.png') return PNG_MIME_TYPE;
return null;
};

export const getDocumentExtensionForMimeType = (mimeType: string): '.pdf' | '.docx' | '.xlsx' | '.csv' => {
export const getDocumentExtensionForMimeType = (mimeType: string): '.pdf' | '.docx' | '.xlsx' | '.csv' | '.jpg' | '.png' => {
if (mimeType === DOCX_MIME_TYPE) return '.docx';
if (mimeType === XLSX_MIME_TYPE) return '.xlsx';
if (mimeType === CSV_MIME_TYPE || ALTERNATE_CSV_MIME_TYPES.has(mimeType)) return '.csv';
if (mimeType === JPEG_MIME_TYPE || ALTERNATE_JPEG_MIME_TYPES.has(mimeType)) return '.jpg';
if (mimeType === PNG_MIME_TYPE) return '.png';
return '.pdf';
};

Loading