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());
+ }
+ }
+}