{"slug":"imagemagick-image-cropping","meta":{"title":"ImageMagick Image Cropping \u0026 Transparency","slug":"imagemagick-image-cropping","category":"Media","summary":"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.","tags":["imagemagick","images","assets","logo","transparency","recolor","cli"],"status":"stable","visibility":"public","source_project":"llamapress.ai","layers":["view"]},"body":"# ImageMagick Image Cropping \u0026 Transparency\n\n\u003e ⚠️ **Cookbook example — not live code.** (KEEP THIS CALLOUT.) Every code block below\n\u003e is an **example snippet**, **not part of the llamapress.ai codebase**, and **not\n\u003e running on this server**. This is a reference recipe for a **Leo instance (an AI coding\n\u003e agent) to implement in its own app** — read it to understand the pattern, then recreate\n\u003e it there.\n\nA user uploads a logo or icon that arrives with a big white background, off-center\nplacement, or a wildly non-square aspect ratio. This recipe turns that raw upload into a\nclean asset — whitespace trimmed, background transparent, centered in a square canvas\nwith optional breathing room — using nothing but the `convert` / `identify` CLI, then\nswaps it into the view with `image_tag`.\n\n\u003e **When to use:** a real image file (logo, icon, product shot) needs cleanup before it\n\u003e looks right in the UI. **When not to:** you need per-user runtime processing at scale —\n\u003e that's Active Storage variants (`image.variant(...)`), not one-off shell commands.\n\n---\n\n## The 80/20 in one breath\n\n1. `identify` the original to learn its dimensions and aspect ratio.\n2. One `convert` pass: `-transparent white` (with fuzz) + `-trim +repage` → trimmed,\n   transparent PNG.\n3. Read the trimmed width/height, take the larger side, and `-extent` onto a\n   transparent square canvas (add 25–50% padding if the design needs breathing room).\n4. Verify transparency with `identify -format '%[opaque]'` — expect `False`.\n5. Drop the file in `app/assets/images/` and reference it with `image_tag`.\n\n---\n\n## Layer 1 — Inspect the original\n\n```bash\n# run inside the Rails container (e.g. docker compose exec llamapress bash)\nidentify /rails/app/assets/images/\u003cfilename\u003e\n```\n\nThis returns dimensions, resolution, and color space. Note the aspect ratio — a\nwide/horizontal logo behaves differently from a tall/vertical one when you square it up.\n\n## Layer 2 — Trim whitespace \u0026 make the background transparent\n\n```bash\n# one pass: background → transparent, then auto-crop to the content\nconvert \u003cinput\u003e.png \\\n  -fuzz 15% -transparent white \\\n  -bordercolor none -border 1x1 \\\n  -fuzz 20% -trim +repage \\\n  \u003coutput\u003e-trimmed.png\n```\n\n**Why each flag:**\n\n- `-fuzz 15% -transparent white` — matches near-white pixels (not just pure `#FFFFFF`),\n  removing the background fringe\n- `-bordercolor none -border 1x1` — adds a 1px transparent border so `-trim` works even\n  if the artwork touches an edge\n- `-fuzz 20% -trim +repage` — auto-crops the canvas to the content's bounding box;\n  `+repage` resets the virtual canvas metadata so later operations see the new size\n\n## Layer 3 — Build padded variants\n\nRead the trimmed dimensions, then extend the canvas (not the image).\n\n\u003e **Square only if the logo is roughly square.** Forcing a wide banner logo into a square\n\u003e canvas adds huge transparent bars above and below it, so the logo renders tiny inside\n\u003e its box. Use the square path for icons, avatars, and favicons. For anything wider than\n\u003e about 2:1, use the proportional padding in the next block instead.\n\n**Square path — for icons and avatars:**\n\n```bash\nW=$(identify -format '%w' logo-trimmed.png)\nH=$(identify -format '%h' logo-trimmed.png)\nif [ \"$W\" -gt \"$H\" ]; then SIZE=$W; else SIZE=$H; fi\n\n# Tight square — centered, no extra padding\nconvert logo-trimmed.png -background none -gravity center \\\n  -extent ${SIZE}x${SIZE} logo-square.png\n\n# Padded square — 25% breathing room\nPAD=$((SIZE + SIZE/4))\nconvert logo-trimmed.png -background none -gravity center \\\n  -extent ${PAD}x${PAD} logo-padded.png\n\n# Wide padded square — 50% breathing room\nWPAD=$((SIZE + SIZE/2))\nconvert logo-trimmed.png -background none -gravity center \\\n  -extent ${WPAD}x${WPAD} logo-widepad.png\n```\n\n**Proportional path — for wide/horizontal logos:**\n\nPad each side by a fraction of that side, so the aspect ratio barely moves:\n\n```bash\nPAD_W=$((W + W/16))   # +6% width\nPAD_H=$((H + H/8))    # +12% height\n\nconvert logo-trimmed.png -background none -gravity center \\\n  -extent ${PAD_W}x${PAD_H} logo-padded.png\n```\n\nThen verify the result actually has transparency:\n\n```bash\nidentify -format '%[channels] - opaque=%[opaque]' logo-padded.png\n# expected: srgba - opaque=False   (alpha channel present, not fully opaque)\n```\n\n## Layer 4 — Recolor the logo white for a dark header\n\nA dark-colored logo disappears on a dark header or footer. Do not put it back inside a\nwhite pill — that reintroduces the box you just removed. Instead keep the alpha channel\nand replace only the RGB values:\n\n```bash\n# 1. Save the shape (the alpha channel) on its own\nconvert logo-trimmed.png -alpha extract /tmp/alpha.png\n\n# 2. Flood the whole image white, discarding its alpha\nconvert logo-trimmed.png -fill white -colorize 100 -alpha off /tmp/white-fill.png\n\n# 3. Paste the saved shape back on as the alpha channel\nconvert /tmp/white-fill.png /tmp/alpha.png \\\n  -alpha off -compose copy_opacity -composite logo-white.png\n\n# 4. Pad it exactly like the color version so both files swap 1:1 in the view\nconvert logo-white.png -background none -gravity center \\\n  -extent ${PAD_W}x${PAD_H} logo-white-padded.png\n```\n\nShip both files. Use the color version on light backgrounds and the white version on\ndark ones. Any solid color works — swap `white` in step 2 for `'#4C3F6D'` or whatever\nthe design calls for.\n\n## Layer 5 — Swap into the view\n\n```erb\n\u003c%# app/views/pages/home.html.erb (or wherever the placeholder lives) %\u003e\n\u003c%= image_tag \"logo-padded.png\", alt: \"Company Name\", class: \"w-48 h-48 object-contain\" %\u003e\n```\n\nNo path prefix is needed for files in `app/assets/images/`. Remove any old inline SVG,\nemoji, or gradient `div` that was standing in as a placeholder before the real image\nexisted. Also remove any `bg-white` wrapper that existed only to make a dark logo\nvisible — Layer 4 replaced the need for it.\n\n---\n\n## Real example — the Metric Motors header logo (Aug 2026)\n\nThe starting point: a wide blue-on-white PNG at `app/assets/images/metric/logo.png`,\n1200×400, rendered at `h-7` inside a white rounded pill on a dark header. Three problems\nat once — a white box that did not belong, a logo too small to read, and a lot of dead\nspace inside the file.\n\n**Step 1 — Inspect**\n\n```bash\nidentify app/assets/images/metric/logo.png\n# =\u003e app/assets/images/metric/logo.png PNG 1200x400 1200x400+0+0 8-bit sRGB\n```\n\n**Step 2 — Trim the whitespace and drop the white background**\n\n```bash\nconvert app/assets/images/metric/logo.png \\\n  -fuzz 12% -transparent white \\\n  -bordercolor none -border 1x1 \\\n  -fuzz 15% -trim +repage \\\n  /tmp/metric-trimmed.png\n\nidentify /tmp/metric-trimmed.png\n# =\u003e /tmp/metric-trimmed.png PNG 984x225\nidentify -format '%[channels] - opaque=%[opaque]' /tmp/metric-trimmed.png\n# =\u003e srgba - opaque=false   (alpha present — good)\n```\n\n1200×400 became 984×225. Roughly 4:1 — a banner, not an icon.\n\n**Step 3 — Pad proportionally, not square**\n\n```bash\nW=$(identify -format '%w' /tmp/metric-trimmed.png)  # 984\nH=$(identify -format '%h' /tmp/metric-trimmed.png)  # 225\nPAD_W=$((W + W/16))   # +6% width  = 1045\nPAD_H=$((H + H/8))    # +12% height = 253\n\nconvert /tmp/metric-trimmed.png -background none -gravity center \\\n  -extent ${PAD_W}x${PAD_H} /tmp/metric-padded.png\n\nidentify /tmp/metric-padded.png\n# =\u003e PNG 1045x253 srgba opaque=false\n```\n\nA square canvas here would have been 984×984 — about 76% empty space.\n\n**Step 4 — Build the white version for the dark header and footer**\n\n```bash\nconvert /tmp/metric-trimmed.png -alpha extract /tmp/alpha.png\nconvert /tmp/metric-trimmed.png -fill white -colorize 100 -alpha off /tmp/white-fill.png\nconvert /tmp/white-fill.png /tmp/alpha.png \\\n  -alpha off -compose copy_opacity -composite /tmp/metric-white.png\n\nconvert /tmp/metric-white.png -background none -gravity center \\\n  -extent ${PAD_W}x${PAD_H} /tmp/metric-white-padded.png\n```\n\n**Step 5 — Promote the files into the app**\n\n```bash\ncp /tmp/metric-padded.png        app/assets/images/metric/logo.png              # color, trimmed\ncp /tmp/metric-padded.png        app/assets/images/metric/logo-transparent.png  # alias\ncp /tmp/metric-white-padded.png  app/assets/images/metric/logo-white.png        # for dark backgrounds\n\nidentify app/assets/images/metric/logo-white.png\n# =\u003e PNG 1045x253 srgba (graya) opaque=false\n```\n\n**Step 6 — Swap it into `app/views/layouts/marketing.html.erb`**\n\nDrop the white pill, switch to the white file, and size it up:\n\n```erb\n\u003c!-- BEFORE (header and footer both looked like this) --\u003e\n\u003cspan class=\"inline-flex items-center bg-white rounded px-2.5 py-1.5\"\u003e\n  \u003c%= image_tag \"metric/logo.png\", alt: \"Metric Motors\", class: \"h-7 w-auto\" %\u003e\n\u003c/span\u003e\n\n\u003c!-- AFTER (header) --\u003e\n\u003c%= image_tag \"metric/logo-white.png\", alt: \"Metric Motors\", class: \"h-9 sm:h-10 w-auto object-contain\" %\u003e\n\n\u003c!-- AFTER (footer) --\u003e\n\u003c%= image_tag \"metric/logo-white.png\", alt: \"Metric Motors\", class: \"h-9 w-auto object-contain\" %\u003e\n```\n\n**Step 7 — Reload**\n\n```bash\nrm -f tmp/restart.txt \u0026\u0026 touch tmp/restart.txt\n```\n\nThe lesson worth carrying: the white pill was a workaround for a dark logo on a dark\nheader. Once the logo itself is transparent and recolorable, the workaround can be\ndeleted instead of restyled.\n\n---\n\n## Gotchas (the hard-won stuff)\n\n- **Use shell arithmetic (`$((...))`) for the padding math, not `bc`** — `bc` is often\n  not installed in the container, and the failure is a silent empty variable.\n- **`-extent` needs `-background none`**, or the new canvas area fills with white and\n  you've undone the transparency you just created.\n- **The 1px transparent border before `-trim` is not optional** — if the artwork touches\n  the image edge, `-trim` has no uniform border to detect and either crops nothing or\n  crops wrong.\n- **Always `+repage` after `-trim`.** Without it the virtual canvas keeps the original\n  offset/size, and later `-extent`/`-gravity` operations position the image bizarrely.\n- **Tune the fuzz per image.** `15–20%` handles typical JPEG-artifact fringe around a\n  white background; drop it if the logo itself contains light grays that are getting\n  eaten, raise it if a halo of off-white pixels survives.\n- **ImageMagick 7 renames `convert` to `magick`.** If `convert` is missing, try\n  `magick` (and `magick identify`). Check with `command -v convert magick`.\n- **Run the commands where the asset lives.** On a Leo box the Rails app is inside the\n  `llamapress` container (`/rails/app/assets/images/`) — a host-side path won't be the\n  same file.\n- **Do not force a wide logo into a square canvas.** `-extent ${SIZE}x${SIZE}` on a 4:1\n  banner adds transparent bars that eat three quarters of the file, so the logo renders\n  tiny inside its own box. Pad proportionally instead.\n- **`-colorize 100` destroys the alpha channel**, which is why Layer 4 extracts the alpha\n  first and composites it back with `-compose copy_opacity`. Recoloring in one pass gives\n  you a solid white rectangle.\n- **New assets may need a recompile in production-style setups.** In development the\n  asset pipeline picks the file up automatically; if the image 404s, clobber assets and\n  restart the web container.\n\n---\n\n## Files this pattern touches\n\n```\napp/assets/images/\u003clogo\u003e-trimmed.png     (generated)\napp/assets/images/\u003clogo\u003e-square.png      (generated)\napp/assets/images/\u003clogo\u003e-padded.png      (generated)\napp/views/\u003cwherever the image renders\u003e.html.erb\n```\n\n## How to adapt to your schema\n\n1. Swap `white` in `-transparent white` for whatever the actual background color is\n   (`'#f5f5f5'`, etc.) — `identify -format '%[pixel:p{0,0}]' input.png` reads the\n   top-left pixel if you're unsure.\n2. Pick the variant the layout needs: tight square for avatars/favicons, 25% padding\n   for cards, 50% for hero placements where the logo shouldn't touch the container edge.\n3. For non-square targets (e.g. a 3:1 navbar strip), pass the exact geometry to\n   `-extent` (`-extent 900x300`) instead of computing a square.\n4. If the image is user-uploaded via Active Storage rather than a static asset, apply\n   the same flags through a variant\n   (`image.variant(fuzz: '15%', transparent: 'white', trim: true)`) instead of shelling\n   out.\n\n## Common flags reference\n\n| Flag | Purpose |\n|------|---------|\n| `-fuzz N%` | Tolerance for matching similar colors (0% = exact, 100% = everything) |\n| `-transparent color` | Make all pixels matching `color` transparent |\n| `-trim` | Auto-crop to remove uniform border |\n| `+repage` | Reset virtual canvas after trim |\n| `-background none` | Fill extra canvas area with transparency |\n| `-gravity center` | Anchor position for extent/resize |\n| `-extent WxH` | Resize the canvas (not the image) |\n| `-strip` | Remove metadata/EXIF |\n| `-quality N` | Compression level (0–100) |\n"}