Skill: zwcih/azure-content-layout (version 1.3.0)

--- name: azure-content-layout description: Extract document structure, text, tables, and figures from documents using Azure Content Understanding prebuilt-layout analyzer. Converts PDF, images, Office docs into Markdown and structured JSON. Use when asked to extract text from documents, convert PDF/images to markdown, perform OCR, analyze document layout, or extract tables from files. Requires AZURE_CU_ENDPOINT and AZURE_CU_API_KEY environment variables. metadata: openclaw: requires: env: - AZURE_CU_ENDPOINT - AZURE_CU_API_KEY - AZURE_CU_API_VERSION primaryEnv: AZURE_CU_API_KEY --- # Azure Content Understanding — Layout Analyzer Extract structured content from documents using Azure's prebuilt-layout analyzer. Outputs Markdown and structured JSON with text, tables, figures, and document hierarchy. ## Setup Set environment variables: ```bash export AZURE_CU_ENDPOINT="https://YOUR_RESOURCE.services.ai.azure.com/" export AZURE_CU_API_KEY="YOUR_KEY_HERE" ``` Optional: set API version (defaults to `2025-05-01-preview`): ```bash export AZURE_CU_API_VERSION="2025-11-01" ``` ## Quick Usage ### Analyze a URL and print Markdown ```bash node scripts/analyze.mjs --url "https://example.com/document.pdf" ``` ### Analyze a local file (pipe via stdin) ```bash cat invoice.pdf | node scripts/analyze.mjs --stdin --markdown output.md --output result.json ``` ### Save both Markdown and full JSON ```bash node scripts/analyze.mjs --url "https://example.com/report.pdf" \ --markdown report.md \ --output report.json ``` ## Direct API Call When the script isn't available, use curl: ```bash # Submit analysis (preview API) curl -s -X POST "$AZURE_CU_ENDPOINT/contentunderstanding/analyzers/prebuilt-layout:analyze?api-version=2025-05-01-preview" \ -H "Ocp-Apim-Subscription-Key: $AZURE_CU_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url":"https://example.com/doc.pdf"}' # Response includes Operation-Location header — poll that URL for results ``` For GA API (2025-11-01), the body format changes: ```json {"inputs": [{"url": "https://example.com/doc.pdf"}]} ``` ## Output ### Markdown The analyzer produces GitHub Flavored Markdown preserving: - **Headings** (h1–h6) - **Tables** (as HTML `<table>` blocks) - **Selection marks** (☒ checked, ☐ unchecked) - **Figures** (with references) - **Paragraphs** with reading order ### Structured JSON The full result includes detailed per-element data: - `pages` — dimensions, word/line counts per page - `paragraphs` — text blocks with bounding regions and semantic roles - `tables` — cells with row/column spans - `figures` — detected images/charts with bounding regions - `sections` — hierarchical document structure ## Supported Formats PDF, JPEG, PNG, BMP, TIFF, HEIF, DOCX, XLSX, PPTX, HTML ## Best Practices - **Async operation** — the API returns 202; poll `Operation-Location` for results - **Poll interval** — 3 seconds is reasonable; results typically arrive in 5–60 seconds - **Large documents** — up to 2,000 pages supported; processing time scales linearly - **File upload** — use `Content-Type: application/octet-stream` with binary body - **Tables** — rendered as HTML in markdown for complex layouts (merged cells, etc.) ## API Reference See [references/api.md](references/api.md) for full request/response details.

path: references/api.md
content: # Azure Content Understanding Layout API Reference

## Endpoint

```
POST {endpoint}/contentunderstanding/analyzers/prebuilt-layout:analyze?api-version={version}
```

## API Versions

| Version | Status | Request Body Format |
|---------|--------|-------------------|
| `2025-05-01-preview` | Preview | `{"url": "..."}` |
| `2025-11-01` | GA | `{"inputs": [{"url": "..."}]}` |

## Authentication

```
Ocp-Apim-Subscription-Key: YOUR_KEY_HERE
```

## Request (URL source)

**Preview (2025-05-01-preview):**
```json
{"url": "https://example.com/document.pdf"}
```

**GA (2025-11-01):**
```json
{"inputs": [{"url": "https://example.com/document.pdf"}]}
```

## Request (File upload)

```
Content-Type: application/octet-stream
Body: <binary file data>
```

## Response Flow

1. **POST** returns `202 Accepted` with `Operation-Location` header
2. **GET** the `Operation-Location` URL to poll for results
3. Status transitions: `Running` → `Succeeded` / `Failed`

## Result Structure

```json
{
  "id": "...",
  "status": "Succeeded",
  "result": {
    "analyzerId": "prebuilt-layout",
    "contents": [{
      "markdown": "# Document Title\n\nExtracted content...",
      "kind": "document",
      "pages": [...],
      "paragraphs": [...],
      "tables": [...],
      "figures": [...],
      "sections": [...]
    }]
  }
}
```

## Key Output Fields

- **markdown** — Full document content as GitHub Flavored Markdown (headings, tables as HTML, checkboxes as ☒/☐)
- **pages** — Per-page metadata (dimensions, word count)
- **paragraphs** — Individual text blocks with bounding regions and roles
- **tables** — Structured table data with cells, spans, and content
- **figures** — Detected figures/images with bounding regions
- **sections** — Hierarchical document structure

## Supported File Types

PDF, JPEG, PNG, BMP, TIFF, HEIF, DOCX, XLSX, PPTX, HTML

## Limits

- Max file size: 500 MB (paid tier), 4 MB (free tier)
- Max pages: 2,000 per document
- Image dimensions: 50×50 to 10,000×10,000 pixels
- Processing time: typically 5–60 seconds depending on document size


path: scripts/analyze.mjs
content: #!/usr/bin/env node
/**
 * Azure Content Understanding - Layout Analyzer
 *
 * Extracts document structure as Markdown and structured JSON using
 * Azure Content Understanding prebuilt-layout analyzer.
 *
 * Usage:
 *   node analyze.mjs --url "https://example.com/doc.pdf" [--output result.json] [--markdown result.md]
 *   cat document.pdf | node analyze.mjs --stdin [--output result.json] [--markdown result.md]
 *
 * Environment:
 *   AZURE_CU_ENDPOINT  - Azure Content Understanding endpoint (e.g. https://xxx.services.ai.azure.com/)
 *   AZURE_CU_API_KEY   - Subscription key (Ocp-Apim-Subscription-Key)
 *   AZURE_CU_API_VERSION - API version (default: 2025-05-01-preview)
 */

import { writeFileSync } from "fs";
import { argv, env, exit, stdin } from "process";

const API_VERSION = env.AZURE_CU_API_VERSION || "2025-05-01-preview";
const POLL_INTERVAL_MS = 3000;
const MAX_POLL_ATTEMPTS = 120; // 6 minutes max

function parseArgs() {
  const args = {};
  for (let i = 2; i < argv.length; i++) {
    if (argv[i] === "--url" && argv[i + 1]) args.url = argv[++i];
    else if (argv[i] === "--stdin") args.stdin = true;
    else if (argv[i] === "--output" && argv[i + 1]) args.output = argv[++i];
    else if (argv[i] === "--markdown" && argv[i + 1]) args.markdown = argv[++i];
    else if (argv[i] === "--api-version" && argv[i + 1]) args.apiVersion = argv[++i];
  }
  return args;
}

function sleep(ms) {
  return new Promise((r) => setTimeout(r, ms));
}

function readStdin() {
  return new Promise((resolve, reject) => {
    const chunks = [];
    stdin.on("data", (chunk) => chunks.push(chunk));
    stdin.on("end", () => resolve(Buffer.concat(chunks)));
    stdin.on("error", reject);
  });
}

async function submitAnalysis(endpoint, apiKey, apiVersion, source) {
  const analyzeUrl = `${endpoint.replace(/\/+$/, "")}/contentunderstanding/analyzers/prebuilt-layout:analyze?api-version=${apiVersion}`;

  let res;
  if (source.url) {
    // URL-based: body format depends on API version
    const body = apiVersion >= "2025-11-01"
      ? { inputs: [{ url: source.url }] }
      : { url: source.url };

    res = await fetch(analyzeUrl, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Ocp-Apim-Subscription-Key": apiKey,
      },
      body: JSON.stringify(body),
    });
  } else {
    // Binary data from stdin
    res = await fetch(analyzeUrl, {
      method: "POST",
      headers: {
        "Content-Type": "application/octet-stream",
        "Ocp-Apim-Subscription-Key": apiKey,
      },
      body: source.data,
    });
  }

  if (res.status !== 202) {
    const text = await res.text();
    console.error(`Submit failed (${res.status}): ${text}`);
    exit(1);
  }

  const operationLocation = res.headers.get("operation-location");
  const body = await res.json();
  return { operationLocation, id: body.id };
}

async function pollResult(operationLocation, apiKey) {
  for (let i = 0; i < MAX_POLL_ATTEMPTS; i++) {
    await sleep(POLL_INTERVAL_MS);
    const res = await fetch(operationLocation, {
      headers: { "Ocp-Apim-Subscription-Key": apiKey },
    });
    const data = await res.json();

    if (data.status === "Succeeded") return data;
    if (data.status === "Failed") {
      console.error("Analysis failed:", JSON.stringify(data, null, 2));
      exit(1);
    }
    process.stderr.write(".");
  }
  console.error("\nTimeout waiting for analysis result");
  exit(1);
}

async function main() {
  const args = parseArgs();
  const endpoint = env.AZURE_CU_ENDPOINT;
  const [REDACTED_SECRET];
  const apiVersion = args.apiVersion || API_VERSION;

  if (!endpoint || !apiKey) {
    console.error("Error: Set AZURE_CU_ENDPOINT and AZURE_CU_API_KEY environment variables");
    exit(1);
  }
  if (!args.url && !args.stdin) {
    console.error("Error: --url or --stdin is required");
    console.error("  node analyze.mjs --url \"https://example.com/doc.pdf\"");
    console.error("  cat doc.pdf | node analyze.mjs --stdin");
    exit(1);
  }

  let source;
  if (args.url) {
    source = { url: args.url };
    console.error(`Analyzing: ${args.url}`);
  } else {
    console.error("Reading from stdin...");
    const data = await readStdin();
    source = { data };
    console.error(`Analyzing: stdin (${data.length} bytes)`);
  }

  const { operationLocation } = await submitAnalysis(endpoint, apiKey, apiVersion, source);
  console.error("Waiting for result");
  const result = await pollResult(operationLocation, apiKey);
  console.error("\nDone!");

  // Extract markdown from first content
  const content = result.result?.contents?.[0];
  const markdown = content?.markdown || "";

  // Save outputs
  if (args.markdown) {
    writeFileSync(args.markdown, markdown);
    console.error(`Markdown saved: ${args.markdown} (${markdown.length} chars)`);
  }

  if (args.output) {
    writeFileSync(args.output, JSON.stringify(result, null, 2));
    console.error(`Full JSON saved: ${args.output}`);
  }

  // If no output files specified, print markdown to stdout
  if (!args.markdown && !args.output) {
    console.log(markdown);
  }
}

main().catch((e) => {
  console.error(e);
  exit(1);
});


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)