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