Search code examples
node.jsgraphicsmagicknode-imagemagick

nodejs - GraphicMagick - Add images in a frame to create another image


I have a sample image frame(see below) saved in the server and a function that will take an array of image URLs, crop the images into a circular shape to match the height /width of a cat image in the frame (60px x 60px) and replace cats from the left depending on the number of images and finally save the image back I need to know how to do this using graphicMagick in node.js.

Just to be more clear

  • I have an image frame.png
  • I have an array of two URLs ['www.someurl.com/image1.jpg', 'www.someurl.com/image2.jpg]
  • for each URL in the array
    • Download image i from URL into a temp location
    • Crop the image i into a circular shape of radius 60.
    • Put it on top of frame.png at some position (xi,yi)
  • Save the new composite image
  • upload it back

Sample frame


Solution

  • Here I am posting my solution what I did to circularize image and then compose them on a frame image (png). The only downside to this solution is that there are multiple writes to the file and I couldn't do it in a single write.

    I hope this helps someone in the future,

    const gm = require('gm').subClass({ imageMagick: true })
    
    export async function convertToCircularImage(imagePath: string, resultPath: string) {
      const radius = 180
    
      return new Promise(function (resolve, reject) {
        gm(imagePath)
          .autoOrient()
          .gravity('Center')
          .resize(radius, radius, '^')
          .extent(radius, radius)
          .noProfile()
          .setFormat('png')
          .out('(')
          .rawSize(radius, radius)
          .out('xc:Black')
          .fill('White')
          .drawCircle(radius / 2, radius / 2, radius / 2, 1)
          .out('-alpha', 'Copy')
          .out(')')
          .compose('CopyOpacity')
          .out('-composite')
          .trim()
          .write(resultPath, (err: Error) => {
            if (err) {
              console.error('Failed to crop image.', err)
              reject(err)
            } else {
              console.log(`Cropped image at ${imagePath} and saved it at ${resultPath}`)
              resolve(resultPath)
            }
          })
      })
    }
    
    export async function composite(
      frameImagePath: string,
      circularImagesPaths: string[],
      resultImagePath: string,
      points: string[],
    ) {
    
      let frameImage = frameImagePath
      let index = 0
      for (const circularImagePath of circularImagesPaths) {
        const point = points[index]
        try {
          // this method return the resultImagePath which is then used as a frame for next composition
          frameImage = await composeImage(frameImage, circularImagePath, point, resultImagePath)
        } catch (e) {
          console.log('Composite: some error', e)
        }
        index = index + 1
      }
    }
    
    async function composeImage(
      frameImage: string,
      circularImage: string,
      point: string,
      resultPath: string,
    ): Promise<string | any> {
      console.log('Composing circular image to frame...', frameImage, circularImage)
      return new Promise(function (resolve, reject) {
        gm(frameImage)
          .composite(circularImage)
          .in('-compose', 'Dst_Over') // to only overlap on transparent parts
          .geometry(point)
          .in()
          .write(resultPath, function (err: any) {
            if (err) {
              console.error('Composing failed', err)
              reject(err)
            } else {
              console.log('Composing complete')
              resolve(resultPath)
            }
          })
      })
    }