diff --git a/locales/index.d.ts b/locales/index.d.ts index 96d6c890a8..8871a67178 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5605,6 +5605,36 @@ export interface Locale extends ILocale { * 技術的なお問い合わせの際に、以下の情報を併記すると問題の解決に役立つことがあります。 */ "deviceInfoDescription": string; + "_imageLabelEditor": { + /** + * ラベルの編集 + */ + "title": string; + /** + * フレーム + */ + "frameThickness": string; + /** + * 中央揃え + */ + "centered": string; + /** + * キャプション(大) + */ + "captionMain": string; + /** + * キャプション(小) + */ + "captionSub": string; + /** + * 利用可能な変数 + */ + "availableVariables": string; + /** + * 二次元コード + */ + "withQrCode": string; + }; "_compression": { "_quality": { /** diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 8e935b5d9e..5d4e5f7a6f 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1397,6 +1397,15 @@ widgets: "ウィジェット" deviceInfo: "デバイス情報" deviceInfoDescription: "技術的なお問い合わせの際に、以下の情報を併記すると問題の解決に役立つことがあります。" +_imageLabelEditor: + title: "ラベルの編集" + frameThickness: "フレーム" + centered: "中央揃え" + captionMain: "キャプション(大)" + captionSub: "キャプション(小)" + availableVariables: "利用可能な変数" + withQrCode: "二次元コード" + _compression: _quality: high: "高品質" diff --git a/packages/frontend/package.json b/packages/frontend/package.json index bd81d1d2c6..1ad3437e86 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -48,6 +48,7 @@ "estree-walker": "3.0.3", "eventemitter3": "5.0.1", "execa": "9.6.0", + "exifreader": "4.32.0", "frontend-shared": "workspace:*", "icons-subsetter": "workspace:*", "idb-keyval": "6.2.2", diff --git a/packages/frontend/src/components/MkEmbedCodeGenDialog.vue b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue index 0cb8499699..c736ae963d 100644 --- a/packages/frontend/src/components/MkEmbedCodeGenDialog.vue +++ b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue @@ -314,10 +314,16 @@ onUnmounted(() => { .embedCodeGenPreviewRoot { position: relative; - background-color: var(--MI_THEME-bg); - background-size: auto auto; - background-image: repeating-linear-gradient(135deg, transparent, transparent 6px, var(--MI_THEME-panel) 6px, var(--MI_THEME-panel) 12px); cursor: not-allowed; + background-color: var(--MI_THEME-bg); + background-image: linear-gradient(135deg, transparent 30%, var(--MI_THEME-panel) 30%, var(--MI_THEME-panel) 50%, transparent 50%, transparent 80%, var(--MI_THEME-panel) 80%, var(--MI_THEME-panel) 100%); + background-size: 20px 20px; + animation: bg 1.2s linear infinite; +} + +@keyframes bg { + 0% { background-position: 0 0; } + 100% { background-position: -20px -20px; } } .embedCodeGenPreviewWrapper { diff --git a/packages/frontend/src/components/MkImageEffectorDialog.vue b/packages/frontend/src/components/MkImageEffectorDialog.vue index 19ddb81919..248ba8052b 100644 --- a/packages/frontend/src/components/MkImageEffectorDialog.vue +++ b/packages/frontend/src/components/MkImageEffectorDialog.vue @@ -155,8 +155,8 @@ onMounted(async () => { if (w > MAX_W || h > MAX_H) { const scale = Math.min(MAX_W / w, MAX_H / h); - w *= scale; - h *= scale; + w = Math.floor(w * scale); + h = Math.floor(h * scale); } renderer = new ImageEffector({ @@ -373,8 +373,14 @@ function onImagePointerdown(ev: PointerEvent) { .preview { position: relative; background-color: var(--MI_THEME-bg); - background-size: auto auto; - background-image: repeating-linear-gradient(135deg, transparent, transparent 6px, var(--MI_THEME-panel) 6px, var(--MI_THEME-panel) 12px); + background-image: linear-gradient(135deg, transparent 30%, var(--MI_THEME-panel) 30%, var(--MI_THEME-panel) 50%, transparent 50%, transparent 80%, var(--MI_THEME-panel) 80%, var(--MI_THEME-panel) 100%); + background-size: 20px 20px; + animation: bg 1.2s linear infinite; +} + +@keyframes bg { + 0% { background-position: 0 0; } + 100% { background-position: -20px -20px; } } .previewContainer { diff --git a/packages/frontend/src/components/MkImageLabelEditorDialog.vue b/packages/frontend/src/components/MkImageLabelEditorDialog.vue new file mode 100644 index 0000000000..e48321766a --- /dev/null +++ b/packages/frontend/src/components/MkImageLabelEditorDialog.vue @@ -0,0 +1,330 @@ + + + + + + + diff --git a/packages/frontend/src/components/MkWatermarkEditorDialog.vue b/packages/frontend/src/components/MkWatermarkEditorDialog.vue index 3b3f20d8d1..ae138b0a87 100644 --- a/packages/frontend/src/components/MkWatermarkEditorDialog.vue +++ b/packages/frontend/src/components/MkWatermarkEditorDialog.vue @@ -249,8 +249,8 @@ async function initRenderer() { if (w > MAX_W || h > MAX_H) { const scale = Math.min(MAX_W / w, MAX_H / h); - w *= scale; - h *= scale; + w = Math.floor(w * scale); + h = Math.floor(h * scale); } renderer = new WatermarkRenderer({ @@ -380,8 +380,14 @@ function removeLayer(layer: WatermarkPreset['layers'][number]) { .preview { position: relative; background-color: var(--MI_THEME-bg); - background-size: auto auto; - background-image: repeating-linear-gradient(135deg, transparent, transparent 6px, var(--MI_THEME-panel) 6px, var(--MI_THEME-panel) 12px); + background-image: linear-gradient(135deg, transparent 30%, var(--MI_THEME-panel) 30%, var(--MI_THEME-panel) 50%, transparent 50%, transparent 80%, var(--MI_THEME-panel) 80%, var(--MI_THEME-panel) 100%); + background-size: 20px 20px; + animation: bg 1.2s linear infinite; +} + +@keyframes bg { + 0% { background-position: 0 0; } + 100% { background-position: -20px -20px; } } .previewContainer { diff --git a/packages/frontend/src/composables/use-uploader.ts b/packages/frontend/src/composables/use-uploader.ts index e4aa1fda53..df5dc1469d 100644 --- a/packages/frontend/src/composables/use-uploader.ts +++ b/packages/frontend/src/composables/use-uploader.ts @@ -8,6 +8,7 @@ import { readAndCompressImage } from '@misskey-dev/browser-image-resizer'; import isAnimated from 'is-file-animated'; import { EventEmitter } from 'eventemitter3'; import { computed, markRaw, onMounted, onUnmounted, ref, triggerRef } from 'vue'; +import ExifReader from 'exifreader'; import type { MenuItem } from '@/types/menu.js'; import { genId } from '@/utility/id.js'; import { i18n } from '@/i18n.js'; @@ -17,6 +18,7 @@ import { uploadFile, UploadAbortedError } from '@/utility/drive.js'; import * as os from '@/os.js'; import { ensureSignin } from '@/i.js'; import { WatermarkRenderer } from '@/utility/watermark.js'; +import { ImageLabelRenderer } from '@/utility/image-label-renderer.js'; export type UploaderFeatures = { imageEditing?: boolean; @@ -571,6 +573,36 @@ export function useUploader(options: { }); } + const canvas = window.document.createElement('canvas'); + + const exif = await ExifReader.load(await item.file.arrayBuffer()); + + const labelRenderer = new ImageLabelRenderer({ + canvas: canvas, + image: await window.createImageBitmap(preprocessedFile), + exif, + }); + //await labelRenderer.update({ + // title: `${meta_model} + ${meta_lensModel}`, + // text: `${date} ${meta_mm}mm f/${meta_f} ${meta_s}s ISO${meta_iso}`, + //}); + await labelRenderer.update({ + title: 'aaaaaaaaaaaaa', + text: 'bbbbbbbbbbbbbbbbbbbb', + }); + + labelRenderer.render(); + + preprocessedFile = await new Promise((resolve) => { + canvas.toBlob((blob) => { + if (blob == null) { + throw new Error('Failed to convert canvas to blob'); + } + resolve(blob); + labelRenderer.destroy(); + }, 'image/png'); + }); + const compressionSettings = getCompressionSettings(item.compressionLevel); const needsCompress = item.compressionLevel !== 0 && compressionSettings && IMAGE_COMPRESSION_SUPPORTED_TYPES.includes(preprocessedFile.type) && !(await isAnimated(preprocessedFile)); diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue index f58ff4c78c..cf561b8164 100644 --- a/packages/frontend/src/pages/settings/drive.vue +++ b/packages/frontend/src/pages/settings/drive.vue @@ -124,6 +124,49 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + + +
+
+ + + + + +
+ +
+ + +
+
+
+ x.default), { + }, { + ok: (preset: any) => { + //prefer.commit('imageLabelPresets', [...prefer.s.imageLabelPresets, preset]); + }, + closed: () => dispose(), + }); +} + function saveProfile() { misskeyApi('i/update', { alwaysMarkNsfw: !!alwaysMarkNsfw.value, diff --git a/packages/frontend/src/utility/image-effector/ImageEffector.ts b/packages/frontend/src/utility/image-effector/ImageEffector.ts index 26c74bfae5..0a4e7408b1 100644 --- a/packages/frontend/src/utility/image-effector/ImageEffector.ts +++ b/packages/frontend/src/utility/image-effector/ImageEffector.ts @@ -6,7 +6,7 @@ import QRCodeStyling from 'qr-code-styling'; import { url, host } from '@@/js/config.js'; import { getProxiedImageUrl } from '../media-proxy.js'; -import { initShaderProgram } from '../webgl.js'; +import { createTexture, initShaderProgram } from '../webgl.js'; import { ensureSignin } from '@/i.js'; export type ImageEffectorRGB = [r: number, g: number, b: number]; @@ -71,12 +71,17 @@ interface TextureParamDef extends CommonParamDef { } | null; }; +interface TextureRefParamDef extends CommonParamDef { + type: 'textureRef'; + default: string; +}; + interface ColorParamDef extends CommonParamDef { type: 'color'; default: ImageEffectorRGB; }; -type ImageEffectorFxParamDef = NumberParamDef | NumberEnumParamDef | BooleanParamDef | AlignParamDef | SeedParamDef | TextureParamDef | ColorParamDef; +type ImageEffectorFxParamDef = NumberParamDef | NumberEnumParamDef | BooleanParamDef | AlignParamDef | SeedParamDef | TextureParamDef | TextureRefParamDef | ColorParamDef; export type ImageEffectorFxParamDefs = Record; @@ -129,27 +134,26 @@ export class ImageEffector = new Map(); private perLayerResultTextures: Map = new Map(); private perLayerResultFrameBuffers: Map = new Map(); private nopProgram: WebGLProgram; private fxs: [...IEX]; private paramTextures: Map = new Map(); + private registeredTextures: Map = new Map(); constructor(options: { canvas: HTMLCanvasElement; renderWidth: number; renderHeight: number; - image: ImageData | ImageBitmap | HTMLImageElement | HTMLCanvasElement; + image: ImageData | ImageBitmap | HTMLImageElement | HTMLCanvasElement | null; fxs: [...IEX]; }) { this.canvas = options.canvas; this.renderWidth = options.renderWidth; this.renderHeight = options.renderHeight; - this.originalImage = options.image; this.fxs = options.fxs; this.canvas.width = this.renderWidth; @@ -161,9 +165,7 @@ export class ImageEffector { - if (v.type !== 'texture') return [k, null]; - const param = getValue(layer.params, k); - if (param == null) return [k, null]; - const texture = this.paramTextures.get(this.getTextureKeyForParam(param)) ?? null; - return [k, texture]; + if (v.type === 'textureRef') { + const param = getValue(layer.params, k); + if (param == null) return [k, null]; + const texture = this.registeredTextures.get(param) ?? null; + return [k, texture]; + } else if (v.type === 'texture') { + const param = getValue(layer.params, k); + if (param == null) return [k, null]; + const texture = this.paramTextures.get(this.getTextureKeyForParam(param)) ?? null; + return [k, texture]; + } else { + return [k, null]; + } })), }); @@ -271,7 +286,7 @@ export class ImageEffector(layer.params, k); @@ -354,6 +369,28 @@ export class ImageEffector { if (imageUrl == null || imageUrl.trim() === '') return null; diff --git a/packages/frontend/src/utility/image-effector/fxs/label.glsl b/packages/frontend/src/utility/image-effector/fxs/label.glsl new file mode 100644 index 0000000000..7ecc48acd1 --- /dev/null +++ b/packages/frontend/src/utility/image-effector/fxs/label.glsl @@ -0,0 +1,33 @@ +#version 300 es +precision mediump float; + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +in vec2 in_uv; +uniform sampler2D in_texture; +uniform vec2 in_resolution; +uniform sampler2D u_image; +uniform sampler2D u_label; +uniform vec2 u_labelResolution; +uniform bool u_labelEnabled; +uniform float u_imageMarginX; +uniform float u_imageMarginY; +out vec4 out_color; + +void main() { + float labelRatio = u_labelEnabled ? (u_labelResolution.y / in_resolution.y) : 0.0; + vec4 image_color = texture(u_image, (in_uv / vec2(1.0, 1.0 - labelRatio) / vec2(1.0 - u_imageMarginX - u_imageMarginX, 1.0 - u_imageMarginY)) - vec2(u_imageMarginX, u_imageMarginY)); + vec4 label_color = texture(u_label, (in_uv - vec2(0.0, 1.0 - labelRatio)) / vec2(1.0, labelRatio)); + if (in_uv.y > (1.0 - labelRatio)) { + out_color = label_color; + } else { + if (in_uv.x < u_imageMarginX || in_uv.x > (1.0 - u_imageMarginX) || in_uv.y < u_imageMarginY) { + out_color = vec4(1.0, 1.0, 1.0, 1.0); + } else { + out_color = image_color; + } + } +} diff --git a/packages/frontend/src/utility/image-effector/fxs/label.ts b/packages/frontend/src/utility/image-effector/fxs/label.ts new file mode 100644 index 0000000000..b919e448ae --- /dev/null +++ b/packages/frontend/src/utility/image-effector/fxs/label.ts @@ -0,0 +1,57 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { defineImageEffectorFx } from '../ImageEffector.js'; +import shader from './label.glsl'; + +export const FX_label = defineImageEffectorFx({ + id: 'label', + name: '(internal)', + shader, + uniforms: ['image', 'label', 'labelResolution', 'labelEnabled', 'imageMarginX', 'imageMarginY'] as const, + params: { + image: { + type: 'textureRef', + default: null, + }, + label: { + type: 'textureRef', + default: null, + }, + imageMarginX: { + type: 'number', + default: 0.05, + max: 1, + min: 0, + }, + imageMarginY: { + type: 'number', + default: 0.05, + max: 1, + min: 0, + }, + }, + main: ({ gl, u, params, textures }) => { + const image = textures.image; + gl.activeTexture(gl.TEXTURE1); + gl.bindTexture(gl.TEXTURE_2D, image.texture); + gl.uniform1i(u.image, 1); + + gl.uniform1f(u.imageMarginX, params.imageMarginX); + gl.uniform1f(u.imageMarginY, params.imageMarginY); + + const label = textures.label; + if (label) { + gl.activeTexture(gl.TEXTURE2); + gl.bindTexture(gl.TEXTURE_2D, label.texture); + + gl.uniform1i(u.label, 2); + gl.uniform2f(u.labelResolution, label.width, label.height); + gl.uniform1i(u.labelEnabled, 1); + } else { + gl.uniform1i(u.labelEnabled, 0); + } + }, +}); diff --git a/packages/frontend/src/utility/image-label-renderer.ts b/packages/frontend/src/utility/image-label-renderer.ts new file mode 100644 index 0000000000..b7a25f22f3 --- /dev/null +++ b/packages/frontend/src/utility/image-label-renderer.ts @@ -0,0 +1,210 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import QRCodeStyling from 'qr-code-styling'; +import { url } from '@@/js/config.js'; +import ExifReader from 'exifreader'; +import { FX_label } from './image-effector/fxs/label.js'; +import type { ImageEffectorFx, ImageEffectorLayer } from '@/utility/image-effector/ImageEffector.js'; +import { ImageEffector } from '@/utility/image-effector/ImageEffector.js'; +import { ensureSignin } from '@/i.js'; + +const FXS = [ + FX_label, +] as const satisfies ImageEffectorFx[]; + +export type ImageLabelParams = { + style: 'frame' | 'frameLess'; + frameThickness: number; + title: string; + text: string; + centered: boolean; + withQrCode: boolean; +}; + +export class ImageLabelRenderer { + private effector: ImageEffector; + private renderWidth: number; + private renderHeight: number; + private image: HTMLImageElement | ImageBitmap; + private paddingBottom = 0; + private exif: ExifReader.Tags; + + constructor(options: { + canvas: HTMLCanvasElement, + image: HTMLImageElement | ImageBitmap, + exif: ExifReader.Tags, + renderAsPreview?: boolean, + }) { + this.image = options.image; + this.exif = options.exif; + console.log(this.exif); + + let w = this.image.width; + let h = this.image.height; + + if (options.renderAsPreview) { + const MAX_W = 1000; + const MAX_H = 1000; + + if (w > MAX_W || h > MAX_H) { + const scale = Math.min(MAX_W / w, MAX_H / h); + w = Math.floor(w * scale); + h = Math.floor(h * scale); + } + } + + this.paddingBottom = Math.floor(h * 0.2); + this.renderWidth = w; + this.renderHeight = h + this.paddingBottom; + + this.effector = new ImageEffector({ + canvas: options.canvas, + renderWidth: this.renderWidth, + renderHeight: this.renderHeight, + image: null, + fxs: FXS, + }); + + this.effector.registerTexture('image', this.image); + } + + private interpolateText(text: string) { + return text.replaceAll(/\{(\w+)\}/g, (_: string, key: string) => { + const meta_date = this.exif.DateTimeOriginal ? this.exif.DateTimeOriginal.description : '-'; + const date = meta_date.split(' ')[0].replaceAll(':', '/'); + switch (key) { + case 'date': return date; + case 'model': return this.exif.Model ? this.exif.Model.description : '-'; + case 'lensModel': return this.exif.LensModel ? this.exif.LensModel.description : '-'; + case 'mm': return this.exif.FocalLength ? this.exif.FocalLength.description.replace(' mm', '').replace('mm', '') : '-'; + case 'f': return this.exif.FNumber ? this.exif.FNumber.description.replace('f/', '') : '-'; + case 's': return this.exif.ExposureTime ? this.exif.ExposureTime.description : '-'; + case 'iso': return this.exif.ISOSpeedRatings ? this.exif.ISOSpeedRatings.description : '-'; + default: return '-'; + } + }); + } + + public async update(params: ImageLabelParams): Promise { + const aspectRatio = this.renderWidth / this.renderHeight; + const ctx = window.document.createElement('canvas').getContext('2d')!; + ctx.canvas.width = this.renderWidth; + ctx.canvas.height = this.paddingBottom; + const fontSize = ctx.canvas.height / 6; + const marginX = Math.max(fontSize * 2, (ctx.canvas.width * params.frameThickness) / aspectRatio); + const withQrCode = params.withQrCode; + const qrSize = ctx.canvas.height * 0.6; + const qrMarginX = Math.max((ctx.canvas.height - qrSize) / 2, (ctx.canvas.width * params.frameThickness) / aspectRatio); + + ctx.fillStyle = '#ffffff'; + ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); + + ctx.fillStyle = '#000000'; + ctx.font = `bold ${fontSize}px sans-serif`; + ctx.textBaseline = 'middle'; + + const titleY = params.text === '' ? (ctx.canvas.height / 2) : (ctx.canvas.height / 2) - (fontSize * 0.9); + if (params.centered) { + ctx.textAlign = 'center'; + ctx.fillText(this.interpolateText(params.title), ctx.canvas.width / 2, titleY, ctx.canvas.width - marginX - marginX); + } else { + ctx.textAlign = 'left'; + ctx.fillText(this.interpolateText(params.title), marginX, titleY, ctx.canvas.width - marginX - (withQrCode ? (qrSize + qrMarginX + (fontSize * 1)) : 0)); + } + + ctx.fillStyle = '#00000088'; + ctx.font = `${fontSize * 0.85}px sans-serif`; + ctx.textBaseline = 'middle'; + + const textY = params.title === '' ? (ctx.canvas.height / 2) : (ctx.canvas.height / 2) + (fontSize * 0.9); + if (params.centered) { + ctx.textAlign = 'center'; + ctx.fillText(this.interpolateText(params.text), ctx.canvas.width / 2, textY, ctx.canvas.width - marginX - marginX); + } else { + ctx.textAlign = 'left'; + ctx.fillText(this.interpolateText(params.text), marginX, textY, ctx.canvas.width - marginX - (withQrCode ? (qrSize + qrMarginX + (fontSize * 1)) : 0)); + } + + const $i = ensureSignin(); + + if (withQrCode) { + const qrCodeInstance = new QRCodeStyling({ + width: ctx.canvas.height, + height: ctx.canvas.height, + margin: 0, + type: 'canvas', + data: `${url}/users/${$i.id}`, + //image: $i.avatarUrl, + qrOptions: { + typeNumber: 0, + mode: 'Byte', + errorCorrectionLevel: 'H', + }, + imageOptions: { + hideBackgroundDots: true, + imageSize: 0.3, + margin: 16, + crossOrigin: 'anonymous', + }, + dotsOptions: { + type: 'dots', + roundSize: false, + }, + cornersDotOptions: { + type: 'dot', + }, + cornersSquareOptions: { + type: 'extra-rounded', + }, + }); + + const blob = await qrCodeInstance.getRawData('png') as Blob | null; + if (blob == null) throw new Error('Failed to generate QR code'); + + const qrImageBitmap = await window.createImageBitmap(blob); + + ctx.drawImage( + qrImageBitmap, + ctx.canvas.width - qrSize - qrMarginX, + (ctx.canvas.height - qrSize) / 2, + qrSize, + qrSize, + ); + qrImageBitmap.close(); + } + + const data = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height); + + const padding = params.frameThickness; + const paddingX = padding / aspectRatio; + const paddingY = padding; + + await this.effector.registerTexture('label', data); + + await this.effector.setLayers([{ + fxId: 'label', + id: 'a', + params: { + image: 'image', + label: 'label', + imageMarginX: paddingX, + imageMarginY: paddingY, + }, + }]); + this.render(); + } + + public render(): void { + this.effector.render(); + } + + /* + * disposeCanvas = true だとloseContextを呼ぶため、コンストラクタで渡されたcanvasも再利用不可になるので注意 + */ + public destroy(disposeCanvas = true): void { + this.effector.destroy(disposeCanvas); + } +} diff --git a/packages/frontend/src/utility/webgl.ts b/packages/frontend/src/utility/webgl.ts index ae595b605c..334663b1a1 100644 --- a/packages/frontend/src/utility/webgl.ts +++ b/packages/frontend/src/utility/webgl.ts @@ -38,3 +38,14 @@ export function initShaderProgram(gl: WebGL2RenderingContext, vsSource: string, return shaderProgram; } + +export function createTexture(gl: WebGL2RenderingContext): WebGLTexture { + const texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.bindTexture(gl.TEXTURE_2D, null); + return texture; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b5ffd355e4..6e45b17726 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -797,6 +797,9 @@ importers: execa: specifier: 9.6.0 version: 9.6.0 + exifreader: + specifier: 4.32.0 + version: 4.32.0 frontend-shared: specifier: workspace:* version: link:../frontend-shared @@ -4973,6 +4976,10 @@ packages: resolution: {integrity: sha512-siPY6BD5dQ2SZPl3I0OZBHL27ZqZvLEosObsZRQ1NUB8qcxegwt0T9eKtV96JMFQpIz1elhkzqOg4c/Ri6Dp9A==} engines: {node: ^14.14.0 || >=16.0.0} + '@xmldom/xmldom@0.9.8': + resolution: {integrity: sha512-p96FSY54r+WJ50FIOsCOjyj/wavs8921hG5+kVMmZgKcvIKxMXHTrjNJvRgWa/zuX3B6t2lijLNFaOyuxUH+2A==} + engines: {node: '>=14.6'} + abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} @@ -6505,6 +6512,9 @@ packages: resolution: {integrity: sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==} engines: {node: '>=4'} + exifreader@4.32.0: + resolution: {integrity: sha512-sj1PzjpaPwSE/2MeUqoAYcfc2u7AZOGSby0FzmAkB4jjeCXgDryxzVgMwV+tJKGIkGdWkkWiUWoLSJoPHJ6V5Q==} + exit@0.1.2: resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} engines: {node: '>= 0.8.0'} @@ -7212,6 +7222,7 @@ packages: intersection-observer@0.12.2: resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==} + deprecated: The Intersection Observer polyfill is no longer needed and can safely be removed. Intersection Observer has been Baseline since 2019. ioredis@5.8.1: resolution: {integrity: sha512-Qho8TgIamqEPdgiMadJwzRMW3TudIg6vpg4YONokGDudy4eqRIJtDbVX72pfLBcWxvbn3qm/40TyGUObdW4tLQ==} @@ -15621,6 +15632,9 @@ snapshots: dependencies: arch: 3.0.0 + '@xmldom/xmldom@0.9.8': + optional: true + abbrev@1.1.1: {} abbrev@3.0.1: {} @@ -17465,6 +17479,10 @@ snapshots: dependencies: pify: 2.3.0 + exifreader@4.32.0: + optionalDependencies: + '@xmldom/xmldom': 0.9.8 + exit@0.1.2: {} expand-template@2.0.3: