Skip to content

Commit

Permalink
Merge pull request #30 from clocklimited/feature/blur-sharp
Browse files Browse the repository at this point in the history
  • Loading branch information
Asheboy authored Jun 16, 2021
2 parents e393176 + 34b95a2 commit be1d976
Show file tree
Hide file tree
Showing 9 changed files with 519 additions and 139 deletions.
134 changes: 51 additions & 83 deletions lib/blur.js
Original file line number Diff line number Diff line change
@@ -1,8 +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 @@ -18,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, 'white')

if (!this.masks.length) {
this.masks.push([
Expand All @@ -47,58 +32,45 @@ 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)
}
let mask = '<clipPath id="mask">'

return mask.resize(width, height).quality(100).write(this.maskPath, cb)
}

_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 gm(image)
.gaussian(size.width / 15, size.width / 5)
.write(this.blurPath, cb)
mask += `<polygon points="${coords.join(' ')}" />`
}

return gm(image).resize(96).scale(size.width).write(this.blurPath, cb)
}
mask += '</clipPath>'

// 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' })
gm(image).quality(100).write(this.baseImagePath, cb)
return mask
}

_createBlurred(image, cb) {
this._writeImage(image, (error) => {
if (error) return cb(error)
_createSvg(mask, type, image, size) {
const base64 = image.toString('base64')
const xml = `<svg height="${size.height}" width="${size.width}" xmlns="http://www.w3.org/2000/svg">
${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>`

return Buffer.from(xml)
}

const outImage = gm(this.blurPath)
.composite(this.baseImagePath, this.maskPath)
.quality(this.quality)
_createBlurred(svg, cb) {
const outImage = sharp(svg)

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

outImage.stream(cb)
})
cb(null, outImage)
}

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

pipe(dest, options) {
Expand All @@ -111,33 +83,29 @@ 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)

if (!this.format) {
this.format = imageFileType.ext
}

let svg

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)
})
}
)
try {
const mask = this._createMask(info)
svg = this._createSvg(mask, imageFileType, image, info)
} catch (e) {
return this.output(e)
}
)

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

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"file-type": "^10.7.0",
"gm": "^1.23.1",
"rimraf": "^2.5.2",
"sharp": "^0.28.3",
"temp": "^0.9.0",
"webpinfo": "^1.3.0"
},
Expand Down
86 changes: 36 additions & 50 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,7 +75,8 @@ describe('BlurStream', function () {
})
})

it('should pixellate a 100x100 square', function (done) {
it('should blur a 100x100 square', function (done) {
this.timeout(5000)
const blur = new BlurStream({
masks: [
[
Expand All @@ -107,15 +85,16 @@ describe('BlurStream', function () {
[100, 100],
[100, 0]
]
]
],
method: 'gaussian'
})
const input = join(__dirname, 'fixtures', '500x399.jpeg')

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

const readStream = fs.createReadStream(input)
Expand All @@ -134,7 +113,7 @@ describe('BlurStream', function () {
assert.strictEqual(size.height, 399)

const options = {
file: join(tmp, '500x399-pixel-portion-diff.png'),
file: join(tmp, '500x399-blurred-portion-diff.png'),
tolerance: 0.001,
highlightColor: 'yellow'
}
Expand All @@ -159,26 +138,30 @@ describe('BlurStream', function () {
})
})

it('should blur a 100x100 square', function (done) {
this.timeout(5000)
it('should blur a 100x100 square and a triangle', function (done) {
const blur = new BlurStream({
masks: [
[
[0, 0],
[0, 100],
[100, 100],
[100, 0]
],

[
[300, 200],
[400, 200],
[350, 300]
]
],
method: 'gaussian'
]
})
const input = join(__dirname, 'fixtures', '500x399.jpeg')

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

const readStream = fs.createReadStream(input)
Expand All @@ -197,7 +180,7 @@ describe('BlurStream', function () {
assert.strictEqual(size.height, 399)

const options = {
file: join(tmp, '500x399-blurred-portion-diff.png'),
file: join(tmp, '500x399-pixel-multi-portion-diff.png'),
tolerance: 0.001,
highlightColor: 'yellow'
}
Expand All @@ -222,32 +205,32 @@ describe('BlurStream', function () {
})
})

it('should pixellate a 100x100 square and a triangle', function (done) {
it('should blur a square and a triangle on a large image', function (done) {
const blur = new BlurStream({
masks: [
[
[0, 0],
[0, 100],
[100, 100],
[100, 0]
[0, 500],
[500, 500],
[500, 0]
],

[
[300, 200],
[400, 200],
[350, 300]
[1300, 800],
[1400, 800],
[1350, 1300]
]
]
})
const input = join(__dirname, 'fixtures', '500x399.jpeg')

const out = join(tmp, '500x399-pixel-multi-portion.png')
const input = join(__dirname, 'fixtures', 'massive-image.jpg')

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

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

Expand All @@ -257,14 +240,16 @@ describe('BlurStream', function () {
return gm(img).size(cb)
}

this.timeout(10000)

writeStream.on('close', function () {
getImageSize(input, function (err, size) {
if (err) return done(err)
assert.strictEqual(size.width, 500)
assert.strictEqual(size.height, 399)
assert.strictEqual(size.width, 3024)
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 @@ -282,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 added test/fixtures/massive-image.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit be1d976

Please sign in to comment.