Skip to content

Commit

Permalink
refactor: replace Jimp by Sharp (#338)
Browse files Browse the repository at this point in the history
  • Loading branch information
andy128k authored Oct 19, 2021
1 parent 2554abe commit e0240c7
Show file tree
Hide file tree
Showing 20 changed files with 236 additions and 13,258 deletions.
2 changes: 1 addition & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"no-magic-numbers": [
2,
{
"ignore": [-1, 0, 1, 2, 100, 255],
"ignore": [-1, 0, 1, 2, 3, 4, 100, 255],
"ignoreArrayIndexes": true
}
],
Expand Down
13,068 changes: 11 additions & 13,057 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,12 @@
"clone": "^2.1.2",
"colors": "^1.4.0",
"image-size": "^0.8.3",
"jimp": "^0.16.1",
"jsontoxml": "^1.0.1",
"lodash.defaultsdeep": "^4.6.1",
"png-to-ico": "^2.1.2",
"require-directory": "^2.1.1",
"sharp": "^0.28.2",
"through2": "^4.0.2",
"tinycolor2": "^1.4.2",
"vinyl": "^2.2.1",
"xml2js": "^0.4.23"
},
Expand Down
200 changes: 113 additions & 87 deletions src/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@ const path = require("path");
const url = require("url");
const fs = require("fs");
const { promisify } = require("util");
const color = require("tinycolor2");
const colors = require("colors");
const jsonxml = require("jsontoxml");
const sizeOf = require("image-size");
const Jimp = require("jimp");
const sharp = require("sharp");
const xml2js = require("xml2js");
const PLATFORM_OPTIONS = require("./config/platform-options.json");
Expand Down Expand Up @@ -35,12 +33,6 @@ module.exports = function (options) {
}
}

function parseColor(hex) {
const { r, g, b, a } = color(hex).toRgb();

return Jimp.rgbaToInt(r, g, b, a * 255);
}

// sharp renders the SVG in its source width and height with 72 DPI which can
// cause a blurry result in case the source SVG is defined in lower size than
// the target size. To avoid this, resize the source SVG to the needed size
Expand Down Expand Up @@ -263,14 +255,24 @@ module.exports = function (options) {
} background`
);

return Jimp.create(
properties.width,
properties.height,
properties.transparent ? 0 : parseColor(properties.background)
);
let image = sharp({
create: {
width: properties.width,
height: properties.height,
channels: properties.transparent ? 4 : 3,
background: properties.transparent
? "#00000000"
: properties.background,
},
});

if (properties.transparent) {
image = image.ensureAlpha();
}
return image.png().toBuffer();
},

render(sourceset, properties, offset) {
async render(sourceset, properties, offset) {
log(
"Image:render",
`Find nearest icon to ${properties.width}x${properties.height} with offset ${offset}`
Expand All @@ -282,106 +284,130 @@ module.exports = function (options) {
(source) => source.size.type === "svg"
);

let promise = null;

if (svgSource) {
const background = { r: 0, g: 0, b: 0, alpha: 0 };

log("Image:render", `Rendering SVG to ${width}x${height}`);
promise = svgtool
.ensureSize(svgSource, width, height)
.then((svgBuffer) =>
sharp(svgBuffer)
.resize({
background,
width,
height,
fit: sharp.fit.contain,
})
.toBuffer()
)
.then(Jimp.read);
} else {
const sideSize = Math.max(width, height);
const svgBuffer = await svgtool.ensureSize(svgSource, width, height);

let nearestIcon = sourceset[0];
let nearestSideSize = Math.max(
nearestIcon.size.width,
nearestIcon.size.height
);
return await sharp(svgBuffer).resize({
background: "#00000000",
width,
height,
fit: sharp.fit.contain,
});
}

for (const icon of sourceset) {
const max = Math.max(icon.size.width, icon.size.height);
const sideSize = Math.max(width, height);

if (
(nearestSideSize > max || nearestSideSize < sideSize) &&
max >= sideSize
) {
nearestIcon = icon;
nearestSideSize = max;
}
}
let nearestIcon = sourceset[0];
let nearestSideSize = Math.max(
nearestIcon.size.width,
nearestIcon.size.height
);

log("Images:render", `Resizing PNG to ${width}x${height}`);

promise = Jimp.read(nearestIcon.file).then((image) =>
image.contain(
width,
height,
Jimp.HORIZONTAL_ALIGN_CENTER | Jimp.VERTICAL_ALIGN_MIDDLE,
options.pixel_art &&
width >= image.bitmap.width &&
height >= image.bitmap.height
? Jimp.RESIZE_NEAREST_NEIGHBOR
: null
)
);
for (const icon of sourceset) {
const max = Math.max(icon.size.width, icon.size.height);

if (
(nearestSideSize > max || nearestSideSize < sideSize) &&
max >= sideSize
) {
nearestIcon = icon;
nearestSideSize = max;
}
}

return promise.then((image) => image);
log("Images:render", `Resizing PNG to ${width}x${height}`);

const image = await sharp(nearestIcon.file).ensureAlpha();
const metadata = await image.metadata();

return image.resize({
width,
height,
fit: sharp.fit.contain,
background: "#00000000",
kernel:
options.pixel_art &&
width >= metadata.width &&
height >= metadata.height
? "nearest"
: "lanczos3",
});
},

mask: Jimp.read(path.join(__dirname, "mask.png")),
overlayGlow: Jimp.read(path.join(__dirname, "overlay-glow.png")),
mask: path.join(__dirname, "mask.png"),
overlayGlow: path.join(__dirname, "overlay-glow.png"),
// Gimp drop shadow filter: input: mask.png, config: X: 2, Y: 5, Offset: 5, Color: black, Opacity: 20
overlayShadow: Jimp.read(path.join(__dirname, "overlay-shadow.png")),
overlayShadow: path.join(__dirname, "overlay-shadow.png"),

async maskImage(image, mask) {
const pipeline = sharp(image);
const meta = await pipeline.metadata();

const maskBuffer = await sharp(mask)
.resize({
width: meta.width,
height: meta.height,
fit: sharp.fit.contain,
background: "#00000000",
})
.toColourspace("b-w")
.toBuffer();

return await pipeline.joinChannel(maskBuffer).png().toBuffer();
},

async overlay(image, coverPath) {
const pipeline = sharp(image);
const meta = await pipeline.metadata();

const cover = await sharp(coverPath)
.resize({
width: meta.width,
height: meta.height,
fit: sharp.fit.contain,
})
.png()
.toBuffer();

return await pipeline
.composite([{ input: cover, left: 0, top: 0 }])
.png()
.toBuffer();
},

composite(canvas, image, properties, offset, max) {
async composite(canvas, image, properties, offset) {
if (properties.mask) {
log("Images:composite", "Masking composite image on circle");
return Promise.all([
this.mask,
this.overlayGlow,
this.overlayShadow,
]).then(([mask, glow, shadow]) => {
canvas.mask(mask.clone().resize(max, Jimp.AUTO), 0, 0);
if (properties.overlayGlow) {
canvas.composite(glow.clone().resize(max, Jimp.AUTO), 0, 0);
}
if (properties.overlayShadow) {
canvas.composite(shadow.clone().resize(max, Jimp.AUTO), 0, 0);
}
properties = Object.assign({}, properties, {
mask: false,
});

return this.composite(canvas, image, properties, offset, max);
});
canvas = await this.maskImage(canvas, this.mask);

if (properties.overlayGlow) {
canvas = await this.overlay(canvas, this.overlayGlow);
}
if (properties.overlayShadow) {
canvas = await this.overlay(canvas, this.overlayShadow);
}
}

log(
"Images:composite",
`Compositing favicon on ${properties.width}x${properties.height} canvas with offset ${offset}`
);

canvas.composite(image, offset, offset);
const input = await image.toBuffer();

let pipeline = sharp(canvas).composite([
{ input, left: offset, top: offset },
]);

if (properties.rotate) {
const degrees = 90;

log("Images:render", `Rotating image by ${degrees}`);
canvas.rotate(degrees, false);
pipeline = pipeline.rotate(degrees, false);
}
return canvas.getBufferAsync(Jimp.MIME_PNG);
return await pipeline.toFormat("png").toBuffer();
},
},
};
Expand Down
2 changes: 1 addition & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ function favicons(source, options = {}, next = undefined) {
µ.Images.render(sourceset, mergedProperties, offset),
])
.then(([canvas, buffer]) =>
µ.Images.composite(canvas, buffer, mergedProperties, offset, maximum)
µ.Images.composite(canvas, buffer, mergedProperties, offset)
)
.then((contents) => ({ name, contents }));
}
Expand Down
Binary file modified test/snapshots/array.test.js.snap
Binary file not shown.
Binary file modified test/snapshots/background.test.js.snap
Binary file not shown.
Binary file modified test/snapshots/default.test.js.snap
Binary file not shown.
Binary file modified test/snapshots/loadManifestWithCredentials.test.js.snap
Binary file not shown.
Loading

0 comments on commit e0240c7

Please sign in to comment.