feat(photo-editor): Ajax Image Editor client (Photoshop-parity tools)

Photoshop-parity editor client: layers, selection/masking, brushes,
smart filters, LUT import, Quick Edit, mobile gestures, workspaces,
batch operations, and the service-worker versioning (v978->v1006).

Source: vendor/photocraft (ad1cff44) photo-editor client files, merged
onto upstream/dev (a8c147b2). Conflict resolution documented in
pr_work/A_conflicts.md; editor files taken from vendor/photocraft
(upstream's editor is an earlier snapshot of the same feature).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
pewdiepie-archdaemon
2026-10-15 11:57:35 +00:00
co-authored by Claude Opus 5.5
parent c74f475679
commit 5fac693c1c
290 changed files with 34358 additions and 766 deletions
+68 -2
View File
@@ -31,12 +31,24 @@ import {
initialPlacedMatrix,
renderPlacedLayer,
} from './placed-layer.js';
import { parsePsd, decodeLayeredPsd } from './psd.js';
const PSD_TO_EDITOR_BLEND = {
norm: 'source-over', mul: 'multiply', scrn: 'screen', over: 'overlay',
dark: 'darken', lite: 'lighten', div: 'color-dodge', idiv: 'color-burn',
hLit: 'hard-light', sLit: 'soft-light', diff: 'difference', smud: 'exclusion',
hue: 'hue', sat: 'saturation', colr: 'color', lum: 'luminosity',
};
function editorBlendForPsd(key) {
return PSD_TO_EDITOR_BLEND[(key || '').trim()] || null;
}
export function wireImport({ container, saveState, createLayer, composite, renderLayerPanel, uiModule }) {
// Hidden <input type="file"> the topbar + File buttons both click.
const importFileInput = document.createElement('input');
importFileInput.type = 'file';
importFileInput.accept = 'image/*';
importFileInput.accept = 'image/*,.psd,.psb';
importFileInput.style.display = 'none';
container.appendChild(importFileInput);
@@ -82,16 +94,70 @@ export function wireImport({ container, saveState, createLayer, composite, rende
if (uiModule) uiModule.showToast('Image imported — drag to position');
}
const PSD_BLEND = {
norm: 'source-over', 'mul ': 'multiply', scrn: 'screen', over: 'overlay',
dark: 'darken', lite: 'lighten', 'div ': 'color-dodge', idiv: 'color-burn',
hLit: 'hard-light', sLit: 'soft-light', diff: 'difference', smud: 'exclusion',
'hue ': 'hue', 'sat ': 'saturation', colr: 'color', 'lum ': 'luminosity',
};
function handleImportedLayers(decoded, sourceName = 'Imported PSD') {
if (!state.editorOpen) return;
saveState('Import PSD layers');
const created = [];
for (const layer of decoded.layers) {
if (!layer.width || !layer.height) continue;
const canvas = document.createElement('canvas');
canvas.width = layer.width;
canvas.height = layer.height;
const ctx = canvas.getContext('2d');
const image = ctx.createImageData(layer.width, layer.height);
image.data.set(layer.data);
ctx.putImageData(image, 0, 0);
const createdLayer = createLayer(layer.name || 'Layer', layer.width, layer.height);
createdLayer.canvas.width = layer.width;
createdLayer.canvas.height = layer.height;
createdLayer.ctx = createdLayer.canvas.getContext('2d');
createdLayer.ctx.clearRect(0, 0, layer.width, layer.height);
createdLayer.ctx.drawImage(canvas, 0, 0);
createdLayer.kind = 'raster';
if (Number.isFinite(layer.opacity)) createdLayer.opacity = layer.opacity;
if (layer.visible === false) createdLayer.visible = false;
if (PSD_BLEND[layer.blendKey]) createdLayer.blendMode = PSD_BLEND[layer.blendKey];
state.layerOffsets.set(createdLayer.id, { x: layer.x || 0, y: layer.y || 0 });
state.layers.push(createdLayer);
created.push(createdLayer);
}
if (created.length) state.activeLayerId = created[created.length - 1].id;
const importSec = document.getElementById('ge-import-section');
if (importSec) importSec.style.display = 'none';
composite();
renderLayerPanel();
if (uiModule) uiModule.showToast(`Imported ${created.length} PSD layer${created.length === 1 ? '' : 's'} from ${sourceName}`);
}
importFileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
const isPsd = /\.(psd|psb)$/i.test(file.name || '');
const reader = new FileReader();
reader.onload = (ev) => {
if (isPsd) {
try {
const decoded = decodeLayeredPsd(ev.target.result);
if (decoded.layers && decoded.layers.length) handleImportedLayers(decoded);
else handleImportedImage(parsePsd(ev.target.result).canvas, file.name || 'Imported PSD');
} catch (err) {
if (uiModule) uiModule.showToast('Failed to import PSD: ' + err.message);
}
return;
}
const img = new Image();
img.onload = () => handleImportedImage(img, file.name || 'Imported image');
img.src = ev.target.result;
};
reader.readAsDataURL(file);
if (isPsd) reader.readAsArrayBuffer(file);
else reader.readAsDataURL(file);
importFileInput.value = '';
});