Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ package-lock.json
.astro
docs/dist
release-notes.md
.cache
Copy link
Author

@eddienubes eddienubes Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I use clang for suggestions and type checking:

  • it creates a .cache folder by default
  • needs compile_commands.json flags to know where to pull 3rd-party header files from.
    I thought future contributors may benefit from this change.

compile_commands.json
1 change: 1 addition & 0 deletions lib/constructor.js
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ const Sharp = function (input, options) {
trimBackground: [],
trimThreshold: -1,
trimLineArt: false,
trimMargin: 0,
dilateWidth: 0,
erodeWidth: 0,
gamma: 0,
Expand Down
2 changes: 2 additions & 0 deletions lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1608,6 +1608,8 @@ declare namespace sharp {
threshold?: number | undefined;
/** Does the input more closely resemble line art (e.g. vector) rather than being photographic? (optional, default false) */
lineArt?: boolean | undefined;
/** Applies margin in pixels to trim edges leaving extra space around trimmed content. (optional, default 0) */
margin?: number | undefined;
}

interface RawOptions {
Expand Down
16 changes: 16 additions & 0 deletions lib/resize.js
Original file line number Diff line number Diff line change
Expand Up @@ -540,9 +540,18 @@ function extract (options) {
* })
* .toBuffer();
*
* @example
* // Trim image but leave extra space around its content–rectangle of interest.
* const output = await sharp(input)
* .trim({
* margin: 10
* })
* .toBuffer();
*
* @param {Object} [options]
* @param {string|Object} [options.background='top-left pixel'] - Background colour, parsed by the [color](https://www.npmjs.org/package/color) module, defaults to that of the top-left pixel.
* @param {number} [options.threshold=10] - Allowed difference from the above colour, a positive number.
* @param {number} [options.margin=0] - Applies margin in pixels to trim edges leaving extra space around trimmed content.
* @param {boolean} [options.lineArt=false] - Does the input more closely resemble line art (e.g. vector) rather than being photographic?
* @returns {Sharp}
* @throws {Error} Invalid parameters
Expand All @@ -564,6 +573,13 @@ function trim (options) {
if (is.defined(options.lineArt)) {
this._setBooleanOption('trimLineArt', options.lineArt);
}
if (is.defined(options.margin)) {
if (is.number(options.margin) && options.margin >= 0) {
this.options.trimMargin = options.margin
} else {
throw is.invalidParameterError('margin', 'positive number', options.margin);
}
}
} else {
throw is.invalidParameterError('trim', 'object', options);
}
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@
"Lachlan Newman <[email protected]>",
"Dennis Beatty <[email protected]>",
"Ingvar Stepanyan <[email protected]>",
"Don Denton <[email protected]>"
"Don Denton <[email protected]>",
"Dmytro Tiapukhin <[email protected]>"
],
"scripts": {
"build": "node install/build.js",
Expand Down
22 changes: 18 additions & 4 deletions src/operations.cc
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ namespace sharp {
/*
Trim an image
*/
VImage Trim(VImage image, std::vector<double> background, double threshold, bool const lineArt) {
VImage Trim(VImage image, std::vector<double> background, double threshold, bool const lineArt, int const margin) {
if (image.width() < 3 && image.height() < 3) {
throw VError("Image to trim must be at least 3x3 pixels");
}
Expand All @@ -304,6 +304,13 @@ namespace sharp {
} else {
background.resize(image.bands());
}
auto applyMargin = [&](int left, int top, int width, int height) {
Copy link
Author

@eddienubes eddienubes Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

debatable, could be moved to a separate utility method, let me know what you think.
I did it to avoid duplicating those 4 variables 3 times later on.

int const marginLeft = std::max(0, left - margin);
int const marginTop = std::max(0, top - margin);
int const marginWidth = std::min(image.width(), left + width + margin) - marginLeft;
int const marginHeight = std::min(image.height(), top + height + margin) - marginTop;
return std::make_tuple(marginLeft, marginTop, marginWidth, marginHeight);
};
int left, top, width, height;
left = image.find_trim(&top, &width, &height, VImage::option()
->set("background", background)
Expand All @@ -324,15 +331,22 @@ namespace sharp {
int const topB = std::min(top, topA);
int const widthB = std::max(left + width, leftA + widthA) - leftB;
int const heightB = std::max(top + height, topA + heightA) - topB;
return image.extract_area(leftB, topB, widthB, heightB);

// Combined bounding box + margin
auto [ml, mt, mw, mh] = applyMargin(leftB, topB, widthB, heightB);
return image.extract_area(ml, mt, mw, mh);
} else {
// Use alpha only
return image.extract_area(leftA, topA, widthA, heightA);
// Bounding box + margin
auto [ml, mt, mw, mh] = applyMargin(leftA, topA, widthA, heightA);
return image.extract_area(ml, mt, mw, mh);
}
}
}
if (width > 0 && height > 0) {
return image.extract_area(left, top, width, height);
// Bounding box + margin
auto [ml, mt, mw, mh] = applyMargin(left, top, width, height);
return image.extract_area(ml, mt, mw, mh);
}
return image;
}
Expand Down
2 changes: 1 addition & 1 deletion src/operations.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ namespace sharp {
/*
Trim an image
*/
VImage Trim(VImage image, std::vector<double> background, double threshold, bool const lineArt);
VImage Trim(VImage image, std::vector<double> background, double threshold, bool const lineArt, int const margin);

/*
* Linear adjustment (a * in + b)
Expand Down
3 changes: 2 additions & 1 deletion src/pipeline.cc
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ class PipelineWorker : public Napi::AsyncWorker {
if (baton->trimThreshold >= 0.0) {
MultiPageUnsupported(nPages, "Trim");
image = sharp::StaySequential(image);
image = sharp::Trim(image, baton->trimBackground, baton->trimThreshold, baton->trimLineArt);
image = sharp::Trim(image, baton->trimBackground, baton->trimThreshold, baton->trimLineArt, baton->trimMargin);
baton->trimOffsetLeft = image.xoffset();
baton->trimOffsetTop = image.yoffset();
}
Expand Down Expand Up @@ -1615,6 +1615,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
baton->trimBackground = sharp::AttrAsVectorOfDouble(options, "trimBackground");
baton->trimThreshold = sharp::AttrAsDouble(options, "trimThreshold");
baton->trimLineArt = sharp::AttrAsBool(options, "trimLineArt");
baton->trimMargin = sharp::AttrAsUint32(options, "trimMargin");
baton->gamma = sharp::AttrAsDouble(options, "gamma");
baton->gammaOut = sharp::AttrAsDouble(options, "gammaOut");
baton->linearA = sharp::AttrAsVectorOfDouble(options, "linearA");
Expand Down
2 changes: 2 additions & 0 deletions src/pipeline.h
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ struct PipelineBaton {
bool trimLineArt;
int trimOffsetLeft;
int trimOffsetTop;
int trimMargin;
std::vector<double> linearA;
std::vector<double> linearB;
int dilateWidth;
Expand Down Expand Up @@ -281,6 +282,7 @@ struct PipelineBaton {
trimLineArt(false),
trimOffsetLeft(0),
trimOffsetTop(0),
trimMargin(0),
linearA{},
linearB{},
dilateWidth(0),
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ module.exports = {

inputPng: getPath('50020484-00001.png'), // http://c.searspartsdirect.com/lis_png/PLDM/50020484-00001.png
inputPngGradients: getPath('gradients-rgb8.png'),
inputPngWithSlightGradientBorder: getPath('slight-gradient-border.png'), // https://pixabay.com/photos/cat-bury-cat-animal-sitting-cat-3038243/
inputPngWithTransparency: getPath('blackbug.png'), // public domain
inputPngCompleteTransparency: getPath('full-transparent.png'),
inputPngWithGreyAlpha: getPath('grey-8bit-alpha.png'),
Expand Down
Binary file added test/fixtures/slight-gradient-border.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 8 additions & 8 deletions test/types/sharp.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,7 @@ const vertexSplitQuadraticBasisSpline: string = sharp.interpolators.vertexSplitQ
// Triming
sharp(input).trim({ background: '#000' }).toBuffer();
sharp(input).trim({ threshold: 10, lineArt: true }).toBuffer();
sharp(input).trim({ background: '#bf1942', threshold: 30 }).toBuffer();
sharp(input).trim({ background: '#bf1942', threshold: 30, margin: 20 }).toBuffer();

// Text input
sharp({
Expand Down Expand Up @@ -705,20 +705,20 @@ sharp(input)
// https://github.com/lovell/sharp/pull/4048
sharp(input).composite([
{
input: 'image.gif',
Copy link
Author

@eddienubes eddienubes Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VSCode insisted on removing these extra spaces when opening the file. Let me know if I should revert these.

animated: true,
limitInputPixels: 536805378,
density: 144,
input: 'image.gif',
animated: true,
limitInputPixels: 536805378,
density: 144,
failOn: "warning",
autoOrient: true
}
])
sharp(input).composite([
{
input: 'image.png',
input: 'image.png',
animated: false,
limitInputPixels: 178935126,
density: 72,
limitInputPixels: 178935126,
density: 72,
failOn: "truncated"
}
])
Expand Down
39 changes: 39 additions & 0 deletions test/unit/trim.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,9 @@ describe('Trim borders', () => {
},
'Invalid lineArt': {
lineArt: 'fail'
},
'Invalid margin': {
margin: -1
}
}).forEach(([description, parameter]) => {
it(description, () => {
Expand Down Expand Up @@ -289,4 +292,40 @@ describe('Trim borders', () => {
assert.strictEqual(trimOffsetLeft, 0);
});
});

describe('Margin around content', () => {
it('Should trim complex gradients', async () => {
const { info } = await sharp(fixtures.inputPngGradients)
.trim({ threshold: 50, margin: 100 }).toBuffer({ resolveWithObject: true });

const { width, height, trimOffsetTop, trimOffsetLeft } = info;
assert.strictEqual(width, 1000);
assert.strictEqual(height, 443);
assert.strictEqual(trimOffsetTop, -557);
assert.strictEqual(trimOffsetLeft, 0);
});

it('Should trim simple gradients', async () => {
const { info } = await sharp(fixtures.inputPngWithSlightGradientBorder)
.trim({ threshold: 70, margin: 50 }).toBuffer({ resolveWithObject: true });

const { width, height, trimOffsetTop, trimOffsetLeft } = info;
assert.strictEqual(width, 900);
assert.strictEqual(height, 900);
assert.strictEqual(trimOffsetTop, -50);
assert.strictEqual(trimOffsetLeft, -50);
});

it('Should not overflow image bounding box', async () => {

const { info } = await sharp(fixtures.inputPngWithSlightGradientBorder)
.trim({ threshold: 70, margin: 9999999 }).toBuffer({ resolveWithObject: true });

const { width, height, trimOffsetTop, trimOffsetLeft } = info;
assert.strictEqual(width, 1000);
assert.strictEqual(height, 1000);
assert.strictEqual(trimOffsetTop, 0);
assert.strictEqual(trimOffsetLeft, 0);
});
});
});