> ## Documentation Index
> Fetch the complete documentation index at: https://docs.eigenpal.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Crop Regions

> Render a PDF or image and crop normalized bounding boxes into JPEG run output artifacts.

`transform.crop-regions` rasterizes a PDF or image, crops each normalized
bounding box you supply, and stores every crop as a JPEG run output file. Use it
when downstream steps need image artifacts — figure chips, table snippets, or
any layout-driven region — without baking figure semantics into the step itself.

Regions are plain data: `{ id?, pageIndex, bbox }` with `bbox` as normalized
`[x0, y0, x1, y1]` on the rendered page (origin top-left). Build the list with
[`transform.script`](/steps/transform/script) from [`ai.parse`](/steps/ai/parse)
layout output, or pass any workflow-authored array.

Typical preprocessing chain for parsed figures:

```yaml theme={null}
steps:
  - name: parse
    type: ai.parse
    with:
      input: "{{ input.document }}"
      parseMode: ocr

  - name: list-figures
    type: transform.script
    with:
      inputs:
        parsed: "{{ steps.parse.output }}"
      function: |
        function script(parsed: { pages: { pageIndex: number; width?: number; height?: number; layoutElements?: { type?: string; figureId?: string; boundingRegion?: { polygon?: { x?: number; y?: number }[]; unit?: string; pageIndex?: number } } }[] }[] }): { regions: { id: string; pageIndex: number; bbox: [number, number, number, number] }[] } {
          function normalizeBbox(
            xs: number[],
            ys: number[],
            unit: string | undefined,
            pageWidth: number,
            pageHeight: number
          ): [number, number, number, number] | null {
            if (!xs.length || pageWidth <= 0 || pageHeight <= 0) return null;
            const x0 = Math.min(...xs);
            const y0 = Math.min(...ys);
            const x1 = Math.max(...xs);
            const y1 = Math.max(...ys);
            if (unit === "normalized" || unit === undefined) {
              return [x0, y0, x1, y1];
            }
            return [x0 / pageWidth, y0 / pageHeight, x1 / pageWidth, y1 / pageHeight];
          }

          const regions: { id: string; pageIndex: number; bbox: [number, number, number, number] }[] = [];
          for (const page of parsed.pages ?? []) {
            const pageWidth = page.width ?? 0;
            const pageHeight = page.height ?? 0;
            for (const el of page.layoutElements ?? []) {
              if (el.type !== "figure") continue;
              const poly = el.boundingRegion?.polygon ?? [];
              const xs = poly.map((p) => p.x ?? 0);
              const ys = poly.map((p) => p.y ?? 0);
              const bbox = normalizeBbox(
                xs,
                ys,
                el.boundingRegion?.unit,
                pageWidth,
                pageHeight
              );
              if (!bbox) continue;
              regions.push({
                id: el.figureId ?? `fig${regions.length + 1}`,
                pageIndex: el.boundingRegion?.pageIndex ?? page.pageIndex,
                bbox,
              });
            }
          }
          return { regions };
        }

  - name: crop-figures
    type: transform.crop-regions
    with:
      input: "{{ input.document }}"
      regions: "{{ steps.list-figures.output.regions }}"
```

Each crop in `steps.crop-figures.output.regions[]` includes `fileId`, `filename`,
and pixel dimensions. Caption crops with [`ai.extract`](/steps/ai/extract) inside
`control.parallel_map`, or inspect full pages with [`ai.vision`](/steps/ai/vision).

## Configuration

Configuration goes inside the step's `with:` block.

<ParamField path="input" type="string" required>
  File input, template expression e.g. \{\{ input.document }} resolving to a PDF or image
</ParamField>

<ParamField path="regions" type="string | array<object>" required>
  Regions to crop, pageIndex + normalized bbox per entry
</ParamField>

<ParamField path="renderScale" type="number" default="1">
  Scale factor when rasterizing PDF pages before cropping
</ParamField>

<ParamField path="imageQuality" type="integer" default="85">
  JPEG quality for cropped outputs
</ParamField>

<ParamField path="paddingFrac" type="number" default="0.02">
  Padding around each bbox as a fraction of the shorter page edge
</ParamField>

<ParamField path="minCropPx" type="integer" default="8">
  Minimum crop width/height in pixels; smaller crops fall back to the full page
</ParamField>

<ParamField path="maxRegions" type="integer" default="100">
  Maximum regions processed per invocation
</ParamField>

## Output

<ResponseField path="regions" type="array<object>" required>
  Successfully cropped regions

  <Expandable title="regions properties">
    <ResponseField path="id" type="string" required>
      Region id (from input or auto-generated)
    </ResponseField>

    <ResponseField path="pageIndex" type="integer" required>
      0-based page index the crop was taken from
    </ResponseField>

    <ResponseField path="bbox" type="array<number>" required>
      Normalized bbox that was cropped
    </ResponseField>

    <ResponseField path="width" type="integer" required>
      Crop width in pixels
    </ResponseField>

    <ResponseField path="height" type="integer" required>
      Crop height in pixels
    </ResponseField>

    <ResponseField path="fileId" type="string" required>
      Run output file id for the JPEG crop
    </ResponseField>

    <ResponseField path="filename" type="string" required>
      Stored crop filename
    </ResponseField>

    <ResponseField path="mimeType" type="string" required>
      Always image/jpeg
    </ResponseField>

    <ResponseField path="size" type="integer" required>
      JPEG size in bytes
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField path="skipped" type="integer">
  Regions skipped (invalid bbox or missing page)
</ResponseField>
