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

feat: crop feature #10

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ node_modules
test/img/
sample/img/
sample/.cache/
package-lock.json
package-lock.json
yarn.lock
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ Defaults values are shown:
// widths: [200] // output 200px maxwidth
// widths: [200, null] // output 200px and original width

// Array of crops
// Optional: use falsy value to skip cropping
crops: null, // ["1600x900", "160x90"] or [ { width: 1600, height: 900 }, { width: 160, height: 90 } ]

// output image formats
formats: ["webp", "jpeg"], // also supported by sharp: "png", "raw", "tiff"

Expand Down
301 changes: 185 additions & 116 deletions img.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,20 @@ const debug = require("debug")("EleventyImg");
const CacheAsset = require("@11ty/eleventy-cache-assets");

const globalOptions = {
src: null,
widths: [null],
formats: ["webp", "jpeg"], // "png"
concurrency: 10,
urlPath: "/img/",
outputDir: "img/",
cacheDuration: "1d", // deprecated, use cacheOptions.duration
cacheOptions: {
// duration: "1d",
// directory: ".cache",
// removeUrlQueryParams: false,
// fetchOptions: {},
},
src: null,
widths: [null],
crops: null,
formats: ["webp", "jpeg"], // "png"
concurrency: 10,
urlPath: "/img/",
outputDir: "img/",
cacheDuration: "1d", // deprecated, use cacheOptions.duration
cacheOptions: {
// duration: "1d",
// directory: ".cache",
// removeUrlQueryParams: false,
// fetchOptions: {},
},
};

const MIME_TYPES = {
Expand Down Expand Up @@ -90,75 +91,133 @@ function transformRawFiles(files = []) {

// src should be a file path to an image or a buffer
async function resizeImage(src, options = {}) {
let sharpImage = sharp(src, {
failOnError: false,
// TODO how to handle higher resolution source images
// density: 72
});

if(typeof src !== "string") {
if(options.sourceUrl) {
src = options.sourceUrl;
} else {
throw new Error(`Expected options.sourceUrl in resizeImage when using Buffer as input.`);
}
}

// Must find the image format from the metadata
// File extensions lie or may not be present in the src url!
let metadata = await sharpImage.metadata();
let outputFilePromises = [];

let formats = getFormatsArray(options.formats);
for(let format of formats) {
let hasAtLeastOneValidMaxWidth = false;
for(let width of options.widths) {
let hasWidth = !!width;
// Set format
let imageFormat = sharpImage.clone();
if(metadata.format !== format) {
imageFormat.toFormat(format);
}

// skip this width because it’s larger than the original and we already
// have at least one output image size that works
if(hasAtLeastOneValidMaxWidth && (!width || width > metadata.width)) {
continue;
}

// Resize the image
if(!width) {
hasAtLeastOneValidMaxWidth = true;
} else {
if(width >= metadata.width) {
// don’t reassign width if it’s falsy
width = null;
hasWidth = false;
hasAtLeastOneValidMaxWidth = true;
} else {
imageFormat.resize({
width: width,
withoutEnlargement: true
});
}
}


let outputFilename = getFilename(src, width, format);
let outputPath = path.join(options.outputDir, outputFilename);
outputFilePromises.push(imageFormat.toFile(outputPath).then(data => {
let stats = getStats(src, format, options.urlPath, data.width, data.height, hasWidth);
stats.outputPath = outputPath;
stats.size = data.size;

return stats;
}));

debug( "Writing %o", outputPath );
}
}
let sharpImage = sharp(src, {
failOnError: false,
// TODO how to handle higher resolution source images
// density: 72
});

if(typeof src !== "string") {
if(options.sourceUrl) {
src = options.sourceUrl;
} else {
throw new Error(`Expected options.sourceUrl in resizeImage when using Buffer as input.`);
}
}

// Must find the image format from the metadata
// File extensions lie or may not be present in the src url!
let metadata = await sharpImage.metadata();
let outputFilePromises = [];

let formats = getFormatsArray(options.formats);
for(let format of formats) {
let hasAtLeastOneValidMaxWidth = false;
if (options.crops) {
let crops = _normalizeCrop(options.crops);
if (crops.length == 0) {
throw new Error(`Expected options.crops should be ["1600x900", "160x90"] or ${JSON.stringify([{ width: 1600, height: 900 }, { width: 160, height: 90 }], null, 2)}`);
}
for (let [width, height] of crops) {
let hasWidth = !!width;
// Set format
let imageFormat = sharpImage.clone();
if(metadata.format !== format) {
imageFormat.toFormat(format);
}

if(width > metadata.width || height > metadata.height) {
continue;
}

imageFormat.resize({
width: width,
height: height,
withoutEnlargement: true
});

let outputFilename = getFilename(src, width + 'x' + height, format);
let outputPath = path.join(options.outputDir, outputFilename);
outputFilePromises.push(imageFormat.toFile(outputPath).then(data => {
let stats = getStats(src, format, options.urlPath, data.width, data.height, hasWidth);
stats.outputPath = outputPath;
stats.size = data.size;

return stats;
}));

debug( "Writing %o", outputPath );
}
} else {
for(let width of options.widths) {
let hasWidth = !!width;
// Set format
let imageFormat = sharpImage.clone();
if(metadata.format !== format) {
imageFormat.toFormat(format);
}

// skip this width because it’s larger than the original and we already
// have at least one output image size that works
if(hasAtLeastOneValidMaxWidth && (!width || width > metadata.width)) {
continue;
}

// Resize the image
if(!width) {
hasAtLeastOneValidMaxWidth = true;
} else {
if(width >= metadata.width) {
// don’t reassign width if it’s falsy
width = null;
hasWidth = false;
hasAtLeastOneValidMaxWidth = true;
} else {
imageFormat.resize({
width: width,
withoutEnlargement: true
});
}
}


let outputFilename = getFilename(src, width, format);
let outputPath = path.join(options.outputDir, outputFilename);
outputFilePromises.push(imageFormat.toFile(outputPath).then(data => {
let stats = getStats(src, format, options.urlPath, data.width, data.height, hasWidth);
stats.outputPath = outputPath;
stats.size = data.size;

return stats;
}));

debug( "Writing %o", outputPath );
}
}
}

return Promise.all(outputFilePromises).then(files => transformRawFiles(files));
}

return Promise.all(outputFilePromises).then(files => transformRawFiles(files));
function _normalizeCrop(options) {
if (options == null) return null;
let filteredOptions = options.map(function(config) {
if (typeof(config) == 'string') {
let val = config.split('x')
if (config.split('x').length != 2) {
return false;
}
return [parseInt(val[0]), parseInt(val[1])]
} else if (typeof(config) == 'object') {
let width = config.hasOwnProperty('width');
let height = config.hasOwnProperty('height');
if (width && height) {
return [parseInt(config.width), parseInt(config.height)];
}
return false;
}
})
return filteredOptions.filter(Boolean);
}

function isFullUrl(url) {
Expand Down Expand Up @@ -229,40 +288,50 @@ Object.defineProperty(module.exports, "concurrency", {
*/

function _statsSync(src, originalWidth, originalHeight, opts) {
let options = Object.assign({}, globalOptions, opts);

let results = [];
let formats = getFormatsArray(options.formats);

for(let format of formats) {
let hasAtLeastOneValidMaxWidth = false;
for(let width of options.widths) {
let hasWidth = !!width;
let height;

if(hasAtLeastOneValidMaxWidth && (!width || width > originalWidth)) {
continue;
}

if(!width) {
width = originalWidth;
height = originalHeight;
hasAtLeastOneValidMaxWidth = true;
} else {
if(width >= originalWidth) {
width = originalWidth;
hasWidth = false;
hasAtLeastOneValidMaxWidth = true;
}
height = Math.floor(width * originalHeight / originalWidth);
}


results.push(getStats(src, format, options.urlPath, width, height, hasWidth));
}
}

return transformRawFiles(results);
let options = Object.assign({}, globalOptions, opts);

let results = [];
let formats = getFormatsArray(options.formats);

for(let format of formats) {
let hasAtLeastOneValidMaxWidth = false;
if (options.crops) {
let crops = _normalizeCrop(options.crops);
for (let [width, height] of crops) {
let hasWidth = !!width
if(width > originalWidth || height > originalWidth) {
continue;
}
results.push(getStats(src, format, options.urlPath, width, height, hasWidth));
}
} else {
for(let width of options.widths) {
let hasWidth = !!width;
let height;

if(hasAtLeastOneValidMaxWidth && (!width || width > originalWidth)) {
continue;
}

if(!width) {
width = originalWidth;
height = originalHeight;
hasAtLeastOneValidMaxWidth = true;
} else {
if(width >= originalWidth) {
width = originalWidth;
hasWidth = false;
hasAtLeastOneValidMaxWidth = true;
}
height = Math.floor(width * originalHeight / originalWidth);
}

results.push(getStats(src, format, options.urlPath, width, height, hasWidth));
}
}
}

return transformRawFiles(results);
};

function statsSync(src, opts) {
Expand All @@ -275,4 +344,4 @@ function statsByDimensionsSync(src, width, height, opts) {
}

module.exports.statsSync = statsSync;
module.exports.statsByDimensionsSync = statsByDimensionsSync;
module.exports.statsByDimensionsSync = statsByDimensionsSync;
Loading