Keeping Track of Useful Shell / CMD Commands / Scripts
I want to keep track of useful shell / cmd commands that I might want to use in the future.
ImageMagick Scripts
Creating Correctly Sized Non-Transparent Banner Image
Task:
Need ImageMagick command / shell script that resizes an image to a certain size. If the image cannot fit to the specified size exactly, then a background color should be added to the image to make it confirm to the specified size. The background color should be determined by:
- Check the color of the top left, top right, bottom left, and bottom right pixels of the image. If they are all the same, then use that color as the background color to extend the image.
- If the image has a transparent background, then you should check the most prevalent color in the image and use the opposite color of that. (E.g., if the most prevalent color in an image is white, then use black as the background color to extend the image)
- Else, use the provided fallback background color.
Use:
I need to use this script for banner images for article previews / for articles. (See below)
In the case of the image above, the image would be resized, and then, because the image's aspect ratio does not fit what I want it to be, the image would have a white background added to it that extends the image to the preferred aspect ratio.
Implementation:
import { Magick } from 'magickwand.js';
import path, { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import child_process from 'node:child_process';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
/**
*
* @param {Magick.Image} im
*/
function handleTransparent(im,{desiredWidth,desiredHeight,width,height,widthAndHeight}) {
// Create temp image for dithering
im.write(path.resolve(__dirname,'temp.png'));
const im2 = new Magick.Image(path.resolve(__dirname,'temp.png'));
// dither image to get the most frequent colors
im2.orderedDither('4x4');
im2.write(path.resolve(__dirname,'temp.png'));
// https://imagemagick.org/Usage/quantize/ Get the most frequent colors
child_process.exec(`magick ${path.resolve(__dirname,'temp.png')} -format %c -depth 8 histogram:info:-`,function (error,stdout,stderr) {
if (error) throw error;
if (stderr) throw new Error(stderr);
const matches = stdout.matchAll(/^\s+(?<count>\d+):\s\((?<red>\d+),(?<green>\d+),(?<blue>\d+),(?<alpha>\d+)\)/gm);
if (!!!matches) throw new Error(`There were no matches for getting image colors: ${stdout}`);
const matchArr = [];
for (let match of matches) {
const obj = { count: parseInt(match.groups.count), red: parseInt(match.groups.red), green: parseInt(match.groups.green), blue: parseInt(match.groups.blue), alpha: parseInt(match.groups.alpha) }
if(obj.alpha!==0) matchArr.push(obj);
}
matchArr.sort((a,b) => b.count-a.count);
// Get tha main color in the image thgat is not black or white, and set the background color to the opposite of that color
var mainColor;
for (let obj of matchArr) {
if (obj.red===255&&obj.green===255&&obj.blue===255) {
continue;
} else if (obj.red===0&&obj.green===0&&obj.blue===0) {
continue;
} else {
mainColor = obj;
break;
}
}
if (!!!mainColor) throw new Error("Unable to get main color from object.");
const color = new Magick.ColorRGB();
color.red(255 - mainColor.red);
color.green(255 - mainColor.green);
color.blue(255 - mainColor.blue);
const canvas = new Magick.Image(widthAndHeight,color);
const geom = new Magick.Geometry(desiredWidth/2,desiredHeight/2)
geom.xOff((desiredWidth-width)/2);
geom.yOff((desiredHeight-height)/2);
canvas.composite(im,geom,2); // https://www.imagemagick.org/Magick++/Enumerations.html#CompositeOperator
canvas.write(path.resolve(__dirname,'output.png'));
})
}
/**
* Resize / add background to banner image to prevent CLS and so that text is readable on both
* light and dark modes
* @param {string} pathToInput Path to Input
* @param {string} widthAndHeight Desired with and height in the form widthxheight
*/
async function convertBannerImage(pathToInput,widthAndHeight) {
const TRANSPARENT = "#0000000000000000";
// Get the desired width and height of the banner image
let desiredWidth, desiredHeight;
const matches = widthAndHeight.match(/\d+(\.\d+)?/g);
[desiredWidth,desiredHeight] = matches;
if (!!!desiredWidth||!!!desiredHeight) throw new Error("Something went wrong getting the desired witdh / height of the image.");
desiredWidth = parseInt(desiredWidth);
desiredHeight = parseInt(desiredHeight);
// Resize the image using ImageMagick - keep aspect ratio
let im = new Magick.Image(pathToInput);
im.resize(widthAndHeight);
// Get the new width and height of the image
const [width,height] = [im.size().width(),im.size().height()];
// Get the colors od the pixels
const [tl,tr,bl,br] = [
im.pixelColor(0,0),
im.pixelColor(width,0),
im.pixelColor(0,height),
im.pixelColor(width,height),
];
// If the image is transparent, then you need to add a background color to the image and resize appropriatly
if (tl.toString()===TRANSPARENT||tr.toString()===TRANSPARENT||bl.toString()===TRANSPARENT||br.toString()===TRANSPARENT) {
handleTransparent(im,{desiredWidth,desiredHeight,width,height,widthAndHeight});
} else if (width!==desiredWidth||height!==desiredHeight) {
var bgColor;
if (tl.toString()===tr.toString()&&tl.toString()===br.toString()&&tl.toString()===bl.toString()) {
bgColor = tl;
} else {
bgColor = im.backgroundColor();
}
// If the background color is not transparent, then create a canvas with the
// specified background color and overlay the image on the colored canvas
if (bgColor.toString()!==TRANSPARENT) {
const canvas = new Magick.Image(widthAndHeight,bgColor);
const geom = new Magick.Geometry(desiredWidth/2,desiredHeight/2)
geom.xOff((desiredWidth-width)/2);
geom.yOff((desiredHeight-height)/2);
canvas.composite(im,geom,2); // https://www.imagemagick.org/Magick++/Enumerations.html#CompositeOperator
canvas.write(path.resolve(__dirname,'output.png'));
} else {
// If the background color id transparent, then you need to add a background color to the image and resize appropriatly
handleTransparent(im,{desiredWidth,desiredHeight,width,height,widthAndHeight});
}
// If the image is the correct size after resize, then return the image
} else {
im.write(path.resolve(__dirname,'output.png'))
}
}
Description:
Given an image, and a desired width and height of a banner image, in the form widthxheight
, first resize the image using the Magick++ equivalent of magick -resize
(preserving the aspect ratio of the original image), then:
- if the image is now in the correct size, return it.
- Else, if the image is not in the correct size:
- If the background color of the image can be detected, use the detected background color.
- If all four corners have the same pixel color and that pixel color is not transparent, then use that pixel color as the background.
- If the background color cannot be detected, then through a process of quantization and dithering, find the most frequent non black / white background color of the image, and set the background color of the appropriately sized image to the opposite of the found most frequent color in the image.
Example of Dithered Image
Note: We use the composite operator from Magick++ to center the image correctly.
Comments
There are currently no comments to show for this article.