mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-27 03:54:57 +02:00
UBERF-10382 Support preview for HEIC and BMP (#9790)
* UBERF-10382 Support preview for HEIC and BMP Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * fix: get rid of any Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> --------- Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
Generated
+37
-1
@@ -1675,6 +1675,9 @@ importers:
|
||||
'@types/express-form-data':
|
||||
specifier: ^2.0.5
|
||||
version: 2.0.5
|
||||
'@types/heic-decode':
|
||||
specifier: ^1.1.2
|
||||
version: 1.1.2
|
||||
'@types/html-to-text':
|
||||
specifier: ^8.1.1
|
||||
version: 8.1.1
|
||||
@@ -1813,6 +1816,9 @@ importers:
|
||||
blurhash:
|
||||
specifier: ^2.0.5
|
||||
version: 2.0.5
|
||||
bmp-ts:
|
||||
specifier: ^1.0.9
|
||||
version: 1.0.9
|
||||
body-parser:
|
||||
specifier: ^1.20.3
|
||||
version: 1.20.3
|
||||
@@ -2020,6 +2026,9 @@ importers:
|
||||
graphql-binding:
|
||||
specifier: ^2.5.2
|
||||
version: 2.5.2
|
||||
heic-decode:
|
||||
specifier: ^2.1.0
|
||||
version: 2.1.0
|
||||
highlight.js:
|
||||
specifier: ^11.11.1
|
||||
version: 11.11.1
|
||||
@@ -5569,7 +5578,7 @@ packages:
|
||||
version: 0.0.0
|
||||
|
||||
'@rush-temp/pod-preview@file:projects/pod-preview.tgz':
|
||||
resolution: {integrity: sha512-APb+wu9FGvuc2FCTz9nGyrGZwA4+o5uL6WWPdRhg1zeC9UniDdSA2InqNP03CxK8fayAwTMkTN9BF3C2g3w2lQ==, tarball: file:projects/pod-preview.tgz}
|
||||
resolution: {integrity: sha512-67MM8HopF5FhImVD91kHzl4UuyYk8WXHYXG+4Wz+jO1py2l/nvW9kNj9A1vPHXfy6clmsAsUMdAlLLLJE7ilmg==, tarball: file:projects/pod-preview.tgz}
|
||||
version: 0.0.0
|
||||
|
||||
'@rush-temp/pod-print@file:projects/pod-print.tgz':
|
||||
@@ -7162,6 +7171,9 @@ packages:
|
||||
'@types/hast@3.0.4':
|
||||
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
|
||||
|
||||
'@types/heic-decode@1.1.2':
|
||||
resolution: {integrity: sha512-E1Dw+gpIuiHJcTpFBHm+V/6eYsPhtWook1Z6MIG0H1ONWBp9Z6l3FKydUxFw9GL9JqrWB1qa/uQHdbn/KUeXYw==}
|
||||
|
||||
'@types/html-minifier-terser@6.1.0':
|
||||
resolution: {integrity: sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==}
|
||||
|
||||
@@ -8082,6 +8094,9 @@ packages:
|
||||
bmp-js@0.1.0:
|
||||
resolution: {integrity: sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==}
|
||||
|
||||
bmp-ts@1.0.9:
|
||||
resolution: {integrity: sha512-cTEHk2jLrPyi+12M3dhpEbnnPOsaZuq7C45ylbbQIiWgDFZq4UVYPEY5mlqjvsj/6gJv9qX5sa+ebDzLXT28Vw==}
|
||||
|
||||
bn.js@4.12.0:
|
||||
resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==}
|
||||
|
||||
@@ -10297,6 +10312,10 @@ packages:
|
||||
resolution: {integrity: sha512-trFMIq3PATiFRiQmNNeHtsrkwYRByIXUbYNbotiY9RLVfMkdwZdd2eQ38mGt7BRiCKBaj1DyBAIHmm7mmXPuuw==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
||||
heic-decode@2.1.0:
|
||||
resolution: {integrity: sha512-0fB3O3WMk38+PScbHLVp66jcNhsZ/ErtQ6u2lMYu/YxXgbBtl+oKOhGQHa4RpvE68k8IzbWkABzHnyAIjR758A==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
hexoid@1.0.0:
|
||||
resolution: {integrity: sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -11285,6 +11304,10 @@ packages:
|
||||
engines: {node: '>=16'}
|
||||
hasBin: true
|
||||
|
||||
libheif-js@1.19.8:
|
||||
resolution: {integrity: sha512-vQJWusIxO7wavpON1dusciL8Go9jsIQ+EUrckauFYAiSTjcmLAsuJh3SszLpvkwPci3JcL41ek2n+LUZGFpPIQ==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
libphonenumber-js@1.10.56:
|
||||
resolution: {integrity: sha512-d0GdKshNnyfl5gM7kZ9rXjGiAbxT/zCXp0k+EAzh8H4zrb2R7GXtMCrULrX7UQxtfx6CLy/vz/lomvW79FAFdA==}
|
||||
|
||||
@@ -25626,12 +25649,14 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/cors': 2.8.17
|
||||
'@types/express': 4.17.21
|
||||
'@types/heic-decode': 1.1.2
|
||||
'@types/jest': 29.5.12
|
||||
'@types/morgan': 1.9.9
|
||||
'@types/node': 22.15.29
|
||||
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3)
|
||||
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3)
|
||||
blurhash: 2.0.5
|
||||
bmp-ts: 1.0.9
|
||||
cors: 2.8.5
|
||||
cross-env: 7.0.3
|
||||
dotenv: 16.0.3
|
||||
@@ -25643,6 +25668,7 @@ snapshots:
|
||||
eslint-plugin-node: 11.1.0(eslint@8.56.0)
|
||||
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
|
||||
express: 4.21.2
|
||||
heic-decode: 2.1.0
|
||||
jest: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))
|
||||
lru-cache: 11.1.0
|
||||
morgan: 1.10.0
|
||||
@@ -32109,6 +32135,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/unist': 2.0.10
|
||||
|
||||
'@types/heic-decode@1.1.2': {}
|
||||
|
||||
'@types/html-minifier-terser@6.1.0': {}
|
||||
|
||||
'@types/html-to-text@8.1.1': {}
|
||||
@@ -33246,6 +33274,8 @@ snapshots:
|
||||
|
||||
bmp-js@0.1.0: {}
|
||||
|
||||
bmp-ts@1.0.9: {}
|
||||
|
||||
bn.js@4.12.0: {}
|
||||
|
||||
body-parser@1.20.3:
|
||||
@@ -35944,6 +35974,10 @@ snapshots:
|
||||
|
||||
heap-js@2.6.0: {}
|
||||
|
||||
heic-decode@2.1.0:
|
||||
dependencies:
|
||||
libheif-js: 1.19.8
|
||||
|
||||
hexoid@1.0.0: {}
|
||||
|
||||
highlight.js@11.11.1: {}
|
||||
@@ -37214,6 +37248,8 @@ snapshots:
|
||||
dependencies:
|
||||
isomorphic.js: 0.2.5
|
||||
|
||||
libheif-js@1.19.8: {}
|
||||
|
||||
libphonenumber-js@1.10.56: {}
|
||||
|
||||
lie@3.3.0:
|
||||
|
||||
@@ -51,7 +51,8 @@
|
||||
"typescript": "^5.8.3",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/morgan": "~1.9.9"
|
||||
"@types/morgan": "~1.9.9",
|
||||
"@types/heic-decode": "^1.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/core": "^0.6.32",
|
||||
@@ -69,6 +70,8 @@
|
||||
"cors": "^2.8.5",
|
||||
"morgan": "^1.10.0",
|
||||
"node-poppler": "^8.0.4",
|
||||
"lru-cache": "^11.1.0"
|
||||
"lru-cache": "^11.1.0",
|
||||
"heic-decode": "^2.1.0",
|
||||
"bmp-ts": "^1.0.9"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ import { pipeline } from 'stream/promises'
|
||||
import { getImageMetadata } from '../metadata'
|
||||
import { TemporaryDir } from '../tempdir'
|
||||
import { type PreviewFile, type PreviewMetadata, type PreviewProvider } from '../types'
|
||||
import { bmpToPng } from '../utils/bmp'
|
||||
import { heicToPng } from '../utils/heic'
|
||||
|
||||
export class ImageProvider implements PreviewProvider {
|
||||
constructor (
|
||||
@@ -41,6 +43,20 @@ export class ImageProvider implements PreviewProvider {
|
||||
await pipeline(stream, createWriteStream(path))
|
||||
})
|
||||
|
||||
// Handle HEIC conversion
|
||||
if (contentType === 'image/heic' || contentType === 'image/heif') {
|
||||
const pngFile = await heicToPng(ctx, path)
|
||||
this.tempDir.rm(path)
|
||||
return { mimeType: 'image/png', filePath: pngFile }
|
||||
}
|
||||
|
||||
// Handle BMP conversion
|
||||
if (contentType === 'image/bmp') {
|
||||
const pngFile = await bmpToPng(ctx, path)
|
||||
this.tempDir.rm(path)
|
||||
return { mimeType: 'image/png', filePath: pngFile }
|
||||
}
|
||||
|
||||
return { mimeType: contentType, filePath: path }
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { type MeasureContext } from '@hcengineering/core'
|
||||
import { readFile } from 'fs/promises'
|
||||
import { decode } from 'bmp-ts'
|
||||
import sharp from 'sharp'
|
||||
|
||||
export async function bmpToPng (ctx: MeasureContext, bmpFile: string): Promise<string> {
|
||||
const pngFile: string = bmpFile + '.png'
|
||||
|
||||
const buffer = await readFile(bmpFile)
|
||||
|
||||
const data = await ctx.with('bmp-decode', {}, () => {
|
||||
return decode(Buffer.from(buffer), { toRGBA: false })
|
||||
})
|
||||
|
||||
// Manually convert to RGBA as library does not seem to handle this properly
|
||||
for (let i = 0; i < data.data.length; i += 4) {
|
||||
// const a = data.data[i + 0]
|
||||
const b = data.data[i + 1]
|
||||
const g = data.data[i + 2]
|
||||
const r = data.data[i + 3]
|
||||
data.data[i + 0] = r
|
||||
data.data[i + 1] = g
|
||||
data.data[i + 2] = b
|
||||
data.data[i + 3] = 0xff
|
||||
}
|
||||
|
||||
await ctx.with('sharp', {}, async () => {
|
||||
await sharp(data.data, {
|
||||
raw: {
|
||||
width: data.width,
|
||||
height: data.height,
|
||||
channels: 4
|
||||
}
|
||||
})
|
||||
.png()
|
||||
.toFile(pngFile)
|
||||
})
|
||||
|
||||
return pngFile
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { type MeasureContext } from '@hcengineering/core'
|
||||
import { readFile } from 'fs/promises'
|
||||
import decodeHeic from 'heic-decode'
|
||||
import sharp from 'sharp'
|
||||
|
||||
export async function heicToPng (ctx: MeasureContext, heicFile: string): Promise<string> {
|
||||
const pngFile: string = heicFile + '.png'
|
||||
|
||||
const buffer = await readFile(heicFile)
|
||||
|
||||
const { data, width, height } = await ctx.with('heic-decode', {}, () =>
|
||||
decodeHeic({
|
||||
// @ts-expect-error wrong types
|
||||
buffer: new Uint8Array(buffer)
|
||||
})
|
||||
)
|
||||
|
||||
await ctx.with('sharp', {}, async () => {
|
||||
await sharp(data, {
|
||||
raw: {
|
||||
width,
|
||||
height,
|
||||
channels: 4
|
||||
}
|
||||
})
|
||||
.png()
|
||||
.toFile(pngFile)
|
||||
})
|
||||
|
||||
return pngFile
|
||||
}
|
||||
Reference in New Issue
Block a user