ImageMagick Image Cropping & Transparency | LlamaPress Cookbook
LlamaPress
Cookbook
Media Stable

ImageMagick Image Cropping & Transparency

Trim whitespace, make backgrounds transparent, recolor a logo white for dark headers, and build centered/padded variants with ImageMagick — then swap the result into a Rails view.

Proven on llamapress.ai view

ImageMagick Image Cropping & Transparency

⚠️ Cookbook example — not live code. (KEEP THIS CALLOUT.) Every code block below is an example snippet, not part of the llamapress.ai codebase, and not running on this server. This is a reference recipe for a Leo instance (an AI coding agent) to implement in its own app — read it to understand the pattern, then recreate it there.

A user uploads a logo or icon that arrives with a big white background, off-center placement, or a wildly non-square aspect ratio. This recipe turns that raw upload into a clean asset — whitespace trimmed, background transparent, centered in a square canvas with optional breathing room — using nothing but the convert / identify CLI, then swaps it into the view with image_tag.

When to use: a real image file (logo, icon, product shot) needs cleanup before it looks right in the UI. When not to: you need per-user runtime processing at scale — that's Active Storage variants (image.variant(...)), not one-off shell commands.


The 80/20 in one breath

  1. identify the original to learn its dimensions and aspect ratio.
  2. One convert pass: -transparent white (with fuzz) + -trim +repage → trimmed, transparent PNG.
  3. Read the trimmed width/height, take the larger side, and -extent onto a transparent square canvas (add 25–50% padding if the design needs breathing room).
  4. Verify transparency with identify -format '%[opaque]' — expect False.
  5. Drop the file in app/assets/images/ and reference it with image_tag.

Layer 1 — Inspect the original

# run inside the Rails container (e.g. docker compose exec llamapress bash)
identify /rails/app/assets/images/<filename>

This returns dimensions, resolution, and color space. Note the aspect ratio — a wide/horizontal logo behaves differently from a tall/vertical one when you square it up.

Layer 2 — Trim whitespace & make the background transparent

# one pass: background → transparent, then auto-crop to the content
convert <input>.png \
  -fuzz 15% -transparent white \
  -bordercolor none -border 1x1 \
  -fuzz 20% -trim +repage \
  <output>-trimmed.png

Why each flag:

  • -fuzz 15% -transparent white — matches near-white pixels (not just pure #FFFFFF), removing the background fringe
  • -bordercolor none -border 1x1 — adds a 1px transparent border so -trim works even if the artwork touches an edge
  • -fuzz 20% -trim +repage — auto-crops the canvas to the content's bounding box; +repage resets the virtual canvas metadata so later operations see the new size

Layer 3 — Build padded variants

Read the trimmed dimensions, then extend the canvas (not the image).

Square only if the logo is roughly square. Forcing a wide banner logo into a square canvas adds huge transparent bars above and below it, so the logo renders tiny inside its box. Use the square path for icons, avatars, and favicons. For anything wider than about 2:1, use the proportional padding in the next block instead.

Square path — for icons and avatars:

W=$(identify -format '%w' logo-trimmed.png)
H=$(identify -format '%h' logo-trimmed.png)
if [ "$W" -gt "$H" ]; then SIZE=$W; else SIZE=$H; fi

# Tight square — centered, no extra padding
convert logo-trimmed.png -background none -gravity center \
  -extent ${SIZE}x${SIZE} logo-square.png

# Padded square — 25% breathing room
PAD=$((SIZE + SIZE/4))
convert logo-trimmed.png -background none -gravity center \
  -extent ${PAD}x${PAD} logo-padded.png

# Wide padded square — 50% breathing room
WPAD=$((SIZE + SIZE/2))
convert logo-trimmed.png -background none -gravity center \
  -extent ${WPAD}x${WPAD} logo-widepad.png

Proportional path — for wide/horizontal logos:

Pad each side by a fraction of that side, so the aspect ratio barely moves:

PAD_W=$((W + W/16))   # +6% width
PAD_H=$((H + H/8))    # +12% height

convert logo-trimmed.png -background none -gravity center \
  -extent ${PAD_W}x${PAD_H} logo-padded.png

Then verify the result actually has transparency:

identify -format '%[channels] - opaque=%[opaque]' logo-padded.png
# expected: srgba - opaque=False   (alpha channel present, not fully opaque)

Layer 4 — Recolor the logo white for a dark header

A dark-colored logo disappears on a dark header or footer. Do not put it back inside a white pill — that reintroduces the box you just removed. Instead keep the alpha channel and replace only the RGB values:

# 1. Save the shape (the alpha channel) on its own
convert logo-trimmed.png -alpha extract /tmp/alpha.png

# 2. Flood the whole image white, discarding its alpha
convert logo-trimmed.png -fill white -colorize 100 -alpha off /tmp/white-fill.png

# 3. Paste the saved shape back on as the alpha channel
convert /tmp/white-fill.png /tmp/alpha.png \
  -alpha off -compose copy_opacity -composite logo-white.png

# 4. Pad it exactly like the color version so both files swap 1:1 in the view
convert logo-white.png -background none -gravity center \
  -extent ${PAD_W}x${PAD_H} logo-white-padded.png

Ship both files. Use the color version on light backgrounds and the white version on dark ones. Any solid color works — swap white in step 2 for '#4C3F6D' or whatever the design calls for.

Layer 5 — Swap into the view

<%# app/views/pages/home.html.erb (or wherever the placeholder lives) %>
<%= image_tag "logo-padded.png", alt: "Company Name", class: "w-48 h-48 object-contain" %>

No path prefix is needed for files in app/assets/images/. Remove any old inline SVG, emoji, or gradient div that was standing in as a placeholder before the real image existed. Also remove any bg-white wrapper that existed only to make a dark logo visible — Layer 4 replaced the need for it.


Real example — the Metric Motors header logo (Aug 2026)

The starting point: a wide blue-on-white PNG at app/assets/images/metric/logo.png, 1200×400, rendered at h-7 inside a white rounded pill on a dark header. Three problems at once — a white box that did not belong, a logo too small to read, and a lot of dead space inside the file.

Step 1 — Inspect

identify app/assets/images/metric/logo.png
# => app/assets/images/metric/logo.png PNG 1200x400 1200x400+0+0 8-bit sRGB

Step 2 — Trim the whitespace and drop the white background

convert app/assets/images/metric/logo.png \
  -fuzz 12% -transparent white \
  -bordercolor none -border 1x1 \
  -fuzz 15% -trim +repage \
  /tmp/metric-trimmed.png

identify /tmp/metric-trimmed.png
# => /tmp/metric-trimmed.png PNG 984x225
identify -format '%[channels] - opaque=%[opaque]' /tmp/metric-trimmed.png
# => srgba - opaque=false   (alpha present — good)

1200×400 became 984×225. Roughly 4:1 — a banner, not an icon.

Step 3 — Pad proportionally, not square

W=$(identify -format '%w' /tmp/metric-trimmed.png)  # 984
H=$(identify -format '%h' /tmp/metric-trimmed.png)  # 225
PAD_W=$((W + W/16))   # +6% width  = 1045
PAD_H=$((H + H/8))    # +12% height = 253

convert /tmp/metric-trimmed.png -background none -gravity center \
  -extent ${PAD_W}x${PAD_H} /tmp/metric-padded.png

identify /tmp/metric-padded.png
# => PNG 1045x253 srgba opaque=false

A square canvas here would have been 984×984 — about 76% empty space.

Step 4 — Build the white version for the dark header and footer

convert /tmp/metric-trimmed.png -alpha extract /tmp/alpha.png
convert /tmp/metric-trimmed.png -fill white -colorize 100 -alpha off /tmp/white-fill.png
convert /tmp/white-fill.png /tmp/alpha.png \
  -alpha off -compose copy_opacity -composite /tmp/metric-white.png

convert /tmp/metric-white.png -background none -gravity center \
  -extent ${PAD_W}x${PAD_H} /tmp/metric-white-padded.png

Step 5 — Promote the files into the app

cp /tmp/metric-padded.png        app/assets/images/metric/logo.png              # color, trimmed
cp /tmp/metric-padded.png        app/assets/images/metric/logo-transparent.png  # alias
cp /tmp/metric-white-padded.png  app/assets/images/metric/logo-white.png        # for dark backgrounds

identify app/assets/images/metric/logo-white.png
# => PNG 1045x253 srgba (graya) opaque=false

Step 6 — Swap it into app/views/layouts/marketing.html.erb

Drop the white pill, switch to the white file, and size it up:

<!-- BEFORE (header and footer both looked like this) -->
<span class="inline-flex items-center bg-white rounded px-2.5 py-1.5">
  <%= image_tag "metric/logo.png", alt: "Metric Motors", class: "h-7 w-auto" %>
</span>

<!-- AFTER (header) -->
<%= image_tag "metric/logo-white.png", alt: "Metric Motors", class: "h-9 sm:h-10 w-auto object-contain" %>

<!-- AFTER (footer) -->
<%= image_tag "metric/logo-white.png", alt: "Metric Motors", class: "h-9 w-auto object-contain" %>

Step 7 — Reload

rm -f tmp/restart.txt && touch tmp/restart.txt

The lesson worth carrying: the white pill was a workaround for a dark logo on a dark header. Once the logo itself is transparent and recolorable, the workaround can be deleted instead of restyled.


Gotchas (the hard-won stuff)

  • Use shell arithmetic ($((...))) for the padding math, not bcbc is often not installed in the container, and the failure is a silent empty variable.
  • -extent needs -background none, or the new canvas area fills with white and you've undone the transparency you just created.
  • The 1px transparent border before -trim is not optional — if the artwork touches the image edge, -trim has no uniform border to detect and either crops nothing or crops wrong.
  • Always +repage after -trim. Without it the virtual canvas keeps the original offset/size, and later -extent/-gravity operations position the image bizarrely.
  • Tune the fuzz per image. 15–20% handles typical JPEG-artifact fringe around a white background; drop it if the logo itself contains light grays that are getting eaten, raise it if a halo of off-white pixels survives.
  • ImageMagick 7 renames convert to magick. If convert is missing, try magick (and magick identify). Check with command -v convert magick.
  • Run the commands where the asset lives. On a Leo box the Rails app is inside the llamapress container (/rails/app/assets/images/) — a host-side path won't be the same file.
  • Do not force a wide logo into a square canvas. -extent ${SIZE}x${SIZE} on a 4:1 banner adds transparent bars that eat three quarters of the file, so the logo renders tiny inside its own box. Pad proportionally instead.
  • -colorize 100 destroys the alpha channel, which is why Layer 4 extracts the alpha first and composites it back with -compose copy_opacity. Recoloring in one pass gives you a solid white rectangle.
  • New assets may need a recompile in production-style setups. In development the asset pipeline picks the file up automatically; if the image 404s, clobber assets and restart the web container.

Files this pattern touches

app/assets/images/<logo>-trimmed.png     (generated)
app/assets/images/<logo>-square.png      (generated)
app/assets/images/<logo>-padded.png      (generated)
app/views/<wherever the image renders>.html.erb

How to adapt to your schema

  1. Swap white in -transparent white for whatever the actual background color is ('#f5f5f5', etc.) — identify -format '%[pixel:p{0,0}]' input.png reads the top-left pixel if you're unsure.
  2. Pick the variant the layout needs: tight square for avatars/favicons, 25% padding for cards, 50% for hero placements where the logo shouldn't touch the container edge.
  3. For non-square targets (e.g. a 3:1 navbar strip), pass the exact geometry to -extent (-extent 900x300) instead of computing a square.
  4. If the image is user-uploaded via Active Storage rather than a static asset, apply the same flags through a variant (image.variant(fuzz: '15%', transparent: 'white', trim: true)) instead of shelling out.

Common flags reference

Flag Purpose
-fuzz N% Tolerance for matching similar colors (0% = exact, 100% = everything)
-transparent color Make all pixels matching color transparent
-trim Auto-crop to remove uniform border
+repage Reset virtual canvas after trim
-background none Fill extra canvas area with transparency
-gravity center Anchor position for extent/resize
-extent WxH Resize the canvas (not the image)
-strip Remove metadata/EXIF
-quality N Compression level (0–100)