698 lines
25 KiB
HTML
698 lines
25 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>Dusk Tools / Palette Indexer</title>
|
|
|
|
<style type="text/css">
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-size: 16px;
|
|
}
|
|
|
|
canvas {
|
|
image-rendering: pixelated;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<h1>Dusk Palette Indexer</h1>
|
|
<p>
|
|
This tool takes an input image and creates a palettized version of it.
|
|
You will get two files, a .dpf (Dusk Palette File) and a .dpt
|
|
(Dusk Palettized Texture). The first being the palette, which I recommend
|
|
reusing across multiple images, and the second being the indexed image,
|
|
which uses the colors from the palette.
|
|
</p>
|
|
<p>
|
|
Also, dusk previously supported Alpha textures, but due to so many little
|
|
platform differences I realized it is simpler and about as much work to
|
|
get palettized textures working instead. As a result I suggest creating a
|
|
single palette for your alpha textures, and reusing that globally.
|
|
</p>
|
|
|
|
<div>
|
|
<h2>Input Image</h2>
|
|
<div>
|
|
<input type="file" data-file-input />
|
|
</div>
|
|
<p data-file-error style="color:red;display:none;"></p>
|
|
<canvas data-input-preview style="border:1px solid black;"></canvas>
|
|
</div>
|
|
|
|
<div>
|
|
<h2>Palette</h2>
|
|
<div>
|
|
<button data-palette-add>Add Color</button>
|
|
<button data-palette-append>Append Palette</button>
|
|
<button data-palette-clear>Clear Palette</button>
|
|
<button data-palette-optimize>Optimize Palette</button>
|
|
</div>
|
|
|
|
<div data-palette-entries style="display:grid;grid-template-columns:repeat(auto-fill, minmax(400px, 1fr));gap:16px;margin-top:16px;"></div>
|
|
</div>
|
|
|
|
<div>
|
|
<label>
|
|
Preview Background:
|
|
<button data-page-bg-white>White</button>
|
|
<button data-page-bg-transparent>Black</button>
|
|
<button data-page-bg-checkerboard>Checkerboard</button>
|
|
<button data-page-bg-magenta>Magenta</button>
|
|
<button data-page-bg-blue>Blue</button>
|
|
<button data-page-bg-green>Green</button>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<h2>Palette Preview</h2>
|
|
|
|
<h3>Palette</h3>
|
|
<div>
|
|
<button data-palette-download>Download Palette</button>
|
|
</div>
|
|
<canvas data-palette-preview style="border:1px solid black;"></canvas>
|
|
<p data-palette-information></p>
|
|
|
|
<h3>Indexed Image</h3>
|
|
<div>
|
|
<button data-indexed-download>Download Indexed Image</button>
|
|
</div>
|
|
<div data-output-error style="color:red;display:none;"></div>
|
|
<div>
|
|
<label>
|
|
Preview Scale:
|
|
<input type="number" value="2" data-indexed-preview-scale min="1" step="1" />
|
|
</label>
|
|
</div>
|
|
<canvas data-output-preview style="border:1px solid black;"></canvas>
|
|
</div>
|
|
</body>
|
|
|
|
<script type="text/javascript">
|
|
const elError = document.querySelector('[data-file-error]');
|
|
const elFile = document.querySelector('[data-file-input]');
|
|
const elInputPreview = document.querySelector('[data-input-preview]');
|
|
const elPalettePreview = document.querySelector('[data-palette-preview]');
|
|
const elOutputPreview = document.querySelector('[data-output-preview]');
|
|
const elPaletteInformation = document.querySelector('[data-palette-information]');
|
|
const elSourceFromInput = document.querySelector('[data-source-from-input]');
|
|
const elPaletteAdd = document.querySelector('[data-palette-add]');
|
|
const elPaletteEntries = document.querySelector('[data-palette-entries]');
|
|
const elPreviewScale = document.querySelector('[data-indexed-preview-scale]');
|
|
const elClearPalette = document.querySelector('[data-palette-clear]');
|
|
const elAppendPalette = document.querySelector('[data-palette-append]');
|
|
const elOutputError = document.querySelector('[data-output-error]');
|
|
const elOptimizePalette = document.querySelector('[data-palette-optimize]');
|
|
const btnBackgroundWhite = document.querySelector('[data-page-bg-white]');
|
|
const btnBackgroundTransparent = document.querySelector('[data-page-bg-transparent]');
|
|
const btnBackgroundCheckerboard = document.querySelector('[data-page-bg-checkerboard]');
|
|
const btnBackgroundMagenta = document.querySelector('[data-page-bg-magenta]');
|
|
const btnBackgroundBlue = document.querySelector('[data-page-bg-blue]');
|
|
const btnBackgroundGreen = document.querySelector('[data-page-bg-green]');
|
|
const btnDownloadPalette = document.querySelector('[data-palette-download]');
|
|
const btnDownloadImage = document.querySelector('[data-indexed-download]');
|
|
|
|
let imageWidth = 0;
|
|
let imageHeight = 0;
|
|
let palette = [];// Array of [ r, g, b, a ]
|
|
let indexedImage = [ ];// Array of indexes
|
|
|
|
const nextPowerOfTwo = (x) => {
|
|
return Math.pow(2, Math.ceil(Math.log2(x)));
|
|
}
|
|
|
|
const sourcePaletteFromImage = (image) => {
|
|
// Get unique colors
|
|
const elCanvas = document.createElement('canvas');
|
|
elCanvas.width = image.width;
|
|
elCanvas.height = image.height;
|
|
const ctx = elCanvas.getContext('2d');
|
|
ctx.drawImage(image, 0, 0);
|
|
const imageData = ctx.getImageData(0, 0, image.width, image.height);
|
|
const data = imageData.data;
|
|
|
|
const palette = [];
|
|
for(let i = 0; i < data.length; i += 4) {
|
|
const color = [ data[i], data[i + 1], data[i + 2], data[i + 3] ];
|
|
if(!palette.some(c => c[0] === color[0] && c[1] === color[1] && c[2] === color[2] && c[3] === color[3])) {
|
|
palette.push(color);
|
|
}
|
|
}
|
|
return palette;
|
|
}
|
|
|
|
const updateOutput = () => {
|
|
elOutputError.style.display = 'none';
|
|
|
|
// Update palette preview
|
|
const paletteScale = 8;
|
|
const uniquePalette = palette.filter((color, index) => {
|
|
if(color[3] === 0) {
|
|
// Find any other fully transparent pixel.
|
|
return index === palette.findIndex(c => c[3] === 0);
|
|
}
|
|
return index === palette.findIndex(c => c[0] === color[0] && c[1] === color[1] && c[2] === color[2] && c[3] === color[3]);
|
|
});
|
|
elPalettePreview.width = uniquePalette.length * paletteScale;
|
|
elPalettePreview.height = paletteScale;
|
|
const paletteCtx = elPalettePreview.getContext('2d');
|
|
uniquePalette.forEach((color, index) => {
|
|
paletteCtx.fillStyle = `rgba(${color[0]}, ${color[1]}, ${color[2]}, ${color[3] / 255})`;
|
|
paletteCtx.fillRect(index * paletteScale, 0, paletteScale, paletteScale);
|
|
});
|
|
elPaletteInformation.textContent = `Palette contains ${uniquePalette.length} colors (${palette.length - uniquePalette.length} duplicates).`;
|
|
|
|
// Update palette entries
|
|
elPaletteEntries.innerHTML = '';
|
|
palette.forEach((color, index) => {
|
|
const entry = document.createElement('div');
|
|
|
|
const epar = document.createElement('span');
|
|
epar.textContent = `Index ${index}`;
|
|
|
|
const ecolor = document.createElement('span');
|
|
ecolor.style.display = 'inline-block';
|
|
ecolor.style.width = '16px';
|
|
ecolor.style.height = '16px';
|
|
ecolor.style.marginLeft = '8px';
|
|
ecolor.style.backgroundColor = `rgba(${color[0]}, ${color[1]}, ${color[2]}, ${color[3] / 255})`;
|
|
ecolor.style.border = '1px solid black';
|
|
|
|
const header = document.createElement('div');
|
|
header.appendChild(epar);
|
|
header.appendChild(ecolor);
|
|
|
|
const inputR = document.createElement('input');
|
|
inputR.type = 'number';
|
|
inputR.value = color[0];
|
|
inputR.min = 0;
|
|
inputR.max = 255;
|
|
inputR.setAttribute('data-palette-index', index);
|
|
inputR.setAttribute('data-palette-channel', '0');
|
|
inputR.addEventListener('change', (event) => {
|
|
const idx = parseInt(event.target.getAttribute('data-palette-index'));
|
|
const channel = parseInt(event.target.getAttribute('data-palette-channel'));
|
|
const value = parseInt(event.target.value);
|
|
if(isNaN(value) || value < 0 || value > 255) {
|
|
event.target.value = palette[idx][channel];
|
|
return;
|
|
}
|
|
palette[idx][channel] = value;
|
|
updateOutput();
|
|
});
|
|
|
|
const inputG = document.createElement('input');
|
|
inputG.type = 'number';
|
|
inputG.value = color[1];
|
|
inputG.min = 0;
|
|
inputG.max = 255;
|
|
inputG.setAttribute('data-palette-index', index);
|
|
inputG.setAttribute('data-palette-channel', '1');
|
|
inputG.addEventListener('change', (event) => {
|
|
const idx = parseInt(event.target.getAttribute('data-palette-index'));
|
|
const channel = parseInt(event.target.getAttribute('data-palette-channel'));
|
|
const value = parseInt(event.target.value);
|
|
if(isNaN(value) || value < 0 || value > 255) {
|
|
event.target.value = palette[idx][channel];
|
|
return;
|
|
}
|
|
palette[idx][channel] = value;
|
|
updateOutput();
|
|
});
|
|
|
|
const inputB = document.createElement('input');
|
|
inputB.type = 'number';
|
|
inputB.value = color[2];
|
|
inputB.min = 0;
|
|
inputB.max = 255;
|
|
inputB.setAttribute('data-palette-index', index);
|
|
inputB.setAttribute('data-palette-channel', '2');
|
|
inputB.addEventListener('change', (event) => {
|
|
const idx = parseInt(event.target.getAttribute('data-palette-index'));
|
|
const channel = parseInt(event.target.getAttribute('data-palette-channel'));
|
|
const value = parseInt(event.target.value);
|
|
if(isNaN(value) || value < 0 || value > 255) {
|
|
event.target.value = palette[idx][channel];
|
|
return;
|
|
}
|
|
palette[idx][channel] = value;
|
|
updateOutput();
|
|
});
|
|
|
|
const inputA = document.createElement('input');
|
|
inputA.type = 'number';
|
|
inputA.value = color[3];
|
|
inputA.min = 0;
|
|
inputA.max = 255;
|
|
inputA.setAttribute('data-palette-index', index);
|
|
inputA.setAttribute('data-palette-channel', '3');
|
|
inputA.addEventListener('change', (event) => {
|
|
const idx = parseInt(event.target.getAttribute('data-palette-index'));
|
|
const channel = parseInt(event.target.getAttribute('data-palette-channel'));
|
|
const value = parseInt(event.target.value);
|
|
if(isNaN(value) || value < 0 || value > 255) {
|
|
event.target.value = palette[idx][channel];
|
|
return;
|
|
}
|
|
palette[idx][channel] = value;
|
|
updateOutput();
|
|
});
|
|
|
|
const colorArea = document.createElement('div');
|
|
colorArea.style.display = 'inline-block';
|
|
colorArea.appendChild(inputR);
|
|
colorArea.appendChild(inputG);
|
|
colorArea.appendChild(inputB);
|
|
colorArea.appendChild(inputA);
|
|
|
|
const buttonArea = document.createElement('div');
|
|
buttonArea.style.display = 'inline-block';
|
|
const btnRemove = document.createElement('button');
|
|
btnRemove.textContent = '-';
|
|
btnRemove.setAttribute('data-palette-index', index);
|
|
btnRemove.addEventListener('click', () => {
|
|
palette.splice(index, 1);
|
|
updateOutput();
|
|
});
|
|
buttonArea.appendChild(btnRemove);
|
|
|
|
const btnUp = document.createElement('button');
|
|
btnUp.textContent = '↑';
|
|
btnUp.setAttribute('data-palette-index', index);
|
|
btnUp.addEventListener('click', () => {
|
|
if(index === 0) return;
|
|
const temp = palette[index - 1];
|
|
palette[index - 1] = palette[index];
|
|
palette[index] = temp;
|
|
updateOutput();
|
|
});
|
|
buttonArea.appendChild(btnUp);
|
|
|
|
const btnDown = document.createElement('button');
|
|
btnDown.textContent = '↓';
|
|
btnDown.setAttribute('data-palette-index', index);
|
|
btnDown.addEventListener('click', () => {
|
|
if(index === palette.length - 1) return;
|
|
const temp = palette[index + 1];
|
|
palette[index + 1] = palette[index];
|
|
palette[index] = temp;
|
|
updateOutput();
|
|
});
|
|
buttonArea.appendChild(btnDown);
|
|
|
|
const footer = document.createElement('div');
|
|
footer.appendChild(colorArea);
|
|
footer.appendChild(buttonArea);
|
|
|
|
entry.appendChild(header);
|
|
entry.appendChild(footer);
|
|
elPaletteEntries.appendChild(entry);
|
|
});
|
|
|
|
// Image ready?
|
|
if(!imageWidth || !imageHeight) return;
|
|
|
|
// Update indexed image preview
|
|
elOutputPreview.width = imageWidth;
|
|
elOutputPreview.height = imageHeight;
|
|
const outputCtx = elOutputPreview.getContext('2d');
|
|
const outputImageData = outputCtx.createImageData(imageWidth, imageHeight);
|
|
const outputData = outputImageData.data;
|
|
indexedImage.forEach((index, i) => {
|
|
let color;
|
|
if(palette.length <= index) {
|
|
elOutputError.textContent = `Indexed image references non-existent palette index ${index}.`;
|
|
elOutputError.style.display = 'block';
|
|
color = [0, 0, 0, 0];
|
|
} else {
|
|
color = palette[index];
|
|
}
|
|
outputData[i * 4] = color[0];
|
|
outputData[i * 4 + 1] = color[1];
|
|
outputData[i * 4 + 2] = color[2];
|
|
outputData[i * 4 + 3] = color[3];
|
|
});
|
|
outputCtx.putImageData(outputImageData, 0, 0);
|
|
|
|
if(elOutputError.style.display === 'none') {;
|
|
const unusedPaletteIndexes = palette.map((color, index) => {
|
|
return indexedImage.includes(index) ? null : index;
|
|
}).filter(Boolean);
|
|
if(unusedPaletteIndexes.length > 0) {
|
|
elOutputError.textContent = `Image does not use palette colors; ${unusedPaletteIndexes.join(', ')}.`;
|
|
elOutputError.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
// Rescale canvas
|
|
const scale = parseInt(elPreviewScale.value) || 1;
|
|
elOutputPreview.style.width = `${imageWidth * scale}px`;
|
|
elOutputPreview.style.height = `${imageHeight * scale}px`;
|
|
}
|
|
|
|
const onImage = img => {
|
|
if(!img) return onBadFile('Failed to load image');
|
|
|
|
elError.style.display = 'none';
|
|
imageWidth = img.width;
|
|
imageHeight = img.height;
|
|
indexedImage = [];
|
|
|
|
// Update input preview
|
|
elInputPreview.width = imageWidth;
|
|
elInputPreview.height = imageHeight;
|
|
const ctx = elInputPreview.getContext('2d');
|
|
ctx.drawImage(img, 0, 0);
|
|
|
|
// Source palette from input image
|
|
palette = sourcePaletteFromImage(img);
|
|
|
|
// Index the image
|
|
const imageData = ctx.getImageData(0, 0, imageWidth, imageHeight);
|
|
const data = imageData.data;
|
|
for(let i = 0; i < data.length; i += 4) {
|
|
const color = [ data[i], data[i + 1], data[i + 2], data[i + 3] ];
|
|
const colorIndex = palette.findIndex(c => c[0] === color[0] && c[1] === color[1] && c[2] === color[2] && c[3] === color[3]);
|
|
indexedImage.push(colorIndex);
|
|
}
|
|
|
|
updateOutput();
|
|
}
|
|
|
|
const onPalettizedImage = dataView => {
|
|
if(!dataView) return onBadFile('Failed to load image');
|
|
|
|
// Ensure DPT and ver is 1
|
|
if(dataView.getUint8(0) !== 0x44 || dataView.getUint8(1) !== 0x50 || dataView.getUint8(2) !== 0x54 || dataView.getUint8(3) !== 0x01) {
|
|
return onBadFile('Invalid DPT file. Expected header "DPT" and version 1.');
|
|
}
|
|
|
|
// Width and Height
|
|
elError.style.display = 'none';
|
|
imageWidth = dataView.getUint32(3);
|
|
imageHeight = dataView.getUint32(7);
|
|
|
|
if(imageWidth <= 0 || imageHeight <= 0) {
|
|
return onBadFile('Invalid image dimensions in DPT file.');
|
|
}
|
|
|
|
if(dataView.byteLength < 11 + imageWidth * imageHeight) {
|
|
return onBadFile('DPT file does not contain enough data for the specified dimensions.');
|
|
}
|
|
|
|
const uniqueIndexes = []
|
|
|
|
// Image data
|
|
indexedImage = [];
|
|
for(let i = 11; i < dataView.byteLength; i++) {
|
|
const index = dataView.getUint8(i);
|
|
if(!uniqueIndexes.includes(index)) uniqueIndexes.push(index);
|
|
indexedImage.push(index);
|
|
}
|
|
|
|
const adhocPalette = [];
|
|
for(let i = 0; i < uniqueIndexes.length; i++) {
|
|
const index = uniqueIndexes[i];
|
|
// Get the most different possible color for this index
|
|
const color = [
|
|
(index * 37) % 256,
|
|
(index * 61) % 256,
|
|
(index * 97) % 256,
|
|
255
|
|
];
|
|
adhocPalette[index] = color;
|
|
}
|
|
|
|
elInputPreview.width = imageWidth;
|
|
elInputPreview.height = imageHeight;
|
|
const ctx = elInputPreview.getContext('2d');
|
|
const imageData = ctx.createImageData(imageWidth, imageHeight);
|
|
const data = imageData.data;
|
|
indexedImage.forEach((index, i) => {
|
|
const color = adhocPalette[index] || [0, 0, 0, 0];
|
|
data[i * 4] = color[0];
|
|
data[i * 4 + 1] = color[1];
|
|
data[i * 4 + 2] = color[2];
|
|
data[i * 4 + 3] = color[3];
|
|
});
|
|
ctx.putImageData(imageData, 0, 0);
|
|
|
|
updateOutput();
|
|
}
|
|
|
|
const onBadFile = err => {
|
|
elError.textContent = err;
|
|
elError.style.display = 'block';
|
|
updateOutput();
|
|
}
|
|
|
|
const onFile = file => {
|
|
// Reset preview
|
|
elInputPreview.width = 0;
|
|
elInputPreview.height = 0;
|
|
image = null;
|
|
|
|
if(!file) {
|
|
onImage(null);
|
|
return;
|
|
}
|
|
|
|
// If file is image, load as image.
|
|
if(file.type.startsWith('image/')) {
|
|
elError.style.display = 'none';
|
|
const reader = new FileReader();
|
|
reader.onload = (event) => {
|
|
const img = new Image();
|
|
img.onload = () => onImage(img);
|
|
img.onerror = () => onImage(null);
|
|
img.src = event.target.result;
|
|
};
|
|
reader.onerror = () => onImage(null);
|
|
reader.readAsDataURL(file);
|
|
return;
|
|
|
|
} else if(file.name.endsWith('.dpt')) {
|
|
elError.style.display = 'none';
|
|
const reader = new FileReader();
|
|
reader.onload = (event) => {
|
|
const arrayBuffer = event.target.result;
|
|
const dataView = new DataView(arrayBuffer);
|
|
onPalettizedImage(dataView);
|
|
};
|
|
reader.onerror = () => onPalettizedImage(null);
|
|
reader.readAsArrayBuffer(file);
|
|
return;
|
|
}
|
|
|
|
onBadFile('Selected file is not a supported image type.');
|
|
return;
|
|
}
|
|
|
|
// Listeners
|
|
btnBackgroundWhite.addEventListener('click', () => {
|
|
document.body.style.background = 'white';
|
|
});
|
|
btnBackgroundTransparent.addEventListener('click', () => {
|
|
document.body.style.background = 'black';
|
|
});
|
|
btnBackgroundCheckerboard.addEventListener('click', () => {
|
|
document.body.style.background = 'repeating-conic-gradient(#ccc 0% 25%, #eee 0% 50%) 50% / 20px 20px';
|
|
});
|
|
btnBackgroundMagenta.addEventListener('click', () => {
|
|
document.body.style.background = 'magenta';
|
|
});
|
|
btnBackgroundBlue.addEventListener('click', () => {
|
|
document.body.style.background = 'blue';
|
|
});
|
|
btnBackgroundGreen.addEventListener('click', () => {
|
|
document.body.style.background = 'green';
|
|
});
|
|
|
|
elFile.addEventListener('change', (event) => {
|
|
onFile(event?.target?.files[0]);
|
|
});
|
|
elPaletteAdd.addEventListener('click', () => {
|
|
// Add new random color to palette
|
|
const newColor = [ Math.floor(Math.random() * 256), Math.floor(Math.random() * 256), Math.floor(Math.random() * 256), 255 ];
|
|
palette.push(newColor);
|
|
updateOutput();
|
|
});
|
|
elPreviewScale.addEventListener('change', () => {
|
|
updateOutput();
|
|
});
|
|
elClearPalette.addEventListener('click', () => {
|
|
palette = [];
|
|
updateOutput();
|
|
});
|
|
|
|
elOptimizePalette.addEventListener('click', () => {
|
|
// Remove unused colors and duplicates from the palette. Treat A=0 as equal
|
|
const newPalette = [];
|
|
const indexMap = {};
|
|
palette.forEach((color, index) => {
|
|
// Check if color is already in new palette
|
|
const existingIndex = newPalette.findIndex(c => {
|
|
if(c[3] === 0 && color[3] === 0) return true;
|
|
return c[0] === color[0] && c[1] === color[1] && c[2] === color[2] && c[3] === color[3];
|
|
});
|
|
if(existingIndex !== -1) {
|
|
indexMap[index] = existingIndex;
|
|
} else {
|
|
indexMap[index] = newPalette.length;
|
|
newPalette.push(color);
|
|
}
|
|
});
|
|
|
|
palette = newPalette;
|
|
|
|
updateOutput();
|
|
});
|
|
|
|
elAppendPalette.addEventListener('click', () => {
|
|
// Open file picker
|
|
const input = document.createElement('input');
|
|
input.type = 'file';
|
|
input.addEventListener('change', (event) => {
|
|
const file = event.target.files[0];
|
|
if(!file) return;
|
|
|
|
// Accept either images or .dpf files
|
|
if(file.type.startsWith('image/')) {
|
|
const reader = new FileReader();
|
|
reader.onload = (event) => {
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
const newColors = sourcePaletteFromImage(img);
|
|
palette = palette.concat(newColors);
|
|
updateOutput();
|
|
};
|
|
img.onerror = () => {
|
|
alert('Failed to load image for palette appending.');
|
|
};
|
|
img.src = event.target.result;
|
|
};
|
|
reader.onerror = () => {
|
|
alert('Failed to load image for palette appending.');
|
|
};
|
|
reader.readAsDataURL(file);
|
|
return;
|
|
} else if(file.name.endsWith('.dpf')) {
|
|
const reader = new FileReader();
|
|
reader.onload = (event) => {
|
|
const arrayBuffer = event.target.result;
|
|
const dataView = new DataView(arrayBuffer);
|
|
|
|
// Validate header
|
|
if(dataView.getUint8(0) !== 0x44 || dataView.getUint8(1) !== 0x50 || dataView.getUint8(2) !== 0x46) {
|
|
alert('Invalid DPF file.');
|
|
return;
|
|
}
|
|
|
|
const colorCount = dataView.getUint32(4);
|
|
const expectedLength = 4 * colorCount + 8;// 3 DPF, 1 ver, 4 color count
|
|
if(arrayBuffer.byteLength !== expectedLength) {
|
|
alert('DPF file size does not match expected color count.');
|
|
return;
|
|
}
|
|
|
|
let offset = 8;
|
|
for(let i = 0; i < colorCount; i++) {
|
|
const r = dataView.getUint8(offset);
|
|
const g = dataView.getUint8(offset + 1);
|
|
const b = dataView.getUint8(offset + 2);
|
|
const a = dataView.getUint8(offset + 3);
|
|
palette.push([r, g, b, a]);
|
|
offset += 4;
|
|
}
|
|
|
|
updateOutput();
|
|
};
|
|
reader.onerror = () => {
|
|
alert('Failed to load DPF file for palette appending.');
|
|
};
|
|
reader.readAsArrayBuffer(file);
|
|
return;
|
|
} else {
|
|
alert('Unsupported file type for palette appending. Please use an image file.');
|
|
}
|
|
});
|
|
|
|
input.click();
|
|
});
|
|
|
|
btnDownloadPalette.addEventListener('click', () => {
|
|
// Create Dusk Palette File (.dpf)
|
|
const header = new Uint8Array([0x44, 0x50, 0x46, 0x01]); // 'DPF' + version 1 + number of colors (4 bytes)
|
|
|
|
const colorCount = palette.length;
|
|
const colorCountBytes = new Uint8Array(4);
|
|
colorCountBytes[0] = (colorCount >> 24) & 0xFF;
|
|
colorCountBytes[1] = (colorCount >> 16) & 0xFF;
|
|
colorCountBytes[2] = (colorCount >> 8) & 0xFF;
|
|
colorCountBytes[3] = colorCount & 0xFF;
|
|
|
|
// add color data (palette.length * 4 bytes)
|
|
const colorData = new Uint8Array(palette.length * 4);
|
|
palette.forEach((color, index) => {
|
|
colorData[index * 4] = color[0];
|
|
colorData[index * 4 + 1] = color[1];
|
|
colorData[index * 4 + 2] = color[2];
|
|
colorData[index * 4 + 3] = color[3];
|
|
});
|
|
|
|
const blob = new Blob([header, colorCountBytes, colorData], { type: 'application/octet-stream' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'palette.dpf';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
});
|
|
|
|
btnDownloadImage.addEventListener('click', () => {
|
|
const header = new Uint8Array([0x44, 0x50, 0x54, 0x01]); // 'DPT' + version 1
|
|
|
|
// Width
|
|
const widthBytes = new Uint8Array(4);
|
|
widthBytes[0] = (imageWidth >> 24) & 0xFF;
|
|
widthBytes[1] = (imageWidth >> 16) & 0xFF;
|
|
widthBytes[2] = (imageWidth >> 8) & 0xFF;
|
|
widthBytes[3] = imageWidth & 0xFF;
|
|
|
|
// Height
|
|
const heightBytes = new Uint8Array(4);
|
|
heightBytes[0] = (imageHeight >> 24) & 0xFF;
|
|
heightBytes[1] = (imageHeight >> 16) & 0xFF;
|
|
heightBytes[2] = (imageHeight >> 8) & 0xFF;
|
|
heightBytes[3] = imageHeight & 0xFF;
|
|
|
|
// add indexed image data (imageWidth * imageHeight bytes)
|
|
const imageData = new Uint8Array(indexedImage.length);
|
|
indexedImage.forEach((index, i) => {
|
|
imageData[i] = index;
|
|
});
|
|
|
|
const blob = new Blob([header, widthBytes, heightBytes, imageData], { type: 'application/octet-stream' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'indexed_image.dpt';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
});
|
|
|
|
updateOutput();
|
|
btnBackgroundCheckerboard.click();
|
|
</script>
|
|
</html> |