mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-12 16:57:26 +00:00
Easter egg (#123)
* lol zx quant * Adding ZX option * Improving colour selection so we don't end up with the same colour twice. Also fixing a bug with the colour conflict resolution. * Putting it behind a konami code * Better comments * Adding comment * Removing unnecessary malloc.
This commit is contained in:
@@ -1,4 +1,9 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
|
<style>
|
||||||
|
canvas {
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<script src='imagequant.js'></script>
|
<script src='imagequant.js'></script>
|
||||||
<script>
|
<script>
|
||||||
const Module = imagequant();
|
const Module = imagequant();
|
||||||
@@ -23,6 +28,7 @@
|
|||||||
create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
|
create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
|
||||||
destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
|
destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
|
||||||
quantize: Module.cwrap('quantize', '', ['number', 'number', 'number', 'number', 'number']),
|
quantize: Module.cwrap('quantize', '', ['number', 'number', 'number', 'number', 'number']),
|
||||||
|
zx_quantize: Module.cwrap('zx_quantize', '', ['number', 'number', 'number', 'number']),
|
||||||
free_result: Module.cwrap('free_result', '', ['number']),
|
free_result: Module.cwrap('free_result', '', ['number']),
|
||||||
get_result_pointer: Module.cwrap('get_result_pointer', 'number', []),
|
get_result_pointer: Module.cwrap('get_result_pointer', 'number', []),
|
||||||
};
|
};
|
||||||
@@ -30,7 +36,9 @@
|
|||||||
const image = await loadImage('../example.png');
|
const image = await loadImage('../example.png');
|
||||||
const p = api.create_buffer(image.width, image.height);
|
const p = api.create_buffer(image.width, image.height);
|
||||||
Module.HEAP8.set(image.data, p);
|
Module.HEAP8.set(image.data, p);
|
||||||
api.quantize(p, image.width, image.height, 16, 1.0);
|
//api.quantize(p, image.width, image.height, 256, 1.0);
|
||||||
|
api.zx_quantize(p, image.width, image.height, 1);
|
||||||
|
console.log('done');
|
||||||
const resultPointer = api.get_result_pointer();
|
const resultPointer = api.get_result_pointer();
|
||||||
const resultView = new Uint8Array(Module.HEAP8.buffer, resultPointer, image.width * image.height * 4);
|
const resultView = new Uint8Array(Module.HEAP8.buffer, resultPointer, image.width * image.height * 4);
|
||||||
const result = new Uint8ClampedArray(resultView);
|
const result = new Uint8ClampedArray(resultView);
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
#include "emscripten.h"
|
#include "emscripten.h"
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <inttypes.h>
|
#include <inttypes.h>
|
||||||
|
#include <limits.h>
|
||||||
|
#include <math.h>
|
||||||
|
|
||||||
#include "libimagequant.h"
|
#include "libimagequant.h"
|
||||||
|
|
||||||
@@ -50,6 +52,169 @@ void quantize(uint8_t* image_buffer, int image_width, int image_height, int num_
|
|||||||
liq_attr_destroy(attr);
|
liq_attr_destroy(attr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const liq_color zx_colors[] = {
|
||||||
|
{.a = 255, .r = 0, .g = 0, .b = 0}, // regular black
|
||||||
|
{.a = 255, .r = 0, .g = 0, .b = 215}, // regular blue
|
||||||
|
{.a = 255, .r = 215, .g = 0, .b = 0}, // regular red
|
||||||
|
{.a = 255, .r = 215, .g = 0, .b = 215}, // regular magenta
|
||||||
|
{.a = 255, .r = 0, .g = 215, .b = 0}, // regular green
|
||||||
|
{.a = 255, .r = 0, .g = 215, .b = 215}, // regular cyan
|
||||||
|
{.a = 255, .r = 215, .g = 215, .b = 0}, // regular yellow
|
||||||
|
{.a = 255, .r = 215, .g = 215, .b = 215}, // regular white
|
||||||
|
{.a = 255, .r = 0, .g = 0, .b = 255}, // bright blue
|
||||||
|
{.a = 255, .r = 255, .g = 0, .b = 0}, // bright red
|
||||||
|
{.a = 255, .r = 255, .g = 0, .b = 255}, // bright magenta
|
||||||
|
{.a = 255, .r = 0, .g = 255, .b = 0}, // bright green
|
||||||
|
{.a = 255, .r = 0, .g = 255, .b = 255}, // bright cyan
|
||||||
|
{.a = 255, .r = 255, .g = 255, .b = 0}, // bright yellow
|
||||||
|
{.a = 255, .r = 255, .g = 255, .b = 255} // bright white
|
||||||
|
};
|
||||||
|
|
||||||
|
uint8_t block[8 * 8 * 4];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ZX has one bit per pixel, but can assign two colours to an 8x8 block. The two colours must
|
||||||
|
* both be 'regular' or 'bright'. Black exists as both regular and bright.
|
||||||
|
*/
|
||||||
|
EMSCRIPTEN_KEEPALIVE
|
||||||
|
void zx_quantize(uint8_t* image_buffer, int image_width, int image_height, float dithering) {
|
||||||
|
int size = image_width * image_height;
|
||||||
|
int bytes_per_pixel = 4;
|
||||||
|
result = (int) malloc(size * bytes_per_pixel);
|
||||||
|
uint8_t* image8bit = (uint8_t*) malloc(8 * 8);
|
||||||
|
|
||||||
|
// For each 8x8 grid
|
||||||
|
for (int block_start_y = 0; block_start_y < image_height; block_start_y += 8) {
|
||||||
|
for (int block_start_x = 0; block_start_x < image_width; block_start_x += 8) {
|
||||||
|
int color_popularity[15] = {0};
|
||||||
|
int block_index = 0;
|
||||||
|
int block_width = 8;
|
||||||
|
int block_height = 8;
|
||||||
|
|
||||||
|
// If the block hangs off the right/bottom of the image dimensions, make it smaller to fit.
|
||||||
|
if (block_start_y + block_height > image_height) {
|
||||||
|
block_height = image_height - block_start_y;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (block_start_x + block_width > image_width) {
|
||||||
|
block_width = image_width - block_start_x;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For each pixel in that block:
|
||||||
|
for (int y = block_start_y; y < block_start_y + block_height; y++) {
|
||||||
|
for (int x = block_start_x; x < block_start_x + block_width; x++) {
|
||||||
|
int pixel_start = (y * image_width * bytes_per_pixel) + (x * bytes_per_pixel);
|
||||||
|
int smallest_distance = INT_MAX;
|
||||||
|
int winning_index = -1;
|
||||||
|
|
||||||
|
// Copy pixel data for quantizing later
|
||||||
|
block[block_index++] = image_buffer[pixel_start];
|
||||||
|
block[block_index++] = image_buffer[pixel_start + 1];
|
||||||
|
block[block_index++] = image_buffer[pixel_start + 2];
|
||||||
|
block[block_index++] = image_buffer[pixel_start + 3];
|
||||||
|
|
||||||
|
// Which zx color is this pixel closest to?
|
||||||
|
for (int color_index = 0; color_index < 15; color_index++) {
|
||||||
|
liq_color color = zx_colors[color_index];
|
||||||
|
|
||||||
|
// Using Euclidean distance. LibQuant has better methods, but it requires conversion to
|
||||||
|
// LAB, so I don't think it's worth it.
|
||||||
|
int distance =
|
||||||
|
pow(color.r - image_buffer[pixel_start + 0], 2) +
|
||||||
|
pow(color.g - image_buffer[pixel_start + 1], 2) +
|
||||||
|
pow(color.b - image_buffer[pixel_start + 2], 2);
|
||||||
|
|
||||||
|
if (distance < smallest_distance) {
|
||||||
|
winning_index = color_index;
|
||||||
|
smallest_distance = distance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
color_popularity[winning_index]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the three most popular colours for the block.
|
||||||
|
int first_color_index = 0;
|
||||||
|
int second_color_index = 0;
|
||||||
|
int third_color_index = 0;
|
||||||
|
int highest_popularity = -1;
|
||||||
|
int second_highest_popularity = -1;
|
||||||
|
int third_highest_popularity = -1;
|
||||||
|
|
||||||
|
for (int color_index = 0; color_index < 15; color_index++) {
|
||||||
|
if (color_popularity[color_index] > highest_popularity) {
|
||||||
|
// Store this as the most popular pixel, and demote the current values:
|
||||||
|
third_color_index = second_color_index;
|
||||||
|
third_highest_popularity = second_highest_popularity;
|
||||||
|
second_color_index = first_color_index;
|
||||||
|
second_highest_popularity = highest_popularity;
|
||||||
|
first_color_index = color_index;
|
||||||
|
highest_popularity = color_popularity[color_index];
|
||||||
|
} else if (color_popularity[color_index] > second_highest_popularity) {
|
||||||
|
third_color_index = second_color_index;
|
||||||
|
third_highest_popularity = second_highest_popularity;
|
||||||
|
second_color_index = color_index;
|
||||||
|
second_highest_popularity = color_popularity[color_index];
|
||||||
|
} else if (color_popularity[color_index] > third_highest_popularity) {
|
||||||
|
third_color_index = color_index;
|
||||||
|
third_highest_popularity = color_popularity[color_index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZX images can't mix bright and regular colours, except black which appears in both.
|
||||||
|
// Resolve any conflict:
|
||||||
|
while (1) {
|
||||||
|
// If either colour is black, there's no conflict to resolve.
|
||||||
|
if (first_color_index != 0 && second_color_index != 0) {
|
||||||
|
if (first_color_index >= 8 && second_color_index < 8) {
|
||||||
|
// Make the second color bright
|
||||||
|
second_color_index = second_color_index + 7;
|
||||||
|
} else if (first_color_index < 8 && second_color_index >= 8) {
|
||||||
|
// Make the second color regular
|
||||||
|
second_color_index = second_color_index - 7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If, during conflict resolving, we now have two of the same colour (because we initially
|
||||||
|
// selected the bright & regular version of the same colour), retry again with the third
|
||||||
|
// most popular colour.
|
||||||
|
if (first_color_index == second_color_index) {
|
||||||
|
second_color_index = third_color_index;
|
||||||
|
} else break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quantize
|
||||||
|
attr = liq_attr_create();
|
||||||
|
image = liq_image_create_rgba(attr, block, block_width, block_height, 0);
|
||||||
|
liq_set_max_colors(attr, 2);
|
||||||
|
liq_image_add_fixed_color(image, zx_colors[first_color_index]);
|
||||||
|
liq_image_add_fixed_color(image, zx_colors[second_color_index]);
|
||||||
|
liq_image_quantize(image, attr, &res);
|
||||||
|
liq_set_dithering_level(res, dithering);
|
||||||
|
liq_write_remapped_image(res, image, image8bit, size);
|
||||||
|
const liq_palette *pal = liq_get_palette(res);
|
||||||
|
|
||||||
|
// Turn palletted image back into an RGBA image, and write it into the full size result image.
|
||||||
|
for(int y = 0; y < block_height; y++) {
|
||||||
|
for(int x = 0; x < block_width; x++) {
|
||||||
|
int image8BitPos = y * block_width + x;
|
||||||
|
int resultStartPos = ((block_start_y + y) * bytes_per_pixel * image_width) + ((block_start_x + x) * bytes_per_pixel);
|
||||||
|
((uint8_t*)result)[resultStartPos + 0] = pal->entries[image8bit[image8BitPos]].r;
|
||||||
|
((uint8_t*)result)[resultStartPos + 1] = pal->entries[image8bit[image8BitPos]].g;
|
||||||
|
((uint8_t*)result)[resultStartPos + 2] = pal->entries[image8bit[image8BitPos]].b;
|
||||||
|
((uint8_t*)result)[resultStartPos + 3] = pal->entries[image8bit[image8BitPos]].a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
liq_result_destroy(res);
|
||||||
|
liq_image_destroy(image);
|
||||||
|
liq_attr_destroy(attr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
free(image8bit);
|
||||||
|
}
|
||||||
|
|
||||||
EMSCRIPTEN_KEEPALIVE
|
EMSCRIPTEN_KEEPALIVE
|
||||||
void free_result() {
|
void free_result() {
|
||||||
free(result);
|
free(result);
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
2
codecs/imagequant/package-lock.json
generated
2
codecs/imagequant/package-lock.json
generated
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "mozjpeg_enc",
|
"name": "imagequant",
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ interface ModuleAPI {
|
|||||||
create_buffer(width: number, height: number): number;
|
create_buffer(width: number, height: number): number;
|
||||||
destroy_buffer(pointer: number): void;
|
destroy_buffer(pointer: number): void;
|
||||||
quantize(buffer: number, width: number, height: number, numColors: number, dither: number): void;
|
quantize(buffer: number, width: number, height: number, numColors: number, dither: number): void;
|
||||||
|
zx_quantize(buffer: number, width: number, height: number, dither: number): void;
|
||||||
free_result(): void;
|
free_result(): void;
|
||||||
get_result_pointer(): number;
|
get_result_pointer(): number;
|
||||||
}
|
}
|
||||||
@@ -51,6 +52,7 @@ export default class ImageQuant {
|
|||||||
create_buffer: m.cwrap('create_buffer', 'number', ['number', 'number']),
|
create_buffer: m.cwrap('create_buffer', 'number', ['number', 'number']),
|
||||||
destroy_buffer: m.cwrap('destroy_buffer', '', ['number']),
|
destroy_buffer: m.cwrap('destroy_buffer', '', ['number']),
|
||||||
quantize: m.cwrap('quantize', '', ['number', 'number', 'number', 'number', 'number']),
|
quantize: m.cwrap('quantize', '', ['number', 'number', 'number', 'number', 'number']),
|
||||||
|
zx_quantize: m.cwrap('zx_quantize', '', ['number', 'number', 'number', 'number']),
|
||||||
free_result: m.cwrap('free_result', '', []),
|
free_result: m.cwrap('free_result', '', []),
|
||||||
get_result_pointer: m.cwrap('get_result_pointer', 'number', []),
|
get_result_pointer: m.cwrap('get_result_pointer', 'number', []),
|
||||||
};
|
};
|
||||||
@@ -63,7 +65,11 @@ export default class ImageQuant {
|
|||||||
|
|
||||||
const p = api.create_buffer(data.width, data.height);
|
const p = api.create_buffer(data.width, data.height);
|
||||||
m.HEAP8.set(new Uint8Array(data.data), p);
|
m.HEAP8.set(new Uint8Array(data.data), p);
|
||||||
api.quantize(p, data.width, data.height, opts.maxNumColors, opts.dither);
|
if (opts.zx) {
|
||||||
|
api.zx_quantize(p, data.width, data.height, opts.dither);
|
||||||
|
} else {
|
||||||
|
api.quantize(p, data.width, data.height, opts.maxNumColors, opts.dither);
|
||||||
|
}
|
||||||
const resultPointer = api.get_result_pointer();
|
const resultPointer = api.get_result_pointer();
|
||||||
const resultView = new Uint8Array(
|
const resultView = new Uint8Array(
|
||||||
m.HEAP8.buffer,
|
m.HEAP8.buffer,
|
||||||
|
|||||||
@@ -1,29 +1,55 @@
|
|||||||
import { h, Component } from 'preact';
|
import { h, Component } from 'preact';
|
||||||
import { bind, inputFieldValueAsNumber } from '../../lib/util';
|
import { bind, inputFieldValueAsNumber, konami } from '../../lib/util';
|
||||||
import { QuantizeOptions } from './quantizer';
|
import { QuantizeOptions } from './quantizer';
|
||||||
|
|
||||||
|
const konamiPromise = konami();
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
options: QuantizeOptions;
|
options: QuantizeOptions;
|
||||||
onChange(newOptions: QuantizeOptions): void;
|
onChange(newOptions: QuantizeOptions): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class QuantizerOptions extends Component<Props, {}> {
|
interface State {
|
||||||
|
extendedSettings: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class QuantizerOptions extends Component<Props, State> {
|
||||||
|
state: State = { extendedSettings: false };
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
konamiPromise.then(() => {
|
||||||
|
this.setState({ extendedSettings: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
onChange(event: Event) {
|
onChange(event: Event) {
|
||||||
const form = (event.currentTarget as HTMLInputElement).closest('form') as HTMLFormElement;
|
const form = (event.currentTarget as HTMLInputElement).closest('form') as HTMLFormElement;
|
||||||
|
|
||||||
const options: QuantizeOptions = {
|
const options: QuantizeOptions = {
|
||||||
|
zx: inputFieldValueAsNumber(form.zx),
|
||||||
maxNumColors: inputFieldValueAsNumber(form.maxNumColors),
|
maxNumColors: inputFieldValueAsNumber(form.maxNumColors),
|
||||||
dither: inputFieldValueAsNumber(form.dither),
|
dither: inputFieldValueAsNumber(form.dither),
|
||||||
};
|
};
|
||||||
this.props.onChange(options);
|
this.props.onChange(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
render({ options }: Props) {
|
render({ options }: Props, { extendedSettings }: State) {
|
||||||
return (
|
return (
|
||||||
<form>
|
<form>
|
||||||
<label>
|
<label style={{ display: extendedSettings ? '' : 'none' }}>
|
||||||
Pallette Colors:
|
Type:
|
||||||
|
<select
|
||||||
|
name="zx"
|
||||||
|
value={'' + options.zx}
|
||||||
|
onChange={this.onChange}
|
||||||
|
>
|
||||||
|
<option value="0">Standard</option>
|
||||||
|
<option value="1">ZX</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label style={{ display: options.zx ? 'none' : '' }}>
|
||||||
|
Palette Colors:
|
||||||
<input
|
<input
|
||||||
name="maxNumColors"
|
name="maxNumColors"
|
||||||
type="range"
|
type="range"
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ export async function quantize(data: ImageData, opts: QuantizeOptions): Promise<
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface QuantizeOptions {
|
export interface QuantizeOptions {
|
||||||
|
zx: number;
|
||||||
maxNumColors: number;
|
maxNumColors: number;
|
||||||
dither: number;
|
dither: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultOptions: QuantizeOptions = {
|
export const defaultOptions: QuantizeOptions = {
|
||||||
|
zx: 0,
|
||||||
maxNumColors: 256,
|
maxNumColors: 256,
|
||||||
dither: 1.0,
|
dither: 1.0,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -159,3 +159,25 @@ export function inputFieldValueAsNumber(field: any): number {
|
|||||||
export function inputFieldCheckedAsNumber(field: any): number {
|
export function inputFieldCheckedAsNumber(field: any): number {
|
||||||
return Number((field as HTMLInputElement).checked);
|
return Number((field as HTMLInputElement).checked);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a promise that resolves when the user types the konami code.
|
||||||
|
*/
|
||||||
|
export function konami(): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// Keycodes for: ↑ ↑ ↓ ↓ ← → ← → B A
|
||||||
|
const expectedPattern = '38384040373937396665';
|
||||||
|
let rollingPattern = '';
|
||||||
|
|
||||||
|
const listener = (event: KeyboardEvent) => {
|
||||||
|
rollingPattern += event.keyCode;
|
||||||
|
rollingPattern = rollingPattern.slice(0, expectedPattern.length);
|
||||||
|
if (rollingPattern === expectedPattern) {
|
||||||
|
window.removeEventListener('keydown', listener);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('keydown', listener);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user