// SPDX-License-Identifier: MIT // Copyright (c) 2022 the ethier authors (github.com/divergencetech/ethier) pragma solidity >=0.8.16 <0.9.0; import {DynamicBuffer} from "./DynamicBuffer.sol"; import {RawData} from "./RawData.sol"; /** * @notice Packs raw pixel data into the BMP format. * @dev The library assumes row-major, 24-bit BGR pixel encoding. */ library BMP { using DynamicBuffer for bytes; using RawData for bytes; error InvalidDimensions(uint256 expected, uint256 actual); error InvalidWidth(); uint8 internal constant _BMP_HEADER_SIZE = 54; /** * @notice Returns an 24-bit BMP encoding of the pixels. * @param pixels BGR tuples * @param width Number of horizontal pixels in the image * @param height Number of vertical pixels in the image */ function bmp( bytes memory pixels, uint32 width, uint32 height ) internal pure returns (bytes memory) { (, uint256 paddedLength) = computePadding(width, height); bytes memory buf = DynamicBuffer.allocate( _BMP_HEADER_SIZE + paddedLength ); appendBMP(buf, pixels, width, height); return buf; } /** * @notice Appends the 24-bit BMP encoding of the pixels to a given buffer. * @param pixels BGR tuples * @param width Number of horizontal pixels in the image * @param height Number of vertical pixels in the image */ function appendBMP( bytes memory buf, bytes memory pixels, uint32 width, uint32 height ) internal pure returns (bytes memory) { if (width * height * 3 != pixels.length) { revert InvalidDimensions(width * height * 3, pixels.length); } buf.appendSafe(header(width, height)); appendPixelsSafe(buf, pixels, width, height); return buf; } /** * @notice Returns the header for a 24-bit BMP encoded images * @param width Number of horizontal pixels in the image * @param height Number of vertical pixels in the image * @dev Spec: https://www.digicamsoft.com/bmp/bmp.html * * Layout description with offsets: * http://www.ece.ualberta.ca/~elliott/ee552/studentAppNotes/2003_w/misc/bmp_file_format/bmp_file_format.htm * * N.B. Everything is little-endian, hence the assembly for masking and * shifting. */ function header(uint32 width, uint32 height) internal pure returns (bytes memory) { // Each row of the pixel array must be padded to a multiple of 4 bytes. (, uint256 paddedLength) = computePadding(width, height); // 14 bytes for BITMAPFILEHEADER + 40 for BITMAPINFOHEADER bytes memory buf = new bytes(_BMP_HEADER_SIZE); // BITMAPFILEHEADER buf[0x00] = 0x42; buf[0x01] = 0x4d; // bfType = BM // bfSize; bytes in the entire buffer uint32 bfSize = _BMP_HEADER_SIZE + uint32(paddedLength); buf.writeUint32LE(0x02, bfSize); // Next 4 bytes are bfReserved1 & 2; both = 0 = initial value // bfOffBits; bytes from beginning of file to pixels = 14 + 40 // (see size above) buf.writeUint32LE(0x0a, _BMP_HEADER_SIZE); // BITMAPINFOHEADER // biSize; bytes in this struct = 40 buf.writeUint32LE(0x0e, 40); // biWidth / biHeight buf.writeUint32LE(0x12, width); buf.writeUint32LE(0x16, height); // biPlanes (must be 1) buf.writeUint16LE(0x1a, 0x01); // biBitCount: 24 bits per pixel (full BGR) buf.writeUint16LE(0x1c, 0x18); // biXPelsPerMeter buf.writeUint32LE(0x26, 0x01); // biYPelsPerMeter buf.writeUint32LE(0x2a, 0x01); // We use raw pixels instead of run-length encoding for compression // as these aren't being stored. It's therefore simpler to // avoid the extra computation. Therefore biSize can be 0. Similarly // there's no point checking exactly which colours are used, so // biClrUsed and biClrImportant can be 0 to indicate all colours. This // is therefore the end of BITMAPINFOHEADER. Simples. // Further we use full 24 bit BGR color values instead of an indexed // palette. RGBQUAD is hence left empty. // return abi.encodePacked(buf, pixels); return buf; } /** * @notice Appends the pixels with BMP-conform padding to a given buffer. * @dev This can be used together with `header` to build a full BMP. * @param pixels BGR tuples * @param width Number of horizontal pixels in the image * @param height Number of vertical pixels in the image */ function appendPixelsSafe( bytes memory buffer, bytes memory pixels, uint32 width, uint32 height ) internal pure { (, uint256 paddedLength) = computePadding(width, height); buffer.checkOverflow(paddedLength); appendPixelsUnchecked(buffer, pixels, width, height); } /** * @notice Appends the pixels with BMP-conform padding to a given buffer. * @dev This can be used together with `header` to build a full BMP. * @dev Does not check for out-of-bound writes. * @param pixels BGR tuples * @param width Number of horizontal pixels in the image * @param height Number of vertical pixels in the image */ function appendPixelsUnchecked( bytes memory buf, bytes memory pixels, uint32 width, uint32 height ) internal pure { // pixel data layout // // | word | word | .. | tail | // | word | word | .. | tail | // | word | word | .. | tail | // // buf data layout: // // | word | word | .. | tail | padding | // | word | word | .. | tail | padding | // | word | word | .. | tail | padding | // // Number of full words in a scan line uint256 rowWords = (width * 3) / 32; // Number of bytes remaining in a line uint256 rowTailBytes = (width * 3) % 32; // If a scan row can be divided into words without rest, move a full // word to the tail to simplify looping. if (rowTailBytes == 0) { rowWords -= 1; rowTailBytes = 32; } (uint256 padding, ) = computePadding(width, height); // If we load a full word at the tail, we can only use the first // `rowTailBytes` bytes. The rest needs to be masked uint256 tailMaskInv = ((1 << ((32 - rowTailBytes) * 8)) - 1); uint256 tailMask = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff - tailMaskInv; assembly { let bufPtr := add(add(buf, 0x20), mload(buf)) // Loop over all rows for { let y := 0 let pixelPtr := add(pixels, 0x20) } lt(y, height) { y := add(y, 1) } { // Loop over the words in a row for { let iWord := 0 } lt(iWord, rowWords) { iWord := add(iWord, 1) pixelPtr := add(pixelPtr, 0x20) bufPtr := add(bufPtr, 0x20) } { mstore(bufPtr, mload(pixelPtr)) } // Tail mstore( bufPtr, or( and(mload(pixelPtr), tailMask), // We need to account for the fact that we potentially // write outside the buffer range here. We therefore // load the current data in the remaining bits // and set them again as they are. and(mload(bufPtr), tailMaskInv) ) ) pixelPtr := add(pixelPtr, rowTailBytes) bufPtr := add(bufPtr, add(rowTailBytes, padding)) } // Update buffer length mstore(buf, sub(bufPtr, add(buf, 0x20))) } } /** * @notice Computes the BMP-conform padding of a pixel frame. * @param width Number of horizontal pixels in the image * @param height Number of vertical pixels in the image * @return padding Number of bytes added to each row * @return paddedLength Length of the padded data */ function computePadding(uint256 width, uint256 height) internal pure returns (uint256, uint256) { uint256 stride = width * 3; uint256 padding = (4 - (stride - (((stride) >> 2) << 2))) % 4; uint256 paddedLength = height * (stride + padding); return (padding, paddedLength); } }