小程序拍证件并局部裁剪

1. 什么场景

身份证拍照打印的小程序工具,对拍摄照片要做局部裁剪,只保留身份证的部分.要实现的效果如下:

2. 实现思路

拍照页面,生成一个圆角矩形边框.拍照时,移动手机,让身份证刚好铺满矩形框.此时点击拍照,就可以通过公式,计算出实际照片中,身份证所在位置以及宽高.利用这些信息,结合cavans就可以实现裁剪.裁剪后的图片,再重新绘制到A4规格cavans上.

3. 计算依据

使用wx.getSystemInfoSync(),获取手机屏幕的宽高screenWidth和screenHeight.小程序的camera组件,使用query.select('#cameraEl').boundingClientRect(),获取手机相机的宽高width和height.相机组件之上,覆盖一个半透明遮罩,在遮罩上层显示一个圆角矩形框,使用query.select('#idcardHole').boundingClientRect()获取圆角矩形框的位置和宽高(left,top,width,height).拍照完成后, wx.getImageInfo可以获取到照片的宽高和拍摄方向.在裁剪前,我们知道以下信息

  • A4尺寸: 210mm x 297mm
  • 身份证: 85.6mm x 54mm
  • 屏幕尺寸:{ w: screenW, h: screenH },
  • 相机信息:
  • 边框信息:
  • 照片信息:

拍照时,照片与手机屏幕中显示大小,是有一定缩放比例的,如下图所示:

要完成裁剪,总的来说,要以下三个步骤1. 计算照片和手机屏幕之间缩放比例

scale = max(照片宽度/屏幕宽度,照片高度/屏幕高度)

  1. 以屏幕上边框为参考,结合缩放比例,计算实际需要裁剪的坐标信息
javascript 复制代码
cropX= (holeX-cameraRect.left) *scale;
cropY= (holeY-cameraRect.top) *scale;
cropW=holeW*scale;
cropH=holeH*scale;
  1. 使用canvas绘制图片,完成裁剪

const ctx=canvas.getContext('2d');

ctx.drawImage(img, 0, 0, photoW, photoH);

wx.canvasToTempFilePath({...})

4. 裁剪完整代码

wxml 布局信息如下

xml 复制代码
<!-- 自定义相机弹层 -->
<view class="camera-overlay" wx:if="{{showCamera}}">
  <camera
    id="cameraEl"
    class="camera"
    device-position="back"
    flash="auto"
    resolution="high"
  ></camera>
  <!-- 遮罩层 -->
  <view class="camera-mask">
    <view class="mask-top"></view>
    <view class="mask-middle">
      <view class="mask-side"></view>
      <view class="mask-hole" id="idcardHole">
        <view class="hole-border">
          <view class="hole-portrait">
            <view class="hole-portrait-head"></view>
            <view class="hole-portrait-body"></view>
          </view>
        </view>
      </view>
      <view class="mask-side"></view>
    </view>
    <view class="mask-bottom">
      <text class="mask-tip">
        {{cameraSide === 'front' ? '请将身份证正面放入框内' : '请将身份证反面放入框内'}}
      </text>    
      <view class="camera-actions">
        <view class="btn-cancel" bindtap="closeCamera">取消</view>
        <view class="btn-shoot" bindtap="shootPhoto"></view>
        <view class="btn-album" bindtap="chooseFromAlbum">相册</view>
      </view>
    </view>
  </view>
</view>

JS计算裁剪信息,并完成裁剪

javascript 复制代码
cropImage(imagePath) {
  const that = this;

  return new Promise((resolve, reject) => {
    // 1. 获取设备屏幕信息
    const systemInfo = wx.getSystemInfoSync();
    const screenW = systemInfo.screenWidth;
    const screenH = systemInfo.screenHeight;

    // 2. 获取相机预览层和裁剪框在屏幕上的实际位置
    const query = wx.createSelectorQuery();
    query.select('#cameraEl').boundingClientRect();
    query.select('#idcardHole').boundingClientRect();
    
    query.exec((rectRes) => {
      const cameraRect = rectRes;
      const holeRect = rectRes;

      // 校验节点是否存在
      if (!cameraRect || !holeRect) {
        reject(new Error('无法获取相机或框的位置'));
        return;
      }

      // 提取相机和裁剪框的几何信息
      const camW = cameraRect.width;
      const camH = cameraRect.height;
      const holeX = holeRect.left;
      const holeY = holeRect.top;
      const holeW = holeRect.width;
      const holeH = holeRect.height;
      wx.getImageInfo({
        src: imagePath,
        success: (imgInfo) => {
          const photoW = imgInfo.width;
          const photoH = imgInfo.height;
          const orientation = imgInfo.orientation || 'up'; 
          const scaleX = photoW / screenW;
          const scaleY = photoH / screenH;
          const scale = Math.max(scaleX, scaleY);
          let cropX = (holeX - cameraRect.left) * scale;
          let cropY = (holeY - cameraRect.top) * scale;
          let cropW = holeW * scale;
          let cropH = holeH * scale;
          cropX = Math.max(0, cropX);
          cropY = Math.max(0, cropY);
          cropW = Math.min(cropW, photoW - cropX);
          cropH = Math.min(cropH, photoH - cropY);
          const canvasQuery = wx.createSelectorQuery();
          canvasQuery.select('#cropCanvas')
            .fields({ node: true, size: true })
            .exec((res) => {
              if (!res) {
                reject(new Error('Canvas 初始化失败'));
                return;
              }

              const canvas = res.node;
              const ctx = canvas.getContext('2d');
              canvas.width = photoW;
              canvas.height = photoH;
              that.setData({
                cropCanvasWidth: photoW,
                cropCanvasHeight: photoH
              });
              const img = canvas.createImage();
              img.src = imagePath;
              img.onload = () => {
                ctx.save();
                if (typeof rotate !== 'undefined' && rotate !== 0) {
                  ctx.translate(translateX, translateY);
                  ctx.rotate(rotate);
                }
                ctx.drawImage(img, 0, 0, photoW, photoH);
                ctx.restore();
                wx.canvasToTempFilePath({
                  canvas: canvas,
                  x: cropX,
                  y: cropY,
                  width: cropW,
                  height: cropH,
                  destWidth: cropW,   // 输出宽度
                  destHeight: cropH,  // 输出高度
                  success: (fileRes) => {
                    resolve(fileRes.tempFilePath);
                  },
                  fail: (err) => {
                    reject(err);
                  }
                });
              };

              img.onerror = () => {
                reject(new Error('图片加载失败'));
              };
            });
        },
        fail: (err) => {
          reject(err);
        }
      });
    });
  });
}