-
Notifications
You must be signed in to change notification settings - Fork 83
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Rewrite and rearrange utilities into plugins. -Create sqip.js to run plugins over the input file -Turn SVG, SVGO, primitive and base64 as plugins -Move loadSVG into utils/helpers
- Loading branch information
Efe Gürkan YALAMAN
committed
Jul 17, 2018
1 parent
3fdaaa1
commit 023be72
Showing
9 changed files
with
556 additions
and
1,188 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
const encodeBase64 = rawSVG => Buffer.from(rawSVG).toString('base64') | ||
|
||
class Base64EncodePlugin { | ||
constructor(options) { | ||
this.options = options || { wrapImageTag: true } | ||
} | ||
|
||
apply(svg) { | ||
const shouldWrap = this.options.wrapImageTag | ||
const base64svg = encodeBase64(svg) | ||
|
||
if (shouldWrap) { | ||
return this.wrapImageTag(base64svg) | ||
} else { | ||
return base64svg | ||
} | ||
} | ||
|
||
wrapImageTag(svg) { | ||
const { | ||
dimensions: { width, height }, | ||
filename | ||
} = this.options | ||
|
||
return `<img width="${width}" height="${height}" src="${filename}" alt="Add descriptive alt text" style="background-size: cover; background-image: url(data:image/svg+xml;base64,${svg});">` | ||
} | ||
} | ||
|
||
module.exports = Base64EncodePlugin |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
const childProcess = require('child_process') | ||
const fs = require('fs') | ||
const path = require('path') | ||
const os = require('os') | ||
|
||
const VENDOR_DIR = path.resolve(__dirname, '..', '..', 'vendor') | ||
const PRIMITIVE_TEMP_FILE = os.tmpdir() + '/primitive_tempfile.svg' | ||
let primitiveExecutable = 'primitive' | ||
|
||
// Since Primitive is only interested in the larger dimension of the input image, let's find it | ||
const findLargerImageDimension = ({ width, height }) => | ||
width > height ? width : height | ||
|
||
class PrimitivePlugin { | ||
constructor(options) { | ||
this.options = options || { shouldThrow: false } | ||
} | ||
|
||
apply(filename) { | ||
this.checkForPrimitive() | ||
|
||
const { numberOfPrimitives = 8, mode = 0, dimensions } = this.options | ||
|
||
childProcess.execFileSync(primitiveExecutable, [ | ||
'-i', | ||
filename, | ||
'-o', | ||
PRIMITIVE_TEMP_FILE, | ||
'-n', | ||
numberOfPrimitives, | ||
'-m', | ||
mode, | ||
'-s', | ||
findLargerImageDimension(dimensions) | ||
]) | ||
return fs.readFileSync(PRIMITIVE_TEMP_FILE, { | ||
encoding: 'utf-8' | ||
}) | ||
} | ||
|
||
// Sanity check: use the exit state of 'type' to check for Primitive availability | ||
checkForPrimitive() { | ||
const primitivePath = path.join( | ||
VENDOR_DIR, | ||
`primitive-${os.platform()}-${os.arch()}` | ||
) | ||
|
||
if (fs.existsSync(primitivePath)) { | ||
primitiveExecutable = primitivePath | ||
return | ||
} | ||
|
||
const errorMessage = | ||
'Please ensure that Primitive (https://github.com/fogleman/primitive, written in Golang) is installed and globally available' | ||
try { | ||
if (os.platform() === 'win32') { | ||
childProcess.execSync('where primitive') | ||
} else { | ||
childProcess.execSync('type primitive') | ||
} | ||
} catch (e) { | ||
if (this.options.shouldThrow) { | ||
throw new Error(errorMessage) | ||
} | ||
console.log(errorMessage) | ||
process.exit(1) | ||
} | ||
} | ||
} | ||
|
||
module.exports = PrimitivePlugin |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
const { loadSVG } = require('../utils/helpers') | ||
|
||
const PRIMITIVE_SVG_ELEMENTS = 'circle, ellipse, line, polygon, path, rect, g' | ||
|
||
const patchSVGGroup = svg => { | ||
const $ = loadSVG(svg) | ||
|
||
const $svg = $('svg') | ||
const $primitiveShapes = $svg.children(PRIMITIVE_SVG_ELEMENTS) | ||
|
||
// Check if actual shapes are grouped | ||
if (!$primitiveShapes.filter('g').length !== 1) { | ||
const $group = $('<g/>') | ||
const $realShapes = $primitiveShapes.not('rect:first-child') | ||
|
||
$group.append($realShapes) | ||
$svg.append($group) | ||
} | ||
|
||
return $.html() | ||
} | ||
|
||
class SVGPlugin { | ||
constructor(options) { | ||
this.options = options || {} | ||
} | ||
|
||
apply(svg) { | ||
let retval = this.prepareSVG(svg) | ||
if (this.options.blur) { | ||
retval = this.applyBlurFilter(retval) | ||
} | ||
return retval | ||
} | ||
|
||
// Prepare SVG. For now, this will just ensure that the viewbox attribute is set | ||
prepareSVG(svg) { | ||
const $ = loadSVG(svg) | ||
const $svg = $('svg') | ||
const { width, height } = this.options.dimensions | ||
|
||
// Ensure viewbox | ||
if (!$svg.is('[viewBox]')) { | ||
if (!(width && height)) { | ||
throw new Error( | ||
`SVG is missing viewBox attribute while Width and height were not passed:\n\n${svg}` | ||
) | ||
} | ||
$svg.attr('viewBox', `0 0 ${width} ${height}`) | ||
} | ||
|
||
const $bgRect = $svg | ||
.children(PRIMITIVE_SVG_ELEMENTS) | ||
.filter('rect:first-child[fill]') | ||
|
||
// Check if filling background rectangle exists | ||
// This must exist for proper blur and other transformations | ||
if (!$bgRect.length) { | ||
throw new Error( | ||
`The SVG must have a rect as first shape element which represents the svg background color:\n\n${svg}` | ||
) | ||
} | ||
|
||
// Remove x and y attributes since they default to 0 | ||
// @todo test in rare browsers | ||
$bgRect.attr('x', null) | ||
$bgRect.attr('y', null) | ||
|
||
// Improve compression via simplifying fill | ||
$bgRect.attr('width', '100%') | ||
$bgRect.attr('height', '100%') | ||
|
||
return $.html() | ||
} | ||
|
||
applyBlurFilter(svg) { | ||
if (!this.options.blur) { | ||
return svg | ||
} | ||
const patchedSVG = patchSVGGroup(svg) | ||
const $ = loadSVG(patchedSVG) | ||
const blurFilterId = 'b' | ||
$('svg > g').attr('filter', `url(#${blurFilterId})`) | ||
$('svg').prepend( | ||
`<filter id="${blurFilterId}"><feGaussianBlur stdDeviation="${ | ||
this.options.blur | ||
}" />` | ||
) | ||
|
||
return $.html() | ||
} | ||
} | ||
|
||
module.exports = SVGPlugin |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
const SVGO = require('svgo') | ||
|
||
// SVGO with settings for maximum compression to optimize the Primitive-generated SVG | ||
class SVGOPlugin { | ||
constructor(options) { | ||
this.options = options || {} | ||
} | ||
apply(svg) { | ||
return this.optimize(svg) | ||
} | ||
optimize(svg) { | ||
const { multipass = true, floatPrecision = 1 } = this.options | ||
|
||
const svgo = new SVGO({ multipass, floatPrecision }) | ||
let retVal = '' | ||
svgo.optimize(svg, ({ data }) => (retVal = data)) | ||
return retVal | ||
} | ||
} | ||
|
||
module.exports = SVGOPlugin |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
const fs = require('fs') | ||
const path = require('path') | ||
|
||
const SVGPlugin = require('./plugins/svg') | ||
const SVGOPlugin = require('./plugins/svgo') | ||
const PrimitivePlugin = require('./plugins/primitive') | ||
const Base64EncodePlugin = require('./plugins/base64') | ||
|
||
const { getDimensions, printFinalResult } = require('./utils/helpers') | ||
|
||
const sqip = options => { | ||
// TODO validate Options | ||
const config = Object.assign({}, options) | ||
config.filename = config.input | ||
|
||
if (!config.filename) { | ||
throw new Error( | ||
'Please provide an input image, e.g. sqip({ filename: "input.jpg" })' | ||
) | ||
} | ||
|
||
const inputPath = path.resolve(config.filename) | ||
|
||
try { | ||
fs.accessSync(inputPath, fs.constants.R_OK) | ||
} catch (err) { | ||
throw new Error(`Unable to read input file: ${inputPath}`) | ||
} | ||
const imgDimensions = getDimensions(inputPath) | ||
|
||
let plugins = [] | ||
if (!config.plugins) { | ||
plugins = [ | ||
new PrimitivePlugin({ | ||
numberOfPrimitives: 8, | ||
mode: 0, | ||
filename: config.filename, | ||
dimensions: imgDimensions | ||
}), | ||
new SVGPlugin({ | ||
blur: 12, | ||
dimensions: imgDimensions | ||
}), | ||
new SVGOPlugin({ | ||
multipass: true, | ||
floatPrecision: 1 | ||
}), | ||
new Base64EncodePlugin({ | ||
dimensions: imgDimensions, | ||
filename: config.filename, | ||
wrapImageTag: true | ||
}) | ||
] | ||
} else { | ||
plugins = config.plugins | ||
} | ||
|
||
let finalSvg = config.filename | ||
for (let plugin of plugins) { | ||
finalSvg = plugin.apply(finalSvg) | ||
} | ||
|
||
// Write to disk or output result | ||
if (config.output) { | ||
const outputPath = path.resolve(config.output) | ||
fs.writeFileSync(outputPath, finalSvg) | ||
} else { | ||
printFinalResult(imgDimensions, inputPath, finalSvg) | ||
} | ||
|
||
return { finalSvg, imgDimensions } | ||
} | ||
|
||
module.exports = sqip |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters