# Images and media references

Handle portable media identities and decoded RGBA images.

Portable references are `{assetId,versionId,kind}`; a reference is not an absolute
file path. Current pure image operations require a decoded fixture/preview image:
`{width,height,data:[R,G,B,A,...]}` with 0–255 integer channels, up to 1 megapixel.
Large production media must use a host media adapter; this SDK does not pretend
to transcode an arbitrary asset reference. Line thickening uses a square dilation
kernel and thresholded luminance; it returns a new RGBA image.

Connected Project Type previews include a small, temporary media bridge. If an
image-producing Block returns pixels and the next input accepts an asset,
StillMade encodes a real PNG data URL in memory and supplies a preview media
reference. If a later image input receives that same exact generated reference,
the bridge supplies copied pixels to image-processing code. Original pixel
outputs remain visible as pixel previews; direct image-to-image connections do
not need this conversion.

This behavior is shared by the browser's connected preview, CLI `preview`, and
Project Type admission tests. It does not upload media, fetch external URLs, or
create project assets. Only references generated by that preview session can be
hydrated; changing their kind, asset/version identity, URL or semantic role fails.
Optional dimensions and labels may be omitted or changed; the private pixel copy
remains authoritative. Other references remain
inert references for the caller's supported host runner. Encoding and private
cache costs share the existing 4 MB sample budget and preview deadline, so use
small fixtures. Production execution uses its normal authorized media storage
and decoding path instead of these temporary references.

## Saved media collections
Use an `asset[]`, `audio[]` or `video[]` input to collect original media with
standard controls. Inside a project, these controls upload, select, order and
remove files without custom host code. Project uploads are saved before the
input is published, with exact per-file receipts bound to the Block version.
Compatible direct remixes retain uploaded media. Builder previews use temporary
local files; upload in a project for durable use. Project controls admit up to
64 list entries and 50 MiB per original file, subject to the shared format table.
The sandbox receives typed references, never upload credentials or local paths.

[Import media 2.0.0](/block-sdk/examples/import-media.stillmade-block) is a complete
portable example. Its owned JavaScript preserves metadata and selection order,
optionally filters media kinds or removes exact duplicate references, and returns
an `asset[]` output for subsequent Blocks. It does not transcode media or claim
rights over user files. Its license covers the original Block implementation.
Empty selections return an empty list. The native legacy upload node remains
available to old placements; new SDK placements use the standard sandbox.

Validate and test the downloaded archive with the same public CLI:

```sh
node packages/block-cli/cli.js validate import-media.stillmade-block
node packages/block-cli/cli.js test import-media.stillmade-block
node packages/block-cli/cli.js remix import-media.stillmade-block my-import.stillmade-block
node packages/block-cli/cli.js test my-import.stillmade-block
```

### Mandatory host-controlled exports

Blocks return typed results or `file-bundle` outputs for StillMade to deliver.
Do not create download links, invoke save-file pickers, open export destinations,
or implement a separate export/paywall/referral bypass. The upload admission
report requires **Host-controlled exports** (`host-export-1`), alongside existing
sandbox and interface checks. A passing static check is not a delivery receipt.
StillMade owns the current export policy for all creators; it may add a referral
step, checkout or other requirement without changing your Block. It can also
require the StillMade watermark on exports from accounts without a paid plan;
StillMade draws it while rendering videos and delivering images, so do not add
your own. Block pricing and export access are separate. Never claim payment or referral completion from
Block code. Native integrations must declare the host export service and route
delivery through it; an extracted source folder does not satisfy this boundary.

## Image-processing example
```json
{
  "manifest": {
    "schemaVersion": 1,
    "license": "MIT",
    "provenance": {
      "notice": "MIT License\n\nCopyright (c) 2026 StillMade SDK contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and \nassociated documentation files (the \"Software\"), to deal in the Software without restriction, including \nwithout limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell \ncopies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the \nfollowing conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial \nportions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT \nLIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO \nEVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER \nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE \nUSE OR OTHER DEALINGS IN THE SOFTWARE.\n"
    },
    "sdkVersion": "0.1.0",
    "id": "example.thicken-lines",
    "version": "1.0.1",
    "name": "Thicken line art",
    "description": "Expands dark outlines in an image while preserving surrounding colors, creating a new image for the next production step.",
    "kind": "task",
    "runtime": "recipe",
    "inputs": {
      "image": {
        "type": "image",
        "primary": true,
        "semantic": "source_image",
        "required": true,
        "description": "Line-art image whose dark outlines should be expanded."
      },
      "thickness": {
        "type": "integer",
        "default": 2,
        "min": 1,
        "max": 8,
        "semantic": "line_thickness",
        "required": false,
        "description": "Number of pixels by which to expand dark outlines."
      },
      "threshold": {
        "type": "number",
        "default": 100,
        "min": 0,
        "max": 255,
        "semantic": "darkness_threshold",
        "required": false,
        "description": "Brightness threshold used to identify dark outline pixels."
      }
    },
    "outputs": {
      "image": {
        "type": "image",
        "primary": true,
        "semantic": "processed_image",
        "description": "A new image with expanded dark outlines."
      }
    },
    "permissions": {
      "project": [],
      "network": [],
      "filesystem": [],
      "secrets": []
    },
    "ui": [
      {
        "control": "asset",
        "port": "image"
      },
      {
        "control": "slider",
        "port": "thickness"
      },
      {
        "control": "slider",
        "port": "threshold"
      }
    ]
  },
  "recipe": {
    "schemaVersion": 1,
    "steps": [
      {
        "id": "outlines",
        "op": "image.thicken-lines",
        "args": {
          "image": {
            "$input": "image"
          },
          "thickness": {
            "$input": "thickness"
          },
          "threshold": {
            "$input": "threshold"
          }
        }
      }
    ],
    "outputs": {
      "image": {
        "$step": "outlines"
      }
    }
  },
  "tests": [
    {
      "name": "One dark point expands to a 3×3 square",
      "input": {
        "image": {
          "width": 5,
          "height": 5,
          "data": [
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            0,
            0,
            0,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255
          ]
        },
        "thickness": 1
      },
      "expected": {
        "image": {
          "width": 5,
          "height": 5,
          "data": [
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            0,
            0,
            0,
            255,
            0,
            0,
            0,
            255,
            0,
            0,
            0,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            0,
            0,
            0,
            255,
            0,
            0,
            0,
            255,
            0,
            0,
            0,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            0,
            0,
            0,
            255,
            0,
            0,
            0,
            255,
            0,
            0,
            0,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255,
            255
          ]
        }
      }
    }
  ]
}
```

## Host boundary
The host resolves a selected image into RGBA before execution and stores accepted output as a new media version. A guest receives no filesystem path or network access. Large files require an explicit host-side conversion or resized copy.

See [batch execution](/docs/project-types/batches) for whole-scene and project-wide processing.
