NIP-XX-responsive-images.md raw

NIP-XX: Responsive Image Variants

draft optional

This NIP extends NIP-94 (File Metadata) to support multiple resolution variants of an image, enabling bandwidth-efficient responsive image delivery while preserving content-addressed integrity via Blossom.

Motivation

Modern devices have vastly different display capabilities:

Currently, Nostr clients either:

  1. Serve full-resolution images to all devices (wasteful)
  2. Rely on server-side transforms that break content addressing
  3. Use a single thumbnail that looks poor on larger displays

Additionally, images from cameras contain EXIF metadata that may leak:

This NIP enables clients to:

Specification

Binding Event (Kind 1063)

A responsive image set is represented by a kind 1063 event (per NIP-94) containing multiple imeta tags, one per variant. This follows the pattern established by NIP-71 for video variants.

Each imeta tag MUST include:

The variant field identifies the resolution category:

VariantTarget WidthUse Case
thumb128pxPreviews, galleries, feed thumbnails
mobile-sm512pxSmall mobile portrait
mobile-lg1024pxLarge mobile, small tablets
desktop-sm1536pxLaptops
desktop-md2048pxStandard desktops
desktop-lg2560pxLarge/HiDPI displays
originalnativeFull resolution, EXIF stripped

Variant Generation Rules

  1. No upscaling: Only generate variants smaller than the original image width
  2. Preserve aspect ratio: Scale height proportionally to maintain aspect ratio
  3. Preserve format: Output format MUST match input (JPEG→JPEG, PNG→PNG)
  4. Strip metadata: Remove all EXIF, IPTC, and XMP metadata from all variants
  5. Blurhash: The thumb variant SHOULD include a blurhash for placeholder display

Quality Settings

Recommended JPEG quality settings per variant:

For PNG images, use maximum compression without quality loss.

Variant Selection Rule

Clients SHOULD use next-larger selection: pick the smallest variant >= target width.

targetWidth = containerWidth * devicePixelRatio
selectedVariant = smallest variant where variant.width >= targetWidth

This ensures the client only needs to downscale slightly (or not at all), rather than upscaling which would cause blur. If no variant is large enough, use the largest available.

Event Structure

Example: Large Original (4032x3024)

All variants generated:

{
  "kind": 1063,
  "pubkey": "<publisher-pubkey>",
  "created_at": 1234567890,
  "content": "Sunset over the mountains",
  "tags": [
    ["imeta",
      "url https://blossom.example.com/abc123def456.jpg",
      "x abc123def456789...",
      "m image/jpeg",
      "dim 4032x3024",
      "variant original"
    ],
    ["imeta",
      "url https://blossom.example.com/def456abc789.jpg",
      "x def456abc789012...",
      "m image/jpeg",
      "dim 2560x1920",
      "variant desktop-lg"
    ],
    ["imeta",
      "url https://blossom.example.com/789abc123def.jpg",
      "x 789abc123def345...",
      "m image/jpeg",
      "dim 2048x1536",
      "variant desktop-md"
    ],
    ["imeta",
      "url https://blossom.example.com/012def456abc.jpg",
      "x 012def456abc678...",
      "m image/jpeg",
      "dim 1536x1152",
      "variant desktop-sm"
    ],
    ["imeta",
      "url https://blossom.example.com/234abc567def.jpg",
      "x 234abc567def890...",
      "m image/jpeg",
      "dim 1024x768",
      "variant mobile-lg"
    ],
    ["imeta",
      "url https://blossom.example.com/345abc789def.jpg",
      "x 345abc789def901...",
      "m image/jpeg",
      "dim 512x384",
      "variant mobile-sm"
    ],
    ["imeta",
      "url https://blossom.example.com/678def012abc.jpg",
      "x 678def012abc234...",
      "m image/jpeg",
      "dim 128x96",
      "variant thumb",
      "blurhash eVF$^OI:${M{o#*0-nNFxakD"
    ],
    ["x", "abc123def456789..."],
    ["x", "def456abc789012..."],
    ["x", "789abc123def345..."],
    ["x", "012def456abc678..."],
    ["x", "234abc567def890..."],
    ["x", "345abc789def901..."],
    ["x", "678def012abc234..."]
  ],
  "id": "<event-id>",
  "sig": "<signature>"
}

Note: The separate x tags duplicate the hashes from the imeta tags. This redundancy enables standard NIP-01 tag queries (#x) to discover the binding event by any variant hash, while the imeta tags provide the full metadata for each variant.

Example: Smaller Original (1200x900)

Only smaller variants generated (no desktop variants):

{
  "kind": 1063,
  "pubkey": "<publisher-pubkey>",
  "created_at": 1234567890,
  "content": "Quick snapshot",
  "tags": [
    ["imeta",
      "url https://blossom.example.com/small123.jpg",
      "x small123456789...",
      "m image/jpeg",
      "dim 1200x900",
      "variant original"
    ],
    ["imeta",
      "url https://blossom.example.com/small456.jpg",
      "x small456789012...",
      "m image/jpeg",
      "dim 1024x768",
      "variant mobile-lg"
    ],
    ["imeta",
      "url https://blossom.example.com/small789.jpg",
      "x small789012345...",
      "m image/jpeg",
      "dim 512x384",
      "variant mobile-sm"
    ],
    ["imeta",
      "url https://blossom.example.com/small012.jpg",
      "x small012345678...",
      "m image/jpeg",
      "dim 128x96",
      "variant thumb",
      "blurhash eVF$^OI:${M{o#*0"
    ]
  ],
  "id": "<event-id>",
  "sig": "<signature>"
}

Client Behavior

Publishing Client

  1. Load the image file and extract pixel data (discarding EXIF)
  2. Determine which variants to generate based on original dimensions
  3. Generate each variant using canvas-based scaling
  4. Upload each variant to Blossom server(s)
  5. Collect SHA-256 hashes and URLs for each uploaded blob
  6. Publish kind 1063 event with all imeta tags
  7. Reference the binding event in notes (via e tag or URL)

Consuming Client

  1. Fetch kind 1063 event by event ID or by querying for the x hash
  2. Parse imeta tags to extract available variants
  3. Select appropriate variant based on:

- Current viewport width - Device pixel ratio - Network conditions (optional)

  1. Display blurhash placeholder while loading
  2. Load selected variant's URL
  3. On load failure, fall back to next larger variant
  4. Verify SHA-256 hash matches x tag (optional but recommended)

Variant Selection Algorithm

function selectVariant(variants, viewportWidth, pixelRatio = 1):
    targetWidth = viewportWidth * pixelRatio

    # Sort variants by width ascending
    sorted = variants.sortBy(v => v.width)

    # Find smallest variant >= target width
    for variant in sorted:
        if variant.width >= targetWidth:
            return variant

    # If none large enough, return largest available
    return sorted.last()

Relay Behavior

Relays SHOULD index kind 1063 events by all x hashes present in imeta tags. This enables clients to discover the binding event when they only have one variant's hash.

Query example:

["REQ", "sub1", {"kinds": [1063], "#x": ["<any-variant-hash>"]}]

Discovery Model

Critical: Blossom blob hashes alone are meaningless without the binding event. A client finding a hash on a Blossom server cannot determine:

The binding event (kind 1063) is the authoritative source. Discovery flow:

  1. Client has a hash (from note content or imeta tag)
  2. Client queries relays for kind 1063 events containing that hash
  3. Binding event reveals all variants and their relationships
  4. Client can then fetch appropriate variant from Blossom

Security Considerations

Hash Verification

Clients SHOULD verify that downloaded content matches the x hash in the imeta tag. This prevents:

No Server-Side Transforms

Unlike NIP-96's ?w= parameter, this NIP requires all transforms to happen client-side before upload. This preserves the content-addressing guarantee: the hash always matches the file content.

EXIF Stripping

Publishing clients MUST strip EXIF and other metadata to protect user privacy. This includes:

Immutability

Kind 1063 events are immutable. To update an image (e.g., add a missing variant), publish a new event and update references. Consider using addressable events (kind 31063 with d tag) if updates are needed frequently.

Backward Compatibility

References