ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

iOS PDF电子签章实战:坐标换算、色彩管理与防篡改锁定

iOS PDF电子签章实战:坐标换算、色彩管理与防篡改锁定 简介这是一套面向iOS开发者的原生PDF电子签章轻量库适用于需要在移动端快速展示PDF并完成电子签名、盖章的金融、法务、政务及合同管理类App。资源以zip压缩包提供共7个文件包含4个.a静态库、2个.h头文件和1个.mm实现文件整体仅55.69MB体量小巧便于导入工程。核心控制器TrustSignPDFDSController提供简洁的初始化入口传入文件路径与文件名后即可push展示开发者无需关心底层渲染与签章算法配套的头文件和静态库让调用非常直接适合有一定iOS开发经验、希望快速集成PDF签署能力的中级工程师。目前已有643人学习下载说明集成方案具备一定参考价值。包内目录划分明确包含核心实现、公共依赖库及相关头文件可在离线环境直接编译接入省去自行封装底层复杂的PDF交互逻辑的时间。1. iOS PDF电子签章难点从来不是“画个章”而是坐标、色彩与防篡改在合同审批、工程交付、医疗单据这类场景里iOS PDF电子签章指的是这样一条链路App 拿到后端下发的 PDF 文件用户在指定位置手写签名、加盖公章/骑缝章再回传一份不可抵赖的 PDF。真正做过的人会告诉你卡住你的往往不是“把章画上去”这一步而是三件看起来不起眼的事——UIKit 的 y 轴向下、PDF 的原点却藏在左下角红章在不同屏幕和打印机上偏成暗红以及签完章之后文件仍然能被随意改内容。这三件事任何一个没处理好交付验收时都会翻车。这篇文章按一条完整的落地路径来讲先做选型对比再给出可抄作业的绘制代码和参数接着列出我踩过的四个具体坑位然后补上签章后的锁定方案最后是一份能直接拿去用的验收清单。适合 iOS 开发、Hybrid 应用工程师和做文档中台的后端同学对照着自己的工程看新手能跟步骤走熟手可以直接跳到自己关心的参数和边界部分。2. 选型对比用PDFKit注解还是CGContext重绘来承载签章2.1 两种路线的能力边界与生态现状iOS 上给 PDF 加签章常见做法只有两条路一是 PDFKit 的 PDFAnnotation / PDFPage二是用 Quartz 2DCGContext把原 PDF 重绘到一个新的上下文中在绘制签名和印章后输出新 PDF。PDFKit 注解路线的优势是 API 简单几行代码就能往指定页加一个矩形、签名图片或文本气泡iOS 11 之后的系统内置不需要额外引入第三方库。但它的短板也很明显对自定义印章旋转、透明度、防伪底纹支持不够直接PDFAnnotation 的样式在 iOS 16 前后的渲染差异较大而且生成文件后如果还要做二次加密、数字签名PDFKit 的控制力偏弱。另一个容易被忽略的边界是PDFKit 对“修复畸形 PDF”的能力很一般——后端下发文件里有少量非法对象时PDFKit 会直接拒绝渲染而 CGContext 的 drawPDFPage 对这类文件反而更宽容。我一般会优先选择 CGContext 重绘路线来承载电子签章。原因有三个第一签名和印章本质上是“在指定 PDF 坐标系里叠加绘图操作”CGContext 恰好就是干这个的后续要追加水印、页码、骑缝章都共用同一套管线第二重绘输出的 PDF 仍然是矢量的签章不会因为放大而模糊第三重绘结束后可以直接带上密码和权限标志写入文件一次性完成“签名 上锁”。代价是你需要自己处理页面尺寸、裁剪框和坐标换算这正是本文要展开的部分。2.2 用UIGraphicsPDFRenderer把现有PDF转成可变PDF最小工程在 iOS 上重绘现有 PDF 的最简做法是用 UIGraphicsPDFRenderer。它面向的是“我要生成一个全新的 PDF”的场景但配合 CGPDFDocument 连起来也能用于“读入旧 PDF、原样重画每一页、再叠加新内容”。下面是一个最小工程示例。import UIKit func redrawPDF(sourceURL: URL, to destinationURL: URL) throws { guard let document CGPDFDocument(sourceURL as CFURL) else { throw NSError(domain: PDFSign, code: -1, userInfo: [NSLocalizedDescriptionKey: 无法打开源 PDF]) } let pageCount document.numberOfPages var mediaBox CGRect.zero let renderer UIGraphicsPDFRenderer(bounds: .zero) // 占位实际按页设置 try renderer.writePDF(to: destinationURL) { context in for pageIndex in 1...pageCount { guard let page document.page(at: pageIndex) else { continue } // 读取原始页面尺寸确保输出页大小与源文件一致 mediaBox page.getBoxRect(.mediaBox) UIGraphicsBeginPDFPageWithInfo(mediaBox, nil) // 先原样绘制原页面此操作保持矢量不会把整页栅格化 context.cgContext.saveGState() context.cgContext.translateBy(x: 0, y: mediaBox.height) context.cgContext.scaleBy(x: 1, y: -1) context.cgContext.drawPDFPage(page) context.cgContext.restoreGState() } } }逻辑说明page.getBoxRect(.mediaBox)拿到源页面的完整页面尺寸输出页必须沿用这个尺寸否则后续签章坐标会整体错位。UIGraphicsBeginPDFPageWithInfo(mediaBox, nil)不是在 renderer 的闭包里直接调用而是借助 renderer 内部已经开启的 PDF 上下文在每一页绘制前用该函数声明页面大小。实际上在writePDF闭包内不需要再手动调用这个函数UIGraphicsPDFRenderer会自动开启每一页上面代码里这一步是为了保持讲解清晰真实工程中可以直接用context.beginPage(withBounds:pageInfo:)。translateBy scaleBy做 y 轴翻转PDF 的原点通常在左下角UIKit 的 CGContext 直接绘制时原点在左上角翻转后才能保证原 PDF 内容方向正确。参数说明如果源 PDF 页面有cropBox优先用getBoxRect(.cropBox)而不是.mediaBox否则扫描件类 PDF 会出现大留白甚至内容被裁掉。绘制签章前先把整页画完再叠加签章图层避免签章被原 PDF 内容覆盖。这一步跑通后你就已经拥有了一个“读写 PDF 的管线”。接下来要解决的核心问题只有一个如何把用户手指点到的位置精确映射到这一页的 PDF 坐标上。3. 把签名和公章画上PDF坐标系换算与绘制参数3.1 从触摸点到PDF页面坐标UIKit与PDF坐标系的换算iOS 拿到用户手势位置时得到的是视图UIView坐标系的点y 轴向下PDF 页面绘制坐标系的原点在左下角y 轴向上而我们要写入签名框时业务上通常按“从页面左上角往右、往下”的矩形来定义例如“签名区在页面宽度 50% 处、高度 70% 处”。这里的换算包含两步先由页面左上角往下的归一化坐标转成 PDF 坐标系下的点再由 PDF 坐标点反推 UIKit 视图坐标用于显示。归一化坐标是最稳妥的中间表达方式因为它与屏幕尺寸、PDF 页面尺寸无关。比如后端下发的签章指令里写“x0.45, y0.7”含义是“距页面左上角向右 45%、向下 70%”。换算公式如下func pointInPDF(fromNormalizedX x: CGFloat, y: CGFloat, pageRect: CGRect) - CGPoint { // PDF 坐标系原点在左下角x 向右y 向上 let pdfX pageRect.minX x * pageRect.width // 从左上角往下的位置换算到左下角往上的位置 let pdfY pageRect.maxY - y * pageRect.height return CGPoint(x: pdfX, y: pdfY) }逻辑说明pageRect是这一页的 mediaBox/cropBox。pdfY maxY - y * height把“距顶部百分比”翻转为“距底部百分比”这一步最容易错。很多工程师在这里直接用 UIKit 的 y 值去写 PDF结果签章在真机上看起来正常用 PC 端 PDF 阅读器打开后跑到页面外或者上下颠倒。参数说明pageRect不要用屏幕 bounds也不要用某个 UIView 的 bounds必须来自CGPDFPage.getBoxRect。如果页面被旋转过iOS 的page.rotationAngle不为 0需要先对pageRect做旋转归一化处理。常见做法是当rotationAngle 90 || rotationAngle 270时交换宽高再套用上面的公式。建议所有签章位置用归一化坐标存储到业务字段例如 JSON 里的sign: {x: 0.45, y: 0.7, w: 0.3, h: 0.08}而不是把点像素存死这样同一份签章指令在 iPad、iPhone 和 Android 端渲染结果一致。3.2 手写签名采集与印章绘制贝塞尔曲线和透明PNG手写签名在 iOS 端最常见做法是在一个白底 view 上用UIBezierPath记录触摸轨迹再用draw(_:)或CAShapeLayer实时渲染。采集完成后你需要把这个签名路径转成 UIImage再把 UIImage 画到 PDF 上下文中。印章则更简单用设计好的透明背景 PNG通常包一层白底遮罩删掉后导出画到目标区域。下面这段代码把“签名 印章”一次性写入 PDFfunc drawSignatureAndStamp(on context: CGContext, pageRect: CGRect, signatureImage: UIImage, stampImage: UIImage, signRect: CGRect, stampRect: CGRect) { context.saveGState() // 绘制签名保持原始宽高比居中放进目标区域 let sigScaled aspectFitRect(for: signatureImage.size, in: signRect) if let sigCG signatureImage.cgImage { context.interpolationQuality .high context.draw(sigCG, in: sigScaled) } // 绘制印章支持旋转角度业务上通常干刻/红色公章按 0 度或 30 度倾斜 context.translateBy(x: stampRect.midX, y: stampRect.midY) context.rotate(by: stampRotateAngle) context.translateBy(x: -stampRect.midX, y: -stampRect.midY) if let stampCG stampImage.cgImage { context.draw(stampCG, in: stampRect) } context.restoreGState() } func aspectFitRect(for size: CGSize, in target: CGRect) - CGRect { let scale min(target.width / size.width, target.height / size.height) let w size.width * scale let h size.height * scale return CGRect(x: target.midX - w / 2, y: target.midY - h / 2, width: w, height: h) }逻辑说明aspectFit是为了避免签名字体被拉伸变形尤其当签名框是横向长方形、而手写轨迹是竖向的时候。interpolationQuality .high用于签名这种边缘柔和的光栅图缩小时发虚问题会明显减轻。印章旋转放在绘制坐标系层面完成而不是预先旋转图片再保存这样同一个章资源可以灵活支持业务要求的多个角度。参数建议工程默认值签名框宽度取页面宽度的0.30.4高度取0.080.12太小识别困难太大会盖住正文。公章直径取页面宽度的0.150.2过大显得突兀过小打印出来看不清字。印章图片建议使用sRGB色彩空间导出 PNG不要用 Display P3否则后面会遇到经典的红章偏色问题具体在第 4 章展开。签名导出 UIImage 时按scale UIScreen.main.scale渲染保证 Retina 屏上路径边缘平滑。3.3 嵌入H5/uniapp场景时的签名回传现在不少项目是 Hybrid 架构外层是 uniapp / H5签章能力要么放在原生端要么放在 Web 页里用 canvas 做。uniapp 开发者最常见的处境是iOS 端原生签章能力要自己封装成原生插件uniapp 通过uni.requireNativePlugin调用而 H5 纯 canvas 方案则要处理字体、坐标和导出白图的问题。如果签章能力已经由原生端封装成插件建议在插件里输出“签名完成后的本地 PDF 路径 页面尺寸 签章坐标”三要素H5 只负责展示和触发不做任何绘图运算。这样既绕开了 uniapp 层面 canvas 导出的兼容性问题又能复用原生端的 PDF 重绘管线。如果必须走 H5 canvas 签名那么 canvas 的坐标系要和第 3.1 节里 PDF 的坐标做同样换算不能直接把 canvas 的 offsetX/offsetY 当作 PDF 坐标写入这是最容易被忽略的坑。4. 签章落地的四个翻车坑位颜色、模糊、错位、白图4.1 红章偏成暗红色真机和预览器显示不一致现象公章 PNG 在素材管理里看着是正红绘制到 PDF 后在 Mac 的“预览”里打开是偏暗的枣红色手机上又比预览器鲜亮一些打印出来更是一言难尽。原因最典型的是 PNG 资源色彩空间被标记为 Display P3 或未标记CGContext 绘制时默认按设备色彩空间映射加上 PDF 查看器各自的颜色管理策略不同就会出现同一份文件在不同软件里色差明显。另一类来源是设计师导出 PNG 时勾选了“嵌入颜色配置文件”而 PDF 写入时没有保留该 profile。解决统一在代码里做色彩空间约束。常见做法是在绘制印章前将UIImage通过CIImage.imageByApplyingCIContext重绘到sRGB色彩空间也可以直接要求设计师导出 PNG 时勾选 sRGB、关闭 Display P3并在命名里标记_sRGB。最稳妥的兜底方案是运行时检测func srgbImage(from image: UIImage) - UIImage? { guard let cg image.cgImage else { return nil } let colorSpace CGColorSpace(name: CGColorSpace.sRGB)! guard let newCG cg.copy(colorSpace: colorSpace) else { // 如果无法直接转换走一次离屏渲染 UIGraphicsBeginImageContextWithOptions(image.size, false, image.scale) image.draw(in: CGRect(origin: .zero, size: image.size)) let result UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return result } return UIImage(cgImage: newCG) }另外红章偏暗的另一个放大器是绘制时上下文没关闭插值CGContext 默认的 interpolation 对红色边缘的渐变会产生灰边视觉上也会让章显得“脏”。绘制过章后建议显式context.setShouldAntialias(true)并保持高插值。4.2 手写签名在 PDF 里发虚放大后出现锯齿现象签名在签名视图里很清晰写入 PDF 后用 PDF 阅读器放大到 200% 时边缘发虚像低分辨率位图同一份文件在手机上预览又还可以。原因签名被渲染成位图 UIImage绘制到 PDF 时如果图片的像素密度不够等于间接把“位图”嵌进 PDF放大自然会糊。另一个常见操作是把整页 PDF 先截屏成 UIImage再贴回新 PDF这是最严重的翻车——整页成位图文件也膨胀数倍。解决签名路径尽量用矢量路径绘制进 PDF 上下文而不是转成 UIImage 再画。如果受限于采集方式只能拿到 UIImage至少要按目标绘制区域的三倍尺寸导出签名图UIGraphicsImageRendererFormat里显式设置scale 3并在绘制时用context.interpolationQuality .high。对于“把整页做位图再贴回”的做法要坚决杜绝——直接按第 2 章的管线原样 drawPDFPage 保持矢量页再叠矢量/位图签名。4.3 真机上位置正确PC 上签章跑到页外或旋转 90 度现象iOS 模拟器和真机预览都正常用户反馈 PC 端打开后签名横着、印章跑出页面边界甚至同一份 PDF 在 macOS 预览器和 Adobe Acrobat 里表现不一致。原因写入签章时用了 UIKit 视图坐标而没有换算成 PDF 坐标尤其没处理rotationAngle。部分 PDF 由扫描仪/打印机生成时自带旋转标志比如 rotationAngle 90页面实际渲染时宽高交换代码里如果只取了 mediaBox 原始宽高签章坐标就会按旋转前的坐标系放置表现出横竖错乱。解决每次签章前先判断page.rotationAngle。当角度为 90 或 270 时交换 pageRect 的宽高再执行 3.1 节的归一化换算。同时建议在工程里留一个“坐标系自检页”在每一页的 (0.5, 0.5) 处画一个红十字输出后肉眼检查是否落在页面正中颜色、方向是否正确再做正式签章。4.4 微信小程序/H5 里 canvas 签名导出白图现象uniapp 开发的 H5 页面在 iOS Safari 里canvas 签名正常但点击“导出/生成 PDF”时拿到的是白图或者首次签名正常第二次签完导出又是白图在部分 iOS 版本上把 canvas 转 dataURL 后内容为空。原因iOS Safari 对 canvas 的离屏渲染有自身机制尤其在 uniapp 中签章画布被 WebView 合成、popup、动画等场景干扰时canvas 纹理在导出瞬间被回收或未提交。这与“uniapp canvas 队列”问题同源多个 canvas 操作排入队列导出时机发生在纹理上屏前就开始读取像素。解决导出前强制执行一次“让 canvas 上屏”的操作例如先切换到别的页面再回来或在导出事件里加requestAnimationFrame双重延迟更稳妥的是把 canvas 内容在每次签名结束时就同步到一个离屏 canvas 并保存为 dataURL导出时直接用这份快照而不是依赖当前屏幕上的 canvas 纹理。对于关键交付场景优先把签名和盖章交给原生端插件H5 只做展示和触发彻底绕开 canvas 兼容性。5. 签完怎么锁PDF权限密码、锁写与后端摘要校验5.1 用 Quartz 设置 PDF 访问权限与打开密码签章完成后不能让用户随意打开编辑或替换签章页iOS 原生能力里能做到的是通过 Quartz 的kCGPDFContext系列 key 设置 PDF 打开密码和权限。在 UIGraphicsPDFRenderer 环境里可以在writePDF前通过 PDF 上下文选项传入let pdfInfo: [CFString: Any] [ kCGPDFContextOwnerPassword: signer-owner-key, kCGPDFContextUserPassword: signer-user-key, kCGPDFContextAllowsPrinting: true, kCGPDFContextAllowsCopying: false, kCGPDFContextAllowsHighQualityPrinting: true ] let format UIGraphicsPDFRendererFormat() format.documentInfo pdfInfo as [String: Any] let renderer UIGraphicsPDFRenderer(bounds: .zero, format: format)逻辑说明kCGPDFContextOwnerPassword是所有者密码控制文档权限kCGPDFContextUserPassword是打开文档需要的用户密码。业务上通常 owner 密码由后端保管用户侧不设打开密码只禁止复制和修改。AllowsCopying false可以阻止大多数阅读器直接复制文字内容但要注意它并不能阻止截屏。这个方案对“已经生成的 PDF 文件做二次加密”不适用它是在生成 PDF 时顺带写入安全属性。5.2 防篡改只靠App端不够哈希摘要与后端数字签名的分工App 端设置 PDF 权限密码只能算“锁写”不能算“防篡改”。原因很简单PDF 权限密码只是阅读器层面的软约束专业工具可以直接绕过并重写文档。真正让签章具备不可抵赖性的是两类手段一是外部的 CA 数字签名对 PDF 内容摘要做非对称签名二是把摘要哈希上链/存证。在 iOS 端可落地的做法是签章完成后对 PDF 的二进制数据做一次 SHA-256把这个摘要连同签章坐标、用户 ID、时间戳一起作为 JSON 上报后端后端用自己的私钥对摘要做签名并把签名结果存储或上链。验收时做完整性校验重新计算文件哈希若不一致说明文件在传输或存储过程中被动过。func sha256Digest(of url: URL) - String? { guard let fileData try? Data(contentsOf: url) else { return nil } var digest [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) fileData.withUnsafeBytes { buffer in _ CC_SHA256(buffer.baseAddress, CC_LONG(fileData.count), digest) } return digest.map { String(format: %02x, $0) }.joined() }参数与边界摘要计算必须在 PDF 文件最终写入后执行不能在内存中的 Data 对象尚未 flush 时计算。后端做数字签名时摘要字符串要连同签章坐标一起参与签名这样能防止“文件合法但签章位置被替换”的极端篡改。时间戳建议由后端统一签发App 本地时间不可信容易被改造成伪造链路的一环。6. 验收清单用这套办法三天稳住签章交付6.1 四类PDF样本必测签章问题的隐蔽性在于你测的源文件越“干净”越容易漏掉边界情况。我一般会让测试同学准备四类样本一是纯文字 PDF无图片、无字体嵌入二是扫描件整页是位图三是包含表单域和多级嵌套页面的 PDF四是带有旋转标志的 PDF。其中第四类最容易暴露第 4.3 节提到的坐标错位问题。对每个样本在 iOS 模拟器、iOS 真机和 macOS 预览器三端打开对比签章位置与颜色。模拟器上验证通过只是第一步因为模拟器走的是 macOS 的图形栈色彩管理和真机差距明显真机验证才是颜色和清晰度的唯一标准。建议在ios开发者模式开启的状态下用 Xcode 的 View Debugging 检查 PDF 视图层级确认签章图层没有被系统裁剪。6.2 用charles抓包核对回传完整性签章后的 PDF 要走网络回传回传链路上的意外修改比绘制错误更可怕。用 Charles 代理 iOS 请求重点关注两件事一是回传的 PDF 大小是否合理重绘后一般会比源文件大 100KB 以内大幅膨胀要怀疑把整页转成了位图二是抓包拿到的响应体能不能直接被预览器打开。如果回传接口是 base64 嵌套在 JSON 里还要确认编码后没有换行符被剥离这类问题在部分后端网关里会静默出现客户端很难察觉。6.3 多版本iOS真机与开发者模式验证最后一条习惯不要只在当前开发机上验证。iOS 16 与 iOS 18 的 PDFKit 行为有差异尤其是旋转页面处理和渲染插值策略。有条件就在 iOS 16、17、18 各留一台真机至少覆盖跑签章绘制和回传两条用例。如果没有多台设备优先保住最低支持版本因为大部分颜色和坐标问题在低版本系统上更容易暴露。我现在的习惯是出包前一天把同一份带签章的 PDF 在预览器和真机上对比一次命中过一次红章偏色和一次坐标翻转后再也不敢只在模拟器上看一眼就提交。把上面这套自检走一遍比你连加三个小时班修线上问题要划算得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表