I often use
Netpbm image formats for my code and math experiments, especially the binary P6 format with 24-bit RGB color, because it is trivial to output from a program and easy to display, or convert to e.g. PNG or JPEG.
Here, it is the core idea that matters: using an array of some sort, as a
canvas to draw to, and then output the contents of that canvas in a suitable format.
If you do not have color, only presence/absence –– foreground/background, asterisk or space ––, then using binary, one bit per
pixel, is the most efficient way. This is what Ian.M described in reply
#1.
If you use color, or more generally have more than two states per pixel, then each pixel needs a larger range of values, so it is common to use an array of
chars, or perhaps one of the
uintN_t exact-size integer types as provided by
<stdint.h> or
<inttypes.h>.
For example, you could use
const unsigned char canvas[5][4] = {
{ 1, 3, 0, 0 },
{ 4, 6, 0, 0 },
{ 4, 6, 0, 0 },
{ 4, 7, 2, 3 },
{ 7, 8, 8, 9 },
};
If one uses
char as the array element type, then one can specify each row as a string. However, then the elements of the array are the numbers corresponding to those characters, most often their
ASCII code. (Unicode, ISO Western European, and Windows Western European code page character sets are all compatible with ASCII codes – they just extend the ASCII set, basically –, so that's why we still use the ASCII code table even though we usually use much larger character sets nowadays.)
Also, because each string is terminated by a NUL char (0 or '\0'), one should reserve space for that in the horizontal/last dimension.
Now, those digits are actually not random at all. I chose them using the following logic:
1 2 3 ┌ ─ ┐ 4 5 6 = │ ╳ │ with 0 as blank 7 8 9 └ ─ ┘so that logically, the canvas above describes a 4-column, 5-row figure,
┌┐ ││ ││ │└─┐ └──┘(For this figure, I used the Unicode
Box Drawing block, which is supported by browsers, but may need extra support from your C code to output from a program, especially if you are using Windows, or use a sensible operating system but not an UTF-8 -based locale.)
The reason for doing that is that if you wanted to output the shape in a very large form, you could then define a set of arrays to describe each pixel in your output. If you make those say 8×8, you could then emit the figure as 40 rows and 32 columns. If those define binary pixels (foreground/background, visible/blank), then you can apply an another level of scaling, by outputting each pixel as a rectangular block of pixels all in that same state.
This last step in scaling is probably what OP is actually looking for right now.
The idea is that when we scale each pixel to
R rows and
C columns, and we output in
row-major order (the same order this text is written in, as rows of text from left to right, rows from top to bottom), we repeat each row
R times. Within each row, we duplicate each pixel to
C.
For example, if we use the array
const unsigned char letter_L[5] = {
1, /* 1 + 0 + 0 + 0 */
1, /* 1 + 0 + 0 + 0 */
1, /* 1 + 0 + 0 + 0 */
1, /* 1 + 0 + 0 + 0 */
15, /* 1 + 2 + 4 + 8 */
};
to encode a 4-column, 5-row canvas in the four least significant bits of unsigned chars (they are guaranteed to have at least 8 bits in C), we can emit it using for example
void output_canvas(const unsigned char *canvas, int rowscale, int colscale, int bit1, int bit0, FILE *out)
{
for (int row = 0; row < 5; row++) {
for (int rowrepeat = 0; rowrepeat < rowscale; rowrepeat++) {
for (int col = 0; col < 4; col++) {
const int bit = (canvas[row] >> col) & 1;
for (int colrepeat = 0; colrepeat < colscale; colrepeat++) {
if (bit) {
fputc(bit1, out);
} else {
fputc(bit0, out);
}
}
}
fputc('\n', out);
}
}
}
so that if you called
output_canvas(letter_L, 7, 6, '*', ' ', stdout); you'd get a 7×5=35-row, 6×4=24-column letter L on standard output, consisting of asterisks (
*) and spaces (
).
Consider the four nested loops above. If we added a lookup, so that clear bits are described by one canvas, and set bits by another, both
rowscale rows and
colscale columns, then
bit specifies the lookup canvas,
rowrepeat the row within the lookup canvas, and
colrepeat the column within the lookup canvas.
If we used a structure to describe a canvas, say
#include <stdlib.h>
#include <limits.h>
typedef unsigned long cell;
#define CELL_BITS (sizeof (unsigned long) * CHAR_BIT)
typedef struct {
int rows;
int columns;
size_t stride; /* Number of data cells per row */
cell *data;
} bitcanvas;
void bitcanvas_set(bitcanvas *canvas, int row, int column)
{
if (canvas && row >= 0 && row < canvas->rows && column >= 0 && column < canvas->columns)
canvas->data[(size_t)row * canvas->stride + (size_t)column / CELL_BITS] |= 1 << (column % CELL_BITS);
}
int bitcanvas_get(bitcanvas *canvas, int row, int column)
{
if (canvas && row >= 0 && row < canvas->rows && column >= 0 && column < canvas->columns)
return (canvas->data[(size_t)row * canvas->stride + (size_t)column / CELL_BITS] >> (column % CELL_BITS)) & 1;
else
return 0;
}
void bitcanvas_free(bitcanvas *canvas)
{
if (canvas) {
free(canvas->data);
canvas->rows = 0;
canvas->columns = 0;
canvas->stride = 0;
canvas->data = NULL;
}
}
int bitcanvas_init(bitcanvas *canvas, int rows, int columns)
{
size_t stride;
/* No canvas? */
if (!canvas)
return -1;
/* We're diligent, and clear the structure to known invalid values first. */
canvas->rows = 0;
canvas->columns = 0;
canvas->stride = 0;
canvas->data = NULL;
/* Invalid size? */
if (rows < 1 || columns < 1)
return -1;
/* Number of cells on each row: columns / BITS_PER_CELL, rounded up. */
stride = ((size_t)columns + BITS_PER_CELL - 1) / BITS_PER_CELL;
/* Allocate canvas data dynamically. */
canvas->data = calloc(stride, (size_t)rows);
if (!canvas->data)
return -1;
canvas->rows = rows;
canvas->columns = columns;
canvas->stride = stride;
return 0;
}
then the scaled output –– drawing one canvas using two other canvases (of the same size) for each pixel –– would be rather straightforward:
void output_mapped_bitcanvas(bitcanvas *canvas, bitcanvas *bit1, bitcanvas *bit0, int pixel1, int pixel0, FILE *out)
{
/* Safety checks first. */
if (!canvas || canvas->rows < 1 || canvas->columns < 1 ||
!bit1 || bit1->rows < 1 || bit1->columns < 1 ||
!bit0 || bit0->rows != bit1->rows || bit0->columns != bit1->columns ||
!out)
return;
for (int row = 0; row < canvas->rows; row++) {
for (int subrow = 0; subrow = bit1->rows; subrow++) {
for (int column = 0; column < canvas->columns; column++) {
bitcanvas *bit = bitcanvas_get(canvas, row, column) ? bit1 : bit0;
for (int subcolumn = 0; subcolumn = bit1->columns; subcolumn++) {
fputc(bitcanvas_get(bit, subrow, subcolumn) ? pixel1 : pixel0, out);
}
}
fputc('\n', out);
}
}
}
where
(expression) ? (if-true) : (if-false) is the ternary conditional expression in C. If the expression is true or nonzero, then the statement evaluates to
(i]if-true[/i]), otherwise it evaluates to
(if-false). Consider it a condensed form of an
if statement, except inside an expression so that it always evaluates to one of the results.
The point of this latter part is to show OP exactly why structures and splitting operations into separate functions is so powerful. With a very similar loop that can scale a bitmap using "dumb" repetition, we can scale a bitmap using two other bitmaps to represent each state!
And yes, we could add two or four other bitmaps, to describe each bit in the sub-bitmaps, with not too much effort, but it so quickly becomes so large that standard output no longer suffices. Which brings me a full circle to the beginning of this post, to Netpbm image formats. It would be very straightforward to add a function that saves a
bitcanvas as a PBM file. I could then also add a function that creates a new bitmap by doing what
output_mapped_bitcanvas() does, describing each pixel/bit in one canvas by one of two other canvases. And even the three-layer one, for those really big images. Finally, add a blit function that copies a canvas to a specific position in another canvas, and you can start building some really complex bitmap images. (It also leads to a fun little rabbit hole called
rasterization, which can easily take
years to explore. I'm personally still deep in there somewhere!)
And all this not really
that far from the initial asterisk experiments... A lot to learn, for sure, but fun as heck, too.