Pular para o conteúdo

Imagem

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).

Spec fonte: specs/media/image.kmd

Especificação completa

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 todo 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 devem ter um agrupamento "Mídia" (Media em en-US) na tela de Settings; a sub-seção "Imagem" deve conter, na ordem:

  1. Câmera (media.image.camera) — toggle MUST gate à camera
  2. Galeria (media.image.gallery) — toggle MUST gate ao file picker
  3. EXIF strip em upload (media.image.strip_exif) — checkbox
  4. Qualidade de compressão JPEG (media.image.jpeg_quality) — slider 50-100, default 85

A implementação é via drop-in KoderMediaSettingsTile do koder_kit (host-side da §1 do voice/wake-word.kmd pattern). Nunca desenhar a tile localmente.


2 — MUST: defaults seguros em fresh install

ChaveDefaultNotas
media.image.cameraOFFPrivacy-by-default; primeiro uso dispara permission prompt do SO e atualiza este toggle pra ON
media.image.galleryOFFIdem para Photos/Files permission
media.image.strip_exifONGPS + camera serial + timestamp removidos antes do upload; modo OFF exige opt-in explícito do usuário
media.image.jpeg_quality85Range 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 deve parar imediatamente qualquer capture ativo + liberar handle da câmera.


3 — MUST: privacidade de captura

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

  • NUNCA auto-upload de preview (a UI mostra preview local; user confirma antes do POST)
  • NUNCA enviar imagens a serviços terceiros (analytics, error reporter, etc.); imagens só vão pro endpoint Koder declarado em KoderApp.config().mediaEndpoint
  • NUNCA persistir imagem em diretório acessível por outro app sem ser explicitamente Pictures/Koder/<app>/ (consentido)
  • EXIF strip é default ON (§2); se OFF, o app deve 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):

WidgetFunção
KoderImagePickerSheet com 2 ações: "Tirar foto" (gates media.image.camera) e "Escolher da galeria" (gates media.image.gallery); retorna KoderImageRef
KoderImagePreviewRender de KoderImageRef com fallback (placeholder + retry) e progress overlay durante upload
KoderImageCropperCrop UI com aspect ratio fixo (1:1, 4:5, 16:9, free); retorna nova KoderImageRef cropped
KoderAvatarPickerHelper de alto nível: pick → crop 1:1 → upload pra media/avatar/ (path-prefix conforme multi-tenancy/contract.kmd)
KoderImageThumbnailRender de thumb 128px (default) ou custom size, com lazy-load + LRU cache de 50 MB on-device

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


5 — MUST: format support

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

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

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

Upload 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 devem seguir specs/errors/user-facing-messages.kmd: texto humanizado em pt-BR/en-US, botão "Ver detalhes", ID <APP>-IMAGE-<CODE>-<SEQ>.

CenárioIDTexto 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
  • Nã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 devem 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)
  • Server-side storage: specs/multi-tenancy/contract.kmd (path-prefix obrigatório) + specs/koder-app/behaviors.kmd §3 (auth scope)

Outras capabilities de mídia

Vídeo

Gravação, playback e screen-capture. Recording indicator obrigatório; codecs via engines/kodec; widgets KoderVideoPlayer / KoderVideoRecorder / KoderScreenCapture.

Áudio

Memos, podcast, attachments e ringtones — áudio genérico, separado de voice/wake-word. Widgets KoderAudioPlayer / KoderAudioRecorder.

Documento

Pick, preview e OCR de PDF/DOCX/MD/TXT. OCR local-first (tesseract); fallback services/ai/ocr. Widgets KoderDocumentPicker / KoderOcrButton.