Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: resize oversize images before sending #235

Merged
merged 15 commits into from
Aug 5, 2024
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
SkyTNT marked this conversation as resolved.
Show resolved Hide resolved
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