Skip to content

Commit

Permalink
feat: resize oversize images before sending (#235)
Browse files Browse the repository at this point in the history
* feature: add auto resize

* fix: lint

* use jimp instead of sharp, move resize logic to new function

* use canvas instead of jimp

* revert headers get method, add docs

* optimize resize logic

* fix

* style: description

* fix

* revert docs

* optimise

* docs

* lint fix

* fix

---------

Co-authored-by: 霞飛 <[email protected]>
Co-authored-by: Maiko Tan <[email protected]>
  • Loading branch information
3 people authored Aug 5, 2024
1 parent 39a3ccb commit 6da0d7f
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 4 deletions.
9 changes: 9 additions & 0 deletions docs/zh-CN/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@

设置输出的图片尺寸。

### autoResize

- 类型: `boolean`
- 默认值: `false`

根据 preferSize 自动缩小过大的图片。

- 需要安装提供 canvas 服务的插件

### asset

- 类型: `boolean`
Expand Down
4 changes: 3 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
"booru"
],
"optional": [
"assets"
"assets",
"canvas"
]
}
},
Expand All @@ -50,6 +51,7 @@
"devDependencies": {
"@cordisjs/plugin-proxy-agent": ">=0.3.3",
"@koishijs/assets": "^1.0.2",
"@koishijs/canvas": "^0.2.0",
"koishi": "^4.17.0"
},
"dependencies": {
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/command.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable no-fallthrough */
import { Channel, Context, Random, Session, User } from 'koishi'

import { Config, OutputType, SpoilerType, preferSizes } from '.'
import { Config, OutputType, SpoilerType, preferSizes, sizeNameToFixedWidth } from '.'

export const inject = {
required: ['booru'],
Expand Down Expand Up @@ -82,6 +82,9 @@ export function apply(ctx: Context, config: Config) {
}
}
url ||= image.url
if (session.resolve(config.autoResize) && sizeNameToFixedWidth[config.preferSize]) {
url = await ctx.booru.resizeImageToFixedWidth(url, sizeNameToFixedWidth[config.preferSize])
}

if (config.asset && ctx.assets) {
url = await ctx.booru.imgUrlToAssetUrl(url)
Expand Down
62 changes: 60 additions & 2 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Context, Logger, Quester, Schema, Service, remove } from 'koishi'
import { Computed, Context, Logger, Quester, Schema, Service, remove } from 'koishi'
import LanguageDetect from 'languagedetect'

import * as Command from './command'
import { ImageSource } from './source'
import {} from '@koishijs/assets'
import {} from '@koishijs/canvas'

export * from './source'

Expand All @@ -18,7 +19,7 @@ declare module 'koishi' {
class ImageService extends Service {
static inject = {
required: [],
optional: ['assets'],
optional: ['assets', 'canvas'],
}

private sources: ImageSource[] = []
Expand Down Expand Up @@ -90,6 +91,59 @@ class ImageService extends Service {
return undefined
}

async resizeImageToFixedWidth(url: string, size: number): Promise<string> {
if (!size || size < 0) {
return url
}
if (!this.ctx.canvas) {
logger.warn('Canvas service is not available, thus cannot resize image now.')
return url
}
const resp = await this.ctx
.http(url, { method: 'GET', responseType: 'arraybuffer', proxyAgent: '' })
.catch((err) => {
if (Quester.Error.is(err)) {
logger.warn(
`Request images failed with HTTP status ${err.response?.status}: ${JSON.stringify(err.response?.data)}.`,
)
} else {
logger.error(`Request images failed with unknown error: ${err.message}.`)
}
return null
})
if (!resp?.data) {
return url
}

const buffer = Buffer.from(resp.data)
url = `data:${resp.headers.get('content-type')};base64,${buffer.toString('base64')}`
try {
const img = await this.ctx.canvas.loadImage(buffer)
let width = img.naturalWidth
let height = img.naturalHeight
const ratio = size / Math.max(width, height)
if (ratio < 1) {
width = Math.floor(width * ratio)
height = Math.floor(height * ratio)
const canvas = await this.ctx.canvas.createCanvas(width, height)
const ctx2d = canvas.getContext('2d')
ctx2d.drawImage(img, 0, 0, width, height)
url = await canvas.toDataURL('image/png')
if (typeof canvas.dispose === 'function') {
// skia-canvas does not have this method
await canvas.dispose()
}
}
if (typeof img.dispose === 'function') {
await img.dispose()
}
return url
} catch (err) {
logger.error(`Resize image failed with error: ${err.message}.`)
return url
}
}

async imgUrlToAssetUrl(url: string): Promise<string> {
return await this.ctx.assets.upload(url, Date.now().toString()).catch(() => {
logger.warn('Request failed when trying to store image with assets service.')
Expand Down Expand Up @@ -144,6 +198,7 @@ export interface Config {
output: OutputType
outputMethod: 'one-by-one' | 'merge-multiple' | 'forward-all' | 'forward-multiple'
preferSize: ImageSource.PreferSize
autoResize: Computed<boolean>
nsfw: boolean
asset: boolean
base64: boolean
Expand Down Expand Up @@ -199,6 +254,9 @@ export const Config = Schema.intersect([
])
.description('优先使用图片的最大尺寸。')
.default('large'),
autoResize: Schema.computed(Schema.boolean())
.default(false)
.description('根据 preferSize 自动缩小过大的图片。<br/> - 需要安装提供 canvas 服务的插件'),
asset: Schema.boolean().default(false).description('优先使用 [assets服务](https://assets.koishi.chat/) 转存图片。'),
base64: Schema.boolean().default(false).description('使用 base64 发送图片。'),
spoiler: Schema.union([
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,9 @@ export namespace ImageSource {
}

export const preferSizes = ['thumbnail', 'large', 'medium', 'small', 'original'] as const
export const sizeNameToFixedWidth: Partial<Record<(typeof preferSizes)[number], number>> = {
thumbnail: 128,
small: 320,
medium: 640,
large: 1280,
} as const

0 comments on commit 6da0d7f

Please sign in to comment.