Skip to content

Commit

Permalink
Merge pull request #791 from traPtitech/feat/client-image-resize
Browse files Browse the repository at this point in the history
クライアントサイドでのサムネイル生成
  • Loading branch information
spaspa authored May 7, 2020
2 parents 3fe1d04 + 7ed5bbd commit d9cdc06
Show file tree
Hide file tree
Showing 11 changed files with 244 additions and 36 deletions.
41 changes: 37 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@mdi/js": "^5.1.45",
"@traptitech/traq": "3.0.0-29",
"@traptitech/traq-markdown-it": "^2.2.5",
"@types/pica": "^5.1.0",
"@vue/composition-api": "^0.5.0",
"core-js": "^3.6.5",
"cropperjs": "^1.5.6",
Expand All @@ -26,6 +27,7 @@
"highlight.js": "^10.0.2",
"idb-keyval": "^3.2.0",
"lodash-es": "^4.17.15",
"pica": "^5.1.0",
"portal-vue": "^2.1.7",
"shvl": "^2.0.0",
"skyway-js": "^2.0.5",
Expand Down
7 changes: 4 additions & 3 deletions src/components/Main/MainView/MessageInput/use/postMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ const uploadAttachments = async (
attachments: Attachment[],
channelId: ChannelId
) => {
const responses = await Promise.all(
attachments.map(attachment => apis.postFile(attachment.file, channelId))
)
const responses = []
for (const attachment of attachments) {
responses.push(await apis.postFile(attachment.file, channelId))
}
return responses.map(res => buildFilePathForPost(res.data.id))
}

Expand Down
30 changes: 30 additions & 0 deletions src/lib/resize/canvas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Dimensions } from './size'

export const loadImage = (
url: string,
$img: HTMLImageElement
): Promise<string> => {
return new Promise(resolve => {
$img.addEventListener(
'load',
() => {
resolve()
},
{ once: true }
)
$img.src = url
})
}

export const resetCanvas = (
$canvas: HTMLCanvasElement,
{ width, height }: Dimensions,
$img?: HTMLImageElement
) => {
$canvas.getContext('2d')?.clearRect(0, 0, $canvas.width, $canvas.height)
$canvas.width = width
$canvas.height = height
if ($img) {
$canvas.getContext('2d')?.drawImage($img, 0, 0)
}
}
16 changes: 16 additions & 0 deletions src/lib/resize/dataurl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const convertToDataUrl = (file: File): Promise<string | null> => {
const reader = new FileReader()
reader.readAsDataURL(file)
return new Promise(resolve => {
reader.addEventListener(
'load',
event => {
// `readAsDataURL`を用いるため、結果の型はstring
// see: https://developer.mozilla.org/ja/docs/Web/API/FileReader/result
const thumbnailDataUrl = event.target?.result as string | null
resolve(thumbnailDataUrl)
},
{ once: true }
)
})
}
44 changes: 44 additions & 0 deletions src/lib/resize/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { start, finish, initVars } from './vars'
import { loadImage, resetCanvas } from './canvas'
import { needResize, getThumbnailDimensions } from './size'

export const canResize = (mime: string) =>
['image/png', 'image/jpeg'].includes(mime)

export const resize = async (inputFile: File): Promise<File | null> => {
start()
const { pica, $input, $output, $img } = await initVars()

const inputUrl = URL.createObjectURL(inputFile)

try {
await loadImage(inputUrl, $img)
const inputSize = {
width: $img.width,
height: $img.height
}
if (!needResize(inputSize)) {
return finish(null, inputUrl)
}

const outputSize = getThumbnailDimensions(inputSize)

resetCanvas($input, inputSize, $img)
resetCanvas($output, outputSize)

await pica.resize($input, $output, {
quality: 2
})
const output = await pica.toBlob($output, 'image/png')
const outputFile = new File([output], inputFile.name, {
type: inputFile.type,
lastModified: inputFile.lastModified
})

return finish(outputFile, inputUrl)
} catch (e) {
// eslint-disable-next-line no-console
console.warn(`Failed to generate thumbnail image: ${e}`, e)
return finish(null, inputUrl)
}
}
20 changes: 20 additions & 0 deletions src/lib/resize/size.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const MAX_HEIGHT = 1600
const MAX_WIDTH = 2560

export interface Dimensions {
width: number
height: number
}

export const needResize = ({ width, height }: Dimensions) =>
MAX_WIDTH < width || MAX_HEIGHT < height

export const getThumbnailDimensions = ({
width,
height
}: Dimensions): Dimensions => {
const widthRatio = width / MAX_WIDTH
const heightRatio = height / MAX_HEIGHT
const ratio = Math.max(widthRatio, heightRatio)
return { width: width / ratio, height: height / ratio }
}
53 changes: 53 additions & 0 deletions src/lib/resize/vars.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type Pica from 'pica'

interface ResizeVars {
pica: Pica
$input: HTMLCanvasElement
$output: HTMLCanvasElement
$img: HTMLImageElement
}

// 使いまわす
let resizeVars: ResizeVars | null = null
let resizing = false
const persistTime = 3000 // ms

const loadPica = async () => {
const Pica = (await import('pica')).default
return new Pica()
}

// 存在しなかったらつくる
export const initVars = async (): Promise<ResizeVars> => {
if (resizeVars !== null) return resizeVars

resizeVars = {
pica: await loadPica(),
$input: document.createElement('canvas'),
$output: document.createElement('canvas'),
$img: new Image()
}

setTimeout(deinitVars, persistTime)

return resizeVars
}
// 使っていないときに消す
export const deinitVars = () => {
if (resizing) {
setTimeout(deinitVars, persistTime)
return
}
resizeVars = null
}

// リサイズ中に消さないようにする
export const start = () => {
resizing = true
}
// リサイズ後にリサイズ中に消さないようにするフラグを消す
export const finish = <T>(output: T, inputUrl: string): T => {
URL.revokeObjectURL(inputUrl)
resizing = false
return output
}
63 changes: 34 additions & 29 deletions src/store/ui/fileInput/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { moduleActionContext } from '@/store'
import { fileInput } from './index'
import { mimeToFileType } from '@/lib/util/file'
import { ActionContext } from 'vuex'
import { convertToDataUrl } from '@/lib/resize/dataurl'
import { resize, canResize } from '@/lib/resize'

const imageSizeLimit = 20 * 1000 * 1000 // 20MB

Expand All @@ -11,40 +13,43 @@ export const fileInputActionContext = (
) => moduleActionContext(context, fileInput)

export const actions = defineActions({
addAttachment(context, file: File) {
async addAttachment(context, file: File) {
const { commit, state } = fileInputActionContext(context)
const fileType = mimeToFileType(file.type)

if (fileType === 'image' && file.size > imageSizeLimit) {
window.alert(
'画像サイズは20MBまでです\n大きい画像の共有にはDriveを使用してください'
)
return
}

if (fileType === 'image') {
if (file.size > imageSizeLimit) {
window.alert(
'画像サイズは20MBまでです\n大きい画像の共有にはDriveを使用してください'
)
return
}
const reader = new FileReader()
reader.readAsDataURL(file)
// 最後に追加されたもの
const index = state.attachments.length
const resizable = canResize(file.type)

reader.addEventListener(
'load',
event => {
// `readAsDataURL`を用いるため、結果の型はstring
// see: https://developer.mozilla.org/ja/docs/Web/API/FileReader/result
const thumbnailDataUrl = event.target?.result as string
if (!thumbnailDataUrl) {
return
}
commit.addThumbnailTo({
index,
thumbnailDataUrl
})
},
{ once: true }
)
let resizedFile = file
if (resizable) {
resizedFile = (await resize(file)) ?? file
}

const thumbnailDataUrl = await convertToDataUrl(resizedFile)
if (!thumbnailDataUrl) return

commit.addAttachment({
type: fileType,
file: resizedFile
})
commit.addThumbnailTo({
index,
thumbnailDataUrl
})
} else {
commit.addAttachment({
type: fileType,
file
})
}
commit.addAttachment({
type: fileType,
file
})
}
})
3 changes: 3 additions & 0 deletions src/types/pica.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
declare module 'pica' {
export default Pica
}
1 change: 1 addition & 0 deletions vue-webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ module.exports = {
resolve: {
alias: {
vue$: 'vue/dist/vue.esm.js',
pica: 'pica/dist/pica.js',
// vuex-persist
'lodash.merge': path.resolve('./node_modules/lodash-es/merge.js')
}
Expand Down

0 comments on commit d9cdc06

Please sign in to comment.