Skip to content

图片压缩

图片压缩核心:

  1. 通过 缩放比例,然后通过 ctx.drawImage绘制
  2. 通过 canvas.toBlob 调整 quality 达到效果
压缩格式:image/jpeg
压缩质量:1
压缩大小:0 ( 0 kb)

drawImage 示例代码

警告

示例代码仅完成功能,考虑并不完善!

js
function showImageOnCanvas() {
  if (!canvas.value) {
    nextTick(showImageOnCanvas)
    return
  }

  const image = state.image

  if (!image) return

  const ctx = canvas.value.getContext('2d')
  if (!ctx) return

  // 图片 尺寸
  const imgWidth = image.width
  const imgHeight = image.height

  // 获取 canvas 尺寸
  const canvasClientRect = canvas.value.getBoundingClientRect()

  // 图片 与 canvas 尺寸的比例
  const ratio = canvasClientRect.width / imgWidth

  // 将 canvas 与 图片比例设置一致
  const canvasWidth = canvasClientRect.width
  const canvasHeight = imgHeight * ratio
 
  // 设置 canvas 的尺寸
  canvas.value.width = canvasWidth
  canvas.value.height = canvasHeight 

  // 计算合适的缩放比例
  const scale = Math.min(canvasWidth / imgWidth, canvasHeight / imgHeight)
  const drawWidth = imgWidth * scale
  const drawHeight = imgHeight * scale

  // 计算 x,y,使得图片居中
  const x = (canvasWidth - drawWidth) / 2
  const y = (canvasHeight - drawHeight) / 2
 
  ctx.drawImage(image, x, y, drawWidth, drawHeight)
}

toBlob 示例

js
async function yasuo(type: string, quality: number) {
  if (!canvas.value) {
    nextTick(() => yasuo(type, quality))
    return
  }

  const blob = await new Promise<Blob | null>((resolve, reject) => {
    if (!canvas.value) return reject()
    canvas.value.toBlob(resolve, type, quality)
  })

  if (!blob) throw new Error('blob is null')
 
  state.yasuo.quality = quality
  state.yasuo.type = type
  state.yasuo.size = blob.size

  if (blob.size > state.yasuo.target) {
    setTimeout(() => { yasuo('image/jpeg', quality - 0.05) }, 2000)
  }
}

完整示例代码

vue
<template>
<div class="yasuo-demo"> 
    <button style="border: 1px solid #333;"  @click="onSelectImg">选择图片</button> 
    <button style="border: 1px solid #333;"  @click="downloadImg">下载图片</button> 
    <div class="info-div">
      <div>压缩格式:{{ state.yasuo.type }}</div>
      <div>压缩质量:{{ state.yasuo.quality }}</div>
      <div>压缩大小:{{ state.yasuo.size }} ( {{ state.yasuo.size / 1024 }} kb)</div>
    </div>
    <canvas ref="canvas" class="the-canvas"></canvas>
</div>
</template>
<script setup lang="ts"> 
import { nextTick, reactive, useTemplateRef } from 'vue'

const state = reactive({
  file: null as File | null,
  image: null as HTMLImageElement | null,
  yasuo: {
    target: 200000,
    type: 'image/jpeg',
    quality: 1,
    size: 0,
  },
})

const canvas = useTemplateRef<HTMLCanvasElement>('canvas')



function showImageOnCanvas() {
  if (!canvas.value) {
    nextTick(showImageOnCanvas)
    return
  }

  const image = state.image

  if (!image) return

  const ctx = canvas.value.getContext('2d')
  if (!ctx) return

  // 图片 尺寸
  const imgWidth = image.width
  const imgHeight = image.height

  // 获取 canvas 尺寸
  const canvasClientRect = canvas.value.getBoundingClientRect()

  // 图片 与 canvas 尺寸的比例
  const ratio = canvasClientRect.width / imgWidth

  // 将 canvas 与 图片比例设置一致
  const canvasWidth = canvasClientRect.width
  const canvasHeight = imgHeight * ratio
 
  // 设置 canvas 的尺寸
  canvas.value.width = canvasWidth
  canvas.value.height = canvasHeight 

  // 计算合适的缩放比例
  const scale = Math.min(canvasWidth / imgWidth, canvasHeight / imgHeight)
  const drawWidth = imgWidth * scale
  const drawHeight = imgHeight * scale

  // 计算 x,y,使得图片居中
  const x = (canvasWidth - drawWidth) / 2
  const y = (canvasHeight - drawHeight) / 2
 
  ctx.drawImage(image, x, y, drawWidth, drawHeight)
}


async function yasuo(type: string, quality: number) {
  if (!canvas.value) {
    nextTick(() => yasuo(type, quality))
    return
  }

  const blob = await new Promise<Blob | null>((resolve, reject) => {
    if (!canvas.value) return reject()
    canvas.value.toBlob(resolve, type, quality)
  })

  if (!blob) throw new Error('blob is null')
 
  state.yasuo.quality = quality
  state.yasuo.type = type
  state.yasuo.size = blob.size

  if (blob.size > state.yasuo.target) {
    setTimeout(() => { yasuo('image/jpeg', quality - 0.05) }, 2000)
  }
}



function onSelectImg() {
  openFileSelector({ accept: 'image/*' }).then((res) => {
    console.log('is > ', res)

    state.file = res[0]

    fileToImage(state.file).then((img) => {
      state.image = img
      // dev01.value?.appendChild(img)

      showImageOnCanvas()

      setTimeout(() => yasuo('image/jpeg', 1), 1000)
    })
  })
}

function downloadImg() {
  if (!canvas.value) {
    nextTick(downloadImg)
    return
  }

  canvas.value.toBlob(
    (blob) => {
      if (!blob) return
      const url = URL.createObjectURL(blob)
      downloadFromUrl(url, Date.now() + '.'+ state.yasuo.type.split('/')[1])
    },
    state.yasuo.type,
    state.yasuo.quality,
  )
} 

interface IFileSelectorOptions {
  accept?: string
  multiple?: boolean
}
function openFileSelector(options: IFileSelectorOptions = {}) {
  return new Promise<FileList>((resolve, reject) => {
    const input = document.createElement('input')
    input.type = 'file'
    input.style.opacity = '0'
    input.style.position = 'absolute'
    input.style.top = '-1000px'
    document.body.appendChild(input)

    if (options.accept) {
      input.accept = options.accept
    }

    if (options.multiple) {
      input.multiple = options.multiple
    }

    input.onchange = (e: any) => {
      // console.log(e)
      const file = e.target?.files
      if (file) {
        resolve(file)
      } else {
        reject('')
      }

      document.body.removeChild(input)
    }
    input.click()
  })
}

function fileToImage(file: File) {
  return new Promise<HTMLImageElement>((resolve, reject) => {
    const img = new Image()
    img.src = URL.createObjectURL(file)
    img.onload = () => {
      resolve(img)
    }
    img.onerror = () => {
      reject(new Error('加载图片失败'))
    }
  })
}

function downloadFromUrl(url: string, filename: string) {
  if (!url) {
    throw new Error('url 不能为空')
  }

  if (!filename) {
    throw new Error('filename 不能为空')
  }

  const link = document.createElement('a')
  link.download = filename
  link.href = url

  // 触发点击下载
  document.body.appendChild(link)
  link.click()
  document.body.removeChild(link)
}

 
</script>
<style scoped lang="less">
.yasuo-demo{
    border: 1px solid #333;
    border-radius: 12px;
    padding: 12px;
}
.info-div {
  margin: 12px 0;
}
.the-canvas {
  width: 300px;
  height: 400px;
  border: 1px solid #333;
  border-radius: 12px;
}
</style>