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')"
>
{{ i18n.ts.watermark }}
@@ -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: {