Metadata-Version: 2.4
Name: studio-kit
Version: 0.1.0
Summary: Retoques locales de imagen para las fichas de Omnitok Studio (trim, crop, colores de borde, JPEG).
License-Expression: LicenseRef-Proprietary
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: Pillow>=11
Provides-Extra: test
Requires-Dist: pytest>=8.0; extra == "test"
Dynamic: license-file

# studio-kit

Retoques locales de imagen para armar fichas de Omnitok Studio: recortar el fondo, recortar una caja,
sacar los colores de un borde como degradado CSS y exportar un JPEG liviano. Corre en tu equipo con
Pillow; no sube nada.

## Uso

    uvx studio-kit img trim <imagen> [--margin 2%] [--fuzz 8] [--out <archivo>] [--force]
    uvx studio-kit img crop <imagen> --box x0,y0,x1,y1 [--out <archivo>] [--force]
    uvx studio-kit img edge-colors <imagen> --side left|right [--bands 3]
    uvx studio-kit img jpeg <imagen> [--q 86] [--max 2560] [--bg #ffffff] [--out <archivo>] [--force]
    uvx studio-kit --version

Ningún comando sobrescribe la entrada. Sin `--out`, la salida va al lado con el nombre
`<nombre>.<op>.<ext>` (`foto.trim.png`, `foto.crop.png`, `foto.jpeg.jpg`). Si esa salida (o `--out`) ya
existe, el comando falla con `output_exists`; `--force` la reemplaza. La salida se escribe en un
temporal del mismo directorio y se mueve al final: un fallo no deja un archivo a medias ni pierde el
que había (con `--force` la salida conserva los permisos del archivo reemplazado). Cada comando imprime
una sola línea JSON en stdout, en ASCII (lo no latino sale como `\uXXXX`, así que sirve con cualquier
codificación de stdout):

    $ studio-kit img trim foto.png --margin 2%
    {"ok": true, "out": "/ruta/foto.trim.png", "w": 520, "h": 520, "bytes": 1800}

- **trim**: recorta el fondo de la imagen como se ve (con la orientación EXIF aplicada; la salida ya no
  lleva el tag). Si al menos 1/20 de los pixeles del borde de la imagen son transparentes (alpha <= `--fuzz`) manda solo el alpha: se
  conserva lo visible (alpha > `--fuzz`) y ningún color opaco cuenta como fondo, así que la tapa blanca de una
  botella o la suela de un zapato recortados sobre transparente se quedan (y un marco blanco opaco dentro del
  alpha, también). Con menos transparencia (las esquinas redondeadas de una tarjeta, un pixel suelto) el alpha no
  decide: va la regla por color de abajo, sin contar lo invisible. Sin esa transparencia en el borde, el fondo es la mediana del borde (no un pixel), así que un
  objeto o una mota en una esquina no lo falsean, y se recorta lo que se aleja más de `--fuzz` (0–255, por canal).
  No se adivina: si lo recortado ocupa el ancho o el alto entero de la imagen y es un bloque liso, el producto podría
  ser ese bloque o el resto (una nevera blanca lisa sobre una pared azul es la misma imagen que un producto rojo liso
  con una franja blanca) y trim falla con `ambiguous_background`: marca el producto con `crop`. Solo es ambiguo
  cuando se puede medir: se mira el interior de la caja, sin los 8 px de cada lado (el ringing de un JPEG los ensucia
  junto a un cambio de color). Una caja de 16 px o menos de un lado, o con el interior invisible, no tiene nada que
  medir y se recorta como siempre. El bloque es liso si lo visible de ese interior no varía en ningún canal, alfa
  incluido (un degradado de transparencia no es liso), más que `--fuzz` con tope 8 aunque `--fuzz` sea mayor, y todo él
  se aleja más de `--fuzz` del fondo; si el interior es igual al fondo (un contorno, dos motas en las esquinas) no es
  ambiguo. Un JPEG de calidad baja cuyo interior varía más que eso escapa de la detección y se recorta como siempre
  (límite conocido). Un bloque liso que no ocupa
  un lado entero (un producto chico sobre un fondo liso) es el producto y se recorta. Los PNG de 16 bits se
  leen escalados a 8. `--margin` deja aire alrededor: en px (`10`, `10px`) o en % del lado mayor del recorte (`2%`), sin salirse de la imagen.
- **crop**: recorta la caja `--box x0,y0,x1,y1` en porcentaje (0–100) del ancho y del alto de la
  imagen como se ve (con la orientación EXIF aplicada; la salida ya no lleva el tag de orientación).
- **edge-colors**: lee el borde izquierdo o derecho de la imagen como se ve (franja de hasta 3 px) y devuelve `--bands`
  colores equiespaciados de arriba abajo, más el CSS listo para pegar:

      $ studio-kit img edge-colors foto.png --side left --bands 3
      {"ok": true, "side": "left", "css": "linear-gradient(180deg, #ff0000 0%, #800080 50%, #0000ff 100%)", "colors": ["#ff0000", "#800080", "#0000ff"], "w": 400, "h": 300}

- **jpeg**: aplica la orientación EXIF, aplana la transparencia sobre `--bg`, reduce el lado mayor a
  `--max` (nunca agranda) y guarda con calidad `--q`, conservando el resto del EXIF. `--out` debe ser
  `.jpg` o `.jpeg`. Un CMYK o un gris con perfil ICC se convierte a sRGB con ese perfil; un PNG RGB,
  gris o de 1 bit con tRNS usa `--bg` donde es transparente. Un EXIF de más de 64 KB no cabe en un JPEG: la
  salida va sin EXIF.

trim y crop guardan en el formato de la extensión de salida y conservan el EXIF, salvo los tags de
estructura del TIFF de origen, el perfil ICC, los bloques de Photoshop (recursos y capas) y el XMP (que no
describen la salida; el XMP no se copia). Un EXIF mal formado que Pillow no puede reescribir se descarta:
la salida va sin EXIF, no falla; igual un EXIF de más de 64 KB al guardar en JPEG.
El perfil ICC solo viaja si corresponde al color de la salida (RGB con perfil RGB, gris con perfil de
grises): nunca un perfil CMYK o GRAY sobre pixeles RGB, en ningún formato (WebP incluido). GIF y BMP no llevan perfil: un
gris con perfil se convierte a RGB aplicándolo, igual que a WebP (un RGB con un perfil propio pierde el perfil). Los 16 bits
se conservan en PNG y TIFF y se escalan a 8 en los demás formatos; los TIFF de 32 bits (enteros y
flotantes) se estiran por el rango real de la imagen entera (no del recorte), y si no varía, o tiene NaN o
infinitos, es un error `invalid_image`, no una imagen lisa; al guardar en TIFF no se tocan. Un
modo que el formato no escribe (CMYK a PNG, BMP, GIF o WebP; gris con alfa a BMP) se convierte a RGB o
RGBA, aplicando el perfil si lo hay; la clave de transparencia tRNS de un PNG RGB, gris o de 1 bit pasa a alfa en
WebP, TIFF, BMP y GIF (en 16 bits se compara antes de bajar a 8: exacta en gris; en RGB Pillow ya bajó los
pixeles a 8, así que se compara el byte alto de la clave y otros valores con ese mismo byte también quedan
transparentes; la clave de un gris de 16 bits llega a PNG, WebP y JPEG, no a TIFF ni GIF). De una imagen con varios fotogramas o páginas (GIF animado, TIFF multipágina) se
procesa solo el primero, y el JSON lo avisa con `"warning"` (un PSD no avisa: se procesa el compuesto, no sus capas; tampoco
si una página dañada impide contar los cuadros, aunque siempre se procesa el primero).

## Errores

`{"ok": false, "error": "<código>", "message": "..."}` en stdout, sin escribir archivos ni dejar directorios nuevos.

| código | exit | cuándo |
|---|---|---|
| `invalid_args` | 2 | argumentos inválidos, archivo inexistente, caja de `crop` que queda en 0 px, `--out` igual a la entrada (también por mayúsculas, symlink o hardlink), con una extensión que Pillow no escribe (p. ej. `.psd`) o, en `jpeg`, que no es `.jpg`/`.jpeg`, `--margin` desmesurado, o una salida que el formato no admite por tamaño (WebP, GIF, TGA...) |
| `output_exists` | 1 | la salida por defecto o `--out` ya existe y no se pasó `--force` |
| `nothing_to_trim` | 1 | trim sobre una imagen sin contenido distinto del fondo |
| `ambiguous_background` | 1 | trim sobre una imagen en la que lo distinto del fondo es un bloque liso que ocupa todo el ancho o el alto: no se sabe cuál es el producto; usa `crop` |
| `image_too_large` | 1 | más pixeles que `PIL.Image.MAX_IMAGE_PIXELS` (se rechaza antes de decodificar) |
| `invalid_image` | 1 | el archivo no es una imagen legible o está dañado, o es un TIFF de 32 bits sin variación o con NaN o infinitos |
| `write_failed` | 1 | no se pudo escribir la salida |

## Desarrollo

Desde la raíz del repo `omnitok-studio-mcp`:

    uv pip install --python .venv/bin/python -e packages/studio-kit --no-deps
    .venv/bin/python -m pytest -q packages/studio-kit/tests

Los tests generan sus imágenes con Pillow (`tests/conftest.py`); no hay binarios en el repo.

## Publicación

Solo con un tag `kit-v<versión>` que coincida con `version` del `pyproject.toml`; el workflow
`publish-studio-kit` corre los tests, construye y publica en PyPI con trusted publishing.

El control de que el tag viene de `develop` o `main` es un paso del propio workflow, y GitHub ejecuta el
workflow del commit del tag (no el de la rama por defecto): quien pueda crear un tag sobre un commit con el
workflow editado se salta ese paso. Lo que sí lo frena está en la configuración del repo, no en el archivo, y
debe estar puesto antes de publicar:

- El environment `pypi` con revisores requeridos (que alguien distinto de quien crea el tag apruebe cada corrida) y
  con la política de despliegue que permita el patrón `kit-v*` (el environment se comparte con `publish-psd-extract`,
  que usa `psd-v*`).
- Un ruleset que limite quién puede crear tags `kit-v*`.

## Licencia

Propietaria: todos los derechos reservados (ver `LICENSE`).
