توليد الصور المصغرة عند رفع الصور Firebase Cloud Functions

سنشرح في هذا الدرس كيفية توليد صورة مصغرة عند رفع أي صورة على Firebase Storage.
هذه الفكرة بشكل عام تستخدمها معظم التطبيقات الضخمة مثل Facebook,Twitter الخ.. أو حتى على هاتفك الأندرويد فستجد مجلد باسم .thumbnails يحتوي على كافة الصور المصغرة.
والهدف هو تقليل استهلاك الموارد من Client وإرسال فقط الصورة المصغرة  وعندما يريد الClient رؤية الصورة الكاملة نعطيه الصورة الأصلية..

قبل البدء سنشرح آلية عمل الفكرة 

يجب أن تعرف أن كل مشروع تقوم بإنشاءه على Firebase هو عبارة عن مشروع على Google Cloud Platform وللتأكيد يمكنك الدخول على هذا الرابط وستجد كافة المشاريع عل Firebase.

عندما يتم رفع الصورة الأصلية بحجمها الكامل على Firebase Storage نقوم بتحميلها ل مجلد مؤقت tmp ثم نقوم بإنشاء نفس الصورة  بنفس المجلد tmp ثم تصغيرها باستخدام أداة Image-Magick وهي أداة مضمنة ضمن Google Cloud Storage كما يمكنك إضافة Blur على الصورة إذا أردت ومن ثم إعادة رفع الصورة الجديدة الى Firebase Storage  ثم حذف الصورة الأصلية المؤقتة التي حملناها الى المجلد المؤقت والصورة المصغرة المؤقتة وأخيراً إضافة رابط الصورة الأصلية والمصغرة الى الداتابيز أو إضافة مسار الصورة على الداتابيز

ان لم تتضح لك الفكرة ألقِ نظرة على هذه الصورة

 

كالعادة نقوم بإنشاء مشروع على Firebase ونقوم بربطه بCloud Functions ,يمكنك متابعة هذا الدرس عن كيفية الإعداد

الآن سنحتاج لتثبيت بعض المكتبات عبر NPM

1-Google Cloud Storage التي سنقوم بتحميل الملفات إليها

npm install @google-cloud/storage --save

2-mkdirp-promise  والتي تقوم بإنشاء مجلد tmp وتعود لنا ب Promise عند الإنتهاء

npm install mkdirp-promise --save

3-child-process-promise وهي التي تسمح لنا باستخدام أداة Image-Magick وتعود لنا ب Promise عند الإنتهاء

npm install child-process-promise --save

بعد الإنتهاء من التثبيت نقوم بفتح index.js لنبدأ بكتابة بعض الأكواد

نقوم بتعريف المكتبات functions و admin 

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);

نقوم بتعريف mkdirp-promise

const mkdirp = require('mkdirp-promise');

ثم نقوم بتعريف Google Cloud Storage

ولكن لنستطيع التحكم عبر Google Cloud Storage يجب علينا تحميل ملف يحتوي على بعض الصلاحيات لتستطيع الCloud Functions من التعامل مع Google Cloud Storage
ولهذا يجب عليك الدخول على حسابك في Google Cloud Console  وقم باختيار المشروع الخاص بك ثم ادخل على Credentials واختر Create Credentials ثم اختر Service Account Key

ثم اختر App Engine default service account واختر النوع JSON وأخيراً اختر Create

الآن سيتم تحميل الملف بصيغة json قم بإعادة تسميته الى key.json وضعه في مجلد functions على حاسوبك

الآن نعود الى ملف index.js ونقوم بتعريف Google Cloud Storage ونعطيه ونعطيه key.json

const gcs = require('@google-cloud/storage')({ keyFilename: 'key.json' });

ثم نقوم بتعريف مكتبة child-process-promise ونختار منها spawn فقط (فهي تحتوي على الكثير من الأشياء الأخرى)

const spawn = require('child-process-promise').spawn;

ثم نقوم بتعريف بعض المكتبات الإفتراضية للتعامل مع الملفات والمسارات كما سنرى لاحقاً

const path = require('path');
const os = require('os');
const fs = require('fs');

أخيراً نقوم بتعريف بعض الثوابت الخاصة بالصورة المصغرة كالإرتفاع والعرض

أما عن THUMB_PREFIX فهو اسم سنستعمله عن تصغير أي صورة

كمثال عندما يتم رفع الصورة الأصلية باسم image.jpg فإن الصورة المصغرة سيكون اسمها thumb_image.jpg 

const THUMB_MAX_HEIGHT = 200;
const THUMB_MAX_WIDTH = 200;
// Thumbnail prefix added to file names.
const THUMB_PREFIX = 'thumb_';

الآن نقوم بتعريف function ونسميها generateThumbnailFromImage

exports.generateThumbnailFromImage = functions.storage.object().onChange(event => {
    
});

ونلاحظ أنه قد استخدمنا functions.storage.object.onChange وهذه الfunction تنادى عندما يحدث أي تغيير على Firebase Storage في أي مجلد وبكل الأشكال(حذف,تعديل,إضافة)

وتعود لنا ب event ويحتوي على معلومات الشيئ الذي تغير داخل Firebase Storage (مسار الملف,نوعه,الخ..)

الآن نقوم بأخذ مسار الصورة كاملاً  الذي تم رفعها

سنقوم لاحقاً بطباعة كافة المسارات لنراها لاحقاً في logs

مثال:images/image.jpg

const originalFilePath = event.data.name;

ثم نقوم بأخذ اسم المجلد الذي تم رفع الصورة اليها واستخدمنا مكتبة path التي قامت بأخذ مسار الملف كاملاً وإعادة اسم المجلد فقط

مثال:images/

اما اذا تم رفع الصورة الى المجلد الرئيسي فسيكون اسم المجلد .

const originalFileDir = path.dirname(originalFilePath);

ثم أخذنا اسم الملف واستخدمنا مكتبة path لتعيد لنا اسم الملف فقط دون مساره

مثال:image.jpg

const originalFileName = path.basename(originalFilePath);

 

الآن سنقوم بإنشاء مسار لملف الصورة الأصلية في المجلد المؤقتtmp ومسار آخر لملف الصورة المصغرة المؤقتة في المجلد tmp أيضاً (إنشاء المسار ك string فقط وليس إنشاء الملف)

نبدأ بمسار ملف الصورة الأصلية وقمنا باستخدام مكتبة os لإعطائنا مسار المجلدtmp واستخدمنا مكتبةpath لدمج مسار مجلد tmp مع مسار الصورة الأصلي

مثال: /tmp/images/image.jpg

const tempFilePath = path.join(os.tmpdir(), originalFilePath);

ثم أخذنا ملف tempFilePath واستخرجنا منه مسار المجلد

مثال:/tmp/images

const tempFileDir = path.dirname(tempFilePath);

الآن نقوم بإنشاء مسار ملف الصورة المصغرة النهائي الذي سيتم رفعه الى Firebase Storage

مثال:images/thumb_image.jpg

const thumbFilePath = path.normalize(path.join(originalFileDir, `${THUMB_PREFIX}${originalFileName}`));

ثم مسار ملف الصورة المصغرة المؤقتة وأخذنا المسار الصورة المصغرة الأساسي ولكن وضعناه في tmp
مثال:tmp/images/thumb_image.jpg

const tempThumbFilePath = path.join(os.tmpdir(), thumbFilePath);

انتهينا من تعريف المسارات..

الآن سنقوم ببعض التحققات ,في حال أن الملف المرفوع ليس صورة فسنقوم بعمل return null ولا ننفذ شيئ

واستخدمنا ContentType الذي يوفره event وقمنا بالتحقق اذا لم يكن يبدأ بimage/ لا تقوم بتنفيذ شيئ

 if (!event.data.contentType.startsWith('image/')) {
    console.log('This is not an image.');
    return null;
  }

ولأن function تنادى عند حدوث أي تغيير فهذا يعني أنه عندما نقوم برفع صورة فستعمل function و عندما تقوم function بتوليد الصورة المصغرة فإنه يحدث تغيير على Firebase Storage وبالتالي إعادة عمل function وستضل الfunction تعمل للأبد أو مايسمى ب Recursive

ولهذا يجب علينا التحقق اذا كان الملف المرفوع يبدأ ب thumb_ (لأنه عندما تقوم function بتوليد الصورة المصغرة فسيكون اسمها يبدأ بهذا الإسم) وعندها لا نقوم بتنفيذ أي شيئ

 if (originalFileName.startsWith(THUMB_PREFIX)) {
    console.log('Already a Thumbnail.');
    return null ;
  }

وأخيراً نقوم بالتحقق اذا كان حدث التغيير هو حذف فلا ننفذ شيئ

 if (event.data.resourceState === 'not_exists') {
    console.log('This is a deletion event.');
    return null;
  }

الآن نبدأ بتعريف بما يدعى ب Bucket ويمكن أن تعتبره بأنه الشيئ الذي يحتوي على الملفات ضمن Google Cloud Storage

const bucket = gcs.bucket(event.data.bucket);

ونقوم بتعريف الBucket الخاص بالصورة الأساسية والمصغرة

const file = bucket.file(originalFilePath);
const thumbFile = bucket.file(thumbFilePath);

الآن نقوم بإنشاء المجلد المؤقتtmp باستخدام مكتبة mkdirp والتي تعيد لنا Promise عند الإنتهاء ثم نقوم بتحميل ملف الصورة الأساسية الى المجلد tmp

return mkdirp(tempFileDir).then(() => {
    // Download file from bucket.
    return file.download({ destination: tempFilePath });
  })

ثم نقوم باستخدام ImageMagick لتصغير الصورة وهي تأخذ بعض البارامترات المخصصة ولكن مايهمنا هو عرض الصورة وارتفاعها الذي نريد وأخير مسار الملف المؤقت الذي سيتم حفظ الصورة فيه

.then(() => {


    console.log('The file has been downloaded to', tempFilePath);
    // Generate a thumbnail using ImageMagick.

    return spawn('convert', [tempFilePath, '-thumbnail', `${THUMB_MAX_WIDTH}x${THUMB_MAX_HEIGHT}>`, tempThumbFilePath]);


  })

إذا أردت إضافة بعض Blur يمكنك استخدام أيضاً ImageMagick والتي توفر هذا الشيئ

وللتحكم بدرجةBlur يمكنك تغيير 0x8 الى أن تحصل على درجة ملائمة

ولاحظ أنه قمنا بحفظ الصورة التي تم عمل عليها Blur بنفس الصورة المصغرة وليست نسختين

.then(() => {

    return spawn('convert', [tempThumbFilePath, '-channel', 'RGBA', '-blur', '0x8', tempThumbFilePath]);


  })

ثم نقوم برفع الصورة المصغرة من المسار tmp الى Firebase Storage 

.then(() => {

    // Uploading the Thumbnail.
    return bucket.upload(tempThumbFilePath, { destination: thumbFilePath });

  })

الآن سنقوم بحذف الملفات المؤقتة من tmp

.then(() => {
    // Once the image has been uploaded delete the local files to free up disk space.
    console.log('Thumbnail uploaded to Storage at', thumbFilePath);
    
    fs.unlinkSync(tempFilePath);
    fs.unlinkSync(tempThumbFilePath);
})

ثم سقون بإنشاء روابط للصورة الأساسية والمصغرة وسنقوم بإعطاء بعض الصلاحيات للصورة

read أي أنها قابلة للقراءة فقط ,و expires أي متى تنتهي صلاحية الرابط

واستخدمنا Promise.all وهي تعيد مصفوفة من Promise

.then(() => {
    // Once the image has been uploaded delete the local files to free up disk space.
    console.log('Thumbnail uploaded to Storage at', thumbFilePath);
    
    fs.unlinkSync(tempFilePath);
    fs.unlinkSync(tempThumbFilePath);
    const config = {
      action: 'read',
      expires: '03-01-2500',
    };
    return Promise.all([
      thumbFile.getSignedUrl(config),
      file.getSignedUrl(config),
    ]);
  })

أخيراً نقوم بأخذ النتائج من مصفوفة Promise ونقوم بحفظها في الداتابيز

.then((results) => {
    console.log('Got Signed URLs.');
    const thumbResult = results[0];
    const originalResult = results[1];
    const thumbFileUrl = thumbResult[0];
    const fileUrl = originalResult[0];
    // Add the URLs to the Database
    return admin.database().ref('images').push({ path: fileUrl, thumbnail: thumbFileUrl });
})

ليصبح الكود كاملاً كالتالي

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);

const mkdirp = require('mkdirp-promise');
// Include a Service Account Key to use a Signed URL
const gcs = require('@google-cloud/storage')({ keyFilename: 'key.json' });



const spawn = require('child-process-promise').spawn;

const path = require('path');
const os = require('os');
const fs = require('fs');

const THUMB_MAX_HEIGHT = 200;
const THUMB_MAX_WIDTH = 200;
// Thumbnail prefix added to file names.
const THUMB_PREFIX = 'thumb_';

exports.generateThumbnailFromImage = functions.storage.object().onChange(event => {
  //RAW folder/Image.png
  const originalFilePath = event.data.name;


  console.log("filePath IS ", originalFilePath);


  //FILE DIR "."
  const originalFileDir = path.dirname(originalFilePath);

  console.log("FILE DIR IS ", originalFileDir);

  //Image.png
  const originalFileName = path.basename(originalFilePath);

  console.log("FILE NAME IS ", originalFileName);


  // /tmp/folder/Image.png
  const tempFilePath = path.join(os.tmpdir(), originalFilePath);
  console.log("tempLocalFile IS ", tempFilePath);

  // /tmp/folder
  const tempFileDir = path.dirname(tempFilePath);
  console.log("tempLocalDir IS ", tempFileDir);

  // thumb_image.png
  const thumbFilePath = path.normalize(path.join(originalFileDir, `${THUMB_PREFIX}${originalFileName}`));
  console.log("thumbFilePath IS ", thumbFilePath);


  //tmp/thumb_image.png  
  const tempThumbFilePath = path.join(os.tmpdir(), thumbFilePath);
  console.log("tempLocalThumbFile IS ", tempThumbFilePath);


  if (!event.data.contentType.startsWith('image/')) {
    console.log('This is not an image.');
    return null;
  }


  // Exit if the image is already a thumbnail.
  if (originalFileName.startsWith(THUMB_PREFIX)) {
    console.log('Already a Thumbnail.');
    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;
  }

  // Cloud Storage files.
  const bucket = gcs.bucket(event.data.bucket);
  console.log("BUCKET IS ",bucket);
  const file = bucket.file(originalFilePath);
  console.log("BUCKET FILE ",bucket);
  
  const thumbFile = bucket.file(thumbFilePath);
  console.log("BUCKET THUMB FILE ",thumbFile);
  


  // Create the temp directory where the storage file will be downloaded.
  return mkdirp(tempFileDir).then(() => {
    // Download file from bucket.
    return file.download({ destination: tempFilePath });
  }).then(() => {


    console.log('The file has been downloaded to', tempFilePath);
    // Generate a thumbnail using ImageMagick.

    return spawn('convert', [tempFilePath, '-thumbnail', `${THUMB_MAX_WIDTH}x${THUMB_MAX_HEIGHT}>`, tempThumbFilePath]);


  }).then(() => {

    return spawn('convert', [tempThumbFilePath, '-channel', 'RGBA', '-blur', '0x8', tempThumbFilePath]);

    console.log('Thumbnail created at', tempThumbFilePath);

  }).then(() => {



    // Uploading the Thumbnail.
    return bucket.upload(tempThumbFilePath, { destination: thumbFilePath });
    

  }).then(() => {
    // Once the image has been uploaded delete the local files to free up disk space.
    console.log('Thumbnail uploaded to Storage at', thumbFilePath);
    
    fs.unlinkSync(tempFilePath);
    fs.unlinkSync(tempThumbFilePath);
    const config = {
      action: 'read',
      expires: '03-01-2500',
    };
    return Promise.all([
      thumbFile.getSignedUrl(config),
      file.getSignedUrl(config),
    ]);
  }).then((results) => {
    console.log('Got Signed URLs.');
    const thumbResult = results[0];
    const originalResult = results[1];
    const thumbFileUrl = thumbResult[0];
    const fileUrl = originalResult[0];
    // Add the URLs to the Database
    return admin.database().ref('images').push({ path: fileUrl, thumbnail: thumbFileUrl });
  }).then(() => console.log('Thumbnail URLs saved to database.'));
});

الآن حان وقت التجربة 😀

سنقوم بإنشاء مجلد على Firebase Storage ثم نقوم برفع صورة لأحد الأشخاص المعروفين(قم بالتخمين من هو ?)

بعد رفع الصورة ننتظر بعض الثواني ثم نقوم بتحديث الصفحة لنجد صورة جديد باسم thumb_image.jpg وبحجم 200×200 مع بعض Blur

هل حزرت من هو هذا الشخص ؟ 😀

الآن نذهب الى الداتابيز لنرى هل تم حفظ الروابط أم لا

 

المشروع كاملاً على Github

بعض المصادر التي قد تفيدك

1 2

 

عن 3llomi

Just A GEEK :)

شاهد أيضاً

تحميل المزيد عند السحب للأسفل | Pagination In Firebase

سنقوم في هذا الدرس بشرح عملية Pagination في Firebase باستخدام مكتبة Infinite-Fire ماهو Pagination؟ لنفترض …

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *