Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Change-Id: I168cf7b41965f011817f88eeda94a1c02e4bd4c1
- Loading branch information
Nicolas Garnier
committed
Oct 25, 2016
1 parent
dc99790
commit 1ab05d2
Showing
10 changed files
with
429 additions
and
0 deletions.
There are no files selected for viewing
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,44 @@ | ||
# Automatically Extract Images Metadata | ||
|
||
This sample demonstrates how to automatically extract images metadata that are uploaded to Firebase Storage ImageMagick. | ||
|
||
|
||
## Functions Code | ||
|
||
See file [functions/index.js](functions/index.js) for the email sending code. | ||
|
||
The image metadata is provided using ImagMagick `identify` tool which is installed by default on all Firebase Functions. This is a CLI for which we use a NodeJS wrapper. The image is first downloaded locally from the Firebase Storage bucket to the `tmp` folder using the [google-cloud](https://github.com/GoogleCloudPlatform/google-cloud-node) SDK. | ||
|
||
The dependencies are listed in [functions/package.json](functions/package.json). | ||
|
||
|
||
## Trigger rules | ||
|
||
The function triggers on upload of any file to the Firebase Functions bucket. | ||
|
||
|
||
## Storage and Database Structure | ||
|
||
Users Upload an image to Firebase Storage to the path `/<timestamp>/<filename>` and in return the Function will write to the `/<timestamp>/<filename>` path in the database. The filename typically contains illegal characters for a Firebase Realtime Database keys (such as `.`) so we're replacing all these by the `*` character. | ||
|
||
For example the metadata for the file at path `/1477402116302/mypic.jpg` will be written to the corresponding Database path `/1477402116302/mypic*jpg` | ||
|
||
|
||
## Setting up the sample | ||
|
||
This sample comes with a Function and web-based UI for testing the function. To configure it: | ||
|
||
- Create a Firebase project on the [Firebase Console](https://console.firebase.google.com) and visit the **Storage** tab. | ||
- Enable Anonymous sign in the Auth section | ||
- In `functions/index.js` replace the placeholder `FIREBASE_STORAGE_BUCKET_NAME` with the name of the Firebase Storage bucket which can be found in the **Storage** tab of your Firebase project's console. It is typically of the form `<project-id>.appspot.com`. | ||
- Import and configure Firebase in the `index.html` where the `TODO` is located | ||
|
||
|
||
## Deploy and test | ||
|
||
To test the sample: | ||
|
||
- Deploy your project using `firebase deploy` | ||
- Open the Deploy Web UI using `firebase open`, typically at the URL `https://<projectID>.firebaseapp.com` | ||
- Upload an image using the Web UI. | ||
- You should see the metadata displayed below after a bit. |
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,6 @@ | ||
{ | ||
"rules": { | ||
".read": true, | ||
".write": false | ||
} | ||
} |
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,8 @@ | ||
{ | ||
"database": { | ||
"rules": "database.rules.json" | ||
}, | ||
"hosting": { | ||
"public": "public" | ||
} | ||
} |
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,138 @@ | ||
/** | ||
* Copyright 2016 Google Inc. All Rights Reserved. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for t`he specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
'use strict'; | ||
|
||
const im = require('imagemagick'); | ||
const Q = require('q'); | ||
const functions = require('firebase-functions'); | ||
const mkdirp = require('mkdirp-then'); | ||
const gcs = require('@google-cloud/storage')(); | ||
|
||
/** | ||
* When an image is uploaded in the Storage bucket the information and metadata of the image (the | ||
* output of ImageMagick's `identify -verbose`) is saved in the Realtime Database. | ||
*/ | ||
// TODO(DEVELOPER): Replace the placeholder below with the name of the Firebase Functions bucket. | ||
exports.metadata = functions.cloud.storage(FIREBASE_STORAGE_BUCKET_NAME).onChange(event => { | ||
console.log(event); | ||
|
||
const filePath = event.data.name; | ||
const filePathSplit = filePath.split('/'); | ||
const fileName = filePathSplit.pop(); | ||
const fileDir = filePathSplit.join('/') + (filePathSplit.length > 0 ? '/' : ''); | ||
const tempLocalDir = `/tmp/${fileDir}`; | ||
const tempLocalFile = `${tempLocalDir}${fileName}`; | ||
|
||
// Exit if this is triggered on a file that is not an image. | ||
if (!event.data.contentType.startsWith('image/')) { | ||
console.log('This is not an image.'); | ||
return null; | ||
} | ||
|
||
// Exit if this is a move or deletion event. | ||
if (event.data.resourceState === 'not_exists') { | ||
console.log('This is a deletion event.'); | ||
return null; | ||
} | ||
|
||
// Create the temp directory where the storage file will be downloaded. | ||
return mkdirp(tempLocalDir).then(() => { | ||
// Download file from bucket. | ||
return promisedDownloadFile(event.data.bucket, filePath, tempLocalFile).then(() => { | ||
// Get Metadata from image. | ||
return promisedImageMagickMetadata(tempLocalFile).then(metadata => { | ||
// Save metadata to realtime datastore. | ||
return functions.app.database().ref(makeKeyFirebaseCompatible(filePath)).set(metadata).then(() => { | ||
console.log('Wrote to:', fileDir, 'data:', metadata); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); | ||
|
||
/** | ||
* Returns a promise that resolves when the given file has been downloaded from the bucket. | ||
*/ | ||
function promisedDownloadFile(bucketName, filePath, tempLocalFile) { | ||
const result = Q.defer(); | ||
const bucket = gcs.bucket(bucketName); | ||
bucket.file(filePath).download({ | ||
destination: tempLocalFile | ||
}, err => { | ||
if (err) { | ||
result.reject(err); | ||
} else { | ||
console.log('The file has been downloaded to', tempLocalFile); | ||
result.resolve(); | ||
} | ||
}); | ||
return result.promise; | ||
} | ||
|
||
/** | ||
* Returns a promise that resolves with the metadata extracted from the given file. | ||
*/ | ||
function promisedImageMagickMetadata(localFile) { | ||
const result = Q.defer(); | ||
|
||
im.identify(['-verbose', localFile], (err, output) => { | ||
if (err) { | ||
console.error('Error', err); | ||
result.reject(err); | ||
} else { | ||
result.resolve(imageMagickOutputToObject(output)); | ||
} | ||
}); | ||
return result.promise; | ||
} | ||
|
||
/** | ||
* Convert the output of ImageMagick's `identify -verbose` command to a JavaScript Object. | ||
*/ | ||
function imageMagickOutputToObject(output) { | ||
let previousLineIndent = 0; | ||
const lines = output.match(/[^\r\n]+/g); | ||
lines.shift(); // Remove First line | ||
lines.forEach((line, index) => { | ||
const currentIdent = line.search(/\S/); | ||
line = line.trim(); | ||
if (line.endsWith(':')) { | ||
lines[index] = makeKeyFirebaseCompatible(`"${line.replace(':', '":{')}`); | ||
} else { | ||
const split = line.replace('"', '\\"').split(': '); | ||
split[0] = makeKeyFirebaseCompatible(split[0]); | ||
lines[index] = `"${split.join('":"')}",`; | ||
} | ||
if (currentIdent < previousLineIndent) { | ||
lines[index - 1] = lines[index - 1].substring(0, lines[index - 1].length - 1); | ||
lines[index] = new Array(1 + (previousLineIndent - currentIdent) / 2).join('}') + ',' + lines[index]; | ||
} | ||
previousLineIndent = currentIdent; | ||
}); | ||
output = lines.join(''); | ||
output = '{' + output.substring(0, output.length - 1) + '}'; // remove trailing comma. | ||
output = JSON.parse(output); | ||
console.log('Metadata extracted from image', output); | ||
return output; | ||
} | ||
|
||
/** | ||
* Makes sure the given string does not contain characters that can't be used as Firebase | ||
* Realtime Database keys such as '.' and replaces them by '*'. | ||
*/ | ||
function makeKeyFirebaseCompatible(key) { | ||
return key.replace(/\./g, '*'); | ||
} |
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,12 @@ | ||
{ | ||
"name": "functions", | ||
"description": "Firebase Functions", | ||
"dependencies": { | ||
"@google-cloud/storage": "^0.2.0", | ||
"firebase": "^3.4.1", | ||
"firebase-functions": "https://storage.googleapis.com/firebase-preview-drop/node/firebase-functions/firebase-functions-preview.latest.tar.gz", | ||
"imagemagick": "^0.1.3", | ||
"mkdirp-then": "^1.2.0", | ||
"q": "^1.4.1" | ||
} | ||
} |
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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,78 @@ | ||
<!DOCTYPE html> | ||
<!-- | ||
Copyright (c) 2016 Google Inc. | ||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
http://www.apache.org/licenses/LICENSE-2.0 | ||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
--> | ||
<html> | ||
<head> | ||
<meta charset=utf-8 /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
<title>Image Metadata Extractor</title> | ||
|
||
<!-- Material Design Theming --> | ||
<link rel="stylesheet" href="https://code.getmdl.io/1.1.3/material.orange-indigo.min.css"> | ||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons"> | ||
<script defer src="https://code.getmdl.io/1.1.3/material.min.js"></script> | ||
|
||
<link rel="stylesheet" href="main.css"> | ||
|
||
<!-- Firebase --> | ||
<!-- ******************************************************************************* | ||
* TODO(DEVELOPER): Paste the initialization snippet by navigating to: | ||
https://console.firebase.google.com | ||
and choosing a project you've created. Then click the red HTML logo at the top | ||
right of the page with the caption "Add Firebase to your web app". | ||
Copy the snippet that appears in place of this comment. | ||
*************************************************************************** --> | ||
|
||
</head> | ||
<body> | ||
<div class="demo-layout mdl-layout mdl-js-layout mdl-layout--fixed-header"> | ||
|
||
<!-- Header section containing title --> | ||
<header class="mdl-layout__header mdl-color-text--white mdl-color--light-blue-700"> | ||
<div class="mdl-cell mdl-cell--12-col mdl-cell--12-col-tablet mdl-grid"> | ||
<div class="mdl-layout__header-row mdl-cell mdl-cell--12-col mdl-cell--12-col-tablet mdl-cell--8-col-desktop"> | ||
<h3>Image Metadata Extractor</h3> | ||
</div> | ||
</div> | ||
</header> | ||
|
||
<main class="mdl-layout__content mdl-color--grey-100"> | ||
<div class="mdl-cell mdl-cell--12-col mdl-cell--12-col-tablet mdl-grid"> | ||
|
||
<!-- Container for the demo --> | ||
<div class="mdl-card mdl-shadow--2dp mdl-cell mdl-cell--12-col mdl-cell--12-col-tablet mdl-cell--12-col-desktop"> | ||
<div class="mdl-card__title mdl-color--light-blue-600 mdl-color-text--white"> | ||
<h2 class="mdl-card__title-text">Upload an image</h2> | ||
</div> | ||
<div class="mdl-card__supporting-text mdl-color-text--grey-600" id="messagesDiv"> | ||
<p>Select an image below. When it is uploaded, a shareable link to the file and the image's metadata will be displayed.</p> | ||
<h6>Choose File</h6> | ||
<input type="file" disabled id="demo-file" name="demo-file" accept="image/*;capture=camera"/> | ||
<h6>Image URL:</h6> | ||
<span id="demo-link"></span> | ||
<h6>Image Metadata:</h6> | ||
<pre><code id="demo-metadata"></code></pre> | ||
</div> | ||
</div> | ||
</div> | ||
</main> | ||
</div> | ||
<script src="main.js"></script> | ||
</body> | ||
</html> |
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,39 @@ | ||
/** | ||
* Copyright 2015 Google Inc. All Rights Reserved. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
html, body { | ||
font-family: 'Roboto', 'Helvetica', sans-serif; | ||
} | ||
.mdl-grid { | ||
max-width: 1024px; | ||
margin: auto; | ||
} | ||
.mdl-layout__header-row { | ||
padding: 0; | ||
} | ||
h3 { | ||
background: url('firebase-logo.png') no-repeat; | ||
background-size: 40px; | ||
padding-left: 50px; | ||
} | ||
pre { | ||
overflow-x: scroll; | ||
line-height: 18px; | ||
} | ||
code { | ||
white-space: pre-wrap; | ||
word-break: break-all; | ||
} |
Oops, something went wrong.