Skip to content

Commit

Permalink
Add the exif-image sample.
Browse files Browse the repository at this point in the history
Change-Id: I168cf7b41965f011817f88eeda94a1c02e4bd4c1
  • Loading branch information
Nicolas Garnier committed Oct 25, 2016
1 parent dc99790 commit 1ab05d2
Show file tree
Hide file tree
Showing 10 changed files with 429 additions and 0 deletions.
6 changes: 6 additions & 0 deletions README.md
Expand Up @@ -30,6 +30,12 @@ Demonstrates how to automatically convert images that are uploaded to Firebase S

Uses a Firebase Storage trigger.

### [Extract Image MEtadata](/exif-images)

Demonstrates how to automatically extract image's metadata using ImageMagick for images that are uploaded to Firebase Storage.

Uses a Firebase Storage trigger.

### [Text Moderation](/text-moderation)

How to moderate user input text for bad words. For example this can be used to moderate usernames, chat or forum messages.
Expand Down
44 changes: 44 additions & 0 deletions exif-images/README.md
@@ -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.
6 changes: 6 additions & 0 deletions exif-images/database.rules.json
@@ -0,0 +1,6 @@
{
"rules": {
".read": true,
".write": false
}
}
8 changes: 8 additions & 0 deletions exif-images/firebase.json
@@ -0,0 +1,8 @@
{
"database": {
"rules": "database.rules.json"
},
"hosting": {
"public": "public"
}
}
138 changes: 138 additions & 0 deletions exif-images/functions/index.js
@@ -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, '*');
}
12 changes: 12 additions & 0 deletions exif-images/functions/package.json
@@ -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"
}
}
Binary file added exif-images/public/firebase-logo.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
78 changes: 78 additions & 0 deletions exif-images/public/index.html
@@ -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>
39 changes: 39 additions & 0 deletions exif-images/public/main.css
@@ -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;
}

0 comments on commit 1ab05d2

Please sign in to comment.