Skip to content

Commit

Permalink
Merge pull request #3 from AtiX/feature/parseImages
Browse files Browse the repository at this point in the history
Image Parsing
  • Loading branch information
AtiX committed Jan 1, 2016
2 parents bd31332 + a7c7d22 commit 4837c6b
Show file tree
Hide file tree
Showing 10 changed files with 216 additions and 3 deletions.
3 changes: 3 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@ language: node_js
node_js:
- "4.1"
- "0.12"
before_install:
- sudo apt-get update -q
- sudo apt-get install -y graphicsmagick
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ At the moment, the following parsers are supported:
- **markdown**: Parses markdown (```*.md```) files and attaches the parsed HTML content to each ```node.markdown.<filename> = "<parsedMarkdown>"```
- **yaml**: Parses yaml (```*.yml```) files and attaches the parsed properties to each ```node.<filename> = { <parsedProperties> }```,
with the exception that the properties of the file ```metadata.yml``` are added directly to the node without the in-between ```<filename>``` object.

- **images**: Parses jpeg (```*.jpg```) files, reads out exif information and creates (if configured with ```createThumbnails: true```) thumbnails.
Adds helper functions that return image/thumbnail data. Note that for generating thumbnails, GraphicsMagick/ImageMagick needs to be installed on the system.

## Usage
Hint: although the examples here are written in coffeescript, the module works with javascript as well.

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
"dependencies": {
"chai": "3.4.0",
"coffee-script": "1.10.0",
"exif": "0.4.0",
"gm": "^1.21.1",
"js-yaml": "3.4.6",
"marked": "0.3.5",
"rimraf": "2.4.3",
Expand Down
1 change: 1 addition & 0 deletions sampleApp/sampleApp.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ FileSystemGraphDatabase = require '../src/FileSystemGraphDatabase'
graph = new FileSystemGraphDatabase({ path: './sampleApp/sampleData'})
graph.registerParser('MarkdownParser')
graph.registerParser('YamlParser')
graph.registerParser('ImageParser', {createThumbnails: true})
graphPromise = graph.load()

# 2.) After having loaded everything, print the graph to stdout
Expand Down
Binary file added sampleApp/sampleData/subnode2/subY/fernImage.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 3 additions & 2 deletions src/FileSystemGraphDatabase.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ module.exports = class FileSystemGraphDatabase
# All registerd parsers are then used when load()-ing files from
# disk.
# @param {FileParser} parserInstance the instance of the parser - or class name, if the parser requires no manual initialization
registerParser: (parserInstance) ->
# @param {Object} parserOptions additional options when initializing the parser via class name
registerParser: (parserInstance, parserOptions = {}) ->
if 'string' == typeof parserInstance
# Try to require class and instanciate
try
ParserClass = require './parser/' + parserInstance
@parserInstances.push new ParserClass()
@parserInstances.push new ParserClass(parserOptions)
return
catch e
throw new Error("Unable to load parser by class name: #{e}")
Expand Down
120 changes: 120 additions & 0 deletions src/parser/ImageParser.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
Parser = require './Parser'
marked = require 'marked'
fs = require 'fs'
path = require 'path'

ExifImage = require('exif').ExifImage
gm = require 'gm'

##
# Parses images and tries to load metadata information from them.
# Adds helper method to access the image content directly
# Constructor options and defaults:
# createThumbnails: false
# thumbnailSize: 400
# thumbnailQuality: 70
module.exports = class ImageParser extends Parser

##
# As for now, only parse jpg images
doesParse: (filename) ->
if not Parser.hasExtension filename, ['jpg']
return false

# Don't re-parse auto generated thumbnails
if filename.indexOf('.thumbnail.jpg', filename.length - 14) != -1
return false

return true

##
# Parses the image (exif information) and adds functions to actually load the file content
parse: (filename, dirNode) =>
return new Promise (resolve, reject) =>
# build up the entry
imageEntry = {}

# Determine key name
imageEntry.name = path.basename filename, '.jpg'
imageEntry.fileName = filename

# Load exif information
p = @loadExifData(filename)
p = p.then (exifData) ->
imageEntry.exifData = exifData

# If desired by the user, create a thumbnail
if @options.createThumbnails
p = p.then =>
@createThumbnail filename, imageEntry

# Attach functions to return the image content
imageEntry.getImageData = () => return @returnImageData(filename)

# Attach to node
if not dirNode.hasProperty 'images'
dirNode.setProperty 'images', {}

images = dirNode.getProperty 'images'
images[imageEntry.name] = imageEntry

resolve(p)

loadExifData: (filename) ->
return new Promise (resolve, reject) ->
try
new ExifImage {image: filename}, (error, exifData) ->
if error != false
reject(error)
return
resolve(exifData)
catch error
reject(error)
return

createThumbnail: (filename, imageEntry) =>
return new Promise (resolve, reject) =>
thumbnailSize = @options.thumbnailSize or 300
thumbnailQuality = @options.thumbnailQuality or 70

thumbnailFilename = filename + '.thumbnail.jpg'

gm(filename)
.resize(thumbnailSize, thumbnailSize)
.noProfile()
.quality(thumbnailQuality)
.compress('JPEG')
.write thumbnailFilename, (err) =>
if err?
reject(err)
return

imageEntry.thumbnail = {
filename: thumbnailFilename
getImageData: () => return @returnImageData(thumbnailFilename)
}
resolve()

##
# Load and cache image data
# returns a promise
returnImageData: (imageName) =>
@imageDataCache ?= {}

if @imageDataCache[imageName]?
return Promise.resolve(@imageDataCache[imageName])

return new Promise (resolve, reject) =>
fs.readFile imageName, (error, data) =>
if error?
reject(error)
return

@imageDataCache[imageName] = data
resolve(data)






5 changes: 5 additions & 0 deletions src/parser/Parser.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
# @class Parser
module.exports = class Parser

##
# Save options to be used when parsing
constructor: (options = {}) ->
@options = options

##
# Parses one file
# @param {String} filename Full path to the file to be parsed
Expand Down
79 changes: 79 additions & 0 deletions test/ImageParser.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
chai = require 'chai'
expect = chai.expect
path = require 'path'
fs = require 'fs'

testUtilities = require './testUtilities'

ImageParser = require '../src/parser/ImageParser.coffee'
TreeNode = require '../src/TreeNode.coffee'

testPath = undefined

describe 'ImageParser', ->
# Create an empty directory to test in
beforeEach (done) ->
testUtilities.createTempDirectory (dirName) ->
testPath = dirName
done()

# Delete temporary directory and all its contents
afterEach (done) ->
testUtilities.deleteTempDirectory(testPath, done)

it 'should only parse .jpg files', ->
parser = new ImageParser()

expect(parser.doesParse('myFile.png')).to.be.false
expect(parser.doesParse('myFile.jpg')).to.be.true

it 'should attach image metadata and helper functions', (done) ->
# Copy test image
fs.createReadStream('./test/ImageParserTest.jpg').pipe(fs.createWriteStream(path.join(testPath ,'testImage.jpg')));

node = new TreeNode()
parser = new ImageParser()

parser.parse(path.join(testPath, 'testImage.jpg'), node)
.then ->
# Expect an image property with the testImage key and exif data
expect(node.hasProperty('images')).to.be.true
imageProperty = node.getProperty 'images'

image = imageProperty.testImage
expect(image).not.to.be.null
expect(image.exifData).not.to.be.null

# Expect Actual data when requesting it
image.getImageData()
.then (imageData) ->
expect(imageData.length).to.eql(9371)
done()
.catch (error) -> done (error)
.catch (error) -> done(error)

it 'should generate a thumbnail if configured to do so', (done) ->
# Copy test image
fs.createReadStream('./test/ImageParserTest.jpg').pipe(fs.createWriteStream(path.join(testPath ,'testImage.jpg')));

node = new TreeNode()
parser = new ImageParser({createThumbnails: true})

parser.parse(path.join(testPath, 'testImage.jpg'), node)
.then ->
# Expect a thumbnail sub-property per image
imageProperty = node.getProperty 'images'
image = imageProperty.testImage

expect(image.thumbnail).not.to.be.null
expect(image.thumbnail.filename).not.to.be.null

# Expect actual data when requesting thumbnail
# (Since the exact image size might differ depending on the gm installation, use a range to test)
image.thumbnail.getImageData()
.then (imageData) ->
expect(imageData.length > 1000 and imageData.length < 3000).to.be.true
done()
.catch (error) -> done (error)
.catch (error) -> done(error)

Binary file added test/ImageParserTest.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 4837c6b

Please sign in to comment.