sharp icon indicating copy to clipboard operation
sharp copied to clipboard

Trim removing too much when transparent vs black

Open greghesp opened this issue 5 years ago • 7 comments

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

greghesp avatar Apr 16 '20 13:04 greghesp

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.

lovell avatar Apr 18 '20 14:04 lovell

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

SilenceLeo avatar Jun 11 '20 17:06 SilenceLeo

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

miltoncandelero avatar Jan 21 '22 16:01 miltoncandelero

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

cryppadotta avatar Feb 05 '22 18:02 cryppadotta

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

miltoncandelero avatar Mar 28 '22 18:03 miltoncandelero

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

rawpixel-vincent avatar May 07 '22 10:05 rawpixel-vincent

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.

lovell avatar Jul 05 '22 17:07 lovell

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

lovell avatar Sep 05 '22 09:09 lovell