Compare commits
9 Commits
master
...
palette-fu
Author | SHA1 | Date |
---|---|---|
Vftdan | 26f4511ceb | |
Vftdan | 91fc5098eb | |
Vftdan | fd810cafea | |
Vftdan | 30b6534fc1 | |
Vftdan | 53c9ba3db5 | |
Vftdan | bedc6d9166 | |
Vftdan | 135c3642b0 | |
Vftdan | 6ebe98e94c | |
Vftdan | 46f8ecd68e |
9
Makefile
|
@ -7,18 +7,15 @@ test-cpi2png: cpi2png
|
||||||
./cpi2png ./cpi-images/rat.cpi /tmp/rat.png
|
./cpi2png ./cpi-images/rat.cpi /tmp/rat.png
|
||||||
|
|
||||||
img2cpi: img2cpi.c cc-common.o dependencies/stb/stb_image.o dependencies/stb/stb_image_resize2.o
|
img2cpi: img2cpi.c cc-common.o dependencies/stb/stb_image.o dependencies/stb/stb_image_resize2.o
|
||||||
$(CC) $(LDFLAGS) $^ $(LDLIBS) -o "$@" $(CPPFLAGS)
|
$(CC) $(LDFLAGS) $^ $(LDLIBS) -o "$@" $(CPPFLAGS) $(CFLAGS) $(INCPATH)
|
||||||
|
|
||||||
cpi2png: cpi2png.c cc-common.o dependencies/stb/stb_image_write.o
|
cpi2png: cpi2png.c cc-common.o dependencies/stb/stb_image_write.o
|
||||||
$(CC) $(LDFLAGS) $^ $(LDLIBS) -o "$@" $(CPPFLAGS)
|
$(CC) $(LDFLAGS) $^ $(LDLIBS) -o "$@" $(CPPFLAGS) $(CFLAGS) $(INCPATH)
|
||||||
|
|
||||||
dependencies/stb/%.o: dependencies/stb/%.h
|
dependencies/stb/%.o: dependencies/stb/%.h
|
||||||
$(CC) -DSTB_IMAGE_IMPLEMENTATION -DSTB_IMAGE_RESIZE_IMPLEMENTATION -DSTB_IMAGE_WRITE_IMPLEMENTATION -x c $^ -c -o "$@"
|
$(CC) -DSTB_IMAGE_IMPLEMENTATION -DSTB_IMAGE_RESIZE_IMPLEMENTATION -DSTB_IMAGE_WRITE_IMPLEMENTATION -x c $^ -c -o "$@" $(CFLAGS)
|
||||||
|
|
||||||
wsvpn: wsvpn.o dependencies/mongoose/mongoose.o
|
wsvpn: wsvpn.o dependencies/mongoose/mongoose.o
|
||||||
$(CC) $(LDFLAGS) $^ $(LOADLIBES) $(LDLIBS) -o "$@"
|
$(CC) $(LDFLAGS) $^ $(LOADLIBES) $(LDLIBS) -o "$@"
|
||||||
|
|
||||||
clean:
|
|
||||||
$(RM) -v img2cpi cpi2png wsvpn wsvpn.o cc-common.o dependencies/stb/*.o
|
|
||||||
|
|
||||||
.PHONY: all
|
.PHONY: all
|
||||||
|
|
61
cc-common.c
|
@ -1,33 +1,48 @@
|
||||||
#include "cc-common.h"
|
#include "cc-common.h"
|
||||||
|
|
||||||
int read_varint(FILE *fp, unsigned int *out) {
|
#include <stdlib.h>
|
||||||
int position = 0;
|
|
||||||
|
|
||||||
while (true) {
|
struct palette *cc_alloc_palette(uint8_t count) {
|
||||||
unsigned char curr = fgetc(fp);
|
struct palette *palette = calloc(1, sizeof(struct palette) + count * sizeof(union color));
|
||||||
*out |= (curr & 0x7F) << position;
|
if (!palette) {
|
||||||
|
return NULL;
|
||||||
if ((curr & 0x80) == 0) break;
|
|
||||||
|
|
||||||
position += 7;
|
|
||||||
|
|
||||||
if (position >= 32) return -position / 7;
|
|
||||||
}
|
}
|
||||||
|
uint8_t *count_ptr = (uint8_t*)&palette->count;
|
||||||
return (position + 7) / 7;
|
*count_ptr = count;
|
||||||
|
return palette;
|
||||||
}
|
}
|
||||||
|
|
||||||
int write_varint(FILE *fp, unsigned int in) {
|
struct palette *cc_realloc_palette(struct palette* ptr, uint8_t count) {
|
||||||
unsigned mask = 0xFFFFFF80;
|
if (!ptr) {
|
||||||
int written = 0;
|
return cc_alloc_palette(count);
|
||||||
while (true) {
|
|
||||||
if ((in & mask) == 0) {
|
|
||||||
fputc(in & 0xff, fp);
|
|
||||||
return written + 1;
|
|
||||||
}
|
}
|
||||||
fputc((in & 0x7F) | 0x80, fp);
|
struct palette *palette;
|
||||||
written++;
|
if (count > ptr->count) {
|
||||||
in >>= 7;
|
palette = realloc(ptr, sizeof(struct palette) + count * sizeof(union color));
|
||||||
|
if (!palette) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
for (int i = palette->count; i < count; i++) {
|
||||||
|
palette->colors[i] = (union color){ .v = 0 };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
palette = ptr;
|
||||||
|
}
|
||||||
|
uint8_t *count_ptr = (uint8_t*)&palette->count;
|
||||||
|
*count_ptr = count;
|
||||||
|
return palette;
|
||||||
|
}
|
||||||
|
|
||||||
|
void cc_assign_palette(struct palette *dst, const struct palette *src) {
|
||||||
|
if (!dst || !src) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int count = dst->count;
|
||||||
|
if (count > src->count) {
|
||||||
|
count = src->count;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < count; ++i) {
|
||||||
|
dst->colors[i] = src->colors[i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
10
cc-common.h
|
@ -2,8 +2,6 @@
|
||||||
#define _CC_COMMON_H_
|
#define _CC_COMMON_H_
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdbool.h>
|
|
||||||
|
|
||||||
typedef uint8_t GlyphBitmap[11];
|
typedef uint8_t GlyphBitmap[11];
|
||||||
|
|
||||||
|
@ -21,11 +19,11 @@ struct palette {
|
||||||
#define LENGTHOF(...) (sizeof(__VA_ARGS__) / sizeof(*(__VA_ARGS__)))
|
#define LENGTHOF(...) (sizeof(__VA_ARGS__) / sizeof(*(__VA_ARGS__)))
|
||||||
#define PALETTE(...) { .count = LENGTHOF((union color[]){__VA_ARGS__}), .colors = {__VA_ARGS__} }
|
#define PALETTE(...) { .count = LENGTHOF((union color[]){__VA_ARGS__}), .colors = {__VA_ARGS__} }
|
||||||
|
|
||||||
|
struct palette *cc_alloc_palette(uint8_t count);
|
||||||
|
struct palette *cc_realloc_palette(struct palette* ptr, uint8_t count);
|
||||||
|
void cc_assign_palette(struct palette *dst, const struct palette *src);
|
||||||
|
|
||||||
const extern GlyphBitmap cc_font_atlas[256];
|
const extern GlyphBitmap cc_font_atlas[256];
|
||||||
const extern struct palette cc_default_palette, cc_default_gray_palette;
|
const extern struct palette cc_default_palette, cc_default_gray_palette;
|
||||||
|
|
||||||
|
|
||||||
int read_varint(FILE *fp, unsigned int *out);
|
|
||||||
int write_varint(FILE *fp, unsigned int in);
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
Before Width: | Height: | Size: 66 KiB |
Before Width: | Height: | Size: 94 KiB |
Before Width: | Height: | Size: 107 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 52 KiB |
33
cpi2png.c
|
@ -10,6 +10,8 @@
|
||||||
|
|
||||||
#include "cc-common.h"
|
#include "cc-common.h"
|
||||||
|
|
||||||
|
bool read_varint(FILE *fp, unsigned int *out);
|
||||||
|
|
||||||
int main(int argc, char **argv) {
|
int main(int argc, char **argv) {
|
||||||
if (argc < 3) {
|
if (argc < 3) {
|
||||||
fprintf(stderr, "Usage: %s [input.cpi] [output.png]\n", argv[0]);
|
fprintf(stderr, "Usage: %s [input.cpi] [output.png]\n", argv[0]);
|
||||||
|
@ -41,8 +43,8 @@ int main(int argc, char **argv) {
|
||||||
height = fgetc(fp_in);
|
height = fgetc(fp_in);
|
||||||
(void)fgetc(fp_in); // XXX: ignore scale
|
(void)fgetc(fp_in); // XXX: ignore scale
|
||||||
} else if (version == 1) {
|
} else if (version == 1) {
|
||||||
assert(read_varint(fp_in, &width) > 0 && "Failed to read width varint");
|
assert(read_varint(fp_in, &width) && "Failed to read width varint");
|
||||||
assert(read_varint(fp_in, &height) > 0 && "Failed to read height varint");
|
assert(read_varint(fp_in, &height) && "Failed to read height varint");
|
||||||
} else {
|
} else {
|
||||||
assert(false && "Failed to read size: unsupported version");
|
assert(false && "Failed to read size: unsupported version");
|
||||||
}
|
}
|
||||||
|
@ -53,7 +55,7 @@ int main(int argc, char **argv) {
|
||||||
// XXX: may change in future when we introduce variable-size palettes
|
// XXX: may change in future when we introduce variable-size palettes
|
||||||
// though, it may never change, if I'm being honest. Why would I choose
|
// though, it may never change, if I'm being honest. Why would I choose
|
||||||
// worse image quality with less colors when I can use all of them?
|
// worse image quality with less colors when I can use all of them?
|
||||||
union color colors[16] = { 0 };
|
union color *colors = calloc(16, sizeof(union color));
|
||||||
|
|
||||||
// NOTE: our `union color` type is 4 bytes long, while palette stored in the
|
// NOTE: our `union color` type is 4 bytes long, while palette stored in the
|
||||||
// file itself uses 3 bytes per color, so we can't just `fread` them at once,
|
// file itself uses 3 bytes per color, so we can't just `fread` them at once,
|
||||||
|
@ -65,13 +67,10 @@ int main(int argc, char **argv) {
|
||||||
colors[i].rgba.a = 0xff;
|
colors[i].rgba.a = 0xff;
|
||||||
}
|
}
|
||||||
|
|
||||||
unsigned char *buffer = calloc(width * height, 2);
|
|
||||||
fread(buffer, 2, width * height, fp_in);
|
|
||||||
|
|
||||||
for (int y = 0; y < height; y++) {
|
for (int y = 0; y < height; y++) {
|
||||||
for (int x = 0; x < width; x++) {
|
for (int x = 0; x < width; x++) {
|
||||||
unsigned char sym = buffer[(x + y * width) * 2];
|
unsigned char sym = fgetc(fp_in);
|
||||||
unsigned char color = buffer[(x + y * width) * 2 + 1];
|
unsigned char color = fgetc(fp_in);
|
||||||
union color background = colors[color & 0xF];
|
union color background = colors[color & 0xF];
|
||||||
union color foreground = colors[color >> 4];
|
union color foreground = colors[color >> 4];
|
||||||
for (int oy = 0; oy < 9; oy++) {
|
for (int oy = 0; oy < 9; oy++) {
|
||||||
|
@ -85,7 +84,25 @@ int main(int argc, char **argv) {
|
||||||
|
|
||||||
stbi_write_png(argv[2], width * 6, height * 9, 4, canvas, 0);
|
stbi_write_png(argv[2], width * 6, height * 9, 4, canvas, 0);
|
||||||
|
|
||||||
|
free(colors);
|
||||||
free(canvas);
|
free(canvas);
|
||||||
fclose(fp_in);
|
fclose(fp_in);
|
||||||
return EXIT_SUCCESS;
|
return EXIT_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool read_varint(FILE *fp, unsigned int *out) {
|
||||||
|
int position = 0;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
unsigned char curr = fgetc(fp);
|
||||||
|
*out |= (curr & 0x7F) << position;
|
||||||
|
|
||||||
|
if ((curr & 0x80) == 0) break;
|
||||||
|
|
||||||
|
position += 7;
|
||||||
|
|
||||||
|
if (position >= 32) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
324
img2cpi.c
|
@ -13,16 +13,12 @@
|
||||||
#include <strings.h>
|
#include <strings.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include "cc-common.h"
|
#include "cc-common.h"
|
||||||
#ifdef USE_OPENMP
|
|
||||||
#include <omp.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#define MAX_COLOR_DIFFERENCE 768
|
#define MAX_COLOR_DIFFERENCE 768
|
||||||
#define K_MEANS_ITERATIONS 4
|
#define K_MEANS_ITERATIONS 4
|
||||||
#define PROGRESS_BAR_WIDTH 24
|
#ifndef HAS_POPCNT
|
||||||
|
# define HAS_POPCNT 1
|
||||||
#define TOSTRNAME(M) #M
|
#endif
|
||||||
#define TOSTR(M) TOSTRNAME(M)
|
|
||||||
|
|
||||||
struct cc_char {
|
struct cc_char {
|
||||||
unsigned char character;
|
unsigned char character;
|
||||||
|
@ -30,9 +26,12 @@ struct cc_char {
|
||||||
};
|
};
|
||||||
|
|
||||||
struct arguments {
|
struct arguments {
|
||||||
bool fast_mode;
|
|
||||||
bool verbose;
|
|
||||||
int width, height;
|
int width, height;
|
||||||
|
enum conversion_mode {
|
||||||
|
CONVERSION_BLOCK,
|
||||||
|
CONVERSION_CHAR_PRECISE,
|
||||||
|
CONVERSION_CHAR_FAST,
|
||||||
|
} conversion_mode;
|
||||||
enum cpi_version {
|
enum cpi_version {
|
||||||
CPI_VERSION_AUTO,
|
CPI_VERSION_AUTO,
|
||||||
CPI_VERSION_RAW,
|
CPI_VERSION_RAW,
|
||||||
|
@ -59,16 +58,16 @@ struct arguments {
|
||||||
char *input_path;
|
char *input_path;
|
||||||
char *output_path;
|
char *output_path;
|
||||||
} args = {
|
} args = {
|
||||||
.fast_mode = false,
|
|
||||||
.verbose = false,
|
|
||||||
.width = 4 * 8 - 1, // 4x3 blocks screen
|
.width = 4 * 8 - 1, // 4x3 blocks screen
|
||||||
.height = 3 * 6 - 2,
|
.height = 3 * 6 - 2,
|
||||||
|
.conversion_mode = CONVERSION_CHAR_PRECISE,
|
||||||
.cpi_version = CPI_VERSION_AUTO,
|
.cpi_version = CPI_VERSION_AUTO,
|
||||||
.placement = PLACEMENT_FULL,
|
.placement = PLACEMENT_FULL,
|
||||||
.input_path = NULL,
|
.input_path = NULL,
|
||||||
.output_path = NULL,
|
.output_path = NULL,
|
||||||
.palette = NULL,
|
.palette = NULL,
|
||||||
.palette_type = PALETTE_AUTO
|
.palette_type = PALETTE_DEFAULT // TODO(kc): change to PALETTE_AUTO when
|
||||||
|
// k-means is implemented
|
||||||
};
|
};
|
||||||
|
|
||||||
struct image {
|
struct image {
|
||||||
|
@ -99,23 +98,17 @@ struct k_means_state {
|
||||||
|
|
||||||
bool parse_cmdline(int argc, char **argv);
|
bool parse_cmdline(int argc, char **argv);
|
||||||
void show_help(const char *progname, bool show_all, FILE *fp);
|
void show_help(const char *progname, bool show_all, FILE *fp);
|
||||||
|
|
||||||
struct image *image_load(const char *fp);
|
struct image *image_load(const char *fp);
|
||||||
struct image *image_new(int w, int h);
|
struct image *image_new(int w, int h);
|
||||||
struct image *image_resize(struct image *original, int new_w, int new_h);
|
struct image *image_resize(struct image *original, int new_w, int new_h);
|
||||||
struct image_pal *image_quantize(struct image *original, const struct palette *palette);
|
struct image_pal *image_quantize(struct image *original, const struct palette *palette);
|
||||||
void image_unload(struct image *img);
|
|
||||||
|
|
||||||
float get_color_difference(union color a, union color b);
|
float get_color_difference(union color a, union color b);
|
||||||
float get_color_brightness(union color clr);
|
float get_color_brightness(union color clr);
|
||||||
|
void image_unload(struct image *img);
|
||||||
void get_size_keep_aspect(int w, int h, int dw, int dh, int *ow, int *oh);
|
void get_size_keep_aspect(int w, int h, int dw, int dh, int *ow, int *oh);
|
||||||
|
|
||||||
void convert_2x3(const struct image_pal *img, struct cc_char *characters);
|
void convert_2x3(const struct image_pal *img, struct cc_char *characters);
|
||||||
void convert_6x9(const struct image_pal *img, struct cc_char *characters);
|
void convert_8x11(const struct image_pal *img, struct cc_char *characters, bool precise);
|
||||||
|
|
||||||
int save_cpi_0(FILE *fp, const struct palette *pal, const struct cc_char *chars, int w, int h);
|
|
||||||
int save_cpi_1(FILE *fp, const struct palette *pal, const struct cc_char *chars, int w, int h);
|
|
||||||
|
|
||||||
// Only one global custom palette is maintained
|
// Only one global custom palette is maintained
|
||||||
struct palette *custom_palette_resize(uint8_t size);
|
struct palette *custom_palette_resize(uint8_t size);
|
||||||
|
@ -126,6 +119,14 @@ bool k_means_iteration(struct k_means_state *state);
|
||||||
void k_means_end(struct k_means_state *state);
|
void k_means_end(struct k_means_state *state);
|
||||||
struct palette *palette_k_means(const struct image *image, const struct palette *prototype);
|
struct palette *palette_k_means(const struct image *image, const struct palette *prototype);
|
||||||
|
|
||||||
|
inline static int popcnt32(uint32_t mask);
|
||||||
|
inline static int glyph_hamming_distance(const GlyphBitmap *lhs, const GlyphBitmap *rhs);
|
||||||
|
inline static float weighted_glyph_hamming_distance(const GlyphBitmap *lhs, const GlyphBitmap *rhs, const typeof(float[11][8]) *weights);
|
||||||
|
uint8_t closest_glyph_symbol_fast(const GlyphBitmap *target);
|
||||||
|
uint8_t closest_glyph_symbol_precise(const GlyphBitmap *target, const typeof(float[11][8]) *weights);
|
||||||
|
void construct_chunk_color_glyph(GlyphBitmap *result, typeof(float[11][8]) *weights, const typeof(float[8][11][0x10]) *chunk_palette_diffs, uint8_t color_pair);
|
||||||
|
inline static uint8_t closest_chunk_color_symbol(const typeof(float[8][11][0x10]) *chunk_palette_diffs, uint8_t color_pair, bool precise);
|
||||||
|
|
||||||
const char *known_file_extensions[] = {
|
const char *known_file_extensions[] = {
|
||||||
".png", ".jpg", ".jpeg", ".jfif", ".jpg", ".gif",
|
".png", ".jpg", ".jpeg", ".jfif", ".jpg", ".gif",
|
||||||
".tga", ".bmp", ".hdr", ".pnm", 0
|
".tga", ".bmp", ".hdr", ".pnm", 0
|
||||||
|
@ -139,8 +140,14 @@ static const struct optiondocs {
|
||||||
struct optiondocs_choice { char *value; char *doc; } *choices;
|
struct optiondocs_choice { char *value; char *doc; } *choices;
|
||||||
} optiondocs[] = {
|
} optiondocs[] = {
|
||||||
{ 'h', "help", 0, "Show help", 0 },
|
{ 'h', "help", 0, "Show help", 0 },
|
||||||
{ 'f', "fast", 0, "Use fast (old) method for picking characters and colors", 0 },
|
{ 'f', "fast", 0, "Use fast (old) method for picking characters and colors\n"
|
||||||
{ 'v', "verbose", 0, "Increase verbosity", 0 },
|
" DEPRECATED: use `--mode block` instead\n", 0 },
|
||||||
|
{ 'm', "mode", "mode", "Set conversion mode",
|
||||||
|
(struct optiondocs_choice[]) {
|
||||||
|
{ "block", "Use fast (old) method for picking characters and colors" },
|
||||||
|
{ "char-precise", "Select among all characters with maximum precision" },
|
||||||
|
{ "char-fast", "Select among all characters with reduced precision" },
|
||||||
|
{ 0, 0 } } },
|
||||||
{ 'W', "width", "width", "Width in characters", 0 },
|
{ 'W', "width", "width", "Width in characters", 0 },
|
||||||
{ 'h', "height", "height", "Height in characters", 0 },
|
{ 'h', "height", "height", "Height in characters", 0 },
|
||||||
{ 'P', "palette", "palette", "Use specific palette.\n"
|
{ 'P', "palette", "palette", "Use specific palette.\n"
|
||||||
|
@ -171,8 +178,6 @@ static const struct optiondocs {
|
||||||
{ 0 }
|
{ 0 }
|
||||||
};
|
};
|
||||||
|
|
||||||
static char progress_bar[PROGRESS_BAR_WIDTH];
|
|
||||||
|
|
||||||
int main(int argc, char **argv) {
|
int main(int argc, char **argv) {
|
||||||
if (!parse_cmdline(argc, argv)) {
|
if (!parse_cmdline(argc, argv)) {
|
||||||
show_help(argv[0], false, stderr);
|
show_help(argv[0], false, stderr);
|
||||||
|
@ -185,16 +190,11 @@ int main(int argc, char **argv) {
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.verbose) {
|
|
||||||
memset(progress_bar, '#', PROGRESS_BAR_WIDTH);
|
|
||||||
printf("Input image: %dx%d\n", src_image->w, src_image->h);
|
|
||||||
}
|
|
||||||
|
|
||||||
struct image *canvas;
|
struct image *canvas;
|
||||||
if (args.fast_mode) {
|
if (args.conversion_mode == CONVERSION_BLOCK) {
|
||||||
canvas = image_new(args.width * 2, args.height * 3);
|
canvas = image_new(args.width * 2, args.height * 3);
|
||||||
} else {
|
} else {
|
||||||
canvas = image_new(args.width * 6, args.height * 9);
|
canvas = image_new(args.width * 8, args.height * 11);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!canvas) {
|
if (!canvas) {
|
||||||
|
@ -202,18 +202,14 @@ int main(int argc, char **argv) {
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.verbose) {
|
|
||||||
printf("Output image canvas: %dx%d\n", canvas->w, canvas->h);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: load palette, maybe calculate it too? k-means?
|
// TODO: load palette, maybe calculate it too? k-means?
|
||||||
const struct palette *palette = &cc_default_palette;
|
const struct palette *palette = &cc_default_palette;
|
||||||
switch (args.palette_type) {
|
switch (args.palette_type) {
|
||||||
case PALETTE_DEFAULT: palette = &cc_default_palette; break;
|
case PALETTE_DEFAULT: palette = &cc_default_palette; break;
|
||||||
case PALETTE_DEFAULT_GRAY: palette = &cc_default_gray_palette; break;
|
case PALETTE_DEFAULT_GRAY: palette = &cc_default_gray_palette; break;
|
||||||
case PALETTE_AUTO: palette = palette_k_means(src_image, &cc_default_palette); break;
|
case PALETTE_AUTO: palette = palette_k_means(src_image, &cc_default_palette); break;
|
||||||
case PALETTE_LIST: assert(0 && "Not implemented"); break; // TODO
|
case PALETTE_LIST: assert(0 && "Not implemented"); break;
|
||||||
case PALETTE_PATH: assert(0 && "Not implemented"); break; // TODO
|
case PALETTE_PATH: assert(0 && "Not implemented"); break;
|
||||||
default: assert(0 && "Unreachable");
|
default: assert(0 && "Unreachable");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -223,10 +219,6 @@ int main(int argc, char **argv) {
|
||||||
int new_w, new_h;
|
int new_w, new_h;
|
||||||
get_size_keep_aspect(src_image->w, src_image->h, canvas->w, canvas->h, &new_w, &new_h);
|
get_size_keep_aspect(src_image->w, src_image->h, canvas->w, canvas->h, &new_w, &new_h);
|
||||||
|
|
||||||
if (args.verbose) {
|
|
||||||
printf("Scaling down to: %dx%d\n", new_w, new_h);
|
|
||||||
}
|
|
||||||
|
|
||||||
scaled_image = image_resize(src_image, new_w, new_h);
|
scaled_image = image_resize(src_image, new_w, new_h);
|
||||||
if (!scaled_image) {
|
if (!scaled_image) {
|
||||||
fprintf(stderr, "Error: failed to open the file\n");
|
fprintf(stderr, "Error: failed to open the file\n");
|
||||||
|
@ -252,33 +244,35 @@ int main(int argc, char **argv) {
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.verbose) {
|
switch (args.conversion_mode) {
|
||||||
printf("Converting image ");
|
case CONVERSION_BLOCK:
|
||||||
}
|
|
||||||
|
|
||||||
if (args.fast_mode) {
|
|
||||||
if (args.verbose) {
|
|
||||||
printf(" using fast method\n");
|
|
||||||
}
|
|
||||||
convert_2x3(quantized_image, characters);
|
convert_2x3(quantized_image, characters);
|
||||||
} else {
|
break;
|
||||||
if (args.verbose) {
|
case CONVERSION_CHAR_PRECISE:
|
||||||
printf(" using slow method\n");
|
convert_8x11(quantized_image, characters, true);
|
||||||
}
|
break;
|
||||||
convert_6x9(quantized_image, characters);
|
case CONVERSION_CHAR_FAST:
|
||||||
}
|
convert_8x11(quantized_image, characters, false);
|
||||||
|
break;
|
||||||
if (args.verbose) {
|
default:
|
||||||
printf("Conversion done, saving image ");
|
fprintf(stderr, "BUG: invalid args.conversion_mode\n");
|
||||||
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: implement something other than CPIv0
|
||||||
FILE *fp = fopen(args.output_path, "wb");
|
FILE *fp = fopen(args.output_path, "wb");
|
||||||
if (args.width < 256 && args.height < 256) {
|
fwrite("CCPI", 1, 4, fp);
|
||||||
printf(" using cpiv0\n");
|
fputc(args.width, fp);
|
||||||
save_cpi_0(fp, palette, characters, args.width, args.height);
|
fputc(args.height, fp);
|
||||||
} else {
|
fputc(0x00, fp);
|
||||||
printf(" using cpiv1\n");
|
for (int i = 0; i < 16; i++) {
|
||||||
save_cpi_1(fp, palette, characters, args.width, args.height);
|
fputc(palette->colors[i].rgba.r, fp);
|
||||||
|
fputc(palette->colors[i].rgba.g, fp);
|
||||||
|
fputc(palette->colors[i].rgba.b, fp);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < args.width * args.height; i++) {
|
||||||
|
fputc(characters[i].character, fp);
|
||||||
|
fputc(characters[i].bg | (characters[i].fg << 4), fp);
|
||||||
}
|
}
|
||||||
fclose(fp);
|
fclose(fp);
|
||||||
|
|
||||||
|
@ -287,51 +281,11 @@ int main(int argc, char **argv) {
|
||||||
return EXIT_SUCCESS;
|
return EXIT_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
int _write_palette_full(FILE *fp, const struct palette *pal) {
|
|
||||||
int written = 0;
|
|
||||||
assert(pal->count == 16 && "Invalid palette size");
|
|
||||||
for (int i = 0; i < 16; i++) {
|
|
||||||
written += fputc(pal->colors[i].rgba.r, fp);
|
|
||||||
written += fputc(pal->colors[i].rgba.g, fp);
|
|
||||||
written += fputc(pal->colors[i].rgba.b, fp);
|
|
||||||
}
|
|
||||||
return written;
|
|
||||||
}
|
|
||||||
|
|
||||||
int _write_pixeldata_v0(FILE *fp, const struct cc_char *chars, int w, int h) {
|
|
||||||
int written = 0;
|
|
||||||
for (int i = 0; i < w * h; i++) {
|
|
||||||
written += fputc(chars[i].character, fp);
|
|
||||||
written += fputc(chars[i].bg | (chars[i].fg << 4), fp);
|
|
||||||
}
|
|
||||||
return written;
|
|
||||||
}
|
|
||||||
|
|
||||||
int save_cpi_0(FILE *fp, const struct palette *pal, const struct cc_char *chars, int w, int h) {
|
|
||||||
int written = 0;
|
|
||||||
written += fwrite("CCPI", 1, 4, fp);
|
|
||||||
written += fputc(w, fp);
|
|
||||||
written += fputc(h, fp);
|
|
||||||
written += fputc(0x00, fp);
|
|
||||||
written += _write_palette_full(fp, pal);
|
|
||||||
written += _write_pixeldata_v0(fp, chars, w, h);
|
|
||||||
return written;
|
|
||||||
}
|
|
||||||
|
|
||||||
int save_cpi_1(FILE *fp, const struct palette *pal, const struct cc_char *chars, int w, int h) {
|
|
||||||
int written = 0;
|
|
||||||
written += fwrite("CPI\x01", 1, 4, fp);
|
|
||||||
written += write_varint(fp, w);
|
|
||||||
written += write_varint(fp, h);
|
|
||||||
written += _write_palette_full(fp, pal);
|
|
||||||
written += _write_pixeldata_v0(fp, chars, w, h);
|
|
||||||
return written;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool parse_cmdline(int argc, char **argv) {
|
bool parse_cmdline(int argc, char **argv) {
|
||||||
static struct option options[] = {
|
static struct option options[] = {
|
||||||
{ "help", no_argument, 0, 'h' },
|
{ "help", no_argument, 0, 'h' },
|
||||||
{ "fast", no_argument, 0, 'f' },
|
{ "fast", no_argument, 0, 'f' },
|
||||||
|
{ "mode", required_argument, 0, 'm' },
|
||||||
{ "width", required_argument, 0, 'W' },
|
{ "width", required_argument, 0, 'W' },
|
||||||
{ "height", required_argument, 0, 'H' },
|
{ "height", required_argument, 0, 'H' },
|
||||||
{ "cpi_version", required_argument, 0, 'V' },
|
{ "cpi_version", required_argument, 0, 'V' },
|
||||||
|
@ -342,7 +296,7 @@ bool parse_cmdline(int argc, char **argv) {
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
int option_index = 0;
|
int option_index = 0;
|
||||||
int c = getopt_long(argc, argv, "hvfW:H:V:p:P:", options, &option_index);
|
int c = getopt_long(argc, argv, "hfm:W:H:V:p:P:", options, &option_index);
|
||||||
if (c == -1) break;
|
if (c == -1) break;
|
||||||
if (c == 0) c = options[option_index].val;
|
if (c == 0) c = options[option_index].val;
|
||||||
if (c == '?') break;
|
if (c == '?') break;
|
||||||
|
@ -353,13 +307,22 @@ bool parse_cmdline(int argc, char **argv) {
|
||||||
exit(EXIT_SUCCESS);
|
exit(EXIT_SUCCESS);
|
||||||
break;
|
break;
|
||||||
case 'f': // --fast
|
case 'f': // --fast
|
||||||
args.fast_mode = true;
|
args.conversion_mode = CONVERSION_BLOCK;
|
||||||
|
fprintf(stderr, "Warning: `--fast` is deprecated, use `--mode block` instead\n");
|
||||||
if (args.cpi_version != CPI_VERSION_AUTO) {
|
if (args.cpi_version != CPI_VERSION_AUTO) {
|
||||||
fprintf(stderr, "Warning: text mode ignores version\n");
|
fprintf(stderr, "Warning: text mode ignores version\n");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'v': // --verbose
|
case 'm': // --mode
|
||||||
args.verbose = true;
|
{
|
||||||
|
if (0 == strcmp(optarg, "block") || 0 == strcmp(optarg, "fast") || 0 == strcmp(optarg, "2x3")) {
|
||||||
|
args.conversion_mode = CONVERSION_BLOCK;
|
||||||
|
} else if (0 == strcmp(optarg, "char") || 0 == strcmp(optarg, "char-precise") || 0 == strcmp(optarg, "8x11") || 0 == strcmp(optarg, "6x9")) {
|
||||||
|
args.conversion_mode = CONVERSION_CHAR_PRECISE;
|
||||||
|
} else if (0 == strcmp(optarg, "char-fast") || 0 == strcmp(optarg, "8x11-fast") || 0 == strcmp(optarg, "6x9-fast")) {
|
||||||
|
args.conversion_mode = CONVERSION_CHAR_FAST;
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'W': // --width
|
case 'W': // --width
|
||||||
args.width = atoi(optarg);
|
args.width = atoi(optarg);
|
||||||
|
@ -622,14 +585,6 @@ float get_color_brightness(union color clr) {
|
||||||
void convert_2x3(const struct image_pal *img, struct cc_char *characters) {
|
void convert_2x3(const struct image_pal *img, struct cc_char *characters) {
|
||||||
int w = img->w / 2, h = img->h / 3;
|
int w = img->w / 2, h = img->h / 3;
|
||||||
for (int y = 0; y < h; y++) {
|
for (int y = 0; y < h; y++) {
|
||||||
if (args.verbose) {
|
|
||||||
int sz = PROGRESS_BAR_WIDTH - (y * PROGRESS_BAR_WIDTH / h);
|
|
||||||
printf("\r[%-" TOSTR(PROGRESS_BAR_WIDTH) ".*s|%7.3f%%|%4d/%4d]",
|
|
||||||
PROGRESS_BAR_WIDTH - sz, progress_bar + sz,
|
|
||||||
100.0 * (y + 1) / h,
|
|
||||||
y + 1, h);
|
|
||||||
fflush(stdout);
|
|
||||||
}
|
|
||||||
for (int x = 0; x < w; x++) {
|
for (int x = 0; x < w; x++) {
|
||||||
unsigned char darkest_i = 0, brightest_i = 0;
|
unsigned char darkest_i = 0, brightest_i = 0;
|
||||||
float darkest_diff = 0xffffff, brightest_diff = 0;
|
float darkest_diff = 0xffffff, brightest_diff = 0;
|
||||||
|
@ -680,13 +635,10 @@ void convert_2x3(const struct image_pal *img, struct cc_char *characters) {
|
||||||
characters[x + y * w].fg = brightest_i;
|
characters[x + y * w].fg = brightest_i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (args.verbose) {
|
|
||||||
printf("\n");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void convert_6x9(const struct image_pal *img, struct cc_char *characters) {
|
void convert_8x11(const struct image_pal *img, struct cc_char *characters, bool precise) {
|
||||||
int w = img->w / 6, h = img->h / 9;
|
int w = img->w / 8, h = img->h / 11;
|
||||||
float palette_self_diffs[0x100][0x10] = {{(float) 0xffffff}};
|
float palette_self_diffs[0x100][0x10] = {{(float) 0xffffff}};
|
||||||
for (int input_color = 0x0; input_color < 0x100 && input_color < img->palette->count; input_color++) {
|
for (int input_color = 0x0; input_color < 0x100 && input_color < img->palette->count; input_color++) {
|
||||||
for (int output_color = 0x0; output_color < 0x10 && output_color < img->palette->count; output_color++) {
|
for (int output_color = 0x0; output_color < 0x10 && output_color < img->palette->count; output_color++) {
|
||||||
|
@ -695,23 +647,12 @@ void convert_6x9(const struct image_pal *img, struct cc_char *characters) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int y = 0; y < h; y++) {
|
for (int y = 0; y < h; y++) {
|
||||||
if (args.verbose) {
|
|
||||||
int sz = PROGRESS_BAR_WIDTH - (y * PROGRESS_BAR_WIDTH / h);
|
|
||||||
printf("\r[%-" TOSTR(PROGRESS_BAR_WIDTH) ".*s|%7.3f%%|%4d/%4d]",
|
|
||||||
PROGRESS_BAR_WIDTH - sz, progress_bar + sz,
|
|
||||||
100.0 * (y + 1) / h,
|
|
||||||
y + 1, h);
|
|
||||||
fflush(stdout);
|
|
||||||
}
|
|
||||||
#ifdef USE_OPENMP
|
|
||||||
#pragma omp parallel for
|
|
||||||
#endif
|
|
||||||
for (int x = 0; x < w; x++) {
|
for (int x = 0; x < w; x++) {
|
||||||
float chunk_palette_diffs[6][9][0x10] = {{{(float) 0xffffff}}};
|
float chunk_palette_diffs[8][11][0x10] = {{{(float) 0xffffff}}};
|
||||||
for (int ox = 0; ox < 6; ox++) {
|
for (int ox = 0; ox < 8; ox++) {
|
||||||
for (int oy = 0; oy < 9; oy++) {
|
for (int oy = 0; oy < 11; oy++) {
|
||||||
uint8_t pixel_unresolved = img->pixels[
|
uint8_t pixel_unresolved = img->pixels[
|
||||||
ox + (x + (y * 9 + oy) * w) * 6
|
ox + (x + (y * 11 + oy) * w) * 8
|
||||||
];
|
];
|
||||||
for (int color = 0x0; color < 0x10 && color < img->palette->count; color++) {
|
for (int color = 0x0; color < 0x10 && color < img->palette->count; color++) {
|
||||||
chunk_palette_diffs[ox][oy][color] = palette_self_diffs[pixel_unresolved][color];
|
chunk_palette_diffs[ox][oy][color] = palette_self_diffs[pixel_unresolved][color];
|
||||||
|
@ -721,15 +662,13 @@ void convert_6x9(const struct image_pal *img, struct cc_char *characters) {
|
||||||
|
|
||||||
float min_diff = 0xffffff;
|
float min_diff = 0xffffff;
|
||||||
char closest_sym = 0x00, closest_color = 0xae;
|
char closest_sym = 0x00, closest_color = 0xae;
|
||||||
for (int sym = 0x01; sym <= 0xFF; sym++) {
|
|
||||||
if (sym == '\t' || sym == '\n' || sym == '\r' || sym == '\x0e') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (int color = 0x00; color <= 0xff; color++) {
|
for (int color = 0x00; color <= 0xff; color++) {
|
||||||
|
{
|
||||||
|
const int sym = closest_chunk_color_symbol(&chunk_palette_diffs, color, precise);
|
||||||
float difference = 0;
|
float difference = 0;
|
||||||
for (int oy = 0; oy < 9; oy++) {
|
for (int oy = 0; oy < 11; oy++) {
|
||||||
unsigned char sym_line = cc_font_atlas[sym][oy];
|
unsigned char sym_line = cc_font_atlas[sym][oy];
|
||||||
for (int ox = 0; ox < 6; ox++) {
|
for (int ox = 0; ox < 8; ox++) {
|
||||||
bool lit = sym_line & (0x80 >> ox);
|
bool lit = sym_line & (0x80 >> ox);
|
||||||
difference += chunk_palette_diffs[ox][oy][lit ? color >> 4 : color & 0xF];
|
difference += chunk_palette_diffs[ox][oy][lit ? color >> 4 : color & 0xF];
|
||||||
}
|
}
|
||||||
|
@ -746,9 +685,6 @@ void convert_6x9(const struct image_pal *img, struct cc_char *characters) {
|
||||||
characters[x + y * w].fg = closest_color >> 4;
|
characters[x + y * w].fg = closest_color >> 4;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (args.verbose) {
|
|
||||||
printf("\n");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct {
|
struct {
|
||||||
|
@ -885,3 +821,99 @@ struct palette *palette_k_means(const struct image *image, const struct palette
|
||||||
|
|
||||||
return palette;
|
return palette;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int popcnt32(uint32_t mask) {
|
||||||
|
#if HAS_POPCNT
|
||||||
|
return __builtin_popcount(mask);
|
||||||
|
#else
|
||||||
|
int res = 0;
|
||||||
|
for (; mask; mask >>= 1) {
|
||||||
|
res += mask & 1;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
int glyph_hamming_distance(const GlyphBitmap *lhs, const GlyphBitmap *rhs) {
|
||||||
|
int dist = 0;
|
||||||
|
dist += popcnt32(*((uint32_t*)&((*lhs)[0])) ^ *((uint32_t*)&((*rhs)[0])));
|
||||||
|
dist += popcnt32(*((uint32_t*)&((*lhs)[4])) ^ *((uint32_t*)&((*rhs)[4])));
|
||||||
|
dist += popcnt32(*((uint16_t*)&((*lhs)[8])) ^ *((uint16_t*)&((*rhs)[8])));
|
||||||
|
dist += popcnt32( ( ((*lhs)[10])) ^ ( ((*rhs)[10])));
|
||||||
|
return dist;
|
||||||
|
}
|
||||||
|
|
||||||
|
float weighted_glyph_hamming_distance(const GlyphBitmap *lhs, const GlyphBitmap *rhs, const typeof(float[11][8]) *weights) {
|
||||||
|
float dist = 0;
|
||||||
|
for (int oy = 0; oy < 11; oy++) {
|
||||||
|
uint8_t sym_line = (*lhs)[oy] ^ (*rhs)[oy];
|
||||||
|
for (int ox = 0; ox < 8; ox++) {
|
||||||
|
bool lit = sym_line & (0x80 >> ox);
|
||||||
|
if (lit) {
|
||||||
|
dist += (*weights)[oy][ox];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dist;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t closest_glyph_symbol_fast(const GlyphBitmap *target) {
|
||||||
|
uint8_t best = 0x01;
|
||||||
|
int best_dist = glyph_hamming_distance(target, &cc_font_atlas[best]);
|
||||||
|
for (int sym = 0x02; sym <= 0xFF; sym++) {
|
||||||
|
if (sym == '\t' || sym == '\n' || sym == '\r' || sym == '\x0e') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int dist = glyph_hamming_distance(target, &cc_font_atlas[sym]);
|
||||||
|
if (dist <= best_dist) {
|
||||||
|
best_dist = dist;
|
||||||
|
best = sym;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t closest_glyph_symbol_precise(const GlyphBitmap *target, const typeof(float[11][8]) *weights) {
|
||||||
|
uint8_t best = 0x01;
|
||||||
|
float best_dist = weighted_glyph_hamming_distance(target, &cc_font_atlas[best], weights);
|
||||||
|
for (int sym = 0x02; sym <= 0xFF; sym++) {
|
||||||
|
if (sym == '\t' || sym == '\n' || sym == '\r' || sym == '\x0e') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
float dist = weighted_glyph_hamming_distance(target, &cc_font_atlas[sym], weights);
|
||||||
|
if (dist <= best_dist) {
|
||||||
|
best_dist = dist;
|
||||||
|
best = sym;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
void construct_chunk_color_glyph(GlyphBitmap *result, typeof(float[11][8]) *weights, const typeof(float[8][11][0x10]) *chunk_palette_diffs, uint8_t color_pair) {
|
||||||
|
uint8_t fg = color_pair >> 4,
|
||||||
|
bg = color_pair & 0xF;
|
||||||
|
for (int oy = 0; oy < 11; oy++) {
|
||||||
|
uint8_t sym_line = 0;
|
||||||
|
for (int ox = 0; ox < 8; ox++) {
|
||||||
|
// We want lit to minimize distance, so lit should be trueish when background color is further from the pixel than foreground color
|
||||||
|
float dist_diff = (*chunk_palette_diffs)[ox][oy][bg] - (*chunk_palette_diffs)[ox][oy][fg];
|
||||||
|
uint8_t lit = dist_diff > 0;
|
||||||
|
sym_line |= lit << (7 - ox);
|
||||||
|
if (weights) {
|
||||||
|
(*weights)[oy][ox] = lit ? dist_diff : -dist_diff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(*result)[oy] = sym_line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t closest_chunk_color_symbol(const typeof(float[8][11][0x10]) *chunk_palette_diffs, uint8_t color_pair, bool precise) {
|
||||||
|
GlyphBitmap glyph;
|
||||||
|
float weights[11][8];
|
||||||
|
construct_chunk_color_glyph(&glyph, precise ? &weights : NULL, chunk_palette_diffs, color_pair);
|
||||||
|
if (precise) {
|
||||||
|
return closest_glyph_symbol_precise(&glyph, &weights);
|
||||||
|
} else {
|
||||||
|
return closest_glyph_symbol_fast(&glyph);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
BIN
mess/avg.png
Before Width: | Height: | Size: 131 B |
BIN
mess/cc_font.png
Before Width: | Height: | Size: 2.8 KiB |
|
@ -1,25 +0,0 @@
|
||||||
from PIL import Image
|
|
||||||
from collections import Counter
|
|
||||||
|
|
||||||
with Image.open("./cc_font.png") as im:
|
|
||||||
pixels = im.load()
|
|
||||||
weights = [0 for _ in range(6 * 9)]
|
|
||||||
for char in range(256):
|
|
||||||
ctx, cty = (char % 16) * 8, (char // 16) * 11
|
|
||||||
for oy in range(9):
|
|
||||||
for ox in range(6):
|
|
||||||
pix = int(pixels[ctx + ox + 1, cty + oy + 1][0]) # type: ignore
|
|
||||||
weights[ox + 6 * oy] += 1 if pix else 0
|
|
||||||
|
|
||||||
with Image.new("L", (6, 9), 0) as im_out:
|
|
||||||
for y in range(9):
|
|
||||||
for x in range(6):
|
|
||||||
print("%3d" % weights[x + 6 * y], end="\t")
|
|
||||||
im_out.putpixel((x, y), weights[x + 6 * y])
|
|
||||||
print()
|
|
||||||
|
|
||||||
im_out.save("avg.png")
|
|
||||||
|
|
||||||
print(dict(enumerate([
|
|
||||||
iv[0] for iv in sorted(enumerate(weights), key=lambda iv: iv[1])
|
|
||||||
])))
|
|