litel litel - 3 months ago 16
Node.js Question

How do I reverse a scanline using the jpeg-js module/node JS buffer?

I've been fiddling around with the jpeg-js module and Node JS Buffer, and attempting to create a small command line program that modifies the decoded JPEG buffer data and creates a pattern of X number of reversed scanlines and X number of normal scanlines before saving a new JPEG. In other words, I'm looking to flip portions of the image, but not the entire image itself (plenty of modules that do such a thing, of course, but not the specific use case I have).

To create the reversed/normal line patterns, I've been reading/writing line by line, and saving a slice of that line to a variable, then starting at the end of scanline and incrementally going down by slices of 4 bytes (the alloc for an RGBA value) until I'm at the beginning of the line. Code for the program:

'use strict';
const fs = require('fs');
const jpeg = require('jpeg-js');
const getPixels = require('get-pixels');

let a = fs.readFileSync('./IMG_0006_2.jpg');
let d = Buffer.allocUnsafe(a.width * a.height * 4);
let c = jpeg.decode(a);

let val = false; // track whether normal or reversed scanlines
let lineWidth = b.width * 4;
let lineCount = 0;
let track = 0;
let track2 = 0;
let track3 = 0;
let curr, currLine; // storage for writing/reading scnalines, respectively
let limit = {
one: Math.floor(Math.random() * 141),
two: Math.floor(Math.random() * 151),
three: Math.floor(Math.random() * 121)
};
if (limit.one < 30) {
limit.one = 30;
}
if (limit.two < 40) {
limit.two = 40;
}
if (limit.two < 20) {
limit.two = 20;
}
let calc = {};
calc.floor = 0;
calc.ceil = 0 + lineWidth;

d.forEach(function(item, i) {
if (i % lineWidth === 0) {
lineCount++;
/* // alternate scanline type, currently disabled to figure out how to succesfully reverse image
if (lineCount > 1 && lineCount % limit.one === 0) {
// val = !val;
}
*/
if (lineCount === 1) {
val = !val; // setting alt scanline check to true initially
} else if (calc.floor + lineWidth < b.data.length - 1) {
calc.floor += lineWidth;
calc.ceil += lineWidth;
}
currLine = c.data.slice(calc.floor, calc.ceil); // current line
track = val ? lineWidth : 0; // tracking variable for reading from scanline
track2 = val ? 4 : 0; // tracking variable for writing from scanline
}
//check if reversed and writing variable has written 4 bytes for RGBA
//if so, set writing source to 4 bytes at end of line and read from there incrementally
if (val && track2 === 4) {
track2 = 0; // reset writing count
curr = currLine.slice(track - 4, track); // store 4 previous bytes as writing source
if (lineCount === 1 && lineWidth - track < 30) console.log(curr); //debug
} else {
curr = currLine; //set normal scanline
}

d[i] = curr[track2];

// check if there is no match between data source and decoded image
if (d[i] !== curr[track2]) {
if (track3 < 50) {
console.log(i);
}
track3++;
}
track2++; //update tracking variable
track = val ? track - 1 : track + 1; //update tracking variable



});


var rawImageData = {
data: d,
width: b.width,
height: b.height
};
console.log(b.data.length);
console.log('errors\t', track3);
var jpegImageData = jpeg.encode(rawImageData, 100);

fs.writeFile('foo2223.jpg', jpegImageData.data);


Alas, the reversed scanline code I've written does not properly. Unfortunately, I've only been able successfully reverse the red channel of my test image (see below left), with the blue and green channels just turning into vague blurs. The color scheme should look something like the right image.

current color output
ideal color output

What am I doing wrong here?

Answer

For reversed lines, you stored slices of 4 bytes(4 bytes = 1 pixel), then write the first value of the pixel(red) correctly. But in the next iteration, you overwrite the slice curr with currLine, rest of channels gets wrong values.

if (val && track2 === 4) {
    track2 = 0; // reset writing count
    curr = currLine.slice(track - 4, track); // store 4 previous bytes as writing source
    if (lineCount === 1 && lineWidth - track < 30) console.log(curr); //debug
} else {
    curr = currLine; //set normal scanline
}
  • Iteration 0: val == true, track2 == 4, set curr to next pixel, write red channel.
  • Iteration 1: val == true, track2 == 1, (val && track2 === 4) == false, set curr to currLine, write green channel.

You can move track2 === 4 branch to avoid this:

if (val) {
  if (track2 === 4) {
    track2 = 0; // reset writing count
    curr = currLine.slice(track - 4, track); // store 4 previous bytes as writing source
    if (lineCount === 1 && lineWidth - track < 30) console.log(curr); //debug
  }
} else {
  curr = currLine; //set normal scanline
}

Fixed code should look like this:

function flipAlt(input, output) {
  const fs = require('fs');
  const jpeg = require('jpeg-js');

  let a = fs.readFileSync(input);
  let b = jpeg.decode(a);
  let d = Buffer.allocUnsafe(b.width * b.height * 4);

  let val = false; // track whether normal or reversed scanlines
  let lineWidth = b.width * 4;
  let lineCount = 0;
  let track = 0;
  let track2 = 0;
  let track3 = 0;
  let curr, currLine; // storage for writing/reading scnalines, respectively
  let limit = {
    one: Math.floor(Math.random() * 141),
    two: Math.floor(Math.random() * 151),
    three: Math.floor(Math.random() * 121)
  };
  if (limit.one < 30) {
    limit.one = 30;
  }
  if (limit.two < 40) {
    limit.two = 40;
  }
  if (limit.two < 20) {
    limit.two = 20;
  }
  let calc = {};
  calc.floor = 0;
  calc.ceil = 0 + lineWidth;

  d.forEach(function(item, i) {
    if (i % lineWidth === 0) {
      lineCount++;
      if (lineCount > 1) {
        val = !val;
      }
      if (lineCount === 1) {
        val = !val; // setting alt scanline check to true initially
      } else if (calc.floor + lineWidth < b.data.length - 1) {
        calc.floor += lineWidth;
        calc.ceil += lineWidth;
      }
      currLine = b.data.slice(calc.floor, calc.ceil); // current line
      track = val ? lineWidth : 0; // tracking variable for reading from scanline
      track2 = val ? 4 : 0; // tracking variable for writing from scanline
    }
    //check if reversed and writing variable has written 4 bytes for RGBA
    //if so, set writing source to 4 bytes at end of line and read from there incrementally
    if (val) {
      if (track2 === 4) {
        track2 = 0; // reset writing count
        curr = currLine.slice(track - 4, track); // store 4 previous bytes as writing source
        if (lineCount === 1 && lineWidth - track < 30) console.log(curr); //debug
      }
    } else {
      curr = currLine; //set normal scanline
    }

    d[i] = curr[track2];

    // check if there is no match between data source and decoded image
    if (d[i] !== curr[track2]) {
      if (track3 < 50) {
        console.log(i);
      }
      track3++;
    }
    track2++; //update tracking variable
    track = val ? track - 1 : track + 1; //update tracking variable

  });

  var rawImageData = {
    data: d,
    width: b.width,
    height: b.height
  };
  console.log(b.data.length);
  console.log('errors\t', track3);
  var jpegImageData = jpeg.encode(rawImageData, 100);

  fs.writeFile(output, jpegImageData.data);
}

flipAlt('input.jpg', 'output.jpg');

flipped image

Instead of tracking array indices, you can use utility library like lodash, it should make things easier:

function flipAlt(input, output) {
  const fs = require('fs');
  const jpeg = require('jpeg-js');
  const _ = require('lodash');

  const image = jpeg.decode(fs.readFileSync(input));
  const lines = _.chunk(image.data, image.width*4);
  const flipped = _.flatten(lines.map((line, index) => {
    if (index % 2 != 0) {
      return line;
    }
    const pixels = _.chunk(line, 4);
    return _.flatten(pixels.reverse());
  }));

  const imageData = jpeg.encode({
    width: image.width,
    height: image.height,
    data: new Buffer(flipped)
  }, 100).data;

  fs.writeFile(output, imageData);
}

flipAlt('input.jpg', 'output.jpg');