Skip to content

Commit

Permalink
> SVG based blur
Browse files Browse the repository at this point in the history
  • Loading branch information
jmurth1234 committed Jun 16, 2021
1 parent 3786ee5 commit 99209e8
Show file tree
Hide file tree
Showing 7 changed files with 58 additions and 184 deletions.
140 changes: 49 additions & 91 deletions lib/blur.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
const DarkroomStream = require('./darkroom-stream')
const gm = require('gm')
const temp = require('temp')
const async = require('async')
const rimraf = require('rimraf')
const sharp = require('sharp')
const fileType = require('file-type')

/**
* Given an image and an array of co-ords arrays, censor portions of an image
Expand All @@ -19,25 +16,12 @@ class Blur extends DarkroomStream {
this.options.colour = this.options.colour || 'none'
this.dimensions = []
this.masks = options.masks || []
this.method = options.method || 'pixellate'
this.blurAmount = options.blurAmount || 15
}

cleanup(cb) {
rimraf(this.tempDir, cb)
}

_createMask(size, cb) {
_createMask(size) {
const width = size.width
const height = size.height
const multiplier = 4

this.maskPath = temp.path({ dir: this.tempDir, suffix: 'mask.png' })

// Multiply each height and width by a set multiplier in order to
// remove jagged edges from resulting image
//
// http://stackoverflow.com/a/22940729
let mask = gm(width * multiplier, height * multiplier, 'transparent')

if (!this.masks.length) {
this.masks.push([
Expand All @@ -48,67 +32,47 @@ class Blur extends DarkroomStream {
])
}

try {
for (const polygon of this.masks) {
const coords = polygon.map(([x, y]) => [x * multiplier, y * multiplier])
mask = mask.fill('black').drawPolygon(...coords)
}
} catch (err) {
this.output(err)
}

return mask.resize(width, height).quality(100).write(this.maskPath, cb)
}
let mask = '<clipPath id="mask">'

_createBlur(image, size, cb) {
this.blurPath = temp.path({
dir: this.tempDir,
suffix: 'blurred-image.png'
})
for (const polygon of this.masks) {
const coords = polygon.map(([x, y]) => `${x},${y}`)

if (this.method === 'gaussian') {
return sharp(image)
.blur(size.width / 15)
.toFile(this.blurPath, cb)
mask += `<polygon points="${coords.join(' ')}" />`
}

sharp(image)
.resize(96, null)
.toBuffer((err, buffer) => {
if (err) return cb(err)
mask += '</clipPath>'

sharp(buffer)
.resize(size.width, size.height, { kernel: 'nearest' })
.toFile(this.blurPath, cb)
})
return mask
}

// gm().composite() doesnt support passing in a buffer
// as a base image, so we need to write it to disk first
_writeImage(image, cb) {
this.baseImagePath = temp.path({ dir: this.tempDir, suffix: 'base-image' })
sharp(image).toFile(this.baseImagePath, cb)
}
_createSVG(type, image, size) {
const base64 = image.toString('base64')
const xml = `<svg height="${size.height}" width="${size.width}" xmlns="http://www.w3.org/2000/svg">
${this.mask}
<filter id="blur">
<feGaussianBlur in="SourceGraphic" stdDeviation="${this.blurAmount}" />
</filter>
<image height="${size.height}" width="${size.width}" href="data:${type.mime};;base64,${base64}" />
<image clip-path="url(#mask)" filter="url(#blur)" height="${size.height}" width="${size.width}" href="data:${type.mime};base64,${base64}" />
</svg>`

_createBlurred(image, cb) {
this._writeImage(image, (error) => {
if (error) return cb(error)
const mask = Buffer.from(xml)

const outImage = sharp(this.blurPath).composite([
{ input: this.maskPath, blend: 'dest-in' },
{ input: this.baseImagePath, blend: 'dest-over' }
])
return mask
}

if (this.format) {
outImage.toFormat(this.format, { quality: this.quality })
}
_createBlurred(cb) {
const outImage = sharp(this.svg)

cb(null, outImage)
})
if (this.format) {
outImage.toFormat(this.format, { quality: this.quality })
}

cb(null, outImage)
}

_getImageSize(image, cb) {
return gm(image).size(cb)
_getImageInfo(image, cb) {
return sharp(image).metadata(cb)
}

pipe(dest, options) {
Expand All @@ -121,33 +85,27 @@ class Blur extends DarkroomStream {

exec() {
const image = Buffer.concat(this.chunks, this.size)
const imageFileType = fileType(this.chunks[0])

async.parallel(
{
tempDir: temp.mkdir.bind(null, 'blur'),
size: this._getImageSize.bind(null, image)
},
(error, results) => {
if (error) return this.output(error)
this._getImageInfo(image, (error, info) => {
if (error) return this.output(error)

const { size, tempDir } = results
this.tempDir = tempDir

async.parallel(
{
blur: this._createBlur.bind(this, image, size),
mask: this._createMask.bind(this, size)
},
(error) => {
if (error) return this.output(error)
this._createBlurred(image, (error, output) => {
if (error) return this.output(error)
this.output(null, output)
})
}
)
if (!this.format) {
this.format = imageFileType.ext
}
)

try {
this.mask = this._createMask(info)
this.svg = this._createSVG(imageFileType, image, info)
} catch (e) {
return this.output(e)
}

this._createBlurred((error, output) => {
if (error) return this.output(error)
this.output(null, output)
})
})
}
}

Expand Down
102 changes: 9 additions & 93 deletions test/blur.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,30 +36,7 @@ describe('BlurStream', function () {
assert(s instanceof DarkroomStream)
})

it('should default to pixelate the whole image', function (done) {
const blur = new BlurStream()
const out = join(tmp, '500x399-blurred-full.png')
const input = join(__dirname, 'fixtures', '500x399.jpeg')
const readStream = fs.createReadStream(input)
const writeStream = fs.createWriteStream(out)

readStream.pipe(blur).pipe(writeStream)

function getImageSize(img, cb) {
return gm(img).size(cb)
}

writeStream.on('close', function () {
getImageSize(input, function (err, size) {
if (err) return done(err)
assert.strictEqual(500, size.width)
assert.strictEqual(399, size.height)
done()
})
})
})

it('should blur the whole image', function (done) {
it('should default to blur the whole image', function (done) {
this.timeout(5000)
const blur = new BlurStream({ method: 'gaussian' })
const out = join(tmp, '500x399-gauss-blurred-full.png')
Expand Down Expand Up @@ -98,67 +75,6 @@ describe('BlurStream', function () {
})
})

it('should pixellate a 100x100 square', function (done) {
const blur = new BlurStream({
masks: [
[
[0, 0],
[0, 100],
[100, 100],
[100, 0]
]
]
})
const input = join(__dirname, 'fixtures', '500x399.jpeg')

const out = join(tmp, '500x399-pixel-portion.png')
const expectedOutput = join(
__dirname,
'fixtures',
'500x399-pixel-portion.png'
)

const readStream = fs.createReadStream(input)
const writeStream = fs.createWriteStream(out)

readStream.pipe(blur).pipe(writeStream)

function getImageSize(img, cb) {
return gm(img).size(cb)
}

writeStream.on('close', function () {
getImageSize(input, function (err, size) {
if (err) return done(err)
assert.strictEqual(size.width, 500)
assert.strictEqual(size.height, 399)

const options = {
file: join(tmp, '500x399-pixel-portion-diff.png'),
tolerance: 0.001,
highlightColor: 'yellow'
}

gm.compare(
out,
expectedOutput,
options,
function (err, isEqual, equality, raw) {
assert.strictEqual(
isEqual,
true,
'Images do not match see ‘' +
options.file +
'’ for a diff.\n' +
raw
)
done()
}
)
})
})
})

it('should blur a 100x100 square', function (done) {
this.timeout(5000)
const blur = new BlurStream({
Expand Down Expand Up @@ -222,7 +138,7 @@ describe('BlurStream', function () {
})
})

it('should pixellate a 100x100 square and a triangle', function (done) {
it('should blur a 100x100 square and a triangle', function (done) {
const blur = new BlurStream({
masks: [
[
Expand All @@ -241,11 +157,11 @@ describe('BlurStream', function () {
})
const input = join(__dirname, 'fixtures', '500x399.jpeg')

const out = join(tmp, '500x399-pixel-multi-portion.png')
const out = join(tmp, '500x399-multi-portion.png')
const expectedOutput = join(
__dirname,
'fixtures',
'500x399-pixel-multi-portion.png'
'500x399-multi-portion.png'
)

const readStream = fs.createReadStream(input)
Expand Down Expand Up @@ -289,7 +205,7 @@ describe('BlurStream', function () {
})
})

it('should pixellate a square and a triangle on a large image', function (done) {
it('should blur a square and a triangle on a large image', function (done) {
const blur = new BlurStream({
masks: [
[
Expand All @@ -309,13 +225,12 @@ describe('BlurStream', function () {

const input = join(__dirname, 'fixtures', 'massive-image.jpg')

const out = join(tmp, 'massive-image-output.png')
const out = join(tmp, 'massive-image-output.jpg')
const expectedOutput = join(
__dirname,
'fixtures',
'massive-image-output.png'
'massive-image-output.jpg'
)

const readStream = fs.createReadStream(input)
const writeStream = fs.createWriteStream(out)

Expand All @@ -334,7 +249,7 @@ describe('BlurStream', function () {
assert.strictEqual(size.height, 2016)

const options = {
file: join(tmp, '500x399-pixel-multi-portion-diff.png'),
file: join(tmp, 'massive-image-output-diff.png'),
tolerance: 0.001,
highlightColor: 'yellow'
}
Expand All @@ -352,6 +267,7 @@ describe('BlurStream', function () {
'’ for a diff.\n' +
raw
)

done()
}
)
Expand Down
Binary file added test/fixtures/500x399-multi-portion.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed test/fixtures/500x399-pixel-multi-portion.png
Binary file not shown.
Binary file removed test/fixtures/500x399-pixel-portion.png
Binary file not shown.
Binary file added test/fixtures/massive-image-output.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed test/fixtures/massive-image-output.png
Binary file not shown.

0 comments on commit 99209e8

Please sign in to comment.