Skip to content

Commit

Permalink
optimize image
Browse files Browse the repository at this point in the history
  • Loading branch information
ITJesse committed Jan 15, 2023
1 parent 550c682 commit 38818ea
Show file tree
Hide file tree
Showing 9 changed files with 521 additions and 14 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,7 @@ dist
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

optimizer.config.js

/cache/*
Empty file added cache/.gitkeep
Empty file.
5 changes: 5 additions & 0 deletions optimizer.config.example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
domains: [
// Add your domains here
],
}
10 changes: 9 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@
"typescript": "^4.7.4"
},
"dependencies": {
"dotenv": "^16.0.3"
"@types/sharp": "^0.31.1",
"axios": "^1.2.2",
"dotenv": "^16.0.3",
"fp-ts": "^2.13.1",
"io-ts": "^2.2.20",
"io-ts-types": "^0.5.19",
"monocle-ts": "^2.3.13",
"newtype-ts": "^0.3.5",
"sharp": "^0.31.3"
}
}
6 changes: 6 additions & 0 deletions src/consts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const AVIF = 'image/avif'
export const WEBP = 'image/webp'
export const PNG = 'image/png'
export const JPEG = 'image/jpeg'
export const GIF = 'image/gif'
export const SVG = 'image/svg+xml'
19 changes: 19 additions & 0 deletions src/lib/fp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export * as A from 'fp-ts/Array'
export * as E from 'fp-ts/Either'
export * as Eq from 'fp-ts/Eq'
export * as IO from 'fp-ts/IO'
export * as IOE from 'fp-ts/IOEither'
export * as M from 'fp-ts/Monoid'
export * as O from 'fp-ts/Option'
export * as Ord from 'fp-ts/Ord'
export * as R from 'fp-ts/Record'
export * as T from 'fp-ts/Task'
export * as TE from 'fp-ts/TaskEither'

export * as B from 'fp-ts/boolean'
export * as N from 'fp-ts/number'
export * as NEA from 'fp-ts/NonEmptyArray'
export * as S from 'fp-ts/string'
export * as Sg from 'fp-ts/Semigroup'

export * as D from 'io-ts/Decoder'
43 changes: 43 additions & 0 deletions src/lib/optimizer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import sharp, { Sharp } from 'sharp'

import { AVIF, JPEG, PNG, WEBP } from '@/consts'

export function optimizeImage({
contentType,
quality,
width,
height,
}: {
contentType: string
quality: number
width?: number
height?: number
}): Sharp {
const transformer = sharp({
sequentialRead: true,
})

transformer.rotate()

if (width || height) {
transformer.resize(width, height, {
withoutEnlargement: true,
})
}

if (contentType === AVIF) {
const avifQuality = quality - 15
transformer.avif({
quality: Math.max(avifQuality, 0),
chromaSubsampling: '4:2:0', // same as webp
})
} else if (contentType === WEBP) {
transformer.webp({ quality })
} else if (contentType === PNG) {
transformer.png({ quality })
} else if (contentType === JPEG) {
transformer.jpeg({ quality })
}

return transformer
}
95 changes: 87 additions & 8 deletions src/server.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,93 @@
require('dotenv').config()
import axios from 'axios'
import { pipe } from 'fp-ts/lib/function'
import { NumberFromString } from 'io-ts-types'
import http from 'node:http'
import querystring from 'node:querystring'

// Create a local server to receive data from
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(
JSON.stringify({
data: 'Hello World!',
}),
)
import config from '../optimizer.config'
import pkg from '../package.json'
import { D, O } from './lib/fp'
import { optimizeImage } from './lib/optimizer'

export const paramsDecoder = (params: any) => ({
url: pipe(D.string.decode(params.url), O.fromEither, O.toUndefined),
width: pipe(
NumberFromString.decode(params.width),
O.fromEither,
O.toUndefined,
),
height: pipe(
NumberFromString.decode(params.width),
O.fromEither,
O.toUndefined,
),
quality: pipe(
NumberFromString.decode(params.quality),
O.fromEither,
O.getOrElse(() => 75),
),
})

const client = axios.create({
headers: {
'User-Agent': `Kemono Games Image Optimizer/${pkg.version}}`,
'Accept-Encoding': 'br;q=1.0, gzip;q=0.8, *;q=0.1',
},
responseType: 'stream',
timeout: 10000,
})

const server = http.createServer(async (req, res) => {
const { method, url, headers } = req

if (method !== 'GET') {
res.writeHead(405)
return res.end('Method not allowed')
}

const qs = url.split('?')[1] ?? ''
const params = paramsDecoder(querystring.parse(qs))
if (!params.url) {
res.writeHead(400)
return res.end('Missing url parameter')
}

let imageUrl: URL
try {
imageUrl = new URL(params.url)
} catch (err) {
res.writeHead(400)
return res.end(err.message || 'Invalid URL')
}

const allowDomains = config.domains ?? []
if (!allowDomains.includes(imageUrl.hostname)) {
res.writeHead(400)
return res.end('Domain not allowed')
}

console.log(imageUrl)
const { data } = await client.get(imageUrl.toString())
const { accept } = headers
const acceptFormats = accept
?.split(',')
.map((e) => e.split(';'))
.flat()
.filter((e) => e.startsWith('image/'))
console.log(acceptFormats)
const targetFormat = acceptFormats[0] ?? 'image/jpeg'
const transformer = optimizeImage({
contentType: targetFormat,
width: params.width,
height: params.height,
quality: params.quality,
})
res.writeHead(200, {
'Content-Type': targetFormat,
})
transformer.pipe(res)
data.pipe(transformer)
})

server.listen(process.env.PORT || '3100')
Loading

0 comments on commit 38818ea

Please sign in to comment.