add bwry dithering support

This commit is contained in:
Shuanglei Tao
2025-06-03 13:46:52 +08:00
parent 95b1aaa99c
commit 03b33b46a9
4 changed files with 79 additions and 34 deletions

View File

@@ -25,6 +25,7 @@
<option value="03">UC8176/UC8276三色屏</option>
<option value="04">SSD1619/SSD1683黑白屏</option>
<option value="02">SSD1619/SSD1683三色屏</option>
<option value="05">JD79668四色屏</option>
</select>
</div>
<div class="flex-group debug">
@@ -60,10 +61,14 @@
<option value="floydsteinberg">floydsteinberg</option>
<option value="Atkinson">Atkinson</option>
</optgroup>
<optgroup id="dithering-bwr" data-driver="02|03" label="三色">
<optgroup data-driver="02|03" label="三色">
<option value="bwr_floydsteinberg">黑白红floydsteinberg</option>
<option value="bwr_Atkinson">黑白红Atkinson</option>
</optgroup>
<optgroup data-driver="05" label="四色">
<option value="bwry_floydsteinberg">黑白红黄floydsteinberg</option>
<option value="bwry_Atkinson">黑白红黄Atkinson</option>
</optgroup>
</select>
</div>
<div class="flex-group">
@@ -96,6 +101,7 @@
<select id="brush-color" title="画笔颜色">
<option value="#000000">黑色</option>
<option value="#FF0000">红色</option>
<option value="#FFFF00">黄色</option>
<option value="#FFFFFF">白色</option>
</select>
<input type="number" id="brush-size" value="2" min="1" max="100" title="画笔大小">

View File

@@ -1,9 +1,22 @@
const bwrPalette = [
[0, 0, 0, 255],
[255, 255, 255, 255],
[255, 0, 0, 255]
const bwPalette = [
[0, 0, 0, 255], // black
[255, 255, 255, 255] // white
]
const bwrPalette = [
[0, 0, 0, 255], // black
[255, 255, 255, 255], // white
[255, 0, 0, 255] // red
]
const bwryPalette = [
[0, 0, 0, 255], // black
[255, 255, 255, 255], // white
[255, 255, 0, 255], // yellow
[255, 0, 0, 255] // red
]
// black-white dithering
function dithering(ctx, width, height, threshold, type) {
const bayerThresholdMap = [
[ 15, 135, 45, 165 ],
@@ -43,7 +56,6 @@ function dithering(ctx, width, height, threshold, type) {
newPixel = imageData.data[currentPixel] < threshold ? 0 : 255;
err = Math.floor((imageData.data[currentPixel] - newPixel) / 16);
imageData.data[currentPixel] = newPixel;
imageData.data[currentPixel + 4 ] += err*7;
imageData.data[currentPixel + 4*w - 4 ] += err*3;
imageData.data[currentPixel + 4*w ] += err*5;
@@ -52,7 +64,6 @@ function dithering(ctx, width, height, threshold, type) {
newPixel = imageData.data[currentPixel] < threshold ? 0 : 255;
err = Math.floor((imageData.data[currentPixel] - newPixel) / 8);
imageData.data[currentPixel] = newPixel;
imageData.data[currentPixel + 4 ] += err;
imageData.data[currentPixel + 8 ] += err;
imageData.data[currentPixel + 4*w - 4 ] += err;
@@ -68,7 +79,6 @@ function dithering(ctx, width, height, threshold, type) {
ctx.putImageData(imageData, 0, 0);
}
// white: 1, black/red: 0
function canvas2bytes(canvas, step = 'bw', invert = false) {
const ctx = canvas.getContext("2d");
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
@@ -79,25 +89,37 @@ function canvas2bytes(canvas, step = 'bw', invert = false) {
for (let y = 0; y < canvas.height; y++) {
for (let x = 0; x < canvas.width; x++) {
const i = (canvas.width * y + x) * 4;
if (step === 'bw') {
buffer.push(imageData.data[i] === 0 && imageData.data[i+1] === 0 && imageData.data[i+2] === 0 ? 0 : 1);
} else {
buffer.push(imageData.data[i] > 0 && imageData.data[i+1] === 0 && imageData.data[i+2] === 0 ? 0 : 1);
}
if (buffer.length === 8) {
const data = parseInt(buffer.join(''), 2);
arr.push(invert ? ~data : data);
buffer = [];
const r = imageData.data[i];
const g = imageData.data[i + 1];
const b = imageData.data[i + 2];
if (step === 'bwry') { // black: 0, white: 1, yellow: 2, red: 3
buffer.push(getNearColorIdx([r, g, b, 255], bwryPalette));
if (buffer.length === 4) {
const byte = (buffer[0] << 6) | (buffer[1] << 4) | (buffer[2] << 2) | buffer[3];
arr.push(invert ? ~byte & 0xFF : byte);
buffer = [];
}
} else { // white: 1, black/red: 0
if (step === 'bw') {
buffer.push(r === 0 && g === 0 && b === 0 ? 0 : 1);
} else if (step === 'red') {
buffer.push(r > 0 && g === 0 && b === 0 ? 0 : 1);
}
if (buffer.length === 8) {
const data = parseInt(buffer.join(''), 2);
arr.push(invert ? ~data : data);
buffer = [];
}
}
}
}
return arr;
}
function getNearColorV2(color, palette) {
function getNearColorIdx(color, palette) {
let minDistanceSquared = 255*255 + 255*255 + 255*255 + 1;
let bestIndex = 0;
for (let i = 0; i < palette.length; i++) {
let rdiff = (color[0] & 0xff) - (palette[i][0] & 0xff);
@@ -109,7 +131,7 @@ function getNearColorV2(color, palette) {
bestIndex = i;
}
}
return palette[bestIndex];
return bestIndex;
}
function updatePixel(imageData, index, color) {
@@ -133,27 +155,32 @@ function updatePixelErr(imageData, index, err, rate) {
imageData[index+2] += err[2] * rate;
}
function ditheringCanvasByPalette(canvas, palette, type) {
palette = palette || bwrPalette;
// Dithering by palette
function ditheringByPalette(canvas, type) {
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const w = imageData.width;
let palette = bwPalette;
if (type.startsWith("bwry")) {
palette = bwryPalette;
} else if (type.startsWith("bwr")) {
palette = bwrPalette;
}
for (let currentPixel = 0; currentPixel <= imageData.data.length; currentPixel+=4) {
const newColor = getNearColorV2(imageData.data.slice(currentPixel, currentPixel+4), palette);
if (type === "bwr_floydsteinberg") {
const err = getColorErr(imageData.data.slice(currentPixel, currentPixel+4), newColor, 16);
const color = imageData.data.slice(currentPixel, currentPixel+4);
const newColor = palette[getNearColorIdx(color, palette)];
if (type.endsWith("floydsteinberg")) {
const err = getColorErr(color, newColor, 16);
updatePixel(imageData.data, currentPixel, newColor);
updatePixelErr(imageData.data, currentPixel +4, err, 7);
updatePixelErr(imageData.data, currentPixel + 4*w - 4, err, 3);
updatePixelErr(imageData.data, currentPixel + 4*w, err, 5);
updatePixelErr(imageData.data, currentPixel + 4*w + 4, err, 1);
} else {
const err = getColorErr(imageData.data.slice(currentPixel, currentPixel+4), newColor, 8);
const err = getColorErr(color, newColor, 8);
updatePixel(imageData.data, currentPixel, newColor);
updatePixelErr(imageData.data, currentPixel +4, err, 1);
updatePixelErr(imageData.data, currentPixel +8, err, 1);

View File

@@ -107,7 +107,7 @@ async function epdWriteImage(step = 'bw') {
for (let i = 0; i < data.length; i += chunkSize) {
let currentTime = (new Date().getTime() - startTime) / 1000.0;
setStatus(`${step == 'bw' ? '黑白' : '色'}块: ${chunkIdx+1}/${count+1}, 总用时: ${currentTime}s`);
setStatus(`${step == 'bw' ? '黑白' : '色'}块: ${chunkIdx+1}/${count+1}, 总用时: ${currentTime}s`);
const payload = [
(step == 'bw' ? 0x0F : 0x00) | ( i == 0 ? 0x00 : 0xF0),
...data.slice(i, i + chunkSize),
@@ -169,15 +169,22 @@ async function sendimg() {
updateButtonStatus(true);
if (appVersion < 0x16) {
if (mode.startsWith('bwr')) {
if (mode.startsWith('bwry')) {
addLog("当前固件版本不支持四色屏幕。");
return;
} if (mode.startsWith('bwr')) {
await epdWrite(driver === "02" ? 0x24 : 0x10, canvas2bytes(canvas, 'bw'));
await epdWrite(driver === "02" ? 0x26 : 0x13, canvas2bytes(canvas, 'red', driver === '02'));
} else {
await epdWrite(driver === "04" ? 0x24 : 0x13, canvas2bytes(canvas, 'bw'));
}
} else {
await epdWriteImage('bw');
if (mode.startsWith('bwr')) await epdWriteImage('red');
if (mode.startsWith('bwry')) {
await epdWriteImage('bwry');
} else {
await epdWriteImage('bw');
if (mode.startsWith('bwr')) await epdWriteImage('red');
}
}
await write(EpdCmd.REFRESH);
@@ -381,7 +388,7 @@ function convertDithering() {
if (mode === '') return;
if (mode.startsWith('bwr')) {
ditheringCanvasByPalette(canvas, bwrPalette, mode);
ditheringByPalette(canvas, mode);
} else {
const threshold = document.getElementById('threshold').value;
dithering(ctx, canvas.width, canvas.height, parseInt(threshold), mode);

View File

@@ -121,6 +121,11 @@ function updateBrushOptions() {
option.removeAttribute('disabled');
else
option.setAttribute('disabled', 'disabled');
} else if (option.value === '#FFFF00') {
if (dithering.startsWith('bwry'))
option.removeAttribute('disabled');
else
option.setAttribute('disabled', 'disabled');
}
}
// Revert brush color to black if red is not allowed