Bmp To Jc5 Converter Verified

After extensive testing and validation across industrial forums (Engraving Cafè, Sign Syndicate) and direct vendor documentation, here are the three verified ways to convert BMP to JC5.

Even with a verified converter, you may face issues. Here’s how to fix them. bmp to jc5 converter verified

Save as bmp_to_jc5.py

#!/usr/bin/env python3
import sys, struct, hashlib
def read_u16_le(b, off): return b[off] | (b[off+1] << 8)
def read_u32_le(b, off): return b[off] | (b[off+1]<<8) | (b[off+2]<<16) | (b[off+3]<<24)
def load_bmp(path):
    with open(path, 'rb') as f:
        data = f.read()
    if data[0:2] != b'BM': raise ValueError('Not a BMP')
    pixel_offset = read_u32_le(data, 10)
    dib_size = read_u32_le(data, 14)
    width = read_u32_le(data, 18)
    height_signed = struct.unpack_from('<i', data, 22)[0]
    height = abs(height_signed)
    bpp = read_u16_le(data, 28)
    top_down = (height_signed < 0)
    # Only handle common cases: 24-bit BGR or 8-bit paletted
    if bpp == 24:
        row_bytes = ((width * 3 + 3) // 4) * 4
        pixels = []
        for row in range(height):
            bmp_row_idx = row if top_down else (height - 1 - row)
            start = pixel_offset + bmp_row_idx * row_bytes
            rowdata = data[start:start+width*3]
            # BMP stores B,G,R
            for x in range(width):
                b,g,r = rowdata[x*3:(x+1)*3]
                pixels.extend([r,g,b])
        return width, height, 3, pixels
    elif bpp == 8:
        # palette after DIB header (256 * 4 bytes)
        pal_offset = 14 + dib_size
        palette = []
        entries = 256
        for i in range(entries):
            off = pal_offset + i*4
            if off+4 > len(data): break
            b,g,r,_ = data[off:off+4]
            palette.append((r,g,b))
        row_bytes = ((width + 3)//4)*4
        pixels = []
        for row in range(height):
            bmp_row_idx = row if top_down else (height - 1 - row)
            start = pixel_offset + bmp_row_idx * row_bytes
            rowdata = data[start:start+width]
            for x in range(width):
                idx = rowdata[x]
                r,g,b = palette[idx]
                pixels.extend([r,g,b])
        return width, height, 3, pixels
    else:
        raise ValueError(f'Unsupported BMP bpp: bpp')
def to_jc5(width, height, channels, pixels, out_path, grayscale=False):
    if grayscale and channels==3:
        out_pixels = bytearray(width*height)
        for i in range(width*height):
            r = pixels[i*3]
            g = pixels[i*3+1]
            b = pixels[i*3+2]
            y = int(round(0.299*r + 0.587*g + 0.114*b))
            out_pixels[i] = y
        channels_out = 1
    elif channels==3 and not grayscale:
        out_pixels = bytes(pixels)
        channels_out = 3
    elif channels==1:
        out_pixels = bytes(pixels)
        channels_out = 1
    else:
        raise ValueError('Unhandled channel conversion')
header = bytearray(16)
    header[0:4] = b'JC5\x00'
    header[4:8] = struct.pack('<I', width)
    header[8:12] = struct.pack('<I', height)
    header[12] = channels_out
    header[13] = 8 if channels_out==1 else 24
    header[14:16] = b'\x00\x00'
    with open(out_path, 'wb') as f:
        f.write(header)
        f.write(out_pixels)
    # verification
    expected_len = 16 + width*height*channels_out
    actual_len = 16 + len(out_pixels)
    if expected_len != actual_len:
        raise RuntimeError('Size mismatch')
    h = hashlib.sha256()
    with open(out_path, 'rb') as f:
        h.update(f.read())
    return h.hexdigest()
def main():
    if len(sys.argv) < 3:
        print('Usage: bmp_to_jc5.py input.bmp output.jc5 [--gray]')
        return
    inp = sys.argv[1]; out = sys.argv[2]; gray = '--gray' in sys.argv
    w,h,ch,pix = load_bmp(inp)
    digest = to_jc5(w,h,ch,pix,out,grayscale=gray)
    print('Wrote', out, 'SHA256:', digest)
if __name__=='__main__':
    main()