Skip to content

Commit

Permalink
Merge pull request #622 from danactive/add-heic-temp-generation
Browse files Browse the repository at this point in the history
Add heic temp generation
  • Loading branch information
danactive authored Jun 30, 2024
2 parents dbf5259 + 3f385dc commit a4a984a
Show file tree
Hide file tree
Showing 13 changed files with 4,816 additions and 2,173 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ module.exports = {
ts: 'never',
tsx: 'never',
}], // support React in TypeScript
'no-restricted-syntax': 'off', // Node.js no longer needs to be transpiled so this is unnecessary
},
overrides: [
{
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v20.12.1
v20.15.0
6,718 changes: 4,605 additions & 2,113 deletions package-lock.json

Large diffs are not rendered by default.

70 changes: 36 additions & 34 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,53 +16,55 @@
"release": "standard-version"
},
"dependencies": {
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@hello-pangea/dnd": "^16.3.0",
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@hello-pangea/dnd": "^16.6.0",
"@mui/joy": "^5.0.0-beta.32",
"boom": "^7.3.0",
"camelcase": "^6.3.0",
"color-thief-react": "^2.1.0",
"glob": "^10.3.4",
"mapbox-gl": "^2.15.0",
"glob": "^10.4.2",
"heic-convert": "^2.1.0",
"mapbox-gl": "^3.4.0",
"mime-types": "^2.1.35",
"next": "^14.1.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"next": "^14.2.4",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-image-gallery": "^1.3.0",
"react-map-gl": "^7.1.5",
"stylis": "^4.3.0",
"react-map-gl": "^7.1.7",
"stylis": "^4.3.2",
"xml2js": "^0.6.2"
},
"devDependencies": {
"@playwright/test": "^1.37.1",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^14.3.1",
"@types/boom": "^7.3.2",
"@types/geojson": "^7946.0.10",
"@types/jest": "^29.5.4",
"@types/mime-types": "^2.1.1",
"@types/node": "^20.5.9",
"@types/react": "^18.2.21",
"@types/react-dom": "^18.2.7",
"@types/react-image-gallery": "^1.2.0",
"@types/xml2js": "^0.4.12",
"eslint": "^8.48.0",
"@playwright/test": "^1.45.0",
"@testing-library/jest-dom": "^6.4.6",
"@testing-library/react": "^15.0.7",
"@types/boom": "^7.3.5",
"@types/geojson": "^7946.0.14",
"@types/heic-convert": "^1.2.3",
"@types/jest": "^29.5.12",
"@types/mime-types": "^2.1.4",
"@types/node": "^20.14.9",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/react-image-gallery": "^1.2.4",
"@types/xml2js": "^0.4.14",
"eslint": "^8.57.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-next": "^14.1.4",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-jest-dom": "^5.1.0",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-testing-library": "^6.0.1",
"eslint-config-next": "^14.2.4",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jest-dom": "^5.4.0",
"eslint-plugin-jsx-a11y": "^6.9.0",
"eslint-plugin-react": "^7.34.3",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-testing-library": "^6.2.2",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"next-test-api-route-handler": "^4.0.5",
"snyk": "^1.1211.0",
"next-test-api-route-handler": "^4.0.8",
"snyk": "^1.1292.1",
"standard-version": "^9.5.0",
"ts-jest": "^29.1.1",
"typescript": "^5.2.2"
"ts-jest": "^29.1.5",
"typescript": "^5.5.2"
},
"engines": {
"node": "20",
Expand Down
32 changes: 25 additions & 7 deletions pages/admin/walk.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { List, ListDivider } from '@mui/joy'
import { useRouter } from 'next/router'
import { Fragment, useEffect, useState } from 'react'
import {
Fragment, useEffect, useRef, useState,
} from 'react'

import OrganizePreviews from '../../src/components/OrganizePreviews'
import ListFile from '../../src/components/Walk/ListFile'
import type { Filesystem, FilesystemBody } from '../../src/lib/filesystems'
import type { Filesystem, FilesystemResponseBody } from '../../src/lib/filesystems'
import type { HeifResponseBody } from '../../src/lib/heifs'
import {
addParentDirectoryNav,
isImage,
Expand All @@ -26,17 +29,32 @@ function WalkPage() {
const [previewList, setPreviewList] = useState<Filesystem[] | null>(null)
const [isLoading, setLoading] = useState(false)
const pathQs = parseHash('path', asPath)
const lastFetchedPath = useRef('')

useEffect(() => {
setLoading(true)
fetch(`/api/admin/filesystems?path=${pathQs ?? '/'}`)
.then((response) => response.json())
.then((result: FilesystemBody) => {
if (asPath !== lastFetchedPath.current) {
setLoading(true)
const fetchData = async () => {
const response = await fetch(`/api/admin/filesystems?path=${pathQs}`)
const resultPossibleHeif: FilesystemResponseBody = await response.json()
const heifResponse = await fetch('/api/admin/heifs', {
body: JSON.stringify({ files: resultPossibleHeif.files, destinationPath: pathQs }),
headers: { 'Content-Type': 'application/json' },
method: 'POST',
})
const resultHeif: HeifResponseBody = await heifResponse.json()
// eslint-disable-next-line no-console
console.log(`Newly created HEIF files ${resultHeif.created.length}`)
const resultResponse = await fetch(`/api/admin/filesystems?path=${pathQs}`)
const result: FilesystemResponseBody = await resultResponse.json()
setLoading(false)
setFileList(result.files)
const itemImages = result.files.filter((file) => isImage(file))
setPreviewList(itemImages)
})
}
fetchData()
lastFetchedPath.current = asPath
}
}, [asPath])

if (isLoading) return <p>Loading...</p>
Expand Down
15 changes: 15 additions & 0 deletions pages/api/admin/heifs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { NextApiRequest, NextApiResponse } from 'next'

import post from '../../../../src/lib/heifs'

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { method, body } = req
switch (method) {
case 'POST': {
const out = await post(body.files, body.destinationPath, true)
return res.status(out.status).json(out.body)
}
default:
return res.status(405)
}
}
4 changes: 2 additions & 2 deletions src/components/OrganizePreviews/ActionButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export default function ActionButtons(
) {
const { asPath } = useRouter()
const pathQs = parseHash('path', asPath)
const path = pathQs ?? '/'
function rename() {
// eslint-disable-next-line no-alert
const date = window.prompt('Date of images (YYYY-MM-DD)?')
Expand All @@ -19,7 +18,7 @@ export default function ActionButtons(
const postBody = {
filenames: items.map((i) => i.filename),
prefix: date,
source_folder: path,
source_folder: pathQs,
preview: false,
raw: true,
rename_associated: true,
Expand All @@ -38,6 +37,7 @@ export default function ActionButtons(
-i http://127.0.0.1:8000/admin/rename -H "Content-Type: application/json"
*/

// eslint-disable-next-line no-console
return fetch('/api/admin/rename', options).then((s) => console.log(s))
}

Expand Down
5 changes: 5 additions & 0 deletions src/lib/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ describe('Utils library', () => {
const expected = 'filename.jpg'
expect(actual).toBe(expected)
})
test('heif', () => {
const actual = unit('filename.heic.heif')
const expected = 'filename.heic.jpg'
expect(actual).toBe(expected)
})
})

test('thumbPath', () => {
Expand Down
24 changes: 12 additions & 12 deletions src/lib/filesystems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,20 @@ import path from 'node:path'
import utilsFactory from './utils'
import transform, { type Filesystem } from '../models/filesystems'

type ErrorOptionalMessage = { files: object[]; error?: { message: string } }
type ResponseBody = {
files: Filesystem[];
destinationPath: string;
}

type ErrorOptionalMessage = ResponseBody & { error?: { message: string } }
const errorSchema = (message: string): ErrorOptionalMessage => {
const out = { files: [] }
const out = { files: [], destinationPath: '' }
if (!message) return out
return { ...out, error: { message } }
}

type FilesystemBody = {
files: Filesystem[];
destinationPath: string;
}

type FilesystemEnvelope = {
body: FilesystemBody;
type ResponseEnvelope = {
body: ResponseBody;
status: number;
}

Expand All @@ -28,7 +28,7 @@ type ErrorOptionalMessageBody = {
async function get<T extends boolean = false>(
destinationPath: string | string[] | undefined,
returnEnvelope?: T,
): Promise<T extends true ? FilesystemEnvelope : FilesystemBody>;
): Promise<T extends true ? ResponseEnvelope : ResponseBody>;

/**
* Get file/folder listing from local filesystem
Expand All @@ -40,7 +40,7 @@ async function get(
destinationPath: string | string[] | undefined = '',
returnEnvelope = false,
): Promise<
FilesystemEnvelope | FilesystemBody | ErrorOptionalMessage | ErrorOptionalMessageBody
ResponseEnvelope | ResponseBody | ErrorOptionalMessage | ErrorOptionalMessageBody
> {
try {
if (destinationPath === null || destinationPath === undefined || Array.isArray(destinationPath)) {
Expand Down Expand Up @@ -73,5 +73,5 @@ async function get(
}
}

export { errorSchema, type Filesystem, type FilesystemBody }
export { errorSchema, type Filesystem, type ResponseBody as FilesystemResponseBody }
export default get
101 changes: 101 additions & 0 deletions src/lib/heifs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import convert from 'heic-convert'
import { readFile, writeFile } from 'node:fs/promises'
import { basename } from 'node:path'
import type { Filesystem } from './filesystems'
import utilsFactory, { isStandardError } from './utils'

type ResponseBody = {
created: string[];
}

type ErrorOptionalMessage = ResponseBody & { error?: { message: string } }
const errorSchema = (message: string): ErrorOptionalMessage => {
const out = { created: [] }
if (!message) return out
return { ...out, error: { message } }
}

type ResponseEnvelope = {
body: ResponseBody;
status: number;
}

function uniqueHeifs(files: Filesystem[]) {
const groupedFiles = files.reduce((groups: Record<string, Filesystem[]>, file) => {
const nameWithoutExt = basename(file.name, file.ext)
if (!groups[nameWithoutExt]) {
// eslint-disable-next-line no-param-reassign
groups[nameWithoutExt] = []
}
groups[nameWithoutExt].push(file)
return groups
}, {})

const heifFilesWithoutJpg = Object.values(groupedFiles)
.filter((filteredFiles) => filteredFiles.some((file) => file.ext === 'heic') && !filteredFiles.some((file) => file.ext === 'jpg'))
.flat()

return heifFilesWithoutJpg
}

const utils = utilsFactory()
async function processHeif(file: Filesystem, destinationPath: string): Promise<string> {
const filenameHeif = utils.filenameAsJpg(file.filename)
// eslint-disable-next-line no-await-in-loop
const inputBuffer = await readFile(`public/${file.path}`)
// eslint-disable-next-line no-await-in-loop
const outputBuffer = await convert({
buffer: inputBuffer, // the HEIF file buffer
format: 'JPEG', // output format
quality: 0.8, // the jpeg compression quality, between 0 and 1
})
// eslint-disable-next-line no-await-in-loop
await writeFile(`public${destinationPath}/${filenameHeif}`, new Uint8Array(outputBuffer))
return filenameHeif
}

async function post<T extends boolean = false>(
files: Filesystem[],
destinationPath: string,
returnEnvelope?: T,
): Promise<T extends true ? ResponseEnvelope : ResponseBody>;

/**
* Generate a photo image from HEIF files
* @param {string} destinationPath path to save the converted files
* @param {boolean} returnEnvelope will enable a return value with HTTP status code and body
* @returns {Promise} files
*/
async function post(
files: Filesystem[],
destinationPath: string,
returnEnvelope = false,
) {
try {
const heifs: string[] = []
for (const file of uniqueHeifs(files)) {
// eslint-disable-next-line no-await-in-loop
heifs.push(await processHeif(file, destinationPath))
}

const body = { created: heifs }
if (returnEnvelope) {
return { body, status: 200 }
}

return body
} catch (e) {
if (returnEnvelope && isStandardError(e)) {
return { body: errorSchema(e.message), status: 400 }
}

if (returnEnvelope) {
return { body: errorSchema('Fail to process HEIFs'), status: 400 }
}

throw e
}
}

export { type ResponseBody as HeifResponseBody }
export default post
13 changes: 11 additions & 2 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import type {

const type = (filepath: string): string => {
if (filepath.lastIndexOf('.') === 0) {
return path.parse(filepath).name.substr(1)
return path.parse(filepath).name.substring(1)
}

return path.extname(filepath).substr(1)
return path.extname(filepath).substring(1)
}

const filenameAsJpg = (filename: Item['filename'][0]) => {
Expand Down Expand Up @@ -75,6 +75,14 @@ function customMime(rawExtension: string) {
return false
}

function isStandardError(error: unknown): error is Error {
if (error instanceof Error) return true
if ('message' in (error as any) && 'stack' in (error as any)) {
return true
}
return false
}

function utils() {
return {
type,
Expand Down Expand Up @@ -133,3 +141,4 @@ function utils() {
}

export default utils
export { isStandardError }
Loading

0 comments on commit a4a984a

Please sign in to comment.