Some "big" programs can output bare binaries (which is IMO better than C sources/headers/whatever).
That's exactly what P5 (PGM), P6 (PPM), and PF and Pf (PFM) format NetPBM files are, if you look at the file after the header.
Even Adobe Photoshop supports these, and that's as "big" as you can get for professional image editing.
In Augmented Backus–Naur format, I do believe the binary formats' headers (with binary data immediately following) can be correctly described as
PBM = "P" "4" sep WIDTH sep HEIGHT wschar PGM = "P" "5" sep WIDTH sep HEIGHT sep MAXVAL wschar PPM = "P" "6" sep WIDTH sep HEIGHT sep MAXVAL wschar PFM = "P" "F" sep WIDTH sep HEIGHT sep RANGE LF PfM = "P" "f" sep WIDTH sep HEIGHT sep RANGE LF PFM4 = "P" "F" "4" sep WIDTH sep HEIGHT sep RANGE LF WIDTH = nonzero *digit ; ASCII positive decimal number
HEIGHT = nonzero *digit ; ASCII positive decimal number
MAXVAL = nonzero *digit ; ASCII positive decimal number between 1 and 65535, inclusive
RANGE = 1*( "-" / "+" ) ( 1*digit / 1*digit "." *digit / *digit "." 1*digit ) nonzero = "1" / "2" / "3" / "4" / "5" / "6" / "7" / "8" / "9" digit = "0" / nonzero wschar = HTAB / LF / VTAB / FF / CR / " " sep = 1*(1*wschar / comment) comment = "#" *(anything except CR or LF)As you can see, it is easy to do it right by reading positive (nonzero) decimal numbers skipping the initial whitespace-and-comments (
sep).
PBM files have one bit per pixel, 0=white, 1=black. Rows are specified from top to bottom: top left pixel is the first, followed by the pixel to its right. If WIDTH is not a multiple of 8, each row is padded to a multiple of 8, so each row consists of Ceil(WIDTH/8) bytes. Each byte specifies eight pixels, with the most significant bit leftmost, and least significant bit rightmost.
PGM files have one (MAXVAL < 256) or two (MAXVAL >= 256) bytes per pixel. Standard byte order is big-endian, but some do use little-endian. A value of 0 corresponds to black, and MAXVAL corresponding to white. Top left pixel is the first, followed by the pixel to its right.
PPM files have three (MAXVAL < 256) or six (MAXVAL >= 256) bytes per pixel. This is like in PGM, except in triplets, first one corresponding to red, second to green, and third to blue. 0 is 0%, and MAXVAL is 100% of that component. Top left pixel is the first, followed by the pixel to its right.
PFM files consist of three IEEE 754 Binary32
floats per pixel, for red, green, and blue triplets. If RANGE is negative, the byte order is little-endian, and 0% corresponds to 0.0f and 100% corresponds to -RANGE. If RANGE is positive, the byte order is big-endian, and 0% corresponds to 0.0f and 100% to RANGE. Bottom left pixel is the first, followed by the pixel to its right.
PfM files consist of one IEEE 754 Binary32
floats per pixel, corresponding to grayscale, 0 corresponding to black. If RANGE is negative, the byte order is little-endian, and -RANGE corresponds to white. If RANGE is positive, the byte order is big-endian, and RANGE corresponds to white. Bottom left pixel is the first, followed by the pixel to its right.
PFM4 files consist of four IEEE 754 Binary32
floats per pixel, for red, green, blue, and alpha/opacity. If RANGE is negative, the byte order is little-endian, and 0% corresponds to 0.0f and 100% corresponds to -RANGE. If RANGE is positive, the byte order is big-endian, and 0% corresponds to 0.0f and 100% to RANGE. Bottom left pixel is the first, followed by the pixel to its right. This is not a widely supported format.
There is also a P7 aka PAM format, but it is not widely supported.
I tend to generate 8-bit PPM files using the sRGB colorspace, and they tend to open in all image editors from GIMP to Photoshop, but usually I use a chain of command-line tools to compress and optimize them into small PNG files (using a 16-256 color palette for minimal size).
For vector stuff, I use SVG (typically
SVG 1.1). It is
ridiculously simple to generate, but not fun to rasterize. You simply emit
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 width height"> <path d="path-data" fill="fill" stroke="stroke" stroke-width="thickness" /> ... </svg>where
path-data consists of single-letter commands followed by numeric parameters (typically coordinates),
width and
height define the visible range of the maximum
x and
y coordinates,
fill and
stroke are either
none or an RGB color (using hexadecimal notation,
#RGB,
#RRGGBB). There are more options, of course, but this is usually sufficient for a lot of stuff.
The
... indicates other elements drawn on top. Mostly I use
paths,
rects (to fill background; it defaults to transparent), and
circle elements (for dots and such).
As long as single-use single-purpose homebrew tools to convert bitmaps into whatever my target needs are concerned, I never could care enough to do it *properly*
I have a header file somewhere that uses
<stdlib.h> and
<stdio.h> and defines accessor functions to load and save 32 bit canvases (R8:G8:B8, R8:G8:B8:A8 with a separate PGM alpha channel, or R10:G10:B10) where each pixel corresponds to an
uint32_t, and the fast getpixel()/setpixel() have bounds checking. I've also got fast 64-bit (for x86-64) RGB blending routines, although the quality of the result is nowhere near as nice if one used e.g. OKLab for the representation.
Point is, it is a perfect format to use in ELF-based toolchains to import pixmap data in customized formats. You can use C, Python, or whatever your favourite language is on the host to convert the image format to whatever happens to be the optimal format for a specific use case on a microcontroller or embedded device. No added library dependencies. If you do decide to depend on the netpbm tools, you can then convert basically from and to any image format. If your compression/optimization code on the host also emits the final versions as PPM, they can be converted to PNG losslessly and included in the generated documentation, for visual verification.