Skip to content
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

Closed
greghesp opened this issue Apr 16, 2020 · 8 comments
Closed

Trim removing too much when transparent vs black #2166

greghesp opened this issue Apr 16, 2020 · 8 comments

Comments

@greghesp
Copy link

greghesp commented Apr 16, 2020

If I pass a simple png like the below, the trim function seems to trim far too much

This is before:
001 Merry Christmas 1

but this is after:
image

      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

@lovell
Copy link
Owner

lovell commented Apr 18, 2020

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.

@SilenceLeo
Copy link

SilenceLeo commented Jun 11, 2020

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,
    });
});

@miltoncandelero
Copy link

So... there is no way to trim all the transparent pixels?

@cryppadotta
Copy link

@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)

@miltoncandelero
Copy link

miltoncandelero commented Mar 28, 2022

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");
});

image

@rawpixel-vincent
Copy link

rawpixel-vincent commented May 7, 2022

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;
};

@lovell
Copy link
Owner

lovell commented Jul 5, 2022

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.

@lovell
Copy link
Owner

lovell commented Sep 5, 2022

v0.31.0 now available with this improvement, thanks all for the help/feedback.

@lovell lovell closed this as completed Sep 5, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants