{"id":50953468,"url":"https://github.com/outfox/paletti","last_synced_at":"2026-06-18T04:01:31.685Z","repository":{"id":362476662,"uuid":"1258620703","full_name":"outfox/paletti","owner":"outfox","description":"Image Palettizer","archived":false,"fork":false,"pushed_at":"2026-06-04T10:45:40.000Z","size":61,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-04T12:19:06.915Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/outfox.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-03T18:53:52.000Z","updated_at":"2026-06-04T10:45:45.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/outfox/paletti","commit_stats":null,"previous_names":["outfox/paletti"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/outfox/paletti","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/outfox%2Fpaletti","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/outfox%2Fpaletti/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/outfox%2Fpaletti/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/outfox%2Fpaletti/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/outfox","download_url":"https://codeload.github.com/outfox/paletti/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/outfox%2Fpaletti/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34475375,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-18T02:00:06.871Z","response_time":128,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-18T04:01:16.978Z","updated_at":"2026-06-18T04:01:31.650Z","avatar_url":"https://github.com/outfox.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"\n\u003cdiv align=\"center\"\u003e\n  \u003cimg src=\"docs/logo-paletti.png\" alt=\"paletti logo\" width=\"500\"\u003e\n\u003c/div\u003e\n\n\u003ch1 align=\"center\"\u003ePaletti - The Image Chameleon\u003c/h1\u003e\n\u003cp align=\"center\"\u003e\u003cem\u003eApply colour palettes to images from the command line.\u003c/em\u003e\u003c/p\u003e\n\n`paletti` is a Python reinterpretation of the palette / dithering shader demonstrated in\n[Palette Shader 2](https://greenf0x.itch.io/paletteshader2) Godot 3 utility by [GreenF0x](https://greenf0x.itch.io/). \n\nFor each pixel it finds the two nearest palette colours and then snaps, blends, or ordered-dithers between them.\n\nPaletti does a bunch more, such as allow easily ad-hoc composed palettes and adjusting metrics and dithering patterns per color channel.\n\nThe utility defaults to the OKLAB color space, enabling it to match perceptually similar colors.\n\n## Showcase\n\n`paletti` works on all kinds of source art — from chunky pixel art to\n3D-rendered lighting. The examples below run a source through a range of\npalettes and dither settings.\n\n\u003ctable\u003e\n  \u003ctr\u003e\n    \u003ctd width=\"50%\" align=\"center\"\u003e\u003cstrong\u003eUse Case: Pixel Art Palette Swaps\u003c/strong\u003e\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e\u003cimg src=\"docs/sample-kenney.png\" alt=\"Pixel art re-painted with paletti\" width=\"448\"\u003e\u003c/td\u003e\n  \u003c/tr\u003e\n\u003c/table\u003e\n\n\u003ctable\u003e\n  \u003ctr\u003e\n    \u003ctd width=\"50%\" align=\"center\"\u003e\u003cstrong\u003eUse Case: SVG Recoloring\u003c/strong\u003e\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e\u003cimg src=\"docs/sample-svg-in.svg\" alt=\"Logo for project 'Newtype'\" width=\"224\"\u003e\n    \u003cimg src=\"docs/sample-svg-out.svg\" alt=\"Logo re-colored with paletti\" width=\"224\"\u003e\u003c/td\u003e\n  \u003c/tr\u003e\n\u003c/table\u003e\n\n\u003ctable\u003e\n  \u003ctr\u003e\n    \u003ctd width=\"50%\" align=\"center\"\u003e\u003cstrong\u003eUse Case: Illustrative Reshading / Dithering\u003c/strong\u003e\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e\u003cimg src=\"docs/sample-illustration-in.png\" alt=\"Pixel art re-painted with paletti\" width=\"224\"\u003e\u003cimg src=\"docs/sample-illustration-out.png\" alt=\"Pixel art re-painted with paletti\" width=\"224\"\u003e\u003c/td\u003e\n  \u003c/tr\u003e\n\u003c/table\u003e\n\n\u003ctable\u003e\n  \u003ctr\u003e\n    \u003ctd width=\"50%\" align=\"center\"\u003e\u003cstrong\u003eUse Case: Re-Lighting Art and Photos\u003c/strong\u003e\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e\u003cvideo src=\"https://github.com/user-attachments/assets/5f70a718-7602-44a9-a1f6-e6cda5a2658c\" width=\"448\" autoplay loop muted\u003e\u003c/video\u003e\u003c/td\u003e\n  \u003c/tr\u003e\n\u003c/table\u003e\n\n## Install / run\n\nThis is a [uv](https://docs.astral.sh/uv/) project:\n\n```sh\nuv sync\nuv run paletti --help\n```\n\n## Usage\n\n```sh\npaletti INPUT [OUTPUT] -p PALETTE [options]\n```\n\nIf `OUTPUT` is omitted, the result is written next to the input as\n`paletti-\u003cinput-name\u003e.png` (e.g. `paletti in.png -p pal.png` → `paletti-in.png`);\nan SVG input defaults to `paletti-\u003cinput-name\u003e.svg` (see [SVG input](#svg-input)).\n\nEach palette source (`-p` / `--palette`) can be:\n\n- **an image** — its distinct colours become the palette\n  (`-p palette.png`, optionally `--max-colors 16`);\n- **a JSON file** — `-p sweetie16.json`;\n- **an inline JSON array** — `-p '[\"#1a1c2c\",\"#5d275d\"]'`;\n- **a bare hex/name colour** — `-p 000`, `-p '#1a1c2c'`, `-p lavender`.\n\n`-p` is **repeatable and variadic**, and every source is concatenated into one\npalette. Repeat the flag or list sources after a single `-p`:\n\n```sh\npaletti in.png out.png -p 000 -p palette.json -p lospec-pal8.png\npaletti in.png out.png -p 000 palette.json lospec-pal8.png lavender\n```\n\nA bare colour token accepts any of:\n\n- **hex** — with or without a leading `#`, in 3- or 6-digit form (`000`, `#000`,\n  `1a1c2c`, `#1a1c2c`);\n- a **CSS/SVG colour name** — `white`, `lavender`, `rebeccapurple`;\n- a **CSS colour function** — `rgb()`, `hsl()`, `hsv()`/`hsb()`, `hwb()`,\n  `lab()`, `lch()`, `oklab()`, `oklch()`, in either the legacy comma form or the\n  modern space-separated CSS Color 4 syntax. Hue units (`deg`/`grad`/`rad`/\n  `turn`) and percentages are honoured; a trailing `/ alpha` is parsed and\n  ignored. Colours outside the sRGB gamut are clipped.\n\n```sh\npaletti in.png out.png -p 'rgb(255 0 0)' 'hsl(120deg 100% 50%)' 'oklch(0.7 0.15 30)'\n```\n\n(Quote any token containing spaces, `#`, or parentheses so the shell passes it\nthrough intact.) A token that names an existing file is read as that file, so\nfiles always win over same-named colours.\n\nJSON palettes accept the same hex strings, colour names and colour functions,\nplus `0..255` integer triples (`[26, 28, 44]`) or `0..1` float triples\n(`[0.1, 0.11, 0.17]`). The numeric range is detected automatically; override\nwith `--palette-range`.\n\nBecause a variadic `-p` greedily consumes the values that follow it, place it\nafter the image paths (or before another flag).\n\n### How the two nearest colours are combined\n\nBy default each pixel snaps to its closest palette colour. Two flags change that:\n\n| selection             | result                                                          |\n|-----------------------|----------------------------------------------------------------|\n| (default)             | snap each pixel to the closest palette colour                   |\n| `--blend`             | smooth lerp between the two nearest colours                     |\n| `--dither KIND`       | ordered dither between the two nearest colours (1-bit edges, or soften with `--antialias`) |\n| `--dither KIND --rgb` | ordered dither each RGB channel independently, then snap to the palette (dissolves banding; great with `--dither bayer` or a blue-noise `--dither texture`). With an RGB `--texture` each colour channel drives the matching image channel; a greyscale texture is reused with a 1/3 phase shift per channel. |\n\n`--blend` and `--dither` are mutually exclusive. `KIND` is one of\n`nearest`, `sine`, `bayer`, `halftone`, `texture`.\n\n### Examples\n**A wonderful place to get appealing palettes is the [lospec](https://lospec.com/palette-list) palette list!**\n```sh\n# Quantise to a palette extracted from an image\npaletti photo.png out.png -p lospec-palette.png\n\n# Dither against a 16-colour palette using an 8x8 Bayer matrix\npaletti photo.png out.png -p sweetie16.json --dither bayer --bayer 8\n\n# Halftone / screentone dots (classic 45-degree grid, 8px dot spacing)\npaletti photo.png out.png -p sweetie16.json --dither halftone --res 8\n\n# Tile an arbitrary dither texture, scaled up 10x\npaletti photo.png out.png -p sweetie16.json \\\n    --dither texture --texture screentone.png --scale 10\n\n# Smooth two-tone blending with an inline palette\npaletti photo.png out.png -p '[[26,28,44],[244,244,244]]' --blend\n\n# Build a palette right on the command line (names or hex)\npaletti photo.png out.png -p white black\npaletti photo.png out.png -p sweetie16.json FFFFFF 000000  # palette + extra colours\n\n# Mix sources freely: a bare colour, a JSON file, an image, and a name\npaletti photo.png out.png -p 000 sweetie16.json lospec-pal8.png lavender\n\n# Per-channel ordered dithering to dissolve banding (Bayer or blue-noise)\npaletti photo.png out.png -p sweetie16.json --dither bayer --rgb --bayer 8\npaletti photo.png out.png -p sweetie16.json --dither texture --rgb --texture bluenoise.png\n\n# Match in HSV space, weighting hue twice as heavily\npaletti photo.png out.png -p sweetie16.json --metric hsv --hsv-weights 2,1,1\n```\n\n### SVG input\n\n`paletti` accepts SVG inputs, rendered via [resvg](https://github.com/linebender/resvg).\nThe **output extension** picks how the SVG is handled:\n\n- **a raster output** (`out.png`, …) **rasterizes** the SVG and runs the full\n  pixel pipeline — every mode, metric, and dither works as usual. SVGs have no\n  inherent resolution, so `--svg-scale N` renders `N`× larger for a crisper\n  result (it re-renders at the larger size rather than upscaling pixels);\n- **a `.svg` output keeps it vector**: each colour the SVG uses — `fill`,\n  `stroke`, gradient `stop-color`, and inline / `\u003cstyle\u003e` CSS — is snapped to its\n  nearest palette colour with the same matching logic, and the vectors are\n  written back unchanged. `none`, `currentColor`, and `url(#…)` references are\n  left as-is. Dither needs pixels, so it doesn't apply here (`--blend` does).\n\n```sh\npaletti logo.svg out.png -p sweetie16.json --svg-scale 8   # rasterize → PNG\npaletti logo.svg out.svg -p sweetie16.json                 # recolour, keep vector\npaletti logo.svg        -p sweetie16.json                  # → paletti-logo.svg (vector)\n```\n\n### Other options\n\n- `--blur SIGMA` — Gaussian-blur the source (sigma in pixels) before\n  palettizing. Matching is per-pixel, so source noise / JPEG blocking / faint\n  gradients near a palette-colour boundary flip the chosen colours and show up\n  as sharp pixel-sized speckle. A small pre-blur (try `0.5`-`2`) makes the\n  selection spatially coherent and cleans that up while leaving the dither\n  pattern intact.\n- `--denoise STRENGTH` — edge-preserving bilateral denoise of the source before\n  palettizing. Like `--blur` it suppresses source noise / JPEG blocking, but\n  it keeps the colour edges that drive palette matching crisp (instead of\n  blurring them), giving cleaner flat regions. `STRENGTH` is the colour sigma in\n  `[0,1]` units (try `0.05`-`0.3`). Requires `scikit-image`; slower than\n  `--blur`. Can be combined with `--blur`.\n- `--metric {oklab,rgb,hsl,hsv,hue,luma}` — colour-distance metric used for\n  matching (default `oklab`, which measures perceptual difference). `rgb` is\n  plain Euclidean; `hsl`/`hsv` compare in cylindrical space (hue on its circle);\n  `hue` and `luma` match on that single axis alone. `--hsv-weights` tunes the\n  per-axis weighting of `hsv`.\n- `--hsv-adjust H,S,V` — pre-shift hue (add) and scale saturation/value\n  (multiply) before matching; identity is `0,1,1`.\n- `--dither {nearest,sine,bayer,halftone,texture}`, `--res`, `--bayer`,\n  `--angle`, `--texture` — control the dither pattern. `halftone` reproduces the\n  Godot project's \"Screentone\" pattern as procedural dots: `--res` sets the dot\n  spacing in pixels (try 6-12) and `--angle` rotates the grid (`45` = classic\n  screentone, `0` = an axis-aligned square grid). `texture` tiles an arbitrary\n  image (e.g. the original `screentonesdf.png`) via `--texture`, and `--scale`\n  zooms that tiled texture (e.g. `10` for 10x, `0.5` to shrink). The texture is\n  laid over the image at a 1:1 pixel ratio and repeated to fill it, so\n  `--scale 1.0` is an exact 1:1 mapping; other values zoom the tiled field about\n  the origin via seamless bilinear sampling.\n- `--antialias` — anti-alias dithered edges (e.g. halftone/texture dots). `0`\n  (default) gives hard 1-bit edges. For plain `--dither` it is the smoothstep\n  blend width across the A/B boundary (`~1` blends the two colours across the\n  whole dot). For `--rgb` the crisp per-channel result is strictly on-palette\n  (and A/B are nearest neighbours with no colour between them), so its edges\n  can't be softened on-palette — they are anti-aliased in the render by a blur\n  that grows with the value (try `~0.5`-`1.5`).\n- `--prefer-smallest` — when dithering, bias toward the darker of the two\n  colours.\n- `-ehb` / `--extra-half-brite` — double the palette by appending a\n  half-brightness copy of every colour before matching, giving a quick set of darker shades.\n\nTransparency in the source image is preserved.\n\n\u003e Options that the current run doesn't use are reported as a `warning:` on\n\u003e stderr (e.g. `--bayer` without `--dither`, `--hsv-weights` with `--metric rgb`)\n\u003e rather than being silently ignored.\n\n## Project layout\n\n```\nsrc/paletti/\n  cli.py        # click command-line interface\n  core.py       # the shader port: two-nearest match + mode rendering\n  color.py      # vectorised rgb\u003c-\u003ehsv (ports rgb2hsv / hsv2rgb)\n  dither.py     # ordered-dither value sources (nearest/sine/bayer/texture)\n  palette.py    # load palettes from images or JSON\n  imageio.py    # image load/save with alpha preservation (incl. SVG rasterizing)\n  svg.py        # recolour an SVG's vector colours onto a palette (vector output)\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foutfox%2Fpaletti","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Foutfox%2Fpaletti","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foutfox%2Fpaletti/lists"}