mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-20 13:52:24 +02:00
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:
co-authored by
Claude Opus 5.5
parent
c74f475679
commit
5fac693c1c
@@ -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 = '';
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user