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
identifythe original to learn its dimensions and aspect ratio.- One
convertpass:-transparent white(with fuzz) +-trim +repage→ trimmed, transparent PNG. - Read the trimmed width/height, take the larger side, and
-extentonto a transparent square canvas (add 25–50% padding if the design needs breathing room). - Verify transparency with
identify -format '%[opaque]'— expectFalse. - Drop the file in
app/assets/images/and reference it withimage_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-trimworks even if the artwork touches an edge-fuzz 20% -trim +repage— auto-crops the canvas to the content's bounding box;+repageresets 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, notbc—bcis often not installed in the container, and the failure is a silent empty variable. -extentneeds-background none, or the new canvas area fills with white and you've undone the transparency you just created.- The 1px transparent border before
-trimis not optional — if the artwork touches the image edge,-trimhas no uniform border to detect and either crops nothing or crops wrong. - Always
+repageafter-trim. Without it the virtual canvas keeps the original offset/size, and later-extent/-gravityoperations 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
converttomagick. Ifconvertis missing, trymagick(andmagick identify). Check withcommand -v convert magick. - Run the commands where the asset lives. On a Leo box the Rails app is inside the
llamapresscontainer (/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 100destroys 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
- Swap
whitein-transparent whitefor whatever the actual background color is ('#f5f5f5', etc.) —identify -format '%[pixel:p{0,0}]' input.pngreads the top-left pixel if you're unsure. - 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.
- For non-square targets (e.g. a 3:1 navbar strip), pass the exact geometry to
-extent(-extent 900x300) instead of computing a square. - 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) |