diff --git a/packages/frontend/src/components/MkWatermarkEditorDialog.vue b/packages/frontend/src/components/MkWatermarkEditorDialog.vue index ad19f543e6..72b65b26f7 100644 --- a/packages/frontend/src/components/MkWatermarkEditorDialog.vue +++ b/packages/frontend/src/components/MkWatermarkEditorDialog.vue @@ -9,8 +9,9 @@ SPDX-License-Identifier: AGPL-3.0-only :width="1000" :height="600" :scroll="false" - :withOkButton="false" + :withOkButton="true" @close="cancel()" + @ok="save()" @closed="emit('closed')" > @@ -56,13 +57,15 @@ function cancel() { dialogEl.value?.close(); } -function close() { +function save() { + emit('ok'); dialogEl.value?.close(); } //#endregion //#region 設定 const useWatermark = computed(defaultStore.makeGetterSetter('useWatermark')); +const watermarkConfig = ref(defaultStore.state.watermarkConfig); //#endregion //#region Canvasの制御 diff --git a/packages/frontend/src/scripts/watermark.ts b/packages/frontend/src/scripts/watermark.ts new file mode 100644 index 0000000000..f6b03568c6 --- /dev/null +++ b/packages/frontend/src/scripts/watermark.ts @@ -0,0 +1,109 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { getProxiedImageUrl } from "@/scripts/media-proxy.js"; + +export type WatermarkConfig = { + fileId: string | null; + fileUrl: string | null; + width: number | null; + height: number | null; + enlargement: 'scale-down' | 'contain' | 'cover' | 'crop' | 'pad'; + gravity: 'auto' | 'left' | 'right' | 'top' | 'bottom'; + opacity: number; + repeat: true | false | 'x' | 'y'; + anchor: 'center' | 'top' | 'left' | 'bottom' | 'right' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; + offsetTop: number | null; + offsetLeft: number | null; + offsetBottom: number | null; + offsetRight: number | null; + backgroundColor: string | null; + rotate: number | null; +}; + +/** + * ウォーターマークを適用してキャンバスに描画する + * + * @param img ウォーターマークを適用する画像(stringは画像URL。**プレビュー用途専用**) + * @param el ウォーターマークを適用するキャンバス + * @param config ウォーターマークの設定 + */ +export function applyWatermark(img: string | Blob, el: HTMLCanvasElement, config: WatermarkConfig) { + const canvas = el; + const ctx = canvas.getContext('2d')!; + const imgEl = new Image(); + imgEl.onload = () => { + canvas.width = imgEl.width; + canvas.height = imgEl.height; + ctx.drawImage(imgEl, 0, 0); + if (config.fileUrl) { + const watermark = new Image(); + watermark.onload = () => { + const width = config.width || watermark.width; + const height = config.height || watermark.height; + const x = (() => { + switch (config.anchor) { + case 'center': + case 'top': + case 'bottom': + return (canvas.width - width) / 2; + case 'left': + case 'top-left': + case 'bottom-left': + return 0; + case 'right': + case 'top-right': + case 'bottom-right': + return canvas.width - width; + } + })(); + const y = (() => { + switch (config.anchor) { + case 'center': + case 'left': + case 'right': + return (canvas.height - height) / 2; + case 'top': + case 'top-left': + case 'top-right': + return 0; + case 'bottom': + case 'bottom-left': + case 'bottom-right': + return canvas.height - height; + } + })(); + ctx.globalAlpha = config.opacity; + ctx.drawImage(watermark, x, y, width, height); + }; + watermark.src = config.fileUrl; + } + }; + if (typeof img === 'string') { + imgEl.src = getProxiedImageUrl(img, undefined, true); + } else { + const reader = new FileReader(); + reader.onload = () => { + imgEl.src = reader.result as string; + }; + reader.readAsDataURL(img); + } +} + +/** + * ウォーターマークを適用した画像をBlobとして取得する + * + * @param img ウォーターマークを適用する画像 + * @param config ウォーターマークの設定 + * @returns ウォーターマークを適用した画像のBlob + */ +export function getWatermarkAppliedImage(img: Blob, config: WatermarkConfig): Promise { + const canvas = document.createElement('canvas'); + applyWatermark(img, canvas, config); + return new Promise(resolve => { + canvas.toBlob(blob => { + resolve(blob!); + }); + }); +} diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index 3b2ac3b61d..c5d9f9149b 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -9,6 +9,7 @@ import { hemisphere } from '@@/js/intl-const.js'; import lightTheme from '@@/themes/l-light.json5'; import darkTheme from '@@/themes/d-green-lime.json5'; import type { SoundType } from '@/scripts/sound.js'; +import type { WatermarkConfig } from './scripts/watermark.js'; import { DEFAULT_DEVICE_KIND, type DeviceKind } from '@/scripts/device-kind.js'; import { miLocalStorage } from '@/local-storage.js'; import { Storage } from '@/pizzax.js'; @@ -480,23 +481,7 @@ export const defaultStore = markRaw(new Storage('base', { }, watermarkConfig: { where: 'account', - default: null as { - fileId: string | null; - fileUrl: string | null; - width: number | null; - height: number | null; - enlargement: 'scale-down' | 'contain' | 'cover' | 'crop' | 'pad'; - gravity: 'auto' | 'left' | 'right' | 'top' | 'bottom'; - opacity: number; - repeat: true | false | 'x' | 'y'; - anchor: 'center' | 'top' | 'left' | 'bottom' | 'right' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; - offsetTop: number | null; - offsetLeft: number | null; - offsetBottom: number | null; - offsetRight: number | null; - backgroundColor: string | null; - rotate: number | null; - } | null, + default: null as WatermarkConfig | null, }, sound_masterVolume: {