Migrate assets to webp and fix coin units

This commit is contained in:
2024-10-21 22:16:16 +01:00
parent 6a5b97c999
commit 2a9084f732
13 changed files with 1352 additions and 1320 deletions

1
.gitignore vendored
View File

@@ -37,6 +37,7 @@ next-env.d.ts
# assets
public/photos
public/photos-original
public/done
tmp

12
bin/build-textures.ts Normal file
View File

@@ -0,0 +1,12 @@
import { getTypes } from '@/api/types/get-types';
import { generateImage } from '@/core/utils/merge-textures';
const coins = (await getTypes({ category: 'coins' })).filter(coin => coin.shape !== 'Round');
for (const [coinIndex, coin] of coins.entries()) {
console.log('coin', coinIndex, 'of', coins.length);
await generateImage(coin.id, 'base');
await generateImage(coin.id, 'bump');
}
process.exit(0);

View File

@@ -4,10 +4,11 @@ import { NumistaType } from '@/types/numista';
import { asyncSpawn } from './async-spawn';
import puppeteer, { Page } from 'puppeteer';
import { writeFile } from 'fs/promises';
import { getTextureUrl } from '@/core/utils/texture-urls';
const baseDir = './public';
export const downloadFile = async (page: Page, url: string) => {
export const downloadFile = async (page: Page, url: string): Promise<[string, string, string?]> => {
const filepath = url.split('catalogue')[1];
const folder = path.join(baseDir, filepath.split('/').slice(0, -1).join('/'));
console.log(url);
@@ -20,7 +21,18 @@ export const downloadFile = async (page: Page, url: string) => {
await writeFile(destination, await pageRes.buffer());
}
return destination;
const destinationWebp = destination.replace(/\.\w*$/g, '.webp')
await asyncSpawn('cwebp', [destination, '-o', destinationWebp])
let color: string | undefined;
const palette = await Vibrant.from(destination).getPalette();
if (palette.Vibrant) {
color = 'rgb(' + palette.Vibrant.rgb.join(',') + ')';
}
return [destinationWebp, destination, color];
};
export const fetchTextures = async (type: NumistaType) => {
@@ -31,33 +43,40 @@ export const fetchTextures = async (type: NumistaType) => {
page.setViewport({ width: 1280, height: 926 });
if (type?.obverse?.picture) {
const destination = await downloadFile(page, type.obverse.picture);
type.obverse.picture = destination.split('public')[1];
texturesToBump.push(type.obverse.picture);
const palette = await Vibrant.from(destination).getPalette();
const [destinationWebp, destination, color] = await downloadFile(page, type.obverse.picture);
type.obverse.picture = destinationWebp.split('public')[1];
texturesToBump.push(`./public${destination.split('public')[1]}`);
if (palette.Vibrant) {
type.color = 'rgb(' + palette.Vibrant.rgb.join(',') + ')';
if (color) {
type.color = color;
}
}
if (type?.reverse?.picture) {
const destination = await downloadFile(page, type.reverse.picture);
type.reverse.picture = destination.split('public')[1];
texturesToBump.push(type.reverse.picture);
const [destinationWebp, destination] = await downloadFile(page, type.reverse.picture);
type.reverse.picture = destinationWebp.split('public')[1];
texturesToBump.push(`./public${destination.split('public')[1]}`);
}
if (type?.edge?.picture) {
const destination = await downloadFile(page, type.edge.picture);
type.edge.picture = destination.split('public')[1];
const [destinationWebp, destination] = await downloadFile(page, type.edge.picture);
type.edge.picture = destinationWebp.split('public')[1];
await asyncSpawn('rm', [`./public${destination.split('public')[1]}`])
}
await browser.close();
for (const texture in texturesToBump) {
await asyncSpawn('./bin/generate-depth.sh', [`./public${texture}`]);
const bumpPngs = []
for (const texture of texturesToBump) {
await asyncSpawn('./bin/generate-depth.sh', [texture]);
const bumpTextureWebp = getTextureUrl(texture, 'bump')
const bumpTexturePng = bumpTextureWebp.replace(/(\.[^/.]+)$/, '.png')
bumpPngs.push(bumpTexturePng)
await asyncSpawn('cwebp', [bumpTexturePng, '-o', bumpTextureWebp])
}
await asyncSpawn('rm', [...texturesToBump,...bumpPngs])
return type;
} catch (e) {
console.error(e);

View File

@@ -31,7 +31,7 @@ cp "$file" ./tmp
fi
cd marigold
pip install -r requirements.txt 2>&1 | grep -v "Requirement already satisfied"
pip install -r requirements.txt
PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True python run.py \
--input_rgb ../../tmp \

View File

@@ -1,7 +1,7 @@
import { NumistaType } from '@/types/numista';
import { db } from './db';
const bannedKeys = ['issuing_entity']
const bannedKeys = ['issuing_entity'];
const omit = (data: object, keys: string[] = []) =>
Object.fromEntries(

View File

@@ -4,10 +4,12 @@
"private": true,
"scripts": {
"dev": "next dev",
"prebuild": "bun build-textures",
"build": "next build",
"start": "next start",
"lint": "next lint",
"get-type": "bun ./bin/get-type.ts",
"build-textures": "bun ./bin/build-textures.ts",
"generate-depth": "./bin/generate-depth.sh"
},
"dependencies": {

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,6 @@
generator client {
provider = "prisma-client-js"
binaryTargets = ["native", "debian-openssl-3.0.x"]
}
datasource db {

View File

@@ -0,0 +1,144 @@
/* eslint-disable no-console */
import { getTextureUrl } from '@/core/utils/texture-urls';
import { getType } from '@/api/types/get-type';
import { readFile } from 'fs/promises';
import { spawn } from 'child_process';
import path from 'path';
const SIZE = 2048;
const asyncSpawn = (command: string, args: string[]) => {
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
stdio: 'inherit'
});
child.on('error', reject);
child.stdout?.on('data', process.stdout.write);
child.stderr?.on('data', process.stderr.write);
child.on('close', code => {
if (code === 0) {
resolve(code);
} else {
reject(code);
}
});
});
};
const getEdgeRepeatNumber = (shape?: string) => {
switch (shape?.toLowerCase?.()) {
case 'spanish flower':
return 7;
default:
return 4;
}
};
const getPreviousResult = async (id: number, type: string) => {
try {
return await readFile(path.join('./public/photos/joined', `${id}-${type}.webp`));
} catch {
return;
}
};
type CoinProps = Awaited<ReturnType<typeof getType>>;
export const generateImage = async (coinProps: CoinProps | number, type: 'base' | 'bump') => {
const id = typeof coinProps === 'number' ? coinProps : coinProps?.id;
if (!id) {
return;
}
const previousResult = await getPreviousResult(id, type);
if (previousResult) {
return previousResult;
}
const coin = typeof coinProps === 'number' ? await getType(id) : coinProps;
const edgeRepeatNumber = getEdgeRepeatNumber(coin?.shape);
if (!coin) {
return;
}
let notFounds = 0;
try {
await asyncSpawn('magick', [
path.join('./public', getTextureUrl(coin.obverse?.picture, type)),
'-resize',
`${SIZE}x${SIZE}!`,
path.join('./public/photos/joined', `${id}-${type}-obverse.webp`)
]);
} catch {
notFounds++;
await asyncSpawn('magick', [
'-size',
`${SIZE}x${SIZE}!`,
`xc:${coin.color}`,
path.join('./public/photos/joined', `${id}-${type}-obverse.webp`)
]);
}
try {
await asyncSpawn('magick', [
path.join('./public', getTextureUrl(coin.reverse?.picture, type)),
'-resize',
`${SIZE}x${SIZE}!`,
...(coin.orientation === 'coin' ? ['-rotate', '180'] : []),
path.join('./public/photos/joined', `${id}-${type}-reverse.webp`)
]);
} catch {
notFounds++;
await asyncSpawn('magick', [
'-size',
`${SIZE}x${SIZE}!`,
`xc:${coin.color}`,
path.join('./public/photos/joined', `${id}-${type}-reverse.webp`)
]);
}
try {
await asyncSpawn('magick', [
path.join('./public', getTextureUrl(coin.edge?.picture, type)),
'-resize',
`${SIZE * 2}x${SIZE / edgeRepeatNumber}!`,
path.join('./public/photos/joined', `${id}-${type}-edge.webp`)
]);
} catch {
notFounds++;
await asyncSpawn('magick', [
'-size',
`${SIZE * 2}x${SIZE / edgeRepeatNumber}!`,
`xc:${coin.color}`,
path.join('./public/photos/joined', `${id}-${type}-edge.webp`)
]);
}
if (notFounds >= 3) {
return;
}
await asyncSpawn('magick', [
path.join('./public/photos/joined', `${id}-${type}-obverse.webp`),
path.join('./public/photos/joined', `${id}-${type}-reverse.webp`),
'+append',
path.join('./public/photos/joined', `${id}-${type}.webp`)
]);
for (let index = 0; index < edgeRepeatNumber; index++) {
await asyncSpawn('magick', [
path.join('./public/photos/joined', `${id}-${type}.webp`),
path.join('./public/photos/joined', `${id}-${type}-edge.webp`),
'-append',
path.join('./public/photos/joined', `${id}-${type}.webp`)
]);
}
return await readFile(path.join('./public/photos/joined', `${id}-${type}.webp`));
};

View File

@@ -1,11 +1,11 @@
export function getTextureUrl(url?: string, type?: 'base' | 'bump') {
if(!url) {
throw new Error()
if (!url) {
throw new Error();
}
if(type === 'bump') {
return url?.replace(/(\.[^/.]+)$/, '_bump.png')
if (type === 'bump') {
return url?.replace(/(\.[^/.]+)$/, '_bump.webp');
}
return url
return url;
}

View File

@@ -2,10 +2,10 @@ import '@/styles/globals.css';
import '@/styles/theme.css';
import 'primeicons/primeicons.css';
import 'primereact/resources/primereact.min.css';
import { AppProps } from 'next/app';
import { PrimeReactProvider } from 'primereact/api';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import App, { AppContext, AppProps } from 'next/app';
import Div100vh from 'react-div-100vh';
const queryClient = new QueryClient();
@@ -25,16 +25,3 @@ export default function MyApp({ Component, pageProps }: AppProps) {
</>
);
}
MyApp.getInitialProps = async (appContext: AppContext) => {
const appProps = await App.getInitialProps(appContext);
if (appContext.ctx.res?.statusCode === 404) {
appContext.ctx.res.writeHead(302, { Location: '/' });
appContext.ctx.res.end();
return;
}
return { ...appProps };
};

View File

@@ -1,142 +1,8 @@
/* eslint-disable no-console */
import { NextApiHandler } from 'next';
import { getTextureUrl } from '@/core/utils/texture-urls';
import { getType } from '@/api/types/get-type';
import { readFile } from 'fs/promises';
import { spawn } from 'child_process';
import path from 'path';
import { generateImage } from '@/core/utils/merge-textures';
const SIZE = 2048
const asyncSpawn = (command: string, args: string[]) => {
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
stdio: 'inherit'
});
child.on('error', reject);
child.stdout?.on('data', process.stdout.write);
child.stderr?.on('data', process.stderr.write);
child.on('close', code => {
if (code === 0) {
resolve(code);
} else {
reject(code);
}
});
});
};
const getEdgeRepeatNumber = (shape?: string) => {
switch(shape?.toLowerCase?.()) {
case 'spanish flower':
return 7;
default:
return 4;
}
}
const getPreviousResult = async (id: number, type: string) => {
try {
return await readFile(path.join('./public/photos/joined', `${id}-${type}.jpg`));
} catch {
return;
}
};
const generateImage = async (id: number, type: 'base' | 'bump') => {
const previousResult = await getPreviousResult(id, type);
if (previousResult) {
return previousResult;
}
const coin = await getType(id);
const edgeRepeatNumber = getEdgeRepeatNumber(coin?.shape)
if (!coin) {
return;
}
let notFounds = 0;
try {
await asyncSpawn('magick', [
path.join('./public', getTextureUrl(coin.obverse?.picture, type)),
'-resize',
`${SIZE}x${SIZE}!`,
path.join('./public/photos/joined', `${id}-${type}-obverse.jpg`)
]);
} catch {
notFounds++;
await asyncSpawn('magick', [
'-size',
`${SIZE}x${SIZE}!`,
`xc:${coin.color}`,
path.join('./public/photos/joined', `${id}-${type}-obverse.jpg`)
]);
}
try {
await asyncSpawn('magick', [
path.join('./public', getTextureUrl(coin.reverse?.picture, type)),
'-resize',
`${SIZE}x${SIZE}!`,
...(coin.orientation === 'coin' ? ['-rotate', '180'] : []),
path.join('./public/photos/joined', `${id}-${type}-reverse.jpg`)
]);
} catch {
notFounds++;
await asyncSpawn('magick', [
'-size',
`${SIZE}x${SIZE}!`,
`xc:${coin.color}`,
path.join('./public/photos/joined', `${id}-${type}-reverse.jpg`)
]);
}
try {
await asyncSpawn('magick', [
path.join('./public', getTextureUrl(coin.edge?.picture, type)),
'-resize',
`${SIZE*2}x${SIZE/edgeRepeatNumber}!`,
path.join('./public/photos/joined', `${id}-${type}-edge.jpg`)
]);
} catch {
notFounds++;
await asyncSpawn('magick', [
'-size',
`${SIZE*2}x${SIZE/edgeRepeatNumber}!`,
`xc:${coin.color}`,
path.join('./public/photos/joined', `${id}-${type}-edge.jpg`)
]);
}
if(notFounds >= 3) {
return
}
await asyncSpawn('magick', [
path.join('./public/photos/joined', `${id}-${type}-obverse.jpg`),
path.join('./public/photos/joined', `${id}-${type}-reverse.jpg`),
'+append',
path.join('./public/photos/joined', `${id}-${type}.jpg`)
]);
for (let index = 0; index < edgeRepeatNumber; index++) {
await asyncSpawn('magick', [
path.join('./public/photos/joined', `${id}-${type}.jpg`),
path.join('./public/photos/joined', `${id}-${type}-edge.jpg`),
'-append',
path.join('./public/photos/joined', `${id}-${type}.jpg`)
]);
}
return await readFile(path.join('./public/photos/joined', `${id}-${type}.jpg`));
};
const handler: NextApiHandler = async ({ query: {id, type} }, res) => {
const handler: NextApiHandler = async ({ query: { id, type } }, res) => {
if (!Number(id)) {
res.status(400).json({
message: `Invalid id ${JSON.stringify(id)}`
@@ -145,7 +11,7 @@ const handler: NextApiHandler = async ({ query: {id, type} }, res) => {
return;
}
if(type && !(['bump', 'base'].includes(type as string))) {
if (type && !['bump', 'base'].includes(type as string)) {
res.status(400).json({
message: `Invalid type ${JSON.stringify(type)}`
});
@@ -153,7 +19,7 @@ const handler: NextApiHandler = async ({ query: {id, type} }, res) => {
return;
}
const parsedType = (type ?? 'base') as 'base' | 'bump'
const parsedType = (type ?? 'base') as 'base' | 'bump';
try {
const image = await generateImage(Number(id), parsedType);
@@ -166,7 +32,7 @@ const handler: NextApiHandler = async ({ query: {id, type} }, res) => {
return;
}
res.setHeader('Content-Type', 'image/jpg');
res.setHeader('Content-Type', 'image/webp');
res.status(200).send(image);
} catch (error) {
console.error(error);