Editing 24-bit BMP images in Node.js from scratch: setting up, part II

Previously... we have developed a set of functions to play around with BMP files. However, I have come to the quick realization that the code looks rather messy - since everything is crammed into a single module. Besides, our `readBmp` and `writeBmp` functions could be improved to support streams, which is a necessity if we want to write compose-able and reusable command line tools to work on images.

First things first, let's re-write the `readBmp` and `writeBmp` functions to work on streams as opposed to reading/writing files by its path. Such change will immediately allow us to write compose-able command line tools to work on images (24-bit BMP files in our case) via the process.stdin and process.stdout streams.

For example, assuming we have implemented an `lrot24bmp` "cmdlet" to rotate an image 90 degrees to the left, `hmirror24bmp` to mirror an image horizontally and `colinvert24bmp` to invert colors, a hypothetical user interaction with them could look like so:

cat sample.bmp | lrot24bmp | lrot24bmp | hmirror24bmp | colinvert24bmp > out.bmp

Let's start with `readBmp`. Originally, the code looked like so:

async function readBmp(filepath) {
  Assert.strictEqual(typeof filepath, 'string')

  const bmp_content = await Fs.promises.readFile(filepath)
  const bmp_header = sliceBytes(bmp_content, 0, 2).toString()

  if (bmp_header !== 'BM') {
    throw new Error(
      "This doesn't look like a BMP file I can understand..."
    )
  }

  return { bmp_content }
}

We want `readBmp` to take a Stream.Readable argument instead of a string from which the function can read all the data:

  async function readBmp(readable) {
    Assert(readable, 'readable')


    const bmp_content = await readInput(readable)
    const bmp_header = sliceBytes(bmp_content, 0, 2).toString()


    if (bmp_header !== 'BM') {
      throw new Error(
        "This doesn't look like a BMP file I can understand..."
      )
    }


    return { bmp_content }
  }

`readInput` is the new function that we are going to define now that will take a Stream.Readable and read all the data from it:

  function readInput(readable) {
    Assert(readable, 'readable')


    return new Promise((resolve, reject) => {
      let buf
      buf = Buffer.from([])


      readable.once('error', reject)


      readable.on('data', data      ​=> {
        buf = Buffer.concat([buf, data])
      })


      readable.once('end', () => {
        resolve(buf)
        return
      })
    })
  }

This change will allow us to read images straight from the standard input:

; (async function run() {
  const bmp = await readBmp(process.stdin)
})()

Next, we tweak the `writeBmp` function to take a Stream.Writable:

  async function writeBmp(writable, bmp) {
    Assert.strictEqual(typeof bmp, 'object')


    const bmp_content = fetchProp(bmp, 'bmp_content')


    return writable.write(bmp_content)
  }

A passthrough app that would read in a BMP file and then immediately output it would now look like so:

; (async function run() {

  const bmp = await readBmp(process.stdin)
  await writeBmp(process.stdout)
])()

And this is how a user would use the cmdlet:

cat sample.bmp | node . > sample.copy.bmp

Cool beans. Moving on to splitting the functionality that currently lives in the single index.js file into separate modules.

Currently, we can split our functionality into three modules: Bmp24 - for encapsulating 24-bit files, Rgb - for providing a set of actions to work with RGB pixels and, last but not least, Lib - for other kinds of functionality that is not yet worth extracting into separate modules. Hence, our project directory is going to look like this:

$ ls | sort

bmp24.js
index.js
lib.js
package.json
rgb.js

Now, naturally, some function renaming had to be done in order to make the interface easier to remember and reason about by the user. Since it would be a lot of code to post here, I have prepared a repo with the final result. You may find it here:

https://gitlab.com/lilsweetcaligula/bmp-sandbox-refactored

Do check it out! And as always, pull requests and issues are welcome. Feel free to clone it and play around with 24-bit BMP files.

Stay tuned! In the next few episodes we are going to write some cmdlets to mirror and rotate our BMP files.

To view or add a comment, sign in

More articles by Efim B.

Explore content categories