wip
This commit is contained in:
parent
7754ccb73f
commit
de90b606c1
|
@ -96,6 +96,7 @@ import { isWebpSupported } from '@/utility/isWebpSupported.js';
|
|||
import { uploadFile, UploadAbortedError } from '@/utility/drive.js';
|
||||
import * as os from '@/os.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
import { Watermarker } from '@/utility/watermarker.js';
|
||||
|
||||
const $i = ensureSignin();
|
||||
|
||||
|
@ -153,7 +154,7 @@ const items = ref<{
|
|||
aborted: boolean;
|
||||
compressionLevel: 0 | 1 | 2 | 3;
|
||||
compressedSize?: number | null;
|
||||
compressedImage?: Blob | null;
|
||||
preprocessedFile?: Blob | null;
|
||||
file: File;
|
||||
watermarkPresetId: string | null;
|
||||
abort?: (() => void) | null;
|
||||
|
@ -277,6 +278,7 @@ function showMenu(ev: MouseEvent, item: typeof items.value[0]) {
|
|||
text: i18n.ts.cropImage,
|
||||
action: async () => {
|
||||
const cropped = await os.cropImageFile(item.file, { aspectRatio: null });
|
||||
URL.revokeObjectURL(item.thumbnail);
|
||||
items.value.splice(items.value.indexOf(item), 1, {
|
||||
...item,
|
||||
file: markRaw(cropped),
|
||||
|
@ -287,6 +289,13 @@ function showMenu(ev: MouseEvent, item: typeof items.value[0]) {
|
|||
}
|
||||
|
||||
if (WATERMARK_SUPPORTED_TYPES.includes(item.file.type) && !item.preprocessing && !item.uploading && !item.uploaded) {
|
||||
function changeWatermarkPreset(presetId: string | null) {
|
||||
item.watermarkPresetId = presetId;
|
||||
preprocess(item).then(() => {
|
||||
triggerRef(items);
|
||||
});
|
||||
}
|
||||
|
||||
menu.push({
|
||||
icon: 'ti ti-copyright',
|
||||
text: i18n.ts.watermark,
|
||||
|
@ -295,8 +304,15 @@ function showMenu(ev: MouseEvent, item: typeof items.value[0]) {
|
|||
type: 'radioOption',
|
||||
text: i18n.ts.none,
|
||||
active: computed(() => item.watermarkPresetId == null),
|
||||
action: () => item.watermarkPresetId = null,
|
||||
}],
|
||||
action: () => changeWatermarkPreset(null),
|
||||
}, {
|
||||
type: 'divider',
|
||||
}, ...prefer.s.watermarkPresets.map(preset => ({
|
||||
type: 'radioOption',
|
||||
text: preset.name,
|
||||
active: computed(() => item.watermarkPresetId === preset.id),
|
||||
action: () => changeWatermarkPreset(preset.id),
|
||||
}))],
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -317,6 +333,8 @@ function showMenu(ev: MouseEvent, item: typeof items.value[0]) {
|
|||
text: i18n.ts.none,
|
||||
active: computed(() => item.compressionLevel === 0 || item.compressionLevel == null),
|
||||
action: () => changeCompressionLevel(0),
|
||||
}, {
|
||||
type: 'divider',
|
||||
}, {
|
||||
type: 'radioOption',
|
||||
text: i18n.ts.low,
|
||||
|
@ -384,7 +402,7 @@ async function upload() { // エラーハンドリングなどを考慮してシ
|
|||
item.uploadFailed = false;
|
||||
item.uploading = true;
|
||||
|
||||
const { filePromise, abort } = uploadFile(item.compressedImage ?? item.file, {
|
||||
const { filePromise, abort } = uploadFile(item.preprocessedFile ?? item.file, {
|
||||
name: item.uploadName ?? item.name,
|
||||
folderId: props.folderId,
|
||||
onProgress: (progress) => {
|
||||
|
@ -441,13 +459,58 @@ async function chooseFile(ev: MouseEvent) {
|
|||
}
|
||||
}
|
||||
|
||||
function getImageElement(file: Blob | File): Promise<HTMLImageElement> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => {
|
||||
resolve(img);
|
||||
};
|
||||
|
||||
img.onerror = (error) => {
|
||||
reject(error);
|
||||
};
|
||||
|
||||
img.src = URL.createObjectURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
async function preprocess(item: (typeof items)['value'][number]): Promise<void> {
|
||||
item.preprocessing = true;
|
||||
|
||||
const compressionSettings = getCompressionSettings(item.compressionLevel);
|
||||
const shouldCompress = item.compressionLevel !== 0 && compressionSettings && COMPRESSION_SUPPORTED_TYPES.includes(item.file.type) && !(await isAnimated(item.file));
|
||||
let file: Blob | File = item.file;
|
||||
const img = await getImageElement(file);
|
||||
|
||||
if (shouldCompress) {
|
||||
const needsWatermark = item.watermarkPresetId != null && WATERMARK_SUPPORTED_TYPES.includes(file.type);
|
||||
const preset = prefer.s.watermarkPresets.find(p => p.id === item.watermarkPresetId);
|
||||
if (needsWatermark && preset != null) {
|
||||
const canvas = window.document.createElement('canvas');
|
||||
const renderer = new Watermarker({
|
||||
canvas: canvas,
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
preset: preset,
|
||||
originalImage: img,
|
||||
});
|
||||
|
||||
await renderer.bakeTextures();
|
||||
|
||||
renderer.render();
|
||||
|
||||
file = await new Promise<Blob>((resolve) => {
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob == null) {
|
||||
throw new Error('Failed to convert canvas to blob');
|
||||
}
|
||||
resolve(blob);
|
||||
}, 'image/png');
|
||||
});
|
||||
}
|
||||
|
||||
const compressionSettings = getCompressionSettings(item.compressionLevel);
|
||||
const needsCompress = item.compressionLevel !== 0 && compressionSettings && COMPRESSION_SUPPORTED_TYPES.includes(file.type) && !(await isAnimated(file));
|
||||
|
||||
if (needsCompress) {
|
||||
const config = {
|
||||
mimeType: isWebpSupported() ? 'image/webp' : 'image/jpeg',
|
||||
maxWidth: compressionSettings.maxWidth,
|
||||
|
@ -456,24 +519,28 @@ async function preprocess(item: (typeof items)['value'][number]): Promise<void>
|
|||
};
|
||||
|
||||
try {
|
||||
const result = await readAndCompressImage(item.file, config);
|
||||
if (result.size < item.file.size || item.file.type === 'image/webp') {
|
||||
const result = await readAndCompressImage(file, config);
|
||||
if (result.size < file.size || file.type === 'image/webp') {
|
||||
// The compression may not always reduce the file size
|
||||
// (and WebP is not browser safe yet)
|
||||
item.compressedImage = markRaw(result);
|
||||
file = result;
|
||||
item.compressedSize = result.size;
|
||||
item.uploadName = item.file.type !== config.mimeType ? `${item.name}.${mimeTypeMap[config.mimeType]}` : item.name;
|
||||
item.uploadName = file.type !== config.mimeType ? `${item.name}.${mimeTypeMap[config.mimeType]}` : item.name;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to resize image', err);
|
||||
}
|
||||
} else {
|
||||
item.compressedImage = null;
|
||||
item.compressedSize = null;
|
||||
item.uploadName = item.name;
|
||||
}
|
||||
|
||||
URL.revokeObjectURL(item.thumbnail);
|
||||
item.thumbnail = window.URL.createObjectURL(file);
|
||||
item.preprocessedFile = markRaw(file);
|
||||
item.preprocessing = false;
|
||||
|
||||
URL.revokeObjectURL(img.src);
|
||||
}
|
||||
|
||||
function initializeFile(file: File) {
|
||||
|
|
|
@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:min="0"
|
||||
:max="1"
|
||||
:step="0.01"
|
||||
:textConverter="(v) => (v * 100).toFixed(1) + '%'"
|
||||
continuousUpdate
|
||||
>
|
||||
<template #label>{{ i18n.ts._watermarkEditor.scale }}</template>
|
||||
|
@ -33,6 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:min="0"
|
||||
:max="1"
|
||||
:step="0.01"
|
||||
:textConverter="(v) => (v * 100).toFixed(1) + '%'"
|
||||
continuousUpdate
|
||||
>
|
||||
<template #label>{{ i18n.ts._watermarkEditor.opacity }}</template>
|
||||
|
@ -58,6 +60,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:min="0"
|
||||
:max="1"
|
||||
:step="0.01"
|
||||
:textConverter="(v) => (v * 100).toFixed(1) + '%'"
|
||||
continuousUpdate
|
||||
>
|
||||
<template #label>{{ i18n.ts._watermarkEditor.scale }}</template>
|
||||
|
@ -68,6 +71,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:min="0"
|
||||
:max="1"
|
||||
:step="0.01"
|
||||
:textConverter="(v) => (v * 100).toFixed(1) + '%'"
|
||||
continuousUpdate
|
||||
>
|
||||
<template #label>{{ i18n.ts._watermarkEditor.opacity }}</template>
|
||||
|
|
|
@ -96,6 +96,7 @@ onUnmounted(() => {
|
|||
watch(() => props.preset, async () => {
|
||||
if (renderer != null) {
|
||||
renderer.updatePreset(props.preset);
|
||||
await renderer.bakeTextures();
|
||||
renderer.render();
|
||||
}
|
||||
}, { deep: true });
|
||||
|
|
|
@ -84,12 +84,12 @@ export type WatermarkerLayer = WatermarkerTextLayer | WatermarkerImageLayer;
|
|||
|
||||
export class Watermarker {
|
||||
private canvas: HTMLCanvasElement | null = null;
|
||||
public gl: WebGL2RenderingContext | null = null;
|
||||
public renderTextureProgram!: WebGLProgram;
|
||||
public renderInvertedTextureProgram!: WebGLProgram;
|
||||
public renderWidth!: number;
|
||||
public renderHeight!: number;
|
||||
public originalImage: HTMLImageElement;
|
||||
private gl: WebGL2RenderingContext | null = null;
|
||||
private renderTextureProgram!: WebGLProgram;
|
||||
private renderInvertedTextureProgram!: WebGLProgram;
|
||||
private renderWidth!: number;
|
||||
private renderHeight!: number;
|
||||
private originalImage: ImageData | ImageBitmap | HTMLImageElement | HTMLCanvasElement;
|
||||
private preset: WatermarkPreset;
|
||||
private originalImageTexture: WebGLTexture;
|
||||
private resultTexture: WebGLTexture;
|
||||
|
@ -101,7 +101,7 @@ export class Watermarker {
|
|||
canvas: HTMLCanvasElement;
|
||||
width: number;
|
||||
height: number;
|
||||
originalImage: HTMLImageElement;
|
||||
originalImage: ImageData | ImageBitmap | HTMLImageElement | HTMLCanvasElement;
|
||||
preset: WatermarkPreset;
|
||||
}) {
|
||||
this.canvas = options.canvas;
|
||||
|
@ -131,7 +131,7 @@ export class Watermarker {
|
|||
this.originalImageTexture = this.createTexture();
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.originalImageTexture);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, this.originalImage.width, this.originalImage.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, this.originalImage);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, options.width, options.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, this.originalImage);
|
||||
gl.bindTexture(gl.TEXTURE_2D, null);
|
||||
|
||||
this.resultTexture = this.createTexture();
|
||||
|
@ -253,6 +253,7 @@ export class Watermarker {
|
|||
const margin = Math.min(this.renderWidth, this.renderHeight) / 50;
|
||||
measureCtx.font = `bold ${fontSize}px sans-serif`;
|
||||
const textMetrics = measureCtx.measureText(layer.text);
|
||||
measureCtx.canvas.remove();
|
||||
|
||||
const RESOLUTION_FACTOR = 4;
|
||||
|
||||
|
@ -284,6 +285,8 @@ export class Watermarker {
|
|||
width: textCtx.canvas.width,
|
||||
height: textCtx.canvas.height,
|
||||
});
|
||||
|
||||
textCtx.canvas.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -398,7 +401,7 @@ export class Watermarker {
|
|||
}
|
||||
}
|
||||
|
||||
public async render() {
|
||||
public render() {
|
||||
const gl = this.gl;
|
||||
if (gl == null) {
|
||||
throw new Error('gl is not initialized');
|
||||
|
|
Loading…
Reference in New Issue