Media — Image capture, pick, preview, crop, formats (privacy + widgets)

mandatory

Contrato cross-surface para captura, seleção e preview de imagens em apps Koder. Toggles de privacidade (`media.image.camera`, `media.image.gallery`) em Settings, defaults seguros (tudo OFF até o primeiro use ativar via prompt), formatos canônicos (JPEG/PNG/WebP/AVIF in; JPEG/PNG out), e widgets compartilhados `KoderImagePicker` + `KoderImagePreview` + `KoderImageCropper` em `engines/sdk/koder_kit`. EXIF stripping obrigatório em upload por default. `<APP>-IMAGE-*` error map. Cross-link com `specs/media/video.kmd` (camera permission compartilhada) e `specs/multi-tenancy/contract.kmd` (storage path prefix).

Media — Image Spec — v0.1

Normative cross-surface spec para image I/O em apps Koder. Implementação obrigatória via widgets Koder*Image* em engines/sdk/koder_kit (Flutter) e koder_web_kit (JS) — nunca rolar <input type="file"> ou ImagePicker upstream local.


Scope

Aplica-se a *odo app Koder que exiba, capture, ou faça upload de imagens* avatar do usuário, attachments em chat, galerias de produto, captura de documento via câmera (cross-cuts document.kmd quando o intuito é OCR), upload de logo em settings de workspace, etc.

Surfaces cobertas: Flutter mobile (Android + iOS), Flutter desktop (Linux + macOS + Windows), Flutter web ou templ+HTMX, TV (raro — só quando há media remota; câmera local em TV é fora de escopo). CLI/TUI recebem path arg como hoje.


1 — MUST: expose "Imagem" toggle group in Settings

Apps Koder com mic OU câmera capability *evem*ter um agrupamento "Mídia" (Media em enUS) na tela de Settings; a subseção "Imagem" *eve*conter, na ordem:

  1. *âmera*(media.image.camera) — toggle MUST gate à camera
  2. *aleria*(media.image.gallery) — toggle MUST gate ao file picker
  3. *XIF strip em upload*(media.image.strip_exif) — checkbox
  4. *ualidade de compressão JPEG*(media.image.jpeg_quality) —

    slider 50-100, default 85

A implementação é via drop-in KoderMediaSettingsTile do koder_kit (hostside da §1 do `voice/wakeword.kmd` pattern). *unca*desenhar a tile localmente.


2 — MUST: defaults seguros em fresh install

Chave Default Notas
media.image.camera *FF* Privacybydefault; primeiro uso dispara permission prompt do SO **atualiza este toggle pra ON
media.image.gallery *FF* Idem para Photos/Files permission
media.image.strip_exif *N* GPS + camera serial + timestamp removidos antes do upload; modo OFF exige opt-in explícito do usuário
media.image.jpeg_quality 85 Range 50-100; abaixo de 50 a qualidade fica visivelmente ruim, acima de 95 o ganho é marginal

Quando media.image.camera for toggled OFF em runtime, o app *eve*parar imediatamente qualquer capture ativo + liberar handle da câmera.


3 — MUST: privacidade de captura

Image data *unca*sai do dispositivo sem ação explícita do usuário (tap em "Enviar", drop em conversation, save em form). Especificamente:

  • *UNCA*auto-upload de preview (a UI mostra preview local; user

    confirma antes do POST)

  • *UNCA*enviar imagens a serviços terceiros (analytics, error

    reporter, etc.); imagens só vão pro endpoint Koder declarado em KoderApp.config().mediaEndpoint

  • *UNCA*persistir imagem em diretório acessível por outro app

    sem ser explicitamente Pictures/Koder/<app>/ (consentido)

  • *XIF strip*é default ON (§2); se OFF, o app *eve*mostrar

    warning no flow de upload ("Esta imagem inclui localização GPS e metadados de câmera. Continuar?")


4 — MUST: widget surface no koder_kit

Widgets canônicos a usar (zero implementação local):

Widget Função
KoderImagePicker Sheet com 2 ações: "Tirar foto" (gates media.image.camera) e "Escolher da galeria" (gates media.image.gallery); retorna KoderImageRef
KoderImagePreview Render de KoderImageRef com fallback (placeholder + retry) e progress overlay durante upload
KoderImageCropper Crop UI com aspect ratio fixo (1:1, 4:5, 16:9, free); retorna nova KoderImageRef cropped
KoderAvatarPicker Helper de alto nível: pick → crop 1:1 → upload pra media/avatar/ (pathprefix conforme `multitenancy/contract.kmd`)
KoderImageThumbnail Render de thumb 128px (default) ou custom size, com lazyload + LRU cache de 50 MB ondevice

KoderImageRef é um value type: {uri: String, mime: String, sizeBytes: int, width: int, height: int, koderUserId: String?, workspaceId: String?}. Apps *unca*lidam com bytes brutos — o widget cuida.


5 — MUST: format support

*nput (decode):*JPEG, PNG, WebP, AVIF, HEIC (iOS), GIF (still frame só).

*utput (encode):*JPEG (default, qualidade da §2) ou PNG (lossless quando media.image.format = png). Apps *unca*uploadam HEIC raw — o widget converte para JPEG/PNG antes do POST.

*esize policy (default):*dimensão maior > 2048 px → resize para 2048 mantendo aspect ratio. Configurável via KoderImagePicker(maxDimension: 4096).

*pload size cap:*25 MB pós-compress. Exceder → erro <APP>-IMAGE-SIZE-001 com sugestão de qualidade menor.


6 — MUST: error surface

Erros de imagem que cheguem ao usuário *evem*seguir specs/errors/user-facing-messages.kmd: texto humanizado em ptBR/enUS, botão "Ver detalhes", ID <APP>-IMAGE-<CODE>-<SEQ>.

Cenário ID Texto pt-BR
Permissão de câmera negada <APP>-IMAGE-CAM-001 "Permita o acesso à câmera para tirar fotos."
Permissão de galeria negada <APP>-IMAGE-GAL-001 "Permita o acesso às fotos para escolher uma imagem."
Formato não suportado <APP>-IMAGE-FMT-001 "Este formato de imagem não é suportado. Use JPEG ou PNG."
Tamanho excede 25 MB pós-compress <APP>-IMAGE-SIZE-001 "Imagem muito grande. Reduza a qualidade ou escolha outra."
Upload falhou (rede / endpoint indisponível) <APP>-IMAGE-NET-001 "Falha ao enviar a imagem. Tente novamente."
Decode falhou (arquivo corrompido) <APP>-IMAGE-DEC-001 "Não foi possível abrir esta imagem (arquivo corrompido)."

7 — Observability

  • Counters: media.image.picked, media.image.cropped,

    media.image.uploaded, media.image.upload_error

  • Latency histograms: media.image.compress_ms,

    media.image.upload_ms

  • *ão*emitir métricas que vazem conteúdo (file path, EXIF,

    hash da imagem) — apenas counters + latência


8 — Adoption checklist (per app)

Quando um app Koder ganha image capability, os pull requests *evem*incluir:

  • [ ] Importa KoderMediaSettingsTile (sub-seção Imagem visível)
  • [ ] Usa KoderImagePicker (nunca ImagePicker upstream / <input type="file"> raw)
  • [ ] Defaults da §2 respeitados em fresh install
  • [ ] EXIF strip ON por default + warning quando OFF (§3)
  • [ ] Formats da §5 respeitados (HEIC → JPEG antes do POST)
  • [ ] Error map da §6 implementado
  • [ ] Upload path respeita multi-tenancy/contract.kmd (/u/<koder_user_id>/.../)

Non-normative — referências

  • Sibling specs: specs/media/video.kmd (camera shared),

    specs/media/document.kmd (camera→OCR cross-cut), specs/media/audio.kmd (mic equivalent pattern)

  • Voice equivalent: specs/voice/wake-word.kmd (precedente para o pattern

    de Settings tile + privacy contract)

  • Implementation surface: engines/sdk/koder_kit/lib/src/media/

    ainda não existe (criar junto com KSTACK ticket de adoção)

  • Serverside storage: `specs/multitenancy/contract.kmd` (path-prefix

    obrigatório) + specs/koder-app/behaviors.kmd §3 (auth scope)

Source: ../home/koder/dev/koder/meta/docs/stack/specs/media/image.kmd