canvas实现截图/图片裁剪功能

canvas实现截图/图片裁剪功能

前段时间需要实现一个功能:隐式水印溯源

具体功能是:用户导入一张图片,可以提取这张图片的隐式水印信息,显示在页面上。但是后端同事目前只支持对图片中的屏幕内的内容进行溯源,需要用户标记出图片中屏幕的四个顶点,前端将顶点坐标传给服务器,因此需要前端支持用户标记,

开始考虑类似截图的方式将图片中的屏幕截出来,实现了一版后产品经理说用户用手机拍出的照片可能不一定是矩形,如果是只截图出矩形,明显不符合要求,

后来,又实现了一版,让用户使用画线的方式标记出一个四边形,现将两种方案都分享一下

最终方案实现思路:

首先导入一张图片,当鼠标移入到图片上时,把和图片大小一致的canvas显示出来,监听鼠标点击事件,用户每次点击将用户点击的像素计算出来,并将不同的点连接起来,最终将数据传递给服务器。

1.页面html结构

<div class="container">
    <header :breadcrumb="breadcrumb" return-icon>
    </header>
    <div class="content">
      <el-form
        label-position="top"
        :model="form"
        content-width="640px"
        class="import_form">
        <el-form-item
          element-loading-text="导入中"
          label="导入图片"
          prop="imageName">
          <el-input v-model="form.imageName" placeholder="请选择图片" readonly>
            <el-upload
              slot="append"
              :action="form.url"
              accept=".jpg, .png, .jpeg"
              :show-file-list="false"
              :before-upload="beforeUpload"
              :on-change="onChange"
              :on-success="onSuccess"
              :on-error="onError">
              <el-button icon="h-icon-folder" style="padding: 0 36px" />
            </el-upload>
          </el-input>
        </el-form-item>
      </el-form>
      <div class="main">
        <div class="main_title" :style="{width: uploadSuccess ? imgWidth + 'px' : '1000px'}">
          <div>图片<i class="h-icon-info"></i>调整4个锚点,只覆盖视频内容</div>
          <el-button type="link" v-show="count !== 0" @click="resetCanvas">重置</el-button>
        </div>
        <div class="main_content">
          <div class="upload_image" ref="uploadImg">
            <el-upload
              v-show="!uploadSuccess"
              drag
              :action="form.url"
              accept=".jpg, .png, .jpeg"
              :show-file-list="false"
              :before-upload="beforeUpload"
              :on-change="onChange"
              :on-success="onSuccess"
              :on-error="onError">
              <div class="empty_img">
                <img src="@/assets/images/upload_img.png">
                <div class="empty_text">请上传图片</div>
              </div>
            </el-upload>
            <canvas
              v-show="uploadSuccess"
              ref="canvasRegion"
              class="clip_canvas"
              @mousedown="handleDrawStart">
            </canvas>
            <img v-show="uploadSuccess" ref="img" :src="imgSrc">
          </div>
          <div class="upload_right">
            <el-button
              class="trace_btn"
              :type="traceType"
              :disabled="!uploadSuccess  || disabledTrace"
              :loading="loadingTrace"
              @click="traceMarkContent">
              水印内容提取
            </el-button>
            <div class="water_content">
              <div class="water_content_title" v-show="!traceSuccess && !uploadSuccess"><i class="h-icon-info"></i>请先上传图片</div>
              <div class="water_content_title" v-show="traceSuccess"><i class="h-icon-enable"></i>提取成功</div>
              <el-scrollbar class="water_content_scroll" wrap-class="wrap_scroll">
                <ul>
                  <li
                    v-for="(con, index) in traceContent"
                    :key="index"
                    :title="con">
                    {{con}}
                  </li>
                </ul>
              </el-scrollbar>
              <el-button class="copy_btn" type="link" :disabled="this.traceContent.length == 0" @click="copyWaterContent(traceContent)">复制</el-button>
            </div>
          </div>
        </div>
      </div>
    </div>
</div>

2.js部分

第一步,初始化获取原图与canvas区域尺寸,并将图片按原比例显示

mounted () {
    this.calculateSize()
},
// 计算尺寸
calculateSize () {
  this.$nextTick(() => {
       window.addEventListener('resize', throttle(50, () => {
         this.imgWidth = this.$refs.img.clientWidth
         this.imgHeight = this.$refs.img.clientHeight
         if (this.canvasElement && this.canvasContext()) {
              this.getAreaSize()
         }
       }))
  })
},
// 清空画布
resetCanvas () {
  this.canvasContext.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
  this.count = 0
  this.trace.points = [
       {
         pointX: 0,
         pointY: 0
       },
       {
         pointX: 1000,
         pointY: 0
       },
       {
         pointX: 0,
         pointY: 1000
       },
       {
         pointX: 1000,
         pointY: 1000
       }
  ]

},

第二步,监视鼠标按下事件,记录点位像素。开始连线
// 鼠标按下
handleDrawStart (e) {
  this.count = this.count < 4 ? ++this.count : 1
  this.trace.points.splice(this.count - 1, 1, this.getRelativePosition(e))
  this.drawRect()
},
// 获取鼠标按下的位置
getRelativePosition (e) {
  const clientRect = this.canvasElement.getBoundingClientRect()
  // 这里必须取整数
  const x = Math.round(e.clientX - clientRect.x)
  const y = Math.round(e.clientY - clientRect.y)
  return {
       pointX: x,
       pointY: y
  }
},

// 根据开始和结束点来绘制矩形
drawRect () {
  // 开始绘制
  if (this.count === 1) {
       this.canvasContext.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
       this.canvasContext.beginPath()
       this.canvasContext.moveTo(this.trace.points[0].pointX, this.trace.points[0].pointY)
  } else if (this.count === 2) {
       this.canvasContext.lineTo(this.trace.points[1].pointX, this.trace.points[1].pointY)
  } else if (this.count === 3) {
       this.canvasContext.lineTo(this.trace.points[2].pointX, this.trace.points[2].pointY)
  } else if (this.count === 4) {
       this.canvasContext.lineTo(this.trace.points[3].pointX, this.trace.points[3].pointY)
       this.canvasContext.lineTo(this.trace.points[0].pointX, this.trace.points[0].pointY)
  }
  this.canvasContext.stroke()
},
// 获取区域宽高
getAreaSize () {
  this.canvasWidth = this.imgWidth
  this.canvasHeight = this.imgHeight
  this.canvasElement.width = this.imgWidth
  this.canvasElement.height = this.imgHeight
  this.canvasContext.strokeStyle = '#FC5633'
  this.canvasContext.lineWidth = 2
},
第三步,将canvas绘制的区域顶点计算出来,传给服务器
// 水印内容提取按钮
traceMarkContent () {
  let points
  if (this.count !== 0) {
       let leftTop
       let rightTop
       let leftBottom
       let rightBottom
       const sortPoints = this.quickSort(this.trace.points)
       if (sortPoints[0].pointY < sortPoints[1].pointY) {
         leftTop = sortPoints[0]
         leftBottom = sortPoints[1]
       } else {
         leftTop = sortPoints[1]
         leftBottom = sortPoints[0]
       }
       if (sortPoints[2].pointY < sortPoints[3].pointY) {
         rightTop = sortPoints[2]
         rightBottom = sortPoints[3]
       } else {
         rightTop = sortPoints[3]
         rightBottom = sortPoints[2]
       }
       points = [
         leftTop,
         rightTop,
         leftBottom,
         rightBottom
       ]
       points = points.map(p => {
         return { pointX: Math.round(p.pointX * 1000 / this.imgWidth), pointY: Math.round(p.pointY * 1000 / this.imgHeight) }
       })
  } else {
       points = [...this.trace.points]
  }
  const params = {
       cacheKey: this.trace.cacheKey,
       points
  }
  this.loadingTrace = true
  this.traceContent = []
  mark.hideTrace(params).then(({ code, data, msg }) => {
       this.loadingTrace = false
       if (code === '0') {
         this.traceSuccess = true
         this.traceContent = data.watermarkContent.split('/n')
       }
  }).catch(() => {
       this.traceSuccess = false
       this.loadingTrace = false
  })
},
// 排序
quickSort (arr) {
  for (let i = 0; i < arr.length - 1; i++) {
       for (var j = 0; j < arr.length - i - 1; j++) {
         if (arr[j].pointX > arr[j + 1].pointX) {
              var oldVal = arr[j]
              arr[j] = arr[j + 1]
              arr[j + 1] = oldVal
         }
       }
  }
  return arr
},
// 此处是导入图片与复制溯源结果相关,其中复制是使用了插件vue-clipboard2
// 导入之前
beforeUpload (file) {
  const name = file.name.replace(/.+\./, '')
  if (name !== 'jpg' && name !== 'jpeg' && name !== 'png') {
       this.$message.error('图片格式不正确')
       return false
  }
  return true
},
// 导入改变事件
onChange (file, fileList) {
  this.imgSrc = file.url
},
// 导入成功事件
onSuccess ({ data, code, msg }, file) {
  if (code === '0') {
       setTimeout(() => {
         // 获取DOM元素
         this.canvasElement = this.$refs.canvasRegion
         this.imgEle = this.$refs.img
         this.canvasContext = this.canvasElement.getContext('2d')
         this.imgWidth = this.imgEle.clientWidth
         this.imgHeight = this.imgEle.clientHeight
         this.getAreaSize()
       })
       this.form.imageName = file.name
       this.uploadSuccess = true
       this.traceSuccess = false
       this.traceType = 'primary'
       this.trace.cacheKey = data
       this.count = 0
       this.traceContent = []
  } else {
       this.form.imageName = ''
       this.uploadSuccess = false
       this.traceType = 'default'
       this.$message.error(msg)
  }
},
// 导入失败事件
onError (err, file, fileList) {
  this.form.imageName = ''
  this.uploadSuccess = false
  this.traceType = 'default'
  fileList.pop()
  this.$message.error(JSON.parse(err.message).error)
},
// 复制功能
copyWaterContent (text) {
  const t = text.join(' ')
  this.$copyText(t).then(() => {
       this.$message.success('复制成功')
  }).catch(() => {
       this.$message.error('复制失败')
  })

}

初版实现思路:

首先导入一张图片,当鼠标移入到图片上时,把和图片大小一致的canvas显示出来,监听鼠标事件,记录裁剪开始点和结束点的位置。然后就可以将截取的四个点传给服务起,也可以在页面显示截图后的图片

1.页面html结构(demo)

<template>
    <div class="page">
              <div class="operate-btn">
            <a-button @click="handleBtnClick">{{screenshot?"就决定是你了":"开始截图"}}</a-button>
        </div>
        <div class="clip-area">
            <canvas
                    v-show="screenshot"
                    class="clip-canvas"
                    @mousedown="handleDrawStart"
                    @mouseup="handleMouseUp"></canvas>
            <img class="big-img" :style="bigImgStyle" src="https://juejin.cn/post/assets/duola.jpg" alt="小图">
            <div class="draw-area" :style="drawAreaStyle" v-show="!screenshot"></div>
        </div>
        <div class="thumb">
            <p style="text-align: center">截图预览</p>
            <img class="thumb-img" :src="https://juejin.cn/post/clipBase64" v-show="clipBase64" alt="大图">
        </div>
    </div>
</template>

2.js部分

第一步,初始化获取原图与canvas区域尺寸,并将图片按原比例显示

mounted() {
       this.getDomELe()
       // 监听图片的加载
       this.bigImgEle = document.querySelector(".big-img")
       this.bigImgEle.onload = () => {
              this.getAreaSize()
              this.getImgSize()
              this.setImgContain()
       }
}

// 获取canvas绘制的尺寸,与外层div大小一致
getAreaSize() {
       let areaEle = document.querySelector(".clip-area")
       this.canvasElement.width = this.canvasWidth = areaEle.offsetWidth
       this.canvasElement.height = this.canvasHeight = areaEle.offsetHeight
       this.canvasContext.strokeStyle = "#ff0000"
       this.canvasContext.lineWidth = 1
}
 // 获取原图展示的尺寸
getImgSize() {
       this.imgOriginalWidth = this.bigImgEle.width
       this.imgOriginalHeight = this.bigImgEle.height
}
// 将图片尽可能地填满该区域
setImgContain() {
       // 宽高比例
       let ratio = this.imgOriginalWidth / this.imgOriginalHeight
       // 如果图片宽小于高,则以容器高为准,按比例还原宽度
       if (this.imgOriginalWidth < this.imgOriginalHeight) {
              this.imgDisplayHeight = this.canvasHeight
              this.imgDisplayWidth = ratio * this.imgDisplayHeight
              this.imgOffset = {
                     x: (this.canvasWidth - this.imgDisplayWidth) / 2,
                     y: 0
              }
       } else {
              this.imgDisplayWidth = this.canvasWidth
              this.imgDisplayHeight = this.imgDisplayWidth / ratio
              this.imgOffset = {
                     x: 0,
                     y: (this.canvasHeight - this.imgDisplayHeight) / 2
              }
       }
}

 第二步,用户开始绘制矩形

// 记录在canvas里的开始点-清空画布-监听鼠标移动
handleDrawStart(e) {
       this.points.start = this.getRelativePosition(e)
       document.addEventListener("mousemove", this.handleDrawMove)
},
 getRelativePosition(e) {
       let clientRect = this.canvasElement.getBoundingClientRect()
       let x = Math.round(e.clientX - clientRect.x)
       let y = Math.round(e.clientY - clientRect.y)
       return [x, y]
}
// 记录移动的点位
handleDrawMove(e) {
       this.points.end = this.getRelativePosition(e)
       this.drawRect()
}
// 根据开始和结束点来绘制矩形
drawRect() {
       let {
              yStart,
              yDistance,
              xDistance,
              xStart,
       } = this.formatPointPosition
       // 清空画布
       this.canvasContext.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
       // 开始绘制
       this.canvasContext.strokeRect(xStart, yStart, xDistance, yDistance)
}
formatPointPosition() {
       // 取开始点和结束点的最小值
       let [x, y] = this.points.start
       let [x1, y1] = this.points.end
       let xStart = Math.min(x, x1)
       let yStart = Math.min(y, y1)
       // 计算移动距离
       let xDistance = Math.abs(x - x1)
       let yDistance = Math.abs(y - y1)
       return {
              yDistance,
              xDistance,
              yStart,
              xStart,
       }
},
// 鼠标抬起,取消监听鼠标移动事件
handleMouseUp() {
       document.removeEventListener("mousemove", this.handleDrawMove)
}

第三步,将canvas绘制的区域顶点计算出来(这里主要显示截取的图片供参考)

handleBtnClick() {
       this.screenshot = !this.screenshot
       if (!this.screenshot && this.formatPointPosition.xDistance && this.formatPointPosition.yDistance) {
              this.exportBase64()
       }
}
exportBase64() {
       let scale = this.imgDisplayWidth / this.imgOriginalWidth
       let canvasElement = document.createElement("canvas")
       let canvasContext = canvasElement.getContext("2d")
       let startX = (this.formatPointPosition.xStart - this.imgOffset.x) / scale
       let startY = (this.formatPointPosition.yStart - this.imgOffset.y) / scale
       canvasElement.width = this.formatPointPosition.xDistance / scale
       canvasElement.height = this.formatPointPosition.yDistance / scale
       canvasContext.drawImage(this.bigImgEle,
              startX, startY, canvasElement.width, canvasElement.height,
              0, 0, canvasElement.width, canvasElement.height)
       this.clipBase64 = canvasElement.toDataURL("image/jpeg")
}

总结:

然最后是实现了隐式水印溯源的功能,但对于前端而言,采用的方案依然是不完美的,如果有更好的方案,欢迎一起探讨。

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值