Skip to main content

Multi-density images

The images command at a glance

purgetss images generates every Titanium density variant of your UI images (buttons, illustrations, screen graphics, logos) from a single source per image.

  • Android: res-mdpi, res-hdpi, res-xhdpi, res-xxhdpi, res-xxxhdpi (5 densities)
  • iPhone: @1x, @2x, @3x (3 scales via filename suffix)

Works on Alloy and Classic projects. The layout is auto-detected.

This guide covers the purgetss/images/ convention, the 4× master convention, re-processing single files, and how it fits alongside build.

For a terse reference of every flag, see the images command reference.

Why multi-density?

Android's UI toolkit resolves images by density: a Pixel 7 (xxhdpi ≈ 3×) picks files from res-xxhdpi/, a low-end Moto (hdpi ≈ 1.5×) picks from res-hdpi/. If the right density isn't available, Android upscales the closest one, which looks noticeably blurry on high-DPI screens.

iOS uses the same idea with filename suffixes: icon.png, icon@2x.png, icon@3x.png. iPhone 15 Pro picks @3x, older iPads pick @2x.

Shipping every variant keeps your UI sharp on every device. purgetss images does it in one pass from a single source per image.

Quick start

Drop source images into purgetss/images/, then run the command:

> cp my-hero-illustration.png purgetss/images/
> cp my-button.svg purgetss/images/buttons/primary.svg
> purgetss images

Output in an Alloy project:

app/assets/
├── android/images/
│ ├── res-mdpi/
│ │ ├── my-hero-illustration.png
│ │ └── buttons/primary.svg
│ ├── res-hdpi/…
│ ├── res-xhdpi/…
│ ├── res-xxhdpi/…
│ └── res-xxxhdpi/…
└── iphone/images/
├── my-hero-illustration.png (@1x)
├── my-hero-illustration@2x.png
├── my-hero-illustration@3x.png
└── buttons/
├── primary.svg (@1x)
├── primary@2x.svg
└── primary@3x.svg

The purgetss/images/ convention

init creates purgetss/images/ (alongside fonts/ and brand/), so the folder is already there the first time you look for it, even before you've dropped any sources.

./purgetss/images/
purgetss/images/
├── logo-screen.svg
├── hero.png
├── buttons/
│ ├── primary.png
│ └── secondary.png
└── icons/
└── home.svg

Supported input formats: .svg, .png, .jpg, .jpeg, .webp, .gif.

Subdirectories are preserved in the output. A file at purgetss/images/buttons/primary.png ends up at app/assets/android/images/res-*/buttons/primary.png and app/assets/iphone/images/buttons/primary.png. Organize however makes sense for your project.

Use SVG whenever you can

SVG scales losslessly to every density. A single icon.svg rasterizes perfectly to every res-*dpi folder without pixel loss. PNG/JPG sources must be downscaled and lose a bit of sharpness at smaller densities.

Source sizes — the 4× convention

PurgeTSS (and Titanium Alloy generally) treats every source image as a 4× master (res-xxxhdpi / @xxxhdpi on Android, equivalent to @4x on iPhone if iOS supported it). All smaller densities are downscaled from that source.

That means:

Source size (logical)Use for
1920×1080 or largerFull-screen illustration / hero image
800×800Card, list item, large icon
200×200Button, inline icon
96×96Small inline icon

If your source is smaller than 4×, the tool still runs but the larger density outputs are essentially upscaled — quality drops on high-DPI devices.

Recommended sizes for common UI elements (in source pixels, assumed 4×):

  • Full-screen illustration: at least 1920×1080
  • Tab bar icon: at least 192×192 (source for 48px at xxxhdpi)
  • List-row thumbnail: at least 320×320
  • Button background: match the intended display size × 4

The images: config section

On the first run, purgetss images injects an images: block into your existing purgetss/config.cjs (between brand: and theme:) with these defaults:

./purgetss/config.cjs
images: {
quality: 85, // JPEG/WebP/AVIF quality (0-100)
format: null, // null = keep original; 'webp' | 'jpeg' | 'png' to convert every image
confirmOverwrites: true // prompt before overwriting files (set false to skip)
}

Change whatever you want to override globally; CLI flags still win for one-off runs.

Overwrite confirmation

images writes directly into the project, so it asks before overwriting anything:

Continue? [y/N/a]
  • y / yes — write this time
  • N / no / Enter — abort (nothing is written)
  • a / always — write, then add confirmOverwrites: false to the images: section of config.cjs so the prompt stays quiet on future runs

The prompt is skipped automatically when:

  • stdin is not a TTY (the alloy.jmk hook, CI, a pipe)
  • you pass -y / --yes — one-shot bypass
  • PURGETSS_YES=1 is set in the environment — lasts the whole shell session
  • confirmOverwrites: false is already in the images: config
> purgetss images -y # skip prompt once
> PURGETSS_YES=1 purgetss images # skip for the whole session

Re-processing a single file or subfolder

Common workflow: you tweaked one image in Affinity or Figma and only want to regenerate that one, not the whole folder.

Pass its path directly:

> purgetss images buttons/primary.png

Short paths auto-resolve against purgetss/images/, so you don't need to type purgetss/images/buttons/primary.png. The command tries two interpretations:

  1. purgetss/images/buttons/primary.png (matches the convention folder).
  2. ./buttons/primary.png (fallback, relative to the project root).

Subdirectory structure is preserved in the output whenever the source lives inside purgetss/images/, whether you pass the full folder, a subfolder, or a single file. Re-processing one file produces the same output path it had in a full run.

Re-process a whole subfolder

> purgetss images buttons/

Walks purgetss/images/buttons/ recursively and regenerates every image inside. Everything outside stays untouched.

Pointing to sources outside the convention

If your source images live elsewhere (e.g. next to your design files in docs/screenshots/), pass an absolute or cwd-relative path:

> purgetss images ./docs/screenshots/home-hero.png
> purgetss images /Users/cesar/Design/banner.svg

When the source is outside purgetss/images/, subdirectory preservation uses the directory of the source file as the root instead.

Platform filter

By default, every run generates both Android densities and iPhone scales. Scope to one platform for targeted runs:

> purgetss images --android # Android only (skip iPhone)
> purgetss images --ios # iPhone only (skip Android)

Useful when:

  • You're iterating on an iOS-only screen and don't need to regenerate Android assets every time.
  • You want to tune JPEG quality differently for the two platforms (run the command twice with different flags).

The two flags are mutually exclusive. Pass neither to get both.

Format conversion

The default preserves each source's format: drop in .png, get out .png; drop in .jpg, get out .jpg. Add --format <ext> to convert every output to a single target format:

> purgetss images --format webp # convert every output to WebP
> purgetss images --format jpeg --quality 90

Valid targets: webp, jpeg, png, avif, gif, tiff.

Why WebP?

WebP produces ~25-35% smaller files than JPEG at similar visual quality, and Titanium supports it natively on both platforms. For a large UI asset library, switching to WebP can shave several MB off your APK/IPA.

> purgetss images --format webp --quality 85

Keep --format null (the default) when you need to stay in the same format as the source, for example PNG with alpha that shouldn't be flattened.

Full pipeline alongside build

The typical sequence when iterating on an app:

# 1. Edit your source images in Affinity/Figma, drop into purgetss/images/
# 2. Regenerate the variants
> purgetss images

# 3. Run purgetss build to regenerate utilities.tss if you changed classes
> purgetss build

# 4. Build the app
> ti build -p android -T emulator

If you only tweaked CSS classes (no image changes), you don't need to re-run purgetss images. It's safe to skip.

Cleaning up

purgetss images never deletes files. It only creates them. If you remove an image from purgetss/images/, the previously-generated copies in app/assets/android/images/res-*/ and app/assets/iphone/images/ stay in place. Remove them manually (or via git) when you clean up.

Troubleshooting

The output is blurry on high-DPI devices

Your source is likely smaller than the 4× master convention. A larger source means sharper output across all densities. Aim for at least 4× the intended display size, or use SVG sources when possible.

JPG output has a white background instead of transparency

JPEG doesn't support alpha channels. If your source is PNG with transparency and you convert to JPEG (via --format jpeg), the alpha gets flattened on white. To preserve transparency, keep the format as PNG or WebP:

> purgetss images --format webp # supports alpha
> purgetss images --format png # keeps alpha

My subdirectories aren't preserved in the output

Verify your source path is inside purgetss/images/. When passing sources from outside the convention (e.g. ./docs/screenshots), the directory of the source file is used as the root, so a file at ./docs/screenshots/hero.png outputs to images/hero.png (flat), not images/screenshots/hero.png.

Move the file into purgetss/images/screenshots/ if you want subdirectory preservation.

I want to preview what would happen before writing files

> purgetss images --dry-run

Shows every file that would be written, no side effects.

Can I use this for app icons?

No. App icons need a different pipeline (adaptive icons, mask safe-zones, marketplace flattening, iOS 18+ Dark/Tinted). Use purgetss brand for the launcher icon.

purgetss images is for the UI assets inside your screens: buttons, backgrounds, illustrations, inline icons.