diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/assets.json b/sdk/contentunderstanding/azure-ai-contentunderstanding/assets.json index 58186b3555c8..784cecb42f4e 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/assets.json +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "java", "TagPrefix": "java/contentunderstanding/azure-ai-contentunderstanding", - "Tag": "java/contentunderstanding/azure-ai-contentunderstanding_670ad2966f" + "Tag": "java/contentunderstanding/azure-ai-contentunderstanding_940a862f7e" } diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/src/samples/java/com/azure/ai/contentunderstanding/samples/Sample_Advanced_ContentSource.java b/sdk/contentunderstanding/azure-ai-contentunderstanding/src/samples/java/com/azure/ai/contentunderstanding/samples/Sample_Advanced_ContentSource.java new file mode 100644 index 000000000000..01cd09e109c9 --- /dev/null +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/src/samples/java/com/azure/ai/contentunderstanding/samples/Sample_Advanced_ContentSource.java @@ -0,0 +1,192 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.ai.contentunderstanding.samples; + +import com.azure.ai.contentunderstanding.ContentUnderstandingClient; +import com.azure.ai.contentunderstanding.ContentUnderstandingClientBuilder; +import com.azure.ai.contentunderstanding.models.AnalysisInput; +import com.azure.ai.contentunderstanding.models.AnalysisResult; +import com.azure.ai.contentunderstanding.models.AudioVisualSource; +import com.azure.ai.contentunderstanding.models.ContentAnalyzerAnalyzeOperationStatus; +import com.azure.ai.contentunderstanding.models.ContentField; +import com.azure.ai.contentunderstanding.models.ContentSource; +import com.azure.ai.contentunderstanding.models.DocumentContent; +import com.azure.ai.contentunderstanding.models.DocumentSource; +import com.azure.ai.contentunderstanding.models.PointF; +import com.azure.ai.contentunderstanding.models.Rectangle; +import com.azure.ai.contentunderstanding.models.RectangleF; +import com.azure.core.credential.AzureKeyCredential; +import com.azure.core.util.polling.SyncPoller; +import com.azure.identity.DefaultAzureCredentialBuilder; + +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Demonstrates how to access and use {@link ContentSource} grounding references + * from analysis results. Content sources identify the exact location in the original + * content where a field value was extracted from. + * + *

For document/image content, sources are {@link DocumentSource} instances + * with page number, polygon coordinates, and a computed bounding box.

+ * + *

For audio/video content, sources are {@link AudioVisualSource} instances + * with a timestamp and an optional bounding box.

+ */ +public class Sample_Advanced_ContentSource { + + public static void main(String[] args) { + String endpoint = System.getenv("CONTENTUNDERSTANDING_ENDPOINT"); + String key = System.getenv("CONTENTUNDERSTANDING_KEY"); + + ContentUnderstandingClientBuilder builder = new ContentUnderstandingClientBuilder().endpoint(endpoint); + + ContentUnderstandingClient client; + if (key != null && !key.trim().isEmpty()) { + client = builder.credential(new AzureKeyCredential(key)).buildClient(); + } else { + client = builder.credential(new DefaultAzureCredentialBuilder().build()).buildClient(); + } + + // Analyze an invoice once — reuse the result for all demonstrations. + String invoiceUrl + = "https://raw.githubusercontent.com/Azure-Samples/azure-ai-content-understanding-assets/main/document/invoice.pdf"; + + AnalysisInput input = new AnalysisInput(); + input.setUrl(invoiceUrl); + + SyncPoller operation + = client.beginAnalyze("prebuilt-invoice", Arrays.asList(input)); + + AnalysisResult result = operation.getFinalResult(); + DocumentContent documentContent = (DocumentContent) result.getContents().get(0); + + // ===================================================================== + // Part 1: Document ContentSource from analysis + // ===================================================================== + documentContentSourceFromAnalysis(documentContent); + + // ===================================================================== + // Part 2: DocumentSource.parse() and ContentSource.parseAll() round-trip + // ===================================================================== + contentSourceParseRoundTrip(documentContent); + } + + /** + * Analyzes an invoice and iterates over field grounding sources, + * casting each to {@link DocumentSource} to access page, polygon, and bounding box. + */ + // BEGIN: com.azure.ai.contentunderstanding.advanced.contentsource.fromanalysis + private static void documentContentSourceFromAnalysis(DocumentContent documentContent) { + // Iterate over all fields and access their grounding sources. + for (Map.Entry entry : documentContent.getFields().entrySet()) { + String fieldName = entry.getKey(); + ContentField field = entry.getValue(); + + System.out.println("Field: " + fieldName + " = " + field.getValue()); + + // Sources identify where the field value appears in the original content. + // For documents, each source is a DocumentSource with page number and polygon. + List sources = field.getSources(); + if (sources != null) { + for (ContentSource source : sources) { + if (source instanceof DocumentSource) { + DocumentSource docSource = (DocumentSource) source; + System.out.println(" Source: page " + docSource.getPageNumber()); + + // Polygon: the precise region (rotated quadrilateral) around the text. + List polygon = docSource.getPolygon(); + String coords = polygon.stream() + .map(p -> String.format("(%.4f,%.4f)", p.getX(), p.getY())) + .collect(Collectors.joining(", ")); + System.out.println(" Polygon: [" + coords + "]"); + + // BoundingBox: axis-aligned rectangle computed from the polygon — + // convenient for drawing highlight overlays. + RectangleF bbox = docSource.getBoundingBox(); + System.out.printf(" BoundingBox: x=%.4f, y=%.4f, w=%.4f, h=%.4f%n", + bbox.getX(), bbox.getY(), bbox.getWidth(), bbox.getHeight()); + } + } + } + } + } + // END: com.azure.ai.contentunderstanding.advanced.contentsource.fromanalysis + + /** + * Demonstrates the two public parse methods and {@link ContentSource#toRawString(List)}: + *
    + *
  • {@link DocumentSource#parse(String)} — typed method, returns {@code List}
  • + *
  • {@link ContentSource#parseAll(String)} — base-class method, returns {@code List}
  • + *
+ */ + // BEGIN: com.azure.ai.contentunderstanding.advanced.contentsource.parse + private static void contentSourceParseRoundTrip(DocumentContent documentContent) { + // --- DocumentSource.parse() — typed method --- + // DocumentSource.parse() is the typed convenience method. It returns List + // directly — no casting needed. Use this when you know the source string contains only D() segments. + ContentField multiSourceField = documentContent.getFields().values().stream() + .filter(f -> f.getSources() != null && f.getSources().size() > 1) + .findFirst() + .orElseThrow(() -> new IllegalStateException("No field with multiple sources found")); + String multiWireFormat = ContentSource.toRawString(multiSourceField.getSources()); + System.out.println("Multi-segment wire format: " + multiWireFormat); + + List docSources = DocumentSource.parse(multiWireFormat); + for (DocumentSource ds : docSources) { + RectangleF bbox = ds.getBoundingBox(); + System.out.printf(" parse -> page %d, bbox: x=%.4f, y=%.4f, w=%.4f, h=%.4f%n", + ds.getPageNumber(), bbox.getX(), bbox.getY(), bbox.getWidth(), bbox.getHeight()); + } + + // --- toRawString + ContentSource.parseAll() round-trip --- + // ContentSource.parseAll() is the base-class method that handles both D() and AV() formats. + // It returns List, so you cast each element to the appropriate subclass. + ContentField fieldWithSource = documentContent.getFields().values().stream() + .filter(f -> f.getSources() != null) + .findFirst() + .orElseThrow(() -> new IllegalStateException("No field with sources found")); + + String wireFormat = ContentSource.toRawString(fieldWithSource.getSources()); + System.out.println("Wire format: " + wireFormat); + + List parsed = ContentSource.parseAll(wireFormat); + for (ContentSource cs : parsed) { + if (cs instanceof DocumentSource) { + DocumentSource ds = (DocumentSource) cs; + System.out.println(" parseAll -> DocumentSource: page " + ds.getPageNumber() + + ", polygon points: " + (ds.getPolygon() != null ? ds.getPolygon().size() : 0)); + } + // AudioVisualSource would be handled here once the service returns AV sources. + } + } + // END: com.azure.ai.contentunderstanding.advanced.contentsource.parse + + // TODO: AudioVisualContentSource — demonstrate real AudioVisualSource grounding + // from audio/video analysis. The CU service does not currently return AudioVisualSource + // grounding (field.getSources()) for AI-generated audio fields. Once the service supports + // timestamp-level source grounding for audio/video content, implement a method here that: + // 1. Analyzes an audio/video file with a custom analyzer (estimateFieldSourceAndConfidence = true) + // 2. Iterates over fields and casts getSources() elements to AudioVisualSource + // 3. Shows AudioVisualSource.getTime() (Duration) and AudioVisualSource.getBoundingBox() (optional Rectangle) + // 4. Demonstrates ContentSource.parseAll() with AV(...) format strings + // + // Example of AudioVisualSource parsing (SDK-side API works, just no live source data yet): + // + // List avSources = AudioVisualSource.parse("AV(1500);AV(3200)"); + // for (AudioVisualSource avSource : avSources) { + // Duration time = avSource.getTime(); // e.g., PT1.5S (1500 ms) + // Rectangle box = avSource.getBoundingBox(); // null for audio-only + // System.out.println("Timestamp: " + time.toMillis() + " ms, BoundingBox: " + (box != null ? box : "none")); + // } + // + // // With bounding box (e.g., face detection in video): + // List avWithBox = AudioVisualSource.parse("AV(5000,100,200,50,60)"); + // Rectangle bbox = avWithBox.get(0).getBoundingBox(); // Rectangle(x=100, y=200, w=50, h=60) + // + // See AudioVisualSource and ContentSource.parseAll() for the SDK-side API. +} diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/src/samples/java/com/azure/ai/contentunderstanding/samples/Sample_Advanced_ContentSourceAsync.java b/sdk/contentunderstanding/azure-ai-contentunderstanding/src/samples/java/com/azure/ai/contentunderstanding/samples/Sample_Advanced_ContentSourceAsync.java new file mode 100644 index 000000000000..4ab4e0572f7d --- /dev/null +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/src/samples/java/com/azure/ai/contentunderstanding/samples/Sample_Advanced_ContentSourceAsync.java @@ -0,0 +1,184 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.ai.contentunderstanding.samples; + +import com.azure.ai.contentunderstanding.ContentUnderstandingAsyncClient; +import com.azure.ai.contentunderstanding.ContentUnderstandingClientBuilder; +import com.azure.ai.contentunderstanding.models.AnalysisInput; +import com.azure.ai.contentunderstanding.models.AnalysisResult; +import com.azure.ai.contentunderstanding.models.ContentAnalyzerAnalyzeOperationStatus; +import com.azure.ai.contentunderstanding.models.ContentField; +import com.azure.ai.contentunderstanding.models.ContentSource; +import com.azure.ai.contentunderstanding.models.DocumentContent; +import com.azure.ai.contentunderstanding.models.DocumentSource; +import com.azure.ai.contentunderstanding.models.PointF; +import com.azure.ai.contentunderstanding.models.RectangleF; +import com.azure.core.credential.AzureKeyCredential; +import com.azure.core.util.polling.PollerFlux; +import com.azure.identity.DefaultAzureCredentialBuilder; +import reactor.core.publisher.Mono; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * Async version of {@link Sample_Advanced_ContentSource}. Demonstrates how to access + * and use {@link ContentSource} grounding references from analysis results using the + * async client. + * + *

For document/image content, sources are {@link DocumentSource} instances + * with page number, polygon coordinates, and a computed bounding box.

+ * + *

For audio/video content, sources are {@link AudioVisualSource} instances + * with a timestamp and an optional bounding box.

+ */ +public class Sample_Advanced_ContentSourceAsync { + + public static void main(String[] args) throws InterruptedException { + String endpoint = System.getenv("CONTENTUNDERSTANDING_ENDPOINT"); + String key = System.getenv("CONTENTUNDERSTANDING_KEY"); + + ContentUnderstandingClientBuilder builder = new ContentUnderstandingClientBuilder().endpoint(endpoint); + + ContentUnderstandingAsyncClient client; + if (key != null && !key.trim().isEmpty()) { + client = builder.credential(new AzureKeyCredential(key)).buildAsyncClient(); + } else { + client = builder.credential(new DefaultAzureCredentialBuilder().build()).buildAsyncClient(); + } + + // Analyze an invoice once — reuse the result for all demonstrations. + String invoiceUrl + = "https://raw.githubusercontent.com/Azure-Samples/azure-ai-content-understanding-assets/main/document/invoice.pdf"; + + AnalysisInput input = new AnalysisInput(); + input.setUrl(invoiceUrl); + + PollerFlux operation + = client.beginAnalyze("prebuilt-invoice", Arrays.asList(input)); + + CountDownLatch latch = new CountDownLatch(1); + + operation.last() + .flatMap(pollResponse -> { + if (pollResponse.getStatus().isComplete()) { + return pollResponse.getFinalResult(); + } else { + return Mono.error(new RuntimeException( + "Polling completed unsuccessfully with status: " + pollResponse.getStatus())); + } + }) + .doOnNext(result -> { + DocumentContent documentContent = (DocumentContent) result.getContents().get(0); + + // Part 1: Document ContentSource from analysis + documentContentSourceFromAnalysis(documentContent); + + // Part 2: DocumentSource.parse() and ContentSource.parseAll() round-trip + contentSourceParseRoundTrip(documentContent); + }) + .doFinally(signal -> latch.countDown()) + .subscribe(); + + latch.await(5, TimeUnit.MINUTES); + } + + /** + * Analyzes an invoice asynchronously and iterates over field grounding sources, + * casting each to {@link DocumentSource} to access page, polygon, and bounding box. + */ + // BEGIN: com.azure.ai.contentunderstanding.advanced.contentsource.fromanalysis.async + private static void documentContentSourceFromAnalysis(DocumentContent documentContent) { + // Iterate over all fields and access their grounding sources. + for (Map.Entry entry : documentContent.getFields().entrySet()) { + String fieldName = entry.getKey(); + ContentField field = entry.getValue(); + + System.out.println("Field: " + fieldName + " = " + field.getValue()); + + List sources = field.getSources(); + if (sources != null) { + for (ContentSource source : sources) { + if (source instanceof DocumentSource) { + DocumentSource docSource = (DocumentSource) source; + System.out.println(" Source: page " + docSource.getPageNumber()); + + List polygon = docSource.getPolygon(); + String coords = polygon.stream() + .map(p -> String.format("(%.4f,%.4f)", p.getX(), p.getY())) + .collect(Collectors.joining(", ")); + System.out.println(" Polygon: [" + coords + "]"); + + RectangleF bbox = docSource.getBoundingBox(); + System.out.printf(" BoundingBox: x=%.4f, y=%.4f, w=%.4f, h=%.4f%n", + bbox.getX(), bbox.getY(), bbox.getWidth(), bbox.getHeight()); + } + } + } + } + } + // END: com.azure.ai.contentunderstanding.advanced.contentsource.fromanalysis.async + + /** + * Demonstrates the two public parse methods and {@link ContentSource#toRawString(List)}: + *
    + *
  • {@link DocumentSource#parse(String)} — typed method, returns {@code List}
  • + *
  • {@link ContentSource#parseAll(String)} — base-class method, returns {@code List}
  • + *
+ */ + // BEGIN: com.azure.ai.contentunderstanding.advanced.contentsource.parse.async + private static void contentSourceParseRoundTrip(DocumentContent documentContent) { + // --- DocumentSource.parse() — typed method --- + // DocumentSource.parse() is the typed convenience method. It returns List + // directly — no casting needed. Use this when you know the source string contains only D() segments. + ContentField multiSourceField = documentContent.getFields().values().stream() + .filter(f -> f.getSources() != null && f.getSources().size() > 1) + .findFirst() + .orElseThrow(() -> new IllegalStateException("No field with multiple sources found")); + String multiWireFormat = ContentSource.toRawString(multiSourceField.getSources()); + System.out.println("Multi-segment wire format: " + multiWireFormat); + + List docSources = DocumentSource.parse(multiWireFormat); + for (DocumentSource ds : docSources) { + RectangleF bbox = ds.getBoundingBox(); + System.out.printf(" parse -> page %d, bbox: x=%.4f, y=%.4f, w=%.4f, h=%.4f%n", + ds.getPageNumber(), bbox.getX(), bbox.getY(), bbox.getWidth(), bbox.getHeight()); + } + + // --- toRawString + ContentSource.parseAll() round-trip --- + // ContentSource.parseAll() is the base-class method that handles both D() and AV() formats. + // It returns List, so you cast each element to the appropriate subclass. + ContentField fieldWithSource = documentContent.getFields().values().stream() + .filter(f -> f.getSources() != null) + .findFirst() + .orElseThrow(() -> new IllegalStateException("No field with sources found")); + + String wireFormat = ContentSource.toRawString(fieldWithSource.getSources()); + System.out.println("Wire format: " + wireFormat); + + List parsed = ContentSource.parseAll(wireFormat); + for (ContentSource cs : parsed) { + if (cs instanceof DocumentSource) { + DocumentSource ds = (DocumentSource) cs; + System.out.println(" parseAll -> DocumentSource: page " + ds.getPageNumber() + + ", polygon points: " + (ds.getPolygon() != null ? ds.getPolygon().size() : 0)); + } + // AudioVisualSource would be handled here once the service returns AV sources. + } + } + // END: com.azure.ai.contentunderstanding.advanced.contentsource.parse.async + + // TODO: AudioVisualContentSource — demonstrate real AudioVisualSource grounding + // from audio/video analysis. The CU service does not currently return AudioVisualSource + // grounding (field.getSources()) for AI-generated audio fields. Once the service supports + // timestamp-level source grounding for audio/video content, implement a method here that: + // 1. Analyzes an audio/video file with a custom analyzer (estimateFieldSourceAndConfidence = true) + // 2. Iterates over fields and casts getSources() elements to AudioVisualSource + // 3. Shows AudioVisualSource.getTime() (Duration) and AudioVisualSource.getBoundingBox() (optional Rectangle) + // 4. Demonstrates ContentSource.parseAll() with AV(...) format strings +} diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/src/test/java/com/azure/ai/contentunderstanding/tests/samples/Sample_Advanced_ContentSourceAsyncTest.java b/sdk/contentunderstanding/azure-ai-contentunderstanding/src/test/java/com/azure/ai/contentunderstanding/tests/samples/Sample_Advanced_ContentSourceAsyncTest.java new file mode 100644 index 000000000000..43a22c874b0f --- /dev/null +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/src/test/java/com/azure/ai/contentunderstanding/tests/samples/Sample_Advanced_ContentSourceAsyncTest.java @@ -0,0 +1,176 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.ai.contentunderstanding.tests.samples; + +import com.azure.ai.contentunderstanding.models.AnalysisInput; +import com.azure.ai.contentunderstanding.models.AnalysisResult; +import com.azure.ai.contentunderstanding.models.ContentAnalyzerAnalyzeOperationStatus; +import com.azure.ai.contentunderstanding.models.ContentField; +import com.azure.ai.contentunderstanding.models.ContentSource; +import com.azure.ai.contentunderstanding.models.DocumentContent; +import com.azure.ai.contentunderstanding.models.DocumentSource; +import com.azure.ai.contentunderstanding.models.PointF; +import com.azure.ai.contentunderstanding.models.RectangleF; +import com.azure.core.util.polling.PollerFlux; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Async tests for {@link com.azure.ai.contentunderstanding.samples.Sample_Advanced_ContentSourceAsync}. + * Verifies DocumentSource grounding and ContentSource round-trip parsing using the async client. + */ +public class Sample_Advanced_ContentSourceAsyncTest extends ContentUnderstandingClientTestBase { + + @Test + public void testDocumentContentSourceFromAnalysisAsync() { + String invoiceUrl + = "https://raw.githubusercontent.com/Azure-Samples/azure-ai-content-understanding-assets/main/document/invoice.pdf"; + + AnalysisInput input = new AnalysisInput(); + input.setUrl(invoiceUrl); + + PollerFlux operation + = contentUnderstandingAsyncClient.beginAnalyze("prebuilt-invoice", Arrays.asList(input)); + + AnalysisResult result = operation.last().flatMap(pollResponse -> { + if (pollResponse.getStatus().isComplete()) { + return pollResponse.getFinalResult(); + } else { + return Mono.error( + new RuntimeException("Polling completed unsuccessfully with status: " + pollResponse.getStatus())); + } + }).block(); + + assertNotNull(result, "Analysis result should not be null"); + assertNotNull(result.getContents(), "Result should contain contents"); + assertEquals(1, result.getContents().size(), "Invoice should have exactly one content element"); + + assertInstanceOf(DocumentContent.class, result.getContents().get(0), "Content should be DocumentContent"); + DocumentContent documentContent = (DocumentContent) result.getContents().get(0); + assertNotNull(documentContent.getFields(), "Document should have fields"); + + boolean hasDocumentSource = false; + for (Map.Entry entry : documentContent.getFields().entrySet()) { + String fieldName = entry.getKey(); + ContentField field = entry.getValue(); + + System.out.println("Field: " + fieldName + " = " + field.getValue()); + + List sources = field.getSources(); + if (sources != null) { + for (ContentSource source : sources) { + assertInstanceOf(DocumentSource.class, source, + "Sources for document fields should be DocumentSource, got " + + source.getClass().getSimpleName()); + hasDocumentSource = true; + DocumentSource docSource = (DocumentSource) source; + System.out.println(" Source: page " + docSource.getPageNumber()); + assertTrue(docSource.getPageNumber() >= 1, + "Page number should be >= 1, got " + docSource.getPageNumber()); + + List polygon = docSource.getPolygon(); + assertNotNull(polygon, "Polygon should not be null for document sources with coordinates"); + assertTrue(polygon.size() >= 3, "Polygon should have at least 3 points, got " + polygon.size()); + String coords = polygon.stream() + .map(p -> String.format("(%.4f,%.4f)", p.getX(), p.getY())) + .collect(Collectors.joining(", ")); + System.out.println(" Polygon: [" + coords + "]"); + + RectangleF bbox = docSource.getBoundingBox(); + assertNotNull(bbox, "BoundingBox should be computed from polygon"); + assertTrue(bbox.getWidth() > 0, "BoundingBox width should be > 0"); + assertTrue(bbox.getHeight() > 0, "BoundingBox height should be > 0"); + System.out.printf(" BoundingBox: x=%.4f, y=%.4f, w=%.4f, h=%.4f%n", bbox.getX(), bbox.getY(), + bbox.getWidth(), bbox.getHeight()); + } + } + } + assertTrue(hasDocumentSource, "At least one field should have DocumentSource grounding"); + } + + @Test + public void testContentSourceParseRoundTripAsync() { + String invoiceUrl + = "https://raw.githubusercontent.com/Azure-Samples/azure-ai-content-understanding-assets/main/document/invoice.pdf"; + + AnalysisInput input = new AnalysisInput(); + input.setUrl(invoiceUrl); + + PollerFlux operation + = contentUnderstandingAsyncClient.beginAnalyze("prebuilt-invoice", Arrays.asList(input)); + + AnalysisResult result = operation.last().flatMap(pollResponse -> { + if (pollResponse.getStatus().isComplete()) { + return pollResponse.getFinalResult(); + } else { + return Mono.error( + new RuntimeException("Polling completed unsuccessfully with status: " + pollResponse.getStatus())); + } + }).block(); + + DocumentContent documentContent = (DocumentContent) result.getContents().get(0); + + // --- DocumentSource.parse() — typed method for multi-segment --- + ContentField multiSourceField = documentContent.getFields() + .values() + .stream() + .filter(f -> f.getSources() != null && f.getSources().size() > 1) + .findFirst() + .orElseThrow(() -> new AssertionError("No field with multiple sources found")); + String multiWireFormat = ContentSource.toRawString(multiSourceField.getSources()); + System.out.println("Multi-segment wire format: " + multiWireFormat); + + List docSources = DocumentSource.parse(multiWireFormat); + assertEquals(multiSourceField.getSources().size(), docSources.size(), + "DocumentSource.parse() count should match original source count"); + for (DocumentSource ds : docSources) { + assertTrue(ds.getPageNumber() >= 1, "Page number should be >= 1"); + RectangleF bbox = ds.getBoundingBox(); + assertNotNull(bbox, "BoundingBox should not be null"); + assertTrue(bbox.getWidth() > 0, "BoundingBox width should be > 0"); + assertTrue(bbox.getHeight() > 0, "BoundingBox height should be > 0"); + System.out.printf(" parse -> page %d, bbox: x=%.4f, y=%.4f, w=%.4f, h=%.4f%n", ds.getPageNumber(), + bbox.getX(), bbox.getY(), bbox.getWidth(), bbox.getHeight()); + } + + // --- ContentSource.parseAll() round-trip --- + ContentField fieldWithSource = documentContent.getFields() + .values() + .stream() + .filter(f -> f.getSources() != null) + .findFirst() + .orElseThrow(() -> new AssertionError("No field with sources found")); + + String wireFormat = ContentSource.toRawString(fieldWithSource.getSources()); + assertNotNull(wireFormat, "Wire format should not be null"); + assertFalse(wireFormat.isEmpty(), "Wire format should not be empty"); + System.out.println("Wire format: " + wireFormat); + + List parsed = ContentSource.parseAll(wireFormat); + assertNotNull(parsed, "Parsed sources should not be null"); + assertTrue(parsed.size() >= 1, "Parsed should have at least one source"); + + for (ContentSource cs : parsed) { + assertInstanceOf(DocumentSource.class, cs, "Parsed source should be DocumentSource"); + DocumentSource ds = (DocumentSource) cs; + assertTrue(ds.getPageNumber() >= 1, "Page number should be >= 1"); + assertNotNull(ds.getPolygon(), "Polygon should not be null"); + assertTrue(ds.getPolygon().size() >= 3, + "Polygon should have at least 3 points, got " + ds.getPolygon().size()); + System.out + .println(" parseAll -> page " + ds.getPageNumber() + ", polygon points: " + ds.getPolygon().size()); + } + } +} diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/src/test/java/com/azure/ai/contentunderstanding/tests/samples/Sample_Advanced_ContentSourceTest.java b/sdk/contentunderstanding/azure-ai-contentunderstanding/src/test/java/com/azure/ai/contentunderstanding/tests/samples/Sample_Advanced_ContentSourceTest.java new file mode 100644 index 000000000000..fe4f38a80d06 --- /dev/null +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/src/test/java/com/azure/ai/contentunderstanding/tests/samples/Sample_Advanced_ContentSourceTest.java @@ -0,0 +1,161 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.ai.contentunderstanding.tests.samples; + +import com.azure.ai.contentunderstanding.models.AnalysisInput; +import com.azure.ai.contentunderstanding.models.AnalysisResult; +import com.azure.ai.contentunderstanding.models.ContentAnalyzerAnalyzeOperationStatus; +import com.azure.ai.contentunderstanding.models.ContentField; +import com.azure.ai.contentunderstanding.models.ContentSource; +import com.azure.ai.contentunderstanding.models.DocumentContent; +import com.azure.ai.contentunderstanding.models.DocumentSource; +import com.azure.ai.contentunderstanding.models.PointF; +import com.azure.ai.contentunderstanding.models.RectangleF; +import com.azure.core.util.polling.SyncPoller; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Tests for {@link com.azure.ai.contentunderstanding.samples.Sample_Advanced_ContentSource}. + * Verifies DocumentSource grounding and ContentSource round-trip parsing. + */ +public class Sample_Advanced_ContentSourceTest extends ContentUnderstandingClientTestBase { + + @Test + public void testDocumentContentSourceFromAnalysis() { + // Analyze an invoice to get fields with grounding sources. + String invoiceUrl + = "https://raw.githubusercontent.com/Azure-Samples/azure-ai-content-understanding-assets/main/document/invoice.pdf"; + + AnalysisInput input = new AnalysisInput(); + input.setUrl(invoiceUrl); + + SyncPoller operation + = contentUnderstandingClient.beginAnalyze("prebuilt-invoice", Arrays.asList(input)); + + AnalysisResult result = operation.getFinalResult(); + assertNotNull(result, "Analysis result should not be null"); + assertNotNull(result.getContents(), "Result should contain contents"); + assertEquals(1, result.getContents().size(), "Invoice should have exactly one content element"); + + assertInstanceOf(DocumentContent.class, result.getContents().get(0), "Content should be DocumentContent"); + DocumentContent documentContent = (DocumentContent) result.getContents().get(0); + assertNotNull(documentContent.getFields(), "Document should have fields"); + + boolean hasDocumentSource = false; + for (Map.Entry entry : documentContent.getFields().entrySet()) { + String fieldName = entry.getKey(); + ContentField field = entry.getValue(); + + System.out.println("Field: " + fieldName + " = " + field.getValue()); + + List sources = field.getSources(); + if (sources != null) { + for (ContentSource source : sources) { + assertInstanceOf(DocumentSource.class, source, + "Sources for document fields should be DocumentSource, got " + + source.getClass().getSimpleName()); + hasDocumentSource = true; + DocumentSource docSource = (DocumentSource) source; + System.out.println(" Source: page " + docSource.getPageNumber()); + assertTrue(docSource.getPageNumber() >= 1, + "Page number should be >= 1, got " + docSource.getPageNumber()); + + List polygon = docSource.getPolygon(); + assertNotNull(polygon, "Polygon should not be null for document sources with coordinates"); + assertTrue(polygon.size() >= 3, "Polygon should have at least 3 points, got " + polygon.size()); + String coords = polygon.stream() + .map(p -> String.format("(%.4f,%.4f)", p.getX(), p.getY())) + .collect(Collectors.joining(", ")); + System.out.println(" Polygon: [" + coords + "]"); + + RectangleF bbox = docSource.getBoundingBox(); + assertNotNull(bbox, "BoundingBox should be computed from polygon"); + assertTrue(bbox.getWidth() > 0, "BoundingBox width should be > 0"); + assertTrue(bbox.getHeight() > 0, "BoundingBox height should be > 0"); + System.out.printf(" BoundingBox: x=%.4f, y=%.4f, w=%.4f, h=%.4f%n", bbox.getX(), bbox.getY(), + bbox.getWidth(), bbox.getHeight()); + } + } + } + assertTrue(hasDocumentSource, "At least one field should have DocumentSource grounding"); + } + + @Test + public void testContentSourceParseRoundTrip() { + // Analyze an invoice to get a field with grounding sources. + String invoiceUrl + = "https://raw.githubusercontent.com/Azure-Samples/azure-ai-content-understanding-assets/main/document/invoice.pdf"; + + AnalysisInput input = new AnalysisInput(); + input.setUrl(invoiceUrl); + + SyncPoller operation + = contentUnderstandingClient.beginAnalyze("prebuilt-invoice", Arrays.asList(input)); + + AnalysisResult result = operation.getFinalResult(); + DocumentContent documentContent = (DocumentContent) result.getContents().get(0); + + // --- DocumentSource.parse() — typed method for multi-segment --- + ContentField multiSourceField = documentContent.getFields() + .values() + .stream() + .filter(f -> f.getSources() != null && f.getSources().size() > 1) + .findFirst() + .orElseThrow(() -> new AssertionError("No field with multiple sources found")); + String multiWireFormat = ContentSource.toRawString(multiSourceField.getSources()); + System.out.println("Multi-segment wire format: " + multiWireFormat); + + List docSources = DocumentSource.parse(multiWireFormat); + assertEquals(multiSourceField.getSources().size(), docSources.size(), + "DocumentSource.parse() count should match original source count"); + for (DocumentSource ds : docSources) { + assertTrue(ds.getPageNumber() >= 1, "Page number should be >= 1"); + RectangleF bbox = ds.getBoundingBox(); + assertNotNull(bbox, "BoundingBox should not be null"); + assertTrue(bbox.getWidth() > 0, "BoundingBox width should be > 0"); + assertTrue(bbox.getHeight() > 0, "BoundingBox height should be > 0"); + System.out.printf(" parse -> page %d, bbox: x=%.4f, y=%.4f, w=%.4f, h=%.4f%n", ds.getPageNumber(), + bbox.getX(), bbox.getY(), bbox.getWidth(), bbox.getHeight()); + } + + // --- ContentSource.parseAll() round-trip --- + ContentField fieldWithSource = documentContent.getFields() + .values() + .stream() + .filter(f -> f.getSources() != null) + .findFirst() + .orElseThrow(() -> new AssertionError("No field with sources found")); + + String wireFormat = ContentSource.toRawString(fieldWithSource.getSources()); + assertNotNull(wireFormat, "Wire format should not be null"); + assertFalse(wireFormat.isEmpty(), "Wire format should not be empty"); + System.out.println("Wire format: " + wireFormat); + + List parsed = ContentSource.parseAll(wireFormat); + assertNotNull(parsed, "Parsed sources should not be null"); + assertTrue(parsed.size() >= 1, "Parsed should have at least one source"); + + for (ContentSource cs : parsed) { + assertInstanceOf(DocumentSource.class, cs, "Parsed source should be DocumentSource"); + DocumentSource ds = (DocumentSource) cs; + assertTrue(ds.getPageNumber() >= 1, "Page number should be >= 1"); + assertNotNull(ds.getPolygon(), "Polygon should not be null"); + assertTrue(ds.getPolygon().size() >= 3, + "Polygon should have at least 3 points, got " + ds.getPolygon().size()); + System.out + .println(" parseAll -> page " + ds.getPageNumber() + ", polygon points: " + ds.getPolygon().size()); + } + } +}