8

I need to get a 1-bit bitmap out of an HTML5 Canvas.

The canvas is black-and-white. However, I can only use toUrlData() with png/jpeg outputs, but can't find any way to generate a bitmap (and change the color depth to 1-bit).

Are there any available solutions for this type of conversion? Or maybe a javascript library that can write a bitmap image?

5 Answers 5

13

Converting to 1-bit

There is no built-in mechanism for Canvas that allow you to save 1-bit images. Canvas is 32-bit (24 bit color, 8 bit alpha) and this would require the standard to use a common algorithm to degrade the image (which I likely why you can't save GIF files from it but only formats that support 24-bits or more).

For that you need to go low-level and build up the file format your self with Typed Arrays.

If 1-bit file format is not an absolute requirement, i.e. you want the image to appear as if it is 1-bit you can simply convert the content yourself.

You can use this method to convert an image to "1-bit" by converting RGB to luma values and use a threshold to determine if it should be "on" or "off":

var ctx = c.getContext("2d"), img = new Image();
img.onload = function() {
  ctx.drawImage(img, 0, 0, c.width, c.height);

  // Main code
  var idata = ctx.getImageData(0, 0, c.width, c.height),
      buffer = idata.data,
      len = buffer.length,
      threshold = 127,
      i, luma;

  for (i = 0; i < len; i += 4) {
    // get approx. luma value from RGB
    luma = buffer[i] * 0.3 + buffer[i + 1] * 0.59 + buffer[i + 2] * 0.11;

    // test against some threshold
    luma = luma < threshold ? 0 : 255;

    // write result back to all components
    buffer[i] = luma;
    buffer[i + 1] = luma;
    buffer[i + 2] = luma;
  }
  
  // update canvas with the resulting bitmap data
  ctx.putImageData(idata, 0, 0);
};

img.crossOrigin = "";
img.src = "//i.imgur.com/BrNTgRFl.jpg";
<canvas id=c width=500 height=400></canvas>

To 1-bit

To save it as a BMP file (24-bits though and not supported by all browsers):

var bmp = canvas.toDataURL("image/bmp");

Also see this answer for how you can build and write a BMP format yourself.

You can use this method also with a low-level approach by using the result from this to pack the bits (every 8 "bits" needs to be packed into a single byte, little-endian for BMP format).

Optionally look into the TrueVision TGA file format which is simpler, or the TIFF file format which also allow you to use big-endian bytes (most TIFF formats can be read by browsers btw.).

Sign up to request clarification or add additional context in comments.

2 Comments

Here is an updated fiddle without the broken dependency.
Thank you @Jackson for the efforts (and for prompting the outdated code). I decided to instead inline the code which wasn't an option at the time the answer was originally written, as well as improve the general answer a bit (no pun).
2

You could use a library like FileSaver.js. And write out the bitmap file manually by following the BMP File Format specification.

You'd do something like this with the resulting data:

function saveData() {
    var arrayBuffer = new ArrayBuffer(data.length);
    var dataView = new DataView(arrayBuffer);
    for(var i = 0; i < data.length; i ++) {
        dataView.setUint8(i, data.charCodeAt(i));
    }
    var blob = new Blob([dataView], {type: "application/octet-stream"});
    saveAs(blob, "test.bmp");
}

You'd probably need to look up some info on ArrayBuffer and DataView if you were to do it this way.

Comments

1

You can save an image with true 1 bit depth (aka Monochrome Bitmap) with plain javascript using the below

First create a function that does the processing of the image to true monochrome 1 bit depth using Typed Arrays

/**
 * Convert RGBA image data (Uint8ClampedArray) to a 1-bit BMP (ArrayBuffer)
 * @param {Uint8ClampedArray} imageData - RGBA data from canvas.getImageData().data
 * @param {number} width 
 * @param {number} height 
 * @returns {ArrayBuffer} .BMP file as ArrayBuffer
 */

  function imageTo1BitBMP(imageData, width, height) {
    // BMP header sizes
    var FILE_HEADER_SIZE = 14;
    var INFO_HEADER_SIZE = 40;
    var PALETTE_SIZE = 8; // 2 colors, each 4 bytes

    // Rows must be padded to 4-byte multiples
    var rowSize = Math.floor((width + 31) / 32) * 4;
    var pixelArraySize = rowSize * height;
    var fileSize = FILE_HEADER_SIZE + INFO_HEADER_SIZE + PALETTE_SIZE + pixelArraySize;

    var buffer = new ArrayBuffer(fileSize);
    var dv = new DataView(buffer);

    // ---- BMP FILE HEADER ----
    dv.setUint8(0, 0x42); // 'B'
    dv.setUint8(1, 0x4D); // 'M'
    dv.setUint32(2, fileSize, true); // File size
    dv.setUint16(6, 0, true); // Reserved
    dv.setUint16(8, 0, true); // Reserved
    dv.setUint32(10, FILE_HEADER_SIZE + INFO_HEADER_SIZE + PALETTE_SIZE, true); // Pixel data offset

    // ---- DIB INFO HEADER (BITMAPINFOHEADER 40 bytes) ----
    dv.setUint32(14, INFO_HEADER_SIZE, true); // Header size
    dv.setInt32(18, width, true); // width
    dv.setInt32(22, height, true); // height (positive = bottom-up)
    dv.setUint16(26, 1, true); // Color planes
    dv.setUint16(28, 1, true); // Bits per pixel
    dv.setUint32(30, 0, true); // Compression (none)
    dv.setUint32(34, pixelArraySize, true); // Image size
    dv.setInt32(38, 2835, true); // Horiz. resolution (~72 DPI)
    dv.setInt32(42, 2835, true); // Vert. resolution
    dv.setUint32(46, 2, true); // Colors in palette
    dv.setUint32(50, 0, true); // Important colors

    // ---- PALETTE -- Black, White (B, G, R, reserved) ----
    var paletteOffset = FILE_HEADER_SIZE + INFO_HEADER_SIZE;
    // black
    dv.setUint32(paletteOffset + 0, 0x00000000, true);
    // white
    dv.setUint32(paletteOffset + 4, 0x00FFFFFF, true);

    // ---- PIXEL DATA (bottom up, left-to-right, 1bpp, padded) ----
    // Each pixel row starts at: pixelDataOffset + ((height - 1 - y) * rowSize)
    var pixelDataOffset = FILE_HEADER_SIZE + INFO_HEADER_SIZE + PALETTE_SIZE;
    for (var y = 0; y < height; y++) {
        var bmpRow = height - 1 - y; // BMPs store rows bottom-to-top
        var byteOffset = pixelDataOffset + bmpRow * rowSize;
        var bitPos = 7, curByte = 0;

        for (var x = 0; x < width; x++) {
            // Read RGBA for 1 pixel
            var i = (y * width + x) * 4;
            var r = imageData[i], g = imageData[i+1], b = imageData[i+2], a = imageData[i+3];

            // Luminance threshold (>=128 is white, else black)
            var luminance = 0.299*r + 0.587*g + 0.114*b;
            var bw = luminance >= 128 ? 1 : 0;
            curByte |= (bw << bitPos);

            if (bitPos === 0 || x === width-1) {
                // Write out (partial final byte ok)
                dv.setUint8(byteOffset++, curByte);
                curByte = 0;
                bitPos = 7;
            } else {
                bitPos--;
            }
        }
        // Padding is automatic: pixel row always rowSize bytes
    }

    return buffer;
}

Next call the function where you set the canvas size for the image to be downloaded. Keep a note that it should be stored as a blob for downloading as mentioned below and not the toDataUrl as previously mentioned.

// Convert to 1bpp BMP with simple thresholding (black & white)
        var bmpData = imageTo1BitBMP(imageData, cvs.width, cvs.height);

        // Convert BMP data to a Blob and create a download link
        var blob = new Blob([bmpData], { type: 'image/bmp' });


        var url = URL.createObjectURL(blob);
        // Trigger the download by simulating a click on an anchor element
        var a = document.createElement('a');
        a.href = url;
        a.download = "converted_image.bmp";  // Name of the downloaded file
        document.body.appendChild(a); // Append to body to trigger click
        a.click();
        document.body.removeChild(a); // Remove the link after download

Comments

0

If you really need the image to be 1bit (For POS printers for example):

// Function to convert a bitmap image to ESC/POS byte array
function convertBitmapToESCPOS(imageData, width) {
  const threshold = 128; // Pixel intensity threshold for monochrome conversion
  const bytesPerLine = Math.ceil(width / 8);
  const escposArray = [];

  // Resize the image to the specified width and convert to monochrome
  // You need to implement this part based on your chosen image processing library
  // For example: https://github.com/jimp-dev/jimp/

  // Iterate through each pixel and convert to ESC/POS commands
  for (let y = 0; y < imageData.height; y++) {
    let lineData = [];
    for (let x = 0; x < imageData.width; x++) {
      const pixelIndex = (y * imageData.width + x) * 4;
      const grayscaleValue = (imageData.data[pixelIndex] + imageData.data[pixelIndex + 1] + imageData.data[pixelIndex + 2]) / 3;
      const isBlack = grayscaleValue < threshold;
      const pixelBit = isBlack ? 0 : 1;
      lineData.push(pixelBit);

      // Once we have 8 pixels (1 byte), convert to a byte and push to the ESC/POS array
      if (lineData.length === 8) {
        const byteValue = lineData.reduce((byte, bit, index) => byte | (bit << (7 - index)), 0);
        escposArray.push(byteValue);
        lineData = [];
      }
    }

    // If there are remaining bits in the line, pad with zeros and convert to a byte
    if (lineData.length > 0) {
      while (lineData.length < 8) {
        lineData.push(0);
      }
      const byteValue = lineData.reduce((byte, bit, index) => byte | (bit << (7 - index)), 0);
      escposArray.push(byteValue);
    }
  }

  return escposArray;
}

// Example usage
const imageWidth = 384; // Width of the ESC/POS paper
const imageData = {
  // Replace with actual image data or load from a source
  width: /* image width */,
  height: /* image height */,
  data: /* image pixel data */,
};

const escposByteArray = convertBitmapToESCPOS(imageData, imageWidth);
console.log(escposByteArray);

Comments

0

We can use Dynamsoft Document Viewer which bundles the WASM version of libjpeg and libpng to do this.

  1. Convert the image to black & white:
async function performColorConversion(type){
  const pageUid = doc.pages[0];
  const pageData = await doc.getPageData(pageUid)
  const imageProcess = Dynamsoft.DDV.Experiments.get("ImageProcess");
  const result = await imageProcess.process({type:3, data:pageData.display.data}, {type: type/*1: blackAndWhite 2: gray*/, params:{saveInk:false, level:1}});
  const newImage = new Blob([result.output],{type:result.outputContentType});
  await doc.updatePage(pageUid, newImage, {});
}
  1. Save the image as a 1-bit jpeg.
let blob = await doc.saveToJpeg(0,{quality: 50});

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.