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
103 changes: 103 additions & 0 deletions .github/workflows/document-extraction.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
name: Document extraction

on:
push:
branches: [main]
paths:
- 'DocumentExtraction/**'
- 'Directory.Build.props'
- 'Directory.Build.targets'
- 'Directory.Packages.props'
- 'CommunityToolkit.AI.slnx'
- 'global.json'
- 'eng/Bootstrap-DocumentExtractionCore.ps1'
- 'eng/New-DocumentExtractionNuGetConfig.ps1'
- 'eng/Get-DocumentExtractionHeronModel.ps1'
- 'nuget.config'
- '.github/workflows/document-extraction.yml'
pull_request:
branches: [main, 'copilot/*']
paths:
- 'DocumentExtraction/**'
- 'Directory.Build.props'
- 'Directory.Build.targets'
- 'Directory.Packages.props'
- 'CommunityToolkit.AI.slnx'
- 'global.json'
- 'eng/Bootstrap-DocumentExtractionCore.ps1'
- 'eng/New-DocumentExtractionNuGetConfig.ps1'
- 'eng/Get-DocumentExtractionHeronModel.ps1'
- 'nuget.config'
- '.github/workflows/document-extraction.yml'
workflow_dispatch:

permissions:
contents: read

env:
DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: true

jobs:
bootstrap-core:
name: Pack pinned public upstream core
runs-on: windows-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-dotnet@v5
env:
DOTNET_INSTALL_DIR: ${{ runner.temp }}/document-extraction-sdk
with:
dotnet-version: 10.0.401
- name: Pack original owning upstream projects
shell: pwsh
env:
# Match the verified ordinary owning-project recipe rather than
# enabling upstream CI-only build defaults.
CI: 'false'
run: ./eng/Bootstrap-DocumentExtractionCore.ps1 -OutputDirectory "${{ runner.temp }}/document-extraction-core" -DotnetPath "${{ runner.temp }}/document-extraction-sdk/dotnet.exe"
- uses: actions/upload-artifact@v6
with:
name: document-extraction-core-source-packages
path: ${{ runner.temp }}/document-extraction-core/*.10.10.0-review.20261104.4.nupkg
if-no-files-found: error

build-test-pack:
name: Offline acceptance (${{ matrix.os }})
runs-on: ${{ matrix.os }}
needs: bootstrap-core
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-dotnet@v5
with:
global-json-file: global.json
dotnet-version: 8.0.x
- uses: actions/download-artifact@v7
with:
name: document-extraction-core-source-packages
path: ${{ runner.temp }}/document-extraction-core
- name: Configure isolated source-built core feed
shell: pwsh
run: ./eng/New-DocumentExtractionNuGetConfig.ps1 -CorePackageDirectory "${{ runner.temp }}/document-extraction-core" -OutputPath "${{ runner.temp }}/document-extraction-nuget.config"
- name: Restore exact dependencies
run: dotnet restore DocumentExtraction/DocumentExtraction.slnf --configfile "${{ runner.temp }}/document-extraction-nuget.config"
- name: Build
run: dotnet build DocumentExtraction/DocumentExtraction.slnf --no-restore -c Release
- name: Acquire pinned Apache-2.0 layout model for native inference acceptance
shell: pwsh
run: ./eng/Get-DocumentExtractionHeronModel.ps1 -OutputPath "${{ runner.temp }}/heron/model.onnx"
- name: Offline tests
env:
DOCUMENT_EXTRACTION_HERON_MODEL: ${{ runner.temp }}/heron/model.onnx
run: dotnet test DocumentExtraction/DocumentExtraction.slnf --no-build --no-restore -c Release
- name: Pack preview artifacts
run: dotnet pack DocumentExtraction/DocumentExtraction.slnf --no-build --no-restore -c Release --output artifacts/document-extraction
- uses: actions/upload-artifact@v6
with:
name: document-extraction-preview-packages-${{ matrix.os }}
path: artifacts/document-extraction/*.nupkg
if-no-files-found: error
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,7 @@ examples/perl/**/local/*
**cpanfile.snapshot
**/.modules/
**/*.AppHost.TypeScript/nuget.config

# Private review inputs and local test-generation reports.
/.review-inputs/
/.testagent/
18 changes: 18 additions & 0 deletions CommunityToolkit.AI.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,24 @@
<Project Path="MEVD/src/SqlServer/SqlServer.csproj" />
<Project Path="MEVD/src/Weaviate/Weaviate.csproj" />
</Folder>
<Folder Name="/DocumentExtraction/src/">
<File Path="DocumentExtraction/src/Directory.Build.props" />
<Project Path="DocumentExtraction/src/PdfPig/PdfPig.csproj" />
<Project Path="DocumentExtraction/src/PdfPig.OnnxLayoutAnalysis/PdfPig.OnnxLayoutAnalysis.csproj" />
<Project Path="DocumentExtraction/src/AzureContentUnderstanding/AzureContentUnderstanding.csproj" />
<Project Path="DocumentExtraction/src/AzureDocumentIntelligence/AzureDocumentIntelligence.csproj" />
<Project Path="DocumentExtraction/src/Mistral/Mistral.csproj" />
<Project Path="DocumentExtraction/src/Vision/Vision.csproj" />
</Folder>
<Folder Name="/DocumentExtraction/test/">
<File Path="DocumentExtraction/test/Directory.Build.props" />
<Project Path="DocumentExtraction/test/PdfPig.UnitTests/PdfPig.UnitTests.csproj" />
<Project Path="DocumentExtraction/test/PdfPig.OnnxLayoutAnalysis.UnitTests/PdfPig.OnnxLayoutAnalysis.UnitTests.csproj" />
<Project Path="DocumentExtraction/test/AzureContentUnderstanding.UnitTests/AzureContentUnderstanding.UnitTests.csproj" />
<Project Path="DocumentExtraction/test/AzureDocumentIntelligence.UnitTests/AzureDocumentIntelligence.UnitTests.csproj" />
<Project Path="DocumentExtraction/test/Mistral.UnitTests/Mistral.UnitTests.csproj" />
<Project Path="DocumentExtraction/test/Vision.UnitTests/Vision.UnitTests.csproj" />
</Folder>
<Folder Name="/MEVD/test/">
<File Path="MEVD/test/Directory.Build.props" />
<Project Path="MEVD/test/AzureAISearch.UnitTests/AzureAISearch.UnitTests.csproj" />
Expand Down
15 changes: 15 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,21 @@
<PackageVersion Include="sqlite-vec" Version="0.1.7-alpha.2.1" />
</ItemGroup>

<ItemGroup Label="DocumentExtraction">
<PackageVersion Update="Microsoft.Extensions.AI.Abstractions" Version="10.10.0-review.20261104.4" Condition="'$(DocumentExtractionBuild)' == 'true'" />
<PackageVersion Update="System.Text.Json" Version="10.0.12" Condition="'$(DocumentExtractionBuild)' == 'true'" />
<PackageVersion Include="Microsoft.Extensions.Documents.Abstractions" Version="10.10.0-review.20261104.4" />
<PackageVersion Include="Microsoft.Extensions.DocumentExtraction.Abstractions" Version="10.10.0-review.20261104.4" />
<PackageVersion Include="Azure.AI.ContentUnderstanding" Version="1.1.0" />
<PackageVersion Include="Azure.AI.DocumentIntelligence" Version="1.0.0" />
<PackageVersion Include="Azure.Core" Version="1.53.0" />
<PackageVersion Include="PdfPig" Version="0.1.15" />
<PackageVersion Include="Docnet.Core" Version="2.6.0" />
<PackageVersion Include="Microsoft.ML.OnnxRuntime" Version="1.23.2" />
<PackageVersion Include="SkiaSharp" Version="3.119.0" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="3.119.0" />
</ItemGroup>

<ItemGroup Label="Build">
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.401" />
</ItemGroup>
Expand Down
19 changes: 19 additions & 0 deletions DocumentExtraction/DocumentExtraction.slnf
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"solution": {
"path": "../CommunityToolkit.AI.slnx",
"projects": [
"DocumentExtraction/src/PdfPig/PdfPig.csproj",
"DocumentExtraction/src/PdfPig.OnnxLayoutAnalysis/PdfPig.OnnxLayoutAnalysis.csproj",
"DocumentExtraction/src/AzureContentUnderstanding/AzureContentUnderstanding.csproj",
"DocumentExtraction/src/AzureDocumentIntelligence/AzureDocumentIntelligence.csproj",
"DocumentExtraction/src/Mistral/Mistral.csproj",
"DocumentExtraction/src/Vision/Vision.csproj",
"DocumentExtraction/test/PdfPig.UnitTests/PdfPig.UnitTests.csproj",
"DocumentExtraction/test/PdfPig.OnnxLayoutAnalysis.UnitTests/PdfPig.OnnxLayoutAnalysis.UnitTests.csproj",
"DocumentExtraction/test/AzureContentUnderstanding.UnitTests/AzureContentUnderstanding.UnitTests.csproj",
"DocumentExtraction/test/AzureDocumentIntelligence.UnitTests/AzureDocumentIntelligence.UnitTests.csproj",
"DocumentExtraction/test/Mistral.UnitTests/Mistral.UnitTests.csproj",
"DocumentExtraction/test/Vision.UnitTests/Vision.UnitTests.csproj"
]
}
}
219 changes: 219 additions & 0 deletions DocumentExtraction/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
# Document extraction

Extract text, tables, figures and source locations from documents with a choice
of local PDF parsing, document services or vision-capable language models.
Each provider returns the same `Microsoft.Extensions.Documents.ExtractionDocument`
graph, so downstream processing can work with document structure rather than
backend-specific response formats.

## Choose a provider

All packages target **.NET 8 and .NET 10**. Package IDs start with
`CommunityToolkit.DocumentExtraction.`.

| Package | Client type | Use when you need | Processing and input |
|---|---|---|---|
| [PdfPig](src/PdfPig/README.md) | `PdfPigDocumentExtractionClient` | Existing PDF text and block geometry without a service | Local; searchable PDFs, no OCR |
| [PdfPig.OnnxLayoutAnalysis](src/PdfPig.OnnxLayoutAnalysis/README.md) | `PdfPigOnnxDocumentExtractionClient` | Native PDF text with model-detected headings, tables and pictures | Local; actual page rasterization and an explicitly acquired Heron ONNX model, no OCR |
| [AzureContentUnderstanding](src/AzureContentUnderstanding/README.md) | `AzureContentUnderstandingDocumentExtractionClient` | Document text, tables and layout from a Content Understanding analyzer | Azure; analyzer-supported document formats |
| [AzureDocumentIntelligence](src/AzureDocumentIntelligence/README.md) | `AzureDocumentIntelligenceDocumentExtractionClient` | OCR, layout, tables and optional figure downloads | Azure; model-supported document formats |
| [Mistral](src/Mistral/README.md) | `MistralDocumentExtractionClient` | Native Mistral OCR page text, tables and images, directly or through Microsoft Foundry | Hosted; endpoint-supported documents and images |
| [Vision](src/Vision/README.md) | `VisionDocumentExtractionClient` | Free-form transcription or structured tables with your own vision-capable `IChatClient` | Local or hosted model; images and locally rasterized PDF pages |

Azure and Mistral providers upload document content to the configured service and
can incur charges. Vision sends images to your chosen backend; a PDF makes one inference
call per page. PdfPig reads existing PDF text locally and does not recognize
scanned text. Choose based on document format, required fidelity, latency,
privacy and cost.

`PdfPig.OnnxLayoutAnalysis` is optional: the basic PdfPig package does not bring
in PDFium, ONNX Runtime or model weights. The companion detects layout regions,
not table cells or scanned text; see its setup and result semantics before use.

## Install

Install the provider your application needs:

```shell
dotnet add package CommunityToolkit.DocumentExtraction.PdfPig --prerelease
```

Substitute any package ID from the table. The providers bring in
`Microsoft.Extensions.DocumentExtraction.Abstractions`,
`Microsoft.Extensions.Documents.Abstractions` and
`Microsoft.Extensions.AI.Abstractions` transitively.

Azure examples use the additional `Azure.Identity` package for
`DefaultAzureCredential`. Vision requires an application-supplied chat adapter,
configured model and credentials. Each provider README contains its setup,
supported options and examples.

Mistral uses one package for both hosts. Configure its complete native OCR
operation URI and model/deployment selector with
`MistralDocumentExtractionClientOptions`; authenticate with a bearer API key or,
for an endpoint supporting Entra ID, a `TokenCredential` and the resource's token
scope. Direct Mistral uses `https://api.mistral.ai/v1/ocr`; Foundry operation paths
depend on the deployment. No Chat/Responses adapter or second Mistral package is
required.

## Extract a document

This complete console example uses PdfPig. Supply an existing searchable PDF.

```csharp
using System;
using System.IO;
using System.Threading;
using CommunityToolkit.DocumentExtraction.PdfPig;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DocumentExtraction;
using Microsoft.Extensions.Documents;

using var timeout = new CancellationTokenSource(TimeSpan.FromMinutes(2));
using DocumentExtractionClient extractor = new PdfPigDocumentExtractionClient();
using var input = File.OpenRead("document.pdf");
ExtractionDocument document = await extractor.ExtractAsync(
input, "document.pdf", "application/pdf", cancellationToken: timeout.Token);

foreach (var element in document.EnumerateContent())
{
if (element.Content is TextContent text)
{
Console.WriteLine(text.Text);
}
}
```

All providers use `ExtractAsync(Stream, identifier, mediaType, options,
cancellationToken)`. The identifier labels the result; it is not a URL that the
provider fetches. Supply a readable stream and the actual media type.
`DocumentExtractionOptions.ExtractorName` selects an analyzer/model where the
provider supports it. Provider-specific options belong in `AdditionalProperties`;
unsupported options raise errors.

## Work with the result

`ExtractionDocument` contains sections and a hierarchy of text, images and
tables. Content is local to each element; structural ancestors do not repeat
their children's text. `EnumerateContent()` walks section elements in depth-first
order and yields non-section elements, including tables. It does **not** enter
table cells or return document/section-local payloads. Inspect `document.Content`,
section `Content` and table cells directly when you need those payloads.

| Result | How to use it |
|---|---|
| Text | `ExtractionDocumentTextElement.Content` contains `TextContent`; read its `Text` and the element's optional role. |
| Tables | `ExtractionDocumentTableElement.Cells` exposes `RowCount`, `ColumnCount` and a `[row, column]` indexer. A cell can hold a text element or nested section, with spans and an optional role. Mistral and some vision results instead keep table-local Markdown in the table's `Content`. |
| Images | Read `ExtractionDocumentImageElement.Content` when image data is available. `AlternativeText` is a caption/description; `Text` is recognized image text, when supplied. |
| Sources | `document.SourcePages` describes source frames. Element `Sources` associate content with a page and any reported ordered polygon. Page numbers, dimensions or geometry can be absent. |
| Native data | `document.RawRepresentation` and `source.GetRawRepresentation()` expose backend-specific inspection objects where available. |

### Read table cell content

The cell indexer returns an actual anchor cell or null. Null means either an empty
position or one covered by another anchor's span; it is not an empty text cell.
Read `RowIndex`, `ColumnIndex`, `RowSpan` and `ColumnSpan` from non-null anchors
to understand the occupied range. An anchor can itself have null `Content`.

Continuation of the example above. This walks sections and table anchor cells
explicitly, including section-valued mixed cell content and nested tables:

```csharp
foreach (var page in document.SourcePages)
{
Console.WriteLine($"Source {page.Identifier}, original page {page.PageNumber}");
}

foreach (var section in document.Sections)
{
PrintElement(section);
}

void PrintElement(ExtractionDocumentElement element)
{
if (element.Content is TextContent text)
{
Console.WriteLine(text.Text);
}
if (element is ExtractionDocumentImageElement image &&
image.Content is DataContent data)
{
Console.WriteLine($"{image.AlternativeText}: {data.Data.Length} image bytes");
}
if (element is ExtractionDocumentSection section)
{
foreach (var child in section.Elements)
{
PrintElement(child);
}
}
else if (element is ExtractionDocumentTableElement table)
{
foreach (var cell in table.Cells)
{
if (cell is null)
{
continue;
}

Console.WriteLine(
$"Cell ({cell.RowIndex}, {cell.ColumnIndex}), span {cell.RowSpan} x {cell.ColumnSpan}");
if (cell.Content is { } content)
{
PrintElement(content);
}
}
}
}
```

Source capabilities vary: PdfPig supplies PDF block geometry; Azure services
supply their reported regions; Vision supplies page associations rather than
measured paragraph polygons. Check for data instead of assuming every provider
supplies the same level of detail.

## Lifetimes, cancellation and middleware

Dispose extraction clients when finished. Providers leave caller-supplied input
streams and SDK, HTTP or chat clients open. Returned documents require no
disposal; extracted image bytes and data remain usable after extraction.
Native inspection objects can be mutable and contain sensitive data, so avoid
treating them as a durable interchange format or logging them indiscriminately.

Providers buffer input in memory. Set application file-size limits and pass a
cancellation token; cancellation cannot undo remote processing already accepted.
Service, parsing and validation errors propagate to the caller.

For extraction middleware such as logging, also install
`Microsoft.Extensions.DocumentExtraction`:

```shell
dotnet add package Microsoft.Extensions.DocumentExtraction --prerelease
```

Use `DocumentExtractionClientBuilder` to wrap the provider, and call
`ExtractAsync` on the resulting pipeline. Native `GetService` access is for
backend-specific operations; direct calls to those services bypass extraction
middleware. The [Vision examples](src/Vision/README.md#add-extraction-logging-middleware)
show a logging pipeline.

## Experimental API diagnostics

The shared contracts produce `MEDE0001` and `EXTEXP0019` experimental API
diagnostics. To use these APIs, opt in explicitly in your application project:

```xml
<PropertyGroup>
<NoWarn>$(NoWarn);MEDE0001;EXTEXP0019</NoWarn>
</PropertyGroup>
```

## Feedback and license

[Report issues or request features](https://github.com/CommunityToolkit/AI/issues).
When reporting a problem, include the provider, package versions, input media type
and a minimal reproduction without credentials or sensitive document content.

PDF segmentation, geometry and rendering integration include contributions by
[@luisquintanilla](https://github.com/luisquintanilla).
Licensed under the [MIT license](https://github.com/CommunityToolkit/AI/blob/main/LICENSE).
Loading
Loading