diff --git a/packages/frontend/src/scripts/watermark.ts b/packages/frontend/src/scripts/watermark.ts index d4adaa82ca..2b910d3617 100644 --- a/packages/frontend/src/scripts/watermark.ts +++ b/packages/frontend/src/scripts/watermark.ts @@ -113,146 +113,151 @@ export function canPreview(config: Partial { - canvas.width = imgEl.width; - canvas.height = imgEl.height; - ctx.drawImage(imgEl, 0, 0); +export function applyWatermark(img: string | Blob, el: HTMLCanvasElement | OffscreenCanvas, config: WatermarkConfig | null) { + return new Promise(async (resolve) => { + const canvas = el; + const ctx = canvas.getContext('2d')!; + const imgEl = new Image(); + imgEl.onload = async () => { + canvas.width = imgEl.width; + canvas.height = imgEl.height; + ctx.drawImage(imgEl, 0, 0); - if (config != null) { - if (config.fileUrl) { - const watermark = new Image(); - watermark.onload = () => { - const canvasAspectRatio = canvas.width / canvas.height; // 横長は1より大きい - const watermarkAspectRatio = watermark.width / watermark.height; // 横長は1より大きい - const { width, height } = (() => { - const desiredWidth = canvas.width * (config.sizeRatio ?? 1); - const desiredHeight = canvas.height * (config.sizeRatio ?? 1); + if (config != null) { + if (config.fileUrl) { + const watermark = new Image(); + watermark.onload = () => { + const canvasAspectRatio = canvas.width / canvas.height; // 横長は1より大きい + const watermarkAspectRatio = watermark.width / watermark.height; // 横長は1より大きい + const { width, height } = (() => { + const desiredWidth = canvas.width * (config.sizeRatio ?? 1); + const desiredHeight = canvas.height * (config.sizeRatio ?? 1); - if ( - (watermarkAspectRatio > 1 && canvasAspectRatio > 1) || // 両方横長 - (watermarkAspectRatio < 1 && canvasAspectRatio < 1) // 両方縦長 - ) { - // 横幅を基準にウォーターマークのサイズを決定 - return { - width: desiredWidth, - height: desiredWidth / watermarkAspectRatio, - }; - } else { - // 縦幅を基準にウォーターマークのサイズを決定 - return { - width: desiredHeight * watermarkAspectRatio, - height: desiredHeight, - }; - } - })(); - - ctx.globalAlpha = config.opacity ?? 1; - - if (config.repeat) { - // 余白をもたせた状態のウォーターマークを作成しておく(それをパターン繰り返しする) - const resizedWatermark = document.createElement('canvas'); - resizedWatermark.width = width + (config.padding ? (config.padding.left ?? 0) + (config.padding.right ?? 0) : 0); - resizedWatermark.height = height + (config.padding ? (config.padding.top ?? 0) + (config.padding.bottom ?? 0) : 0); - const resizedCtx = resizedWatermark.getContext('2d')!; - resizedCtx.drawImage( - watermark, - (config.padding ? config.padding.left ?? 0 : 0), - (config.padding ? config.padding.top ?? 0 : 0), - width, - height - ); - - const pattern = ctx.createPattern(resizedWatermark, 'repeat'); - if (pattern) { - ctx.fillStyle = pattern; - if (config.rotate != null && config.rotate !== 0) { - const rotateRad = config.rotate * Math.PI / 180; - ctx.translate(canvas.width / 2, canvas.height / 2); - ctx.rotate(rotateRad); - ctx.translate(-canvas.width / 2, -canvas.height / 2); - const rotatedWidth = Math.abs(canvas.width * Math.cos(rotateRad)) + Math.abs(canvas.height * Math.sin(rotateRad)); - const rotatedHeight = Math.abs(canvas.width * Math.sin(rotateRad)) + Math.abs(canvas.height * Math.cos(rotateRad)); - const x = Math.abs(rotatedWidth - canvas.width) / -2; - const y = Math.abs(rotatedHeight - canvas.height) / -2; - ctx.fillRect(x, y, rotatedWidth, rotatedHeight); + if ( + (watermarkAspectRatio > 1 && canvasAspectRatio > 1) || // 両方横長 + (watermarkAspectRatio < 1 && canvasAspectRatio < 1) // 両方縦長 + ) { + // 横幅を基準にウォーターマークのサイズを決定 + return { + width: desiredWidth, + height: desiredWidth / watermarkAspectRatio, + }; } else { - ctx.fillRect(0, 0, canvas.width, canvas.height); - } - } - } else { - 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 + (config.padding ? config.padding.left ?? 0 : 0); - case 'right': - case 'top-right': - case 'bottom-right': - return canvas.width - width - (config.padding ? config.padding.right ?? 0 : 0); + // 縦幅を基準にウォーターマークのサイズを決定 + return { + width: desiredHeight * watermarkAspectRatio, + height: desiredHeight, + }; } })(); - const y = (() => { - let rotateY = 0; // 回転によるY座標の補正 + ctx.globalAlpha = config.opacity ?? 1; - if (config.rotate != null && config.rotate !== 0 && !config.noBoundingBoxExpansion) { + if (config.repeat) { + // 余白をもたせた状態のウォーターマークを作成しておく(それをパターン繰り返しする) + const resizedWatermark = document.createElement('canvas'); + resizedWatermark.width = width + (config.padding ? (config.padding.left ?? 0) + (config.padding.right ?? 0) : 0); + resizedWatermark.height = height + (config.padding ? (config.padding.top ?? 0) + (config.padding.bottom ?? 0) : 0); + const resizedCtx = resizedWatermark.getContext('2d')!; + resizedCtx.drawImage( + watermark, + (config.padding ? config.padding.left ?? 0 : 0), + (config.padding ? config.padding.top ?? 0 : 0), + width, + height + ); + + const pattern = ctx.createPattern(resizedWatermark, 'repeat'); + if (pattern) { + ctx.fillStyle = pattern; + if (config.rotate != null && config.rotate !== 0) { + const rotateRad = config.rotate * Math.PI / 180; + ctx.translate(canvas.width / 2, canvas.height / 2); + ctx.rotate(rotateRad); + ctx.translate(-canvas.width / 2, -canvas.height / 2); + const rotatedWidth = Math.abs(canvas.width * Math.cos(rotateRad)) + Math.abs(canvas.height * Math.sin(rotateRad)); + const rotatedHeight = Math.abs(canvas.width * Math.sin(rotateRad)) + Math.abs(canvas.height * Math.cos(rotateRad)); + const x = Math.abs(rotatedWidth - canvas.width) / -2; + const y = Math.abs(rotatedHeight - canvas.height) / -2; + ctx.fillRect(x, y, rotatedWidth, rotatedHeight); + } else { + ctx.fillRect(0, 0, canvas.width, canvas.height); + } + } + } else { + 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 + (config.padding ? config.padding.left ?? 0 : 0); + case 'right': + case 'top-right': + case 'bottom-right': + return canvas.width - width - (config.padding ? config.padding.right ?? 0 : 0); + } + })(); + + const y = (() => { + let rotateY = 0; // 回転によるY座標の補正 + + if (config.rotate != null && config.rotate !== 0 && !config.noBoundingBoxExpansion) { + const rotateRad = config.rotate * Math.PI / 180; + rotateY = Math.abs(Math.abs(width * Math.sin(rotateRad)) + Math.abs(height * Math.cos(rotateRad)) - height) / 2; + } + + switch (config.anchor) { + case 'center': + case 'left': + case 'right': + return (canvas.height - height) / 2; + case 'top': + case 'top-left': + case 'top-right': + return rotateY + (config.padding ? config.padding.top ?? 0 : 0); + case 'bottom': + case 'bottom-left': + case 'bottom-right': + return canvas.height - height - (config.padding ? config.padding.bottom ?? 0 : 0) - rotateY; + } + })(); + if (config.rotate) { const rotateRad = config.rotate * Math.PI / 180; - rotateY = Math.abs(Math.abs(width * Math.sin(rotateRad)) + Math.abs(height * Math.cos(rotateRad)) - height) / 2; + ctx.translate(x + width / 2, y + height / 2); + ctx.rotate(rotateRad); + ctx.translate(-x - width / 2, -y - height / 2); } - - switch (config.anchor) { - case 'center': - case 'left': - case 'right': - return (canvas.height - height) / 2; - case 'top': - case 'top-left': - case 'top-right': - return rotateY + (config.padding ? config.padding.top ?? 0 : 0); - case 'bottom': - case 'bottom-left': - case 'bottom-right': - return canvas.height - height - (config.padding ? config.padding.bottom ?? 0 : 0) - rotateY; - } - })(); - if (config.rotate) { - const rotateRad = config.rotate * Math.PI / 180; - ctx.translate(x + width / 2, y + height / 2); - ctx.rotate(rotateRad); - ctx.translate(-x - width / 2, -y - height / 2); + ctx.drawImage(watermark, x, y, width, height); } - ctx.drawImage(watermark, x, y, width, height); + + resolve(); + }; + + let watermarkUrl: string; + if (config.fileUrl == null && config.fileId != null) { + const res = await misskeyApi('drive/files/show', { fileId: config.fileId }); + watermarkUrl = res.url; + // 抜けてたら保存 + defaultStore.set('watermarkConfig', { ...config, fileUrl: watermarkUrl }); + } else { + watermarkUrl = config.fileUrl!; } - }; - let watermarkUrl: string; - if (config.fileUrl == null && config.fileId != null) { - const res = await misskeyApi('drive/files/show', { fileId: config.fileId }); - watermarkUrl = res.url; - // 抜けてたら保存 - defaultStore.set('watermarkConfig', { ...config, fileUrl: watermarkUrl }); - } else { - watermarkUrl = config.fileUrl!; + watermark.src = config.__bypassMediaProxy ? config.fileUrl : getProxiedImageUrl(watermarkUrl, undefined, true); } - - watermark.src = config.__bypassMediaProxy ? config.fileUrl : getProxiedImageUrl(watermarkUrl, undefined, true); } + }; + + if (typeof img === 'string') { + imgEl.src = img; + } else { + imgEl.src = URL.createObjectURL(img); } - }; - if (typeof img === 'string') { - imgEl.src = img; - } else { - imgEl.src = URL.createObjectURL(img); - } + }); } /** @@ -262,12 +267,8 @@ export async function applyWatermark(img: string | Blob, el: HTMLCanvasElement, * @param config ウォーターマークの設定 * @returns ウォーターマークを適用した画像のBlob */ -export function getWatermarkAppliedImage(img: Blob, config: WatermarkConfig): Promise { +export async 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!); - }); - }); + await applyWatermark(img, canvas, config); + return new Promise(resolve => canvas.toBlob(blob => resolve(blob!))); }