-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Trim removing too much when transparent vs black #2166
Comments
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 ( (Remove the divide by two and it works fine though) |
anyone knows if this might work? basically, Ok, this works but if I chain trim it doesn't 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 e0d3c6e 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. |
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:
It's worth noting that I have had success with other images, but it doesn't seem to be consistent
The text was updated successfully, but these errors were encountered: