Tôi tự viết ứng dụng Generate Screenshot bằng Nodejs như thế nào?

Con đường xưa em đi

Nếu ae nào đã từng xa đọa vào làm app mobile dạo, xong up lên store CH Play, App Store như mình. Thì thấm thía cái cảm giác loay hoay đi làm Sreenshot. Mà Seenshot ở đây đâu phải cứ phải chụp cái màn hình điện thoại up phụt cái là xong đâu. Mà ở đây phải thêm text vào nói cho họ hiểu "Nè nè,trong app tao có chức năng này này". Chọn 1 cái phone cho đẹp để bọc cái Screenshot bạn vừa chụp được. Đừng hỏi tại sao phải cầu kì như vậy nhé 😄 . Vì Screenshot là phần đầu tiên người dùng nhìn vào trước khi tải app đó. Nó quyết định rất lớn tới liệu người dùng có muốn tải app về không? . Bên ngoài không đẹp( giao diện ) thì không ai quan tâm hồn (chức năng ) của app bạn đâu. Tin mình đi 😄 .


Ngày xưa, theo phong cách "tay to" truyền thống, cộng thêm cái máu ăn liền nó ngấm vào máu. Mình nhanh chí lên mạng search các keyword kiểu kiểu "Galaxy s8 mock up psd"... . Rồi tải về, rồi mình lại hăm hở dùng Photoshop ra để crop, cắt ghép, thêm text, lọ mọ mãi cũng ra.

Nhìn cái ảnh chỉnh sửa Photoshop trên mình chỉ muốn thở dài, mà thường thì một ứng dụng mobile có từ 5 - 8 ảnh Screenshots 😥 . Công cuộc chỉnh sửa để được một ảnh Sreenshot ưng ý rất mất thời gian, cực kì nhàm chán. Và....

Tại sao mình không làm tool generate Sreenshot?

Mình vốn là Mobile Developer mà những ứng dụng kiểu này thường được hoặc viết trên web, hoặc trên PC vì có thể sử dụng máy tính để dễ thao tác. Làm sao đây? May thay mình cũng học được Nodejs gần tháng trời, 😜 , ngẫm thêm tí nữa thì Nodejs có cả một kho package đồ sộ nhất quả đất. Chẳng lẽ mình không dùng lại được gì chăng? Mình lên mạng search thì có rất nhiều package hỗ trợ xử lý ảnh viết bằng Nodejs . Nhưng nổi lên có Jimp - JavaScript Image Manipulation Program và quả nhiên nó đáp ứng được tất cả. Jimp đã đáp ứng hết những yêu cầu bài toán mình đặt ra.


Các bước thực hiện.

Chúng ta sẽ làm ứng dụng Nodejs CLI, khi chạy ứng dụng tự động quét hết hình ảnh( chính là hình ảnh bạn chụp ở điên thoại) ở folder hiện tại. Với mỗi bức ảnh thì ứng dụng sẽ hỏi người dùng về desciption. Người dùng nhập description xong Enter. Ứng dụng sẽ lặp lại bước này cho các ảnh tiếp theo trong folder cho đến khi đến hết.

Chuẩn bị

Tất nhiên là máy của bạn phải được cài Nodejs( cái này google nhé😜 ) . Tiếp theo bạn khởi tạo một ứng dụng Nodejs mới đặt tên là gì thì tùy bạn nhé:

npm init

Tiếp theo cài các package cần thiết:

npm install --save inquirer is-image jimp
  1. inquirer: Giúp bạn hỏi người dùng 1 câu hỏi và trả về câu trả lời để xử lý.
  2. is-image: Check file có phải là image không?
  3. jimp: Xử lý hình ảnh( crop; insert text, hình ảnh, resize....)

Chưa đủ đâu, bạn cần Photoshop hoặc công cụ chỉnh sửa ảnh khác cũng được. Và hình ảnh bạn mong muốn sau khi được generate.

Bước 1: Đo đạc

Mở Photoshop lên nào, tiếp theo mở luôn bức ảnh bạn đã chuẩn bị ở bước trước nhé.

Bạn mở Photoshop lên đo thế nào thì đo để được tọa độ của (x,y)width, heigh - tất cả ở đơn vị đều là Pixel.Để làm gì à? Thì khi bạn chụp ảnh màn hình xong bạn cẩn resize lại cho đúng kích cỡ (width, height) với phần ảnh bạn muốn insert vào. Còn vị trí chính là tọa độ (x,y) trên hình. Tương tự các bạn cũng tự xác định tọa độ (x,y) và width, height mong muốn cho đoạn description bạn muốn chèn vào ảnh nhé. Rất đơn giản thôi.
Đến đây các bạn chắc cũng đã mường tượng trong code mình cần làm gì đúng không?

Bước 2: Implement các function

Function đầu tiên là lấy tất cả ảnh trong folder hiện thời:

async function getImageFiles() {
  var files = fs.readdirSync(currentFolder);
  if (files.length == 0) {
    console.log(chalk.red("No images in current folder!!!"));
    return;
  }

  for (var i = 0; i < files.length; i++) {
    var file = files[i];
    if (isImage(file)) {
      var result = await askDescriptionImage(file);
      try {
        if (result.confirm === true) {
          await mergeImages(file, result.description, result.layout);
        } else {
          i--;
        }
      } catch (error) {
        console.log("Error occurs while merging image, try again!!!");
        i--;
      }
    }
  }
}

Mỗi lần gặp một hình ảnh ta sẽ hỏi và cho người dùng nhập description mong muốn:

async function askDescriptionImage(fileName) {
  return inquirer.prompt([
    {
      name: "description",
      type: "input",
      message:
        "What's description for " +
        chalk.green(fileName) +
        " (max: 41 characters)?",
      validate: description => description.length <= maxCharacters
    },
    {
      name: "layout",
      type: "list",
      message: "Choose desired layout?",
      choices: ["Show Top", "Show Bottom"]
    },
    {
      name: "confirm",
      type: "confirm",
      message: "Are your sure?"
    }
  ]);
}

Sau khi hỏi xong ta tiến hành gọi đến function để merge image và write text vào hình ảnh:

async function mergeImages(fileName, description, layoutType) {
  if (layoutType === "Show Top") {
    return await mergeImageToTop(fileName, description);
  } else {
    return await mergeImageToBottom(fileName, description);
  }
}

// merge image to top layout
async function mergeImageToTop(fileName, description) {
  return await Jimp.read(currentFolder + fileName)
    .then(importImage => {
      importImage.clone().write(activeImage);
    })
    .then(() => {
      return Jimp.read(activeImage);
    })
    .then(activeImage => {
      console.log(
        "x: " + activeImage.bitmap.width + " y: " + activeImage.bitmap.height
      );
      var croppedImage = activeImage.crop(
        0,
        0,
        activeImage.bitmap.width,
        (activeImage.bitmap.height * 8) / 10
      );
      console.log(
        "x: " + croppedImage.bitmap.width + " y: " + croppedImage.bitmap.height
      );
      return croppedImage;
    })
    .then(croppedImage => {
      croppedImage.resize(widthTop, heightTop);
      return Jimp.read(showtop).then(showtop => {
        var mergedImage = showtop.composite(
          croppedImage,
          xCorrdinateTop,
          yCorrdinateTop,
          [Jimp.BLEND_DESTINATION_OVER, 1, 1]
        );
        return mergedImage;
      });
    }) //load font
    .then(tpl =>
      Jimp.loadFont(Jimp.FONT_SANS_128_BLACK).then(font => [tpl, font])
    )
    .then(data => {
      tpl = data[0];
      font = data[1];

      return tpl.print(
        font,
        textData.placementX,
        textData.placementY,
        {
          text: description,
          alignmentX: Jimp.HORIZONTAL_ALIGN_CENTER,
          alignmentY: Jimp.VERTICAL_ALIGN_MIDDLE
        },
        textData.maxWidth,
        textData.maxHeight
      );
    })
    .then(finalImage => {
      finalImage.quality(100).write(exportFolder + fileName);
      console.log(chalk.red("Generated screenshot!!!"));
    })
    .catch(error => {
      console.error(error);
    });
}
// merge image to bottom layout
async function mergeImageToBottom(fileName, description) {
  return await Jimp.read(currentFolder + fileName)
    .then(importImage => {
      importImage.clone().write(activeImage);
    })
    .then(() => {
      return Jimp.read(activeImage);
    })
    .then(activeImage => {
      console.log(
        "x: " + activeImage.bitmap.width + " y: " + activeImage.bitmap.height
      );
      var croppedImage = activeImage.crop(
        0,
        (activeImage.bitmap.height * 4) / 10,
        activeImage.bitmap.width,
        activeImage.bitmap.height
      );
      console.log(
        "x: " + activeImage.bitmap.width + " y: " + activeImage.bitmap.height
      );
      return croppedImage;
    })
    .then(croppedImage => {
      croppedImage.resize(widthBottom, heightBottom);
      console.log(
        "x: " + croppedImage.bitmap.width + " y: " + croppedImage.bitmap.height
      );
      return Jimp.read(showbottom).then(showbottom => {
        var mergedImage = showbottom.composite(
          croppedImage,
          xCorrdinateBottom,
          yCorrdinateBottom,
          [Jimp.BLEND_DESTINATION_OVER, 1, 1]
        );
        return mergedImage;
      });
    }) //load font
    .then(tpl =>
      Jimp.loadFont(Jimp.FONT_SANS_128_BLACK).then(font => [tpl, font])
    )
    .then(data => {
      tpl = data[0];
      font = data[1];

      return tpl.print(
        font,
        textData.placementX,
        textData.placementY,
        {
          text: description,
          alignmentX: Jimp.HORIZONTAL_ALIGN_CENTER,
          alignmentY: Jimp.VERTICAL_ALIGN_MIDDLE
        },
        textData.maxWidth,
        textData.maxHeight
      );
    })
    .then(finalImage => {
      finalImage.quality(100).write(exportFolder + fileName);
      console.log(chalk.red("Generated screenshot!!!"));
    })
    .catch(error => {
      console.error(error);
    });
}

Về cơ bản là như vậy, bạn nào không hiểu thì là đúng rồi nhé. Vì code bên trên mình cũng đã thêm nếm nhiều để có một chương trình hoàn chỉnh. Các bạn vào repository trên Github của mình ngẫm lại chút là hiểu: Generator Code Github
Mình còn dùng thêm commander package - giúp mình nhận command từ người dùng để biến ứng dụng của mình thành dạng CLI . Làm xong rồi thì publish lên npm nào: Sreenshot Generator NPM. Và giờ chỉ cần mở terminal lên:

npm install mjgenerator -g
mjgenerator generate

Demo

Sau khi được cài đặt từ NPM thì cùng chạy thử chương trình nào:

mjgenerator generate

Như vậy là mình đã trình bày xong làm thế nào để mình có thể tạo được Tool Generate Screenshot cho riêng mình. Và thực sự nó đã giúp mình tiết kiệm được cơ số thời gian.
Cuối cùng thì cảm ơn bạn đã dành thời gian đọc bài viết này.😎