Compose remote images with cURL and ImageMagick
Use cURL to download a background and an overlay, then ImageMagick to resize them and save one JPEG. The Bash script below checks both HTTPS transfers and removes its partial files if a download or image-processing step fails.
Check the tools
This recipe was tested on Linux with Bash 5.3.15, cURL 8.22.0, and ImageMagick 7.1.2-31 with JPEG
and PNG support. These are tested versions, not required patch releases. Use maintained builds of
cURL and ImageMagick 7.
The cURL build must be at least 8.4: earlier versions cannot enforce
--max-filesize when the response size is unknown.
Check bash --version, curl --disable --version, and magick -version before saving the script.
Use single-frame RGB images with an opaque background; the PNG overlay may have transparency.
The example uses a JPEG photo and a PNG logo from NASA. Follow
NASA’s image and media usage guidance,
including its restrictions on the insignia and endorsement.
Save the composition script
Each source is downloaded into a new private directory. Passing <(curl ...) directly to
ImageMagick would make it harder to check the background transfer’s exit status. Saving and checking
the actual inputs also avoids testing a URL with one request and processing a second unchecked
response.
Save this as compose.sh. It takes two trusted HTTPS URLs and a new output directory whose parent
already exists:
#!/bin/bash
set -euo pipefail
if [ "$#" -ne 3 ]; then
echo "Usage: $0 <background-https-url> <overlay-https-url> <new-output-directory>" >&2
exit 1
fi
background_url=$1
overlay_url=$2
output_dir=$3
if ! command -v curl >/dev/null || ! command -v magick >/dev/null; then
echo "Install cURL and ImageMagick 7 before running this script" >&2
exit 1
fi
[[ $output_dir = /* ]] || output_dir=$PWD/$output_dir
if ! mkdir -m 700 -- "$output_dir"; then
echo "Use a new output directory" >&2
exit 1
fi
cleanup() {
local status=$?
rm -f -- "$output_dir/background.image" "$output_dir/overlay.image" "$output_dir/magick.log"
if [ "$status" -ne 0 ]; then
rm -f -- "$output_dir/result.jpg"
rmdir -- "$output_dir"
fi
}
trap cleanup EXIT
fetch_image() {
curl --disable --globoff --fail --silent --show-error --location \
--proto '=https' --proto-redir '=https' --max-redirs 3 \
--connect-timeout 10 --max-time 30 --retry 2 --retry-max-time 60 \
--max-filesize 10485760 --output "$2" --url "$1"
}
fetch_image "$background_url" "$output_dir/background.image"
fetch_image "$overlay_url" "$output_dir/overlay.image"
cd -P -- "$output_dir"
output_dir=$PWD
if ! magick -limit memory 256MiB -limit map 512MiB -limit disk 1GiB \
-limit width 10000 -limit height 10000 \
background.image -resize '1600x1600>' \
\( overlay.image -resize '300x300>' \) \
-gravity southeast -geometry +20+20 -composite result.jpg 2>magick.log ||
[ -s magick.log ]; then
cat magick.log >&2
echo "ImageMagick reported a failure or diagnostic" >&2
exit 1
fi
[ -s result.jpg ] || { echo "No image produced" >&2; exit 1; }
echo "Composite saved: $output_dir/result.jpg"
Run the example
Run the saved script from the directory containing compose.sh:
bash compose.sh \
'https://images-assets.nasa.gov/image/as11-40-5874/as11-40-5874~orig.jpg' \
'https://www.nasa.gov/wp-content/uploads/2023/04/nasa-logo-web-rgb.png' \
nasa-composite
Open nasa-composite/result.jpg: it should show the moon-landing photo with the NASA logo in the
bottom-right corner. The logo’s white rectangle belongs to the source PNG.
The background fits within 1600 × 1600 pixels and the overlay within 300 × 300, preserving aspect
ratios. The quoted > in each resize geometry
prevents enlargement. -gravity southeast -geometry +20+20 places the overlay canvas 20 pixels
from the right and bottom edges. If you substitute smaller images, leave enough room for the overlay
and both margins; the script does not shrink an oversized overlay to fit the background.
A successful job prints Composite saved: followed by the absolute path and leaves only
result.jpg in the new directory. Running it again with the same directory fails before downloading
anything and preserves the previous result. Keep other processes out of the job directory while
the script runs.
Handle failures and retries
HTTP errors, transfers exceeding 10 MiB per source, and undecodable images produce a nonzero exit status and remove the new job directory. Fix the URL or input and retry with the same directory name. An existing directory or a missing parent is a setup failure; the script leaves existing storage alone.
An aborted transfer can briefly write more than the limit; cleanup removes that temporary file.
ImageMagick can recover pixels from a damaged JPEG while reporting warnings and returning success. Here, any diagnostic written to stderr also fails the job, prints the diagnostic, and triggers cleanup. This deliberately rejects warnings as well as errors. It cannot detect damage that a decoder accepts silently, so inspect the finished image before publishing it.
The download helper allows three HTTPS redirects and two retries for cURL’s retryable failures. Each attempt has a 30-second timeout. Its 60-second retry window controls whether another attempt starts; an attempt already running may finish after that window.
Set bounds for trusted inputs
A small compressed file can decode into a large image. The script caps input dimensions and ImageMagick pixel-cache resources, which may spill to disk. These settings do not cap total process memory or processing time. Use ImageMagick’s security policy and operating-system limits when running production workers.
HTTPS-only requests still allow URLs and redirects to private network services. Use trusted sources;
an unrestricted public URL fetcher needs separate network and origin controls. Keep TLS verification
enabled. --disable, placed first, prevents cURL from loading a default configuration file that
could change these download options.
For managed resizing and compositing workflows, explore Transloadit’s image processing service.
