Media — Image capture, pick, preview, crop, formats (privacy + widgets)
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*emengines/sdk/koder_kit(Flutter) ekoder_web_kit(JS) — nunca rolar<input type="file">ouImagePickerupstream 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:
- *âmera*(
media.image.camera) — toggle MUST gate à camera - *aleria*(
media.image.gallery) — toggle MUST gate ao file picker - *XIF strip em upload*(
media.image.strip_exif) — checkbox - *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* | Privacy |
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/ (path |
KoderImageThumbnail |
Render de thumb 128px (default) ou custom size, com lazy |
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(nuncaImagePickerupstream /<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 patternde 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)
- Server
side storage: `specs/multitenancy/contract.kmd` (path-prefixobrigatório) +
specs/koder-app/behaviors.kmd§3 (auth scope)