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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;

namespace Microsoft.Extensions.AI;

/// <summary>Provides an optional base class for an <see cref="IOcrClient"/> that passes through calls to another instance.</summary>
/// <remarks>
/// This is recommended as a base type when building clients that can be chained in any order around an
/// underlying <see cref="IOcrClient"/>. The default implementation simply passes each call to the inner
/// client instance.
/// </remarks>
[Experimental(DiagnosticIds.Experiments.AIOcr, UrlFormat = DiagnosticIds.UrlFormat)]
public class DelegatingOcrClient : IOcrClient
{
/// <summary>Initializes a new instance of the <see cref="DelegatingOcrClient"/> class.</summary>
/// <param name="innerClient">The wrapped client instance.</param>
/// <exception cref="ArgumentNullException"><paramref name="innerClient"/> is <see langword="null"/>.</exception>
protected DelegatingOcrClient(IOcrClient innerClient)
{
InnerClient = Throw.IfNull(innerClient);
}

/// <summary>Gets the inner <see cref="IOcrClient"/>.</summary>
protected IOcrClient InnerClient { get; }

/// <inheritdoc />
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}

/// <inheritdoc />
public virtual Task<OcrResult> ExtractAsync(
Stream document,
string mediaType,
OcrOptions? options = null,
CancellationToken cancellationToken = default)
{
return InnerClient.ExtractAsync(document, mediaType, options, cancellationToken);
}

/// <inheritdoc />
public virtual IAsyncEnumerable<OcrPageResult> ExtractPagesAsync(
Stream document,
string mediaType,
OcrOptions? options = null,
CancellationToken cancellationToken = default)
{
return InnerClient.ExtractPagesAsync(document, mediaType, options, cancellationToken);
}

/// <inheritdoc />
public virtual object? GetService(Type serviceType, object? serviceKey = null)
{
_ = Throw.IfNull(serviceType);

// If the key is non-null, we don't know what it means so pass through to the inner service.
return
serviceKey is null && serviceType.IsInstanceOfType(this) ? this :
InnerClient.GetService(serviceType, serviceKey);
}

/// <summary>Provides a mechanism for releasing unmanaged resources.</summary>
/// <param name="disposing"><see langword="true"/> if being called from <see cref="Dispose()"/>; otherwise, <see langword="false"/>.</param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
InnerClient.Dispose();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Shared.DiagnosticIds;

namespace Microsoft.Extensions.AI;

/// <summary>Represents an optical character recognition (OCR) / document-parsing client.</summary>
/// <remarks>
/// <para>
/// An <see cref="IOcrClient"/> transcribes a document or image into structured output: text,
/// per-page content, tables, layout blocks with bounding regions, and confidence. It is the
/// capability sibling to <see cref="IChatClient"/>, <c>IEmbeddingGenerator</c>, and
/// <c>ISpeechToTextClient</c> for the document-extraction problem.
/// </para>
/// <para>
/// The contract is independent of <see cref="IChatClient"/>. Most OCR / document-AI engines are not
/// chat models: they emit structured output (tables, bounding regions, confidence, reading order)
/// that does not map onto a chat response. An implementation may wrap a vision-capable
/// <see cref="IChatClient"/> as the lowest-fidelity, transcription-only path, but the interface does
/// not require one.
/// </para>
/// <para>
/// Unless otherwise specified, all members of <see cref="IOcrClient"/> are thread-safe for concurrent
/// use. Implementations might mutate the <see cref="OcrOptions"/> supplied to <see cref="ExtractAsync"/>
/// and <see cref="ExtractPagesAsync"/>; consumers should avoid sharing a single options instance across
/// concurrent invocations when that is a concern. The document stream passed to these methods is not
/// disposed by the implementation.
/// </para>
/// </remarks>
[Experimental(DiagnosticIds.Experiments.AIOcr, UrlFormat = DiagnosticIds.UrlFormat)]
public interface IOcrClient : IDisposable
{
/// <summary>Runs OCR / document parsing over a document stream and returns structured output.</summary>
/// <param name="document">The document or image content to parse.</param>
/// <param name="mediaType">The media type of <paramref name="document"/>, for example <c>application/pdf</c> or <c>image/png</c>.</param>
/// <param name="options">The OCR options to configure the request.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>The structured OCR result.</returns>
Task<OcrResult> ExtractAsync(
Stream document,
string mediaType,
OcrOptions? options = null,
CancellationToken cancellationToken = default);

/// <summary>Runs OCR / document parsing over a document stream and streams back structured output as it is produced.</summary>
/// <param name="document">The document or image content to parse.</param>
/// <param name="mediaType">The media type of <paramref name="document"/>, for example <c>application/pdf</c> or <c>image/png</c>.</param>
/// <param name="options">The OCR options to configure the request.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>The structured OCR updates representing the streamed output.</returns>
/// <remarks>
/// Engines that produce pages incrementally (for example, while polling a long-running operation such as
/// Azure Document Intelligence) can yield each page as it completes, letting a consumer begin processing
/// early pages before later pages finish. Synchronous engines may yield a single terminal update. Use
/// <see cref="OcrPageResultExtensions.ToOcrResultAsync"/> to reassemble the stream into an
/// <see cref="OcrResult"/>.
/// </remarks>
IAsyncEnumerable<OcrPageResult> ExtractPagesAsync(
Stream document,
string mediaType,
OcrOptions? options = null,
CancellationToken cancellationToken = default);

/// <summary>Asks the <see cref="IOcrClient"/> for an object of the specified type <paramref name="serviceType"/>.</summary>
/// <param name="serviceType">The type of object being requested.</param>
/// <param name="serviceKey">An optional key that can be used to help identify the target service.</param>
/// <returns>The found object, otherwise <see langword="null"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="serviceType"/> is <see langword="null"/>.</exception>
/// <remarks>
/// The purpose of this method is to allow for the retrieval of strongly typed services that might be
/// provided by the <see cref="IOcrClient"/>, including itself or any services it might be wrapping.
/// </remarks>
object? GetService(Type serviceType, object? serviceKey = null);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;

namespace Microsoft.Extensions.AI;

/// <summary>Represents a positioned layout block, such as a paragraph, heading, or figure.</summary>
[Experimental(DiagnosticIds.Experiments.AIOcr, UrlFormat = DiagnosticIds.UrlFormat)]
public class OcrBlock : OcrElement
{
/// <summary>Initializes a new instance of the <see cref="OcrBlock"/> class.</summary>
/// <param name="text">The text content of the block.</param>
/// <exception cref="System.ArgumentNullException"><paramref name="text"/> is <see langword="null"/>.</exception>
public OcrBlock(string text)
{
Text = Throw.IfNull(text);
}

/// <summary>Gets the text content of the block.</summary>
public string Text { get; }

/// <summary>Gets or sets the kind of block, for example <see cref="OcrBlockKind.Paragraph"/>, <see cref="OcrBlockKind.Title"/>, or <see cref="OcrBlockKind.Figure"/>.</summary>
public OcrBlockKind? Kind { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;

namespace Microsoft.Extensions.AI;

/// <summary>Describes the kind of an <see cref="OcrBlock"/>, such as a paragraph, title, or figure.</summary>
/// <remarks>
/// This is a small open set modeled on <see cref="ChatRole"/>: the well-known values cover the common
/// layout categories, and a provider may introduce its own value when an engine reports a kind that is
/// not represented here.
/// </remarks>
[Experimental(DiagnosticIds.Experiments.AIOcr, UrlFormat = DiagnosticIds.UrlFormat)]
[JsonConverter(typeof(Converter))]
[DebuggerDisplay("{Value,nq}")]
public readonly struct OcrBlockKind : IEquatable<OcrBlockKind>
{
/// <summary>Gets the kind representing a paragraph of body text.</summary>
public static OcrBlockKind Paragraph { get; } = new("paragraph");

/// <summary>Gets the kind representing a title or heading.</summary>
public static OcrBlockKind Title { get; } = new("title");

/// <summary>Gets the kind representing a figure or image region.</summary>
public static OcrBlockKind Figure { get; } = new("figure");

/// <summary>Gets the value associated with this <see cref="OcrBlockKind"/>.</summary>
public string Value { get; }

/// <summary>Initializes a new instance of the <see cref="OcrBlockKind"/> struct with the provided value.</summary>
/// <param name="value">The value to associate with this <see cref="OcrBlockKind"/>.</param>
/// <exception cref="ArgumentNullException"><paramref name="value"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="value"/> is empty or composed entirely of whitespace.</exception>
[JsonConstructor]
public OcrBlockKind(string value)
{
Value = Throw.IfNullOrWhitespace(value);
}

/// <summary>Returns a value indicating whether two <see cref="OcrBlockKind"/> instances are equivalent, using a case-insensitive comparison.</summary>
/// <param name="left">The first <see cref="OcrBlockKind"/> instance to compare.</param>
/// <param name="right">The second <see cref="OcrBlockKind"/> instance to compare.</param>
/// <returns><see langword="true"/> if left and right have equivalent values; otherwise, <see langword="false"/>.</returns>
public static bool operator ==(OcrBlockKind left, OcrBlockKind right)
{
return left.Equals(right);
}

/// <summary>Returns a value indicating whether two <see cref="OcrBlockKind"/> instances are not equivalent, using a case-insensitive comparison.</summary>
/// <param name="left">The first <see cref="OcrBlockKind"/> instance to compare.</param>
/// <param name="right">The second <see cref="OcrBlockKind"/> instance to compare.</param>
/// <returns><see langword="true"/> if left and right have different values; otherwise, <see langword="false"/>.</returns>
public static bool operator !=(OcrBlockKind left, OcrBlockKind right)
{
return !(left == right);
}

/// <inheritdoc/>
public override bool Equals([NotNullWhen(true)] object? obj)
=> obj is OcrBlockKind otherKind && Equals(otherKind);

/// <inheritdoc/>
public bool Equals(OcrBlockKind other)
=> string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase);

/// <inheritdoc/>
public override int GetHashCode()
=> StringComparer.OrdinalIgnoreCase.GetHashCode(Value);

/// <inheritdoc/>
public override string ToString() => Value;

/// <summary>Provides a <see cref="JsonConverter{OcrBlockKind}"/> for serializing <see cref="OcrBlockKind"/> instances.</summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class Converter : JsonConverter<OcrBlockKind>
{
/// <inheritdoc/>
public override OcrBlockKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
new(reader.GetString()!);

/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, OcrBlockKind value, JsonSerializerOptions options) =>
Throw.IfNull(writer).WriteStringValue(value.Value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using Microsoft.Shared.DiagnosticIds;

namespace Microsoft.Extensions.AI;

/// <summary>Represents the axis-aligned bounds of a bounding polygon, in the coordinate space defined by the OCR engine.</summary>
/// <param name="Left">The minimum horizontal coordinate.</param>
/// <param name="Top">The minimum vertical coordinate.</param>
/// <param name="Right">The maximum horizontal coordinate.</param>
/// <param name="Bottom">The maximum vertical coordinate.</param>
[Experimental(DiagnosticIds.Experiments.AIOcr, UrlFormat = DiagnosticIds.UrlFormat)]
public readonly record struct OcrBoundingBox(float Left, float Top, float Right, float Bottom);
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;

namespace Microsoft.Extensions.AI;

/// <summary>Represents a positioned region on a page.</summary>
/// <remarks>
/// The region is a polygon (a clockwise sequence of <see cref="OcrPoint"/> vertices) so it can
/// faithfully carry a possibly rotation-skewed quadrilateral, such as Azure Document Intelligence's
/// <c>BoundingRegion.Polygon</c>, without loss. Engines that emit only an axis-aligned rectangle
/// (such as Mistral OCR) can convert via <see cref="FromRectangle"/>. The same type is reused for
/// layout-block geometry and for field grounding, providing one region primitive across providers.
/// </remarks>
[Experimental(DiagnosticIds.Experiments.AIOcr, UrlFormat = DiagnosticIds.UrlFormat)]
public class OcrBoundingRegion
{
/// <summary>Initializes a new instance of the <see cref="OcrBoundingRegion"/> class.</summary>
/// <param name="pageNumber">The one-based page number the region is on.</param>
/// <param name="polygon">The clockwise polygon vertices.</param>
/// <exception cref="ArgumentNullException"><paramref name="polygon"/> is <see langword="null"/>.</exception>
public OcrBoundingRegion(int pageNumber, IReadOnlyList<OcrPoint> polygon)
{
PageNumber = pageNumber;
Polygon = Throw.IfNull(polygon);
}

/// <summary>Gets the one-based page number the region is on.</summary>
/// <remarks>A region can reference a different page than its parent element.</remarks>
public int PageNumber { get; }

/// <summary>Gets the polygon vertices, in clockwise order.</summary>
/// <remarks>An Azure Document Intelligence quadrilateral is four points.</remarks>
public IReadOnlyList<OcrPoint> Polygon { get; }

/// <summary>Builds a clockwise quadrilateral region from an axis-aligned rectangle.</summary>
/// <param name="pageNumber">The one-based page number the region is on.</param>
/// <param name="left">The left coordinate.</param>
/// <param name="top">The top coordinate.</param>
/// <param name="right">The right coordinate.</param>
/// <param name="bottom">The bottom coordinate.</param>
/// <returns>A region whose polygon is the four corners of the rectangle.</returns>
public static OcrBoundingRegion FromRectangle(int pageNumber, float left, float top, float right, float bottom)
=> new(pageNumber,
[
new OcrPoint(left, top),
new OcrPoint(right, top),
new OcrPoint(right, bottom),
new OcrPoint(left, bottom),
]);

/// <summary>Computes the axis-aligned bounds of the polygon.</summary>
/// <returns>The axis-aligned bounds, or <see langword="null"/> when the polygon has no vertices.</returns>
/// <remarks>
/// Returning <see langword="null"/> for an empty polygon avoids conflating "no geometry" with a real
/// zero-size box located at the origin.
/// </remarks>
public OcrBoundingBox? GetBounds()
{
if (Polygon.Count == 0)
{
return null;
}

float minX = float.MaxValue, minY = float.MaxValue, maxX = float.MinValue, maxY = float.MinValue;
foreach (OcrPoint point in Polygon)
{
minX = Math.Min(minX, point.X);
maxX = Math.Max(maxX, point.X);
minY = Math.Min(minY, point.Y);
maxY = Math.Max(maxY, point.Y);
}

return new OcrBoundingBox(minX, minY, maxX, maxY);
}
}
Loading
Loading