sharp
sharp copied to clipboard
Trim removing too much when transparent vs black
If I pass a simple png like the below, the trim function seems to trim far too much
This is before:

but this is after:

sharp(file)
.trim()
.toFile(`./toUpload/${final}.png`)
It's worth noting that I have had success with other images, but it doesn't seem to be consistent
Hi, the "text" in this image is really an alpha layer over a black background. sharp (and libvips) treats these as separate when trimming.
It's a similar problem to #1597 however the improvement for that issue won't help here as the trim operation on the non-alpha finds the stars and doesn't bother looking at the alpha channel.
A possible enhancement might be to run two searches, over both non-alpha and alpha channels, then crop using the combined largest bounding box.
I think we can use JS solution before only trim alpha support . Although not comparable to C + +, it can be used as a temporary solution.
@lovell Do you think it is feasible?
function getTrimAlphaInfo(
pipeline: Sharp,
width: number,
height: number
): Promise<{
trimOffsetLeft: number;
trimOffsetTop: number;
width: number;
height: number;
}> {
return pipeline
.clone()
.ensureAlpha()
.extractChannel(3)
.toColourspace("b-w")
.raw()
.toBuffer()
.then((data) => {
let topTrim: number = 0;
let bottomTrim: number = 0;
let leftTrim: number = 0;
let rightTrim: number = 0;
let topStatus: boolean = true;
let bottomStatus: boolean = true;
let leftStatus: boolean = true;
let rightStatus: boolean = true;
let h: number = Math.ceil(height / 2);
const w: number = Math.ceil(width / 2);
for (let i = 0; i < h; i++) {
for (let j = 0; j < width; j++) {
if (topStatus && data[i * width + j] > 0) {
topStatus = false;
}
if (bottomStatus && data[(height - i - 1) * width + j] > 0) {
bottomStatus = false;
}
if (!topStatus && !bottomStatus) {
break;
}
}
if (!topStatus && !bottomStatus) {
break;
}
if (topStatus) topTrim++;
if (bottomStatus) bottomTrim++;
}
if (topTrim + bottomTrim >= height) {
// console.log("Is empty image.");
return {
trimOffsetLeft: width * -1,
trimOffsetTop: height * -1,
width: 0,
height: 0,
};
}
h = height - bottomTrim;
for (let i = 0; i < w; i++) {
for (let j = topTrim; j < h; j++) {
if (leftStatus && data[width * j + i] > 0) {
leftStatus = false;
}
if (rightStatus && data[width * j + width - i - 1] > 0) {
rightStatus = false;
}
if (!leftStatus && !rightStatus) {
break;
}
}
if (!leftStatus && !rightStatus) {
break;
}
if (leftStatus) leftTrim++;
if (rightStatus) rightTrim++;
}
return {
trimOffsetLeft: leftTrim * -1,
trimOffsetTop: topTrim * -1,
width: width - leftTrim - rightTrim,
height: height - topTrim - bottomTrim,
};
});
}
use extract
getTrimAlphaInfo(pipeline, width, height).then((info) => {
pipeline.extract({
left: info.trimOffsetLeft * -1,
top: info.trimOffsetTop * -1,
width: info.width,
height: info.height,
});
});
So... there is no way to trim all the transparent pixels?
@SilenceLeo thanks for this snippet, but one bug is that you shouldn't be dividing the height and width (h and w) by 2 - for example, imagine if there is a single pixel in the bottom left hand side of the image, you only search the first half of the image.
(Remove the divide by two and it works fine though)
anyone knows if this might work? https://stackoverflow.com/questions/55779553/how-do-i-trim-transparent-borders-from-my-image-in-an-efficient-way
basically, vips extract_band 3 into vips find_trim into vips crop. Could this be done? 🤔
Ok, this works but if I chain trim it doesn't I am obviously misunderstanding how to use this lib so can you shed some light?
sharp("./test/trimme.png")
.extractChannel(3)
.toColorspace("b-w")
.extend({background:{r:0,g:0,b:0,alpha:0}, top:1, left:1})
//.trim(1) // This doesn't work
.toFile("./test/trimmed.png").then(() =>{
sharp("./test/trimmed.png")
.trim(1) // We load the new exported image and that one trims corectly
.toFile("./test/trimmed2.png");
});

this is what we use to get png trimmed dimensions, thanks to silenceLeo 🙏🏻
/**
* Return bounding box information without outer transparent pixel
* Until sharp implement an equivalent trimTransparent() effect.
* @see https://github.com/lovell/sharp/issues/2166
*
* @param {import('sharp').Sharp} pipeline
* @param {number} width
* @param {number} height
*/
const getTrimAlphaInfo = async (pipeline, width, height) =>
pipeline
.ensureAlpha()
.extractChannel(3)
.toColourspace('b-w')
.raw()
.toBuffer()
.then((data) => {
let topTrim = 0;
let bottomTrim = 0;
let leftTrim = 0;
let rightTrim = 0;
let topStatus = true;
let bottomStatus = true;
let leftStatus = true;
let rightStatus = true;
let h = Math.ceil(height);
const w = Math.ceil(width);
for (let i = 0; i < h; i++) {
for (let j = 0; j < width; j++) {
if (topStatus && data[i * width + j] > 0) {
topStatus = false;
}
if (bottomStatus && data[(height - i - 1) * width + j] > 0) {
bottomStatus = false;
}
if (!topStatus && !bottomStatus) {
break;
}
}
if (!topStatus && !bottomStatus) {
break;
}
if (topStatus) {
topTrim += 1;
}
if (bottomStatus) {
bottomTrim += 1;
}
}
if (topTrim + bottomTrim >= height) {
// console.log("Is empty image.");
return {
trimOffsetLeft: width * -1,
trimOffsetTop: height * -1,
width: 0,
height: 0,
};
}
h = height - bottomTrim;
for (let i = 0; i < w; i++) {
for (let j = topTrim; j < h; j++) {
if (leftStatus && data[width * j + i] > 0) {
leftStatus = false;
}
if (rightStatus && data[width * j + width - i - 1] > 0) {
rightStatus = false;
}
if (!leftStatus && !rightStatus) {
break;
}
}
if (!leftStatus && !rightStatus) {
break;
}
if (leftStatus) {
leftTrim += 1;
}
if (rightStatus) {
rightTrim += 1;
}
}
return {
trimOffsetLeft: leftTrim * -1,
trimOffsetTop: topTrim * -1,
width: width - leftTrim - rightTrim,
height: height - topTrim - bottomTrim,
};
});
use with
const getTrimmedInfo = async (imageBuffer) => {
const image = sharp(imageBuffer, {
limitInputPixels: 500000000,
failOnError: false,
});
const { width, height, hasAlpha } = await image.metadata();
if (!hasAlpha) {
return { width, height };
}
// If the image doesn't have an alpha layer this will fail.
const info = await getTrimAlphaInfo(image, width, height);
const results = await sharp(imageBuffer, {
limitInputPixels: 500000000,
failOnError: false,
})
.extract({
left: info.trimOffsetLeft * -1,
top: info.trimOffsetTop * -1,
width: info.width,
height: info.height,
})
.toBuffer({ resolveWithObject: true });
// results.data will contain the trimmed png Buffer
// results.info contains trimmed width and height
return results.info;
};
Commit https://github.com/lovell/sharp/commit/e0d3c6e05dc6e4caacb4b3b2160178fb7e6f5ca2 changes the logic to always run separate searches over non-alpha and (if present) alpha channels, using the combined bounding box for the resultant trim. This will be in v0.31.0.
@greghesp I've added a smaller version of the example image from this issue as a test case. Please let me know if there are any licencing issues that might prevent this and I'll try to find another example, otherwise I'll assume it is OK to include as part of this repo.
v0.31.0 now available with this improvement, thanks all for the help/feedback.