0
votes

I'm trying to create a Firebase Function that allows me to pass an array of image URLs in to create generate a montage, upload the file to Firebase Storage and then return the generated Download URL. This will be called from my app, so I'm using functions.https.onCall.

const functions = require("firebase-functions");
const admin = require('firebase-admin');
var gm = require('gm').subClass({imageMagick: true});
admin.initializeApp();


exports.createMontage = functions.https.onCall((data, context) => {
var storageRef = admin.storage().bucket( 'gs://xyz-zyx.appspot.com' );
var createdMontage = storageRef.file('createdMontage.jpg');
function generateMontage(list){
  let g = gm()
   list.forEach(function(p){
       g.montage(p);
   })
   g.geometry('+81+81')
   g.density(5000,5000)
   .write(createdMontage, function(err) {
       if(!err) console.log("Written montage image.");
   });
   return true
}

generateMontage(data)

return createdMontage.getDownloadURL();

});

The function generateMontage() works locally on NodeJs (with a local write destination).

Thank you.

3
I think you should check this related question on SO. It is mentioned that the Storage Object you are trying to access doesn't have the ref() functions, only app and bucket().marian.vladoi
I've tried bucket(). The first link is dead in the answer. When I clicked through the API and found this: cloud.google.com/storage/docs/samples/storage-upload-file, this 1) doesn't seem to be going through admin and 2) this means that I have to name and upload a file. I'm not sure how to .write(filename)Ezra Butler
Will the file be in a public folder? As in allow read; as its storage rule.Stratubas
I'm able to post in general. Just not in this. It's not a rules issue.Ezra Butler

3 Answers

2
votes

Have a look at this example from the docs:

https://cloud.google.com/storage/docs/uploading-objects#storage-upload-object-code-sample

2021-01-11 Update

Here's a working example. I'm using regular Cloud Functions and it's limited in that the srcObject, dstObject and bucketName are constants but, it does create montages which is your goal.

PROJECT=[[YOUR-PROJECT]]
BILLING=[[YOUR-BILLING]]
REGION=[[YOUR-REGION]]

FUNCTION=[[YOUR-FUNCTION]]

BUCKET=[[YOUR-BUCKET]]
OBJECT=[[YOUR-OBJECT]] # Path from ${BUCKET} root

gcloud projects create ${PROJECT}

gcloud beta billing projects link ${PROJECT} \
--billing-account=${BILLING}

gcloud services enable cloudfunctions.googleapis.com \
--project=${PROJECT}

gcloud services enable cloudbuild.googleapis.com \
--project=${PROJECT}

gcloud functions deploy ${FUNCTION} \
--memory=4gib \
--max-instances=1
--allow-unauthenticated \
--entry-point=montager \
--set-env-vars=BUCKET=${BUCKET},OBJECT=${OBJECT} \
--runtime=nodejs12 \
--trigger-http \
--project=${PROJECT} \
--region=${REGION}

ENDPOINT=$(\
  gcloud functions describe ${FUNCTION} \
  --project=${PROJECT} \
  --region=${REGION} \
  --format="value(httpsTrigger.url)")

curl \
--request GET \
${ENDPOINT}


`package.json`:
```JSON
{
  "name": "montage",
  "version": "0.0.1",
  "dependencies": {
    "@google-cloud/storage": "5.7.1",
    "gm": "^1.23.1"
  }
}

And index.js:

const { Storage } = require('@google-cloud/storage');
const storage = new Storage();

const gm = require('gm').subClass({ imageMagick: true });

const bucketName = process.env["BUCKET"];
const srcObject = process.env["OBJECT"];
const dstObject = "montage.png";

// Creates 2x2 montage
const list = [
  `/tmp/${srcObject}`,
  `/tmp/${srcObject}`,
  `/tmp/${srcObject}`,
  `/tmp/${srcObject}`
];

const montager = async (req, res) => {
  // Download GCS `srcObject` to `/tmp`
  const f = await storage
    .bucket(bucketName)
    .file(srcObject)
    .download({
      destination: `/tmp/${srcObject}`
    });

  // Creating GCS write stream for montage
  const obj = await storage
    .bucket(bucketName)
    .file(dstObject)
    .createWriteStream();

  let g = gm();
  list.forEach(f => {
    g.montage(f);
  });

  console.log(`Returning`);
  g
    .geometry('+81+81')
    .density(5000, 5000)
    .stream()
    .pipe(obj)
    .on(`finish`, () => {
      console.log(`finish`);
      res.status(200).send(`ok`);
    })
    .on(`error`, (err) => {
      console.log(`error: ${err}`);
      res.status(500).send(`uhoh!`);
    });
}
exports.montager = montager;
0
votes

I have never used 'gm', but, according to its npm page, it has a toBuffer function.

So maybe something like this could work:

const functions = require('firebase-functions');
const admin = require('firebase-admin');
const gm = require('gm').subClass({ imageMagick: true });
admin.initializeApp();

exports.createMontage = functions.https.onCall((data, _context) => {
  const bucketName = 'xyz-zyx'; // not sure, I've always used the default bucket
  const bucket = admin.storage().bucket(bucketName);
  const storagePath = 'createdMontage.jpg';
  const fileRef = bucket.file(storagePath);
  const generateMontage = async (list) => {
    const g = gm();
    list.forEach(function (p) {
      g.montage(p);
    });
    g.geometry('+81+81');
    g.density(5000, 5000);
    return new Promise(resolve => {
      g.toBuffer('JPG', (_err, buffer) => {
        const saveTask = fileRef.save(buffer, { contentType: 'image/jpeg' });
        const baseStorageUrl = `https://firebasestorage.googleapis.com/v0/b/${bucket.name}/o/`;
        const encodedPath = encodeURIComponent(storagePath);
        const postfix = '?alt=media'; // see stackoverflow.com/a/58443247/6002078
        const publicUrl = baseStorageUrl + encodedPath + postfix;
        saveTask.then(() => resolve(publicUrl));
      });
    });
  };
  return generateMontage(data);
});

But it seems it can be done more easily. As Methkal Khalawi commented:

here is a full example on how to use ImageMagic with Functions. Though they are using it for blurring an image but the idea is the same. And here is a tutorial from the documentation.

0
votes

I think you can pipe output stream from gm module to firebase storage object write stream.

const functions = require("firebase-functions");
const admin = require('firebase-admin');
var gm = require('gm').subClass({imageMagick: true});
admin.initializeApp();

exports.createMontage = functions.https.onCall(async (data, context) => {
   var storage = admin.storage().bucket( 'gs://xyz-zyx.appspot.com' );
   
   var downloadURL = await new Promise((resolve, reject) => {
         let g = gm()
         list.forEach(function(p){
             g.montage(p);
         })
         g.geometry('+81+81')
         g.density(5000,5000)
          .stream((err, stdout, stderr) => {
              if (err) {
                  reject();
              }
              stdout.pipe(
                  storage.file('generatedMotent.png).createWriteStream({
                    metadata: {
                        contentType: 'image/png',
                    },
                })
              ).on('finish', () => {
                storage
                    .file('generatedMotent')
                    .getSignedUrl({
                        action: 'read',
                        expires: '03-09-2491', // Non expring public url
                    })
                    .then((url) => {
                        resolve(url);
                    });
              });
         })
   });

   return downloadURL;
});

FYI, Firebase Admin SDK storage object does not have getDownloadURL() function. You should generate non-expiring public signed URL from the storage object.

In addition to, it should cause another problem after some period of time according to this issue. To get rid of this issue happening, you should initialize firebase app with permanent service account.

const admin = require('firebase-admin');
const serviceAccount = require('../your-service-account.json');

admin.initializeApp({
    credential: admin.credential.cert(serviceAccount),
    projectId: JSON.parse(process.env.FIREBASE_CONFIG).projectId,
    databaseURL: JSON.parse(process.env.FIREBASE_CONFIG).databaseURL,
    storageBucket: JSON.parse(process.env.FIREBASE_CONFIG).storageBucket,
});