惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

The Hacker News
The Hacker News
D
Docker
GbyAI
GbyAI
Y
Y Combinator Blog
C
Check Point Blog
M
MIT News - Artificial intelligence
Martin Fowler
Martin Fowler
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Blog — PlanetScale
Blog — PlanetScale
P
Palo Alto Networks Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
V
Vulnerabilities – Threatpost
I
InfoQ
Simon Willison's Weblog
Simon Willison's Weblog
博客园_首页
T
Threat Research - Cisco Blogs
J
Java Code Geeks
Hacker News - Newest:
Hacker News - Newest: "LLM"
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
The Register - Security
The Register - Security
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
阮一峰的网络日志
阮一峰的网络日志
O
OpenAI News
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Recent Announcements
Recent Announcements
Forbes - Security
Forbes - Security
AWS News Blog
AWS News Blog
雷峰网
雷峰网
博客园 - 三生石上(FineUI控件)
S
Security Affairs
H
Heimdal Security Blog
Engineering at Meta
Engineering at Meta
T
Troy Hunt's Blog
T
Tenable Blog
MongoDB | Blog
MongoDB | Blog
H
Hacker News: Front Page
IT之家
IT之家
N
News and Events Feed by Topic
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
G
GRAHAM CLULEY
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
V
Visual Studio Blog
S
Secure Thoughts
U
Unit 42
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
The Last Watchdog
The Last Watchdog
S
Security @ Cisco Blogs
NISL@THU
NISL@THU
T
The Blog of Author Tim Ferriss

博客园 - _herbert

信创-为什么ORACLE使用JDBC查询SYSDATE时,RS.getDate能获取到时间部分? linux宝塔面板使用API自动部署更新文件 JSON JAVA找出哪个类import了不存在的类 LocalDate,LocalDateTime,Date,日期串相互转换 PostMan加载三方JS PostMan内置对象 微信支付集成_JSAPI VUE中使用AXIOS包装API代理 spring中el表达式安全和扩展 Spring使用el表达式 springboot中File默认路径 Springboot启动时记录进程ID ORACLE解析游标生成JSON ORACLE游标序列化 AI教我一条SQL实现明细转树形结构 Springboot构建包使用BOOT-INF中的class覆盖依赖包中的class Maven 构建知识库 MAVEN构建分离依赖JAR JAVA实现读取最后几行日志 信创-ORACLE迁移到KingbaseV9 信创-ORACLE迁移到DM8 sql分组 group by rollup,cube,grouping sets,group_id,groupingId
小程序拍证件并局部裁剪
_herbert · 2026-07-08 · via 博客园 - _herbert

1. 什么场景

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

image.png

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. 以屏幕上边框为参考,结合缩放比例,计算实际需要裁剪的坐标信息

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 布局信息如下

<!-- 自定义相机弹层 -->
<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计算裁剪信息,并完成裁剪


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);
        }
      });
    });
  });
}