توليد الصور المصغرة عند رفع الصور | 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 وبحجم 200x200 مع بعض Blur
هل حزرت من هو هذا الشخص ؟
الآن نذهب الى الداتابيز لنرى هل تم حفظ الروابط أم لا
المشروع كاملاً على Github
التعليقات (0)
لايوجد لديك حساب في عالم البرمجة؟
تحب تنضم لعالم البرمجة؟ وتنشئ عالمك الخاص، تنشر المقالات، الدورات، تشارك المبرمجين وتساعد الآخرين، اشترك الآن بخطوات يسيرة !