canvas是一个可以在上面通过javaScript画图的标签
这是前端展示的一个效果
点击取消,清除画布内的所有元素,点击保存,会将图片传到save-signature路径下
<!DOCTYPE html>
<%@ page contentType="text/html;charset=UTF-8" language="java"%>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>手写签名</title>
</head>
<body>
<!--
大白话就是canvas是一个可以在上面通过javaScript画图的标签,
通过其提供的context(上下文)及Api进行绘制,在这个过程中canvas充当画布的角色
-->
<canvas id="canvas"></canvas>
<div>
<button id="cancel">取消</button>
<button id="save">保存</button>
</div>
</body>
<script>
const cbtn = document.getElementById('cancel')
const sbtn = document.getElementById('save')
/** @type {HTMLCanvasElement} */
// 获取canvas实例
const canvas = document.getElementById("canvas")
// 配置信息
const config = {
width:400,
height:200,
lineWidth:5, // 线宽
strokeStyle:'black', // 线条颜色
lineCap:'round', // 线条两端圆点
lineJoin:'round' // 线条交汇处圆角
}
canvas.width = config.width
canvas.height = config.height
canvas.style.border = '1px solid #000'
// canvas.style.backgroundColor = 'yellow'
// 创建画布
let ctx = canvas.getContext('2d')
let client = {
offsetX:0,
offsetY:0, // 偏移量
endX:0,
endY:0 // 坐标
}
// 鼠标按下或触摸开始的回调,主要操作为获取当前鼠标按下/触摸开始的偏移量和坐标,进行起点绘制,web端可以直接通过event中取到,而移动端则需要在event.changedTouches[0]中取到
const init = (event) => {
const { offsetX,offsetY,pageX,pageY } = mobileStatus ? event.changedTouches[0] : event
client.offsetX = offsetX
client.offsetY = offsetY
client.endX = pageX
client.endY = pageY
// 开始绘制(清除以上一次beginPath之后的所有路径,进行绘制)
ctx.beginPath()
ctx.lineWidth = config.lineWidth
ctx.strokeStyle = config.strokeStyle
ctx.lineCap = config.lineCap
ctx.lineJoin = config.lineJoin
ctx.moveTo(client.endX,client.endY)
// 同时监听鼠标或者手势的移动事件
window.addEventListener(mobileStatus ? 'touchmove' : 'mousemove',draw)
}
// 鼠标或者手势的移动的回调
const draw = (e) => {
// 获取当前坐标点位
const { pageX, pageY } = mobileStatus ? event.changedTouches[0] : event
client.endX = pageX
client.engY = pageY
ctx.lineTo(pageX,pageY)
ctx.stroke()
}
// 手势离开或者鼠标弹起(结束本次绘制)的回调
const closeDraw = () => {
// 结束绘制
ctx.closePath()
window.removeEventListener(mobileStatus ? 'touchmove' : 'mousemove',draw)
}
// 取消
const cancel = () => {
// 清除指定矩形内的像素
ctx.clearRect(0,0,config.width,config.height)
}
const save = () => {
console.log(canvas.toDataURL())
canvas.toBlob(blob => {
const formData = new FormData();
//生成的图片名称signature
formData.append('signature', blob, 'signature.png');
//save-signature是后端接口的路径
fetch('save-signature', {
method: 'POST',
body: formData
}).then(response => {
if (response.ok) {
console.log('Signature saved successfully.');
} else {
console.error('Failed to save signature.');
}
}).catch(error => {
console.error('Error saving signature:', error);
});
});
}
// 初始化
window.addEventListener(mobileStatus ? 'touchstart' : 'mousedown',init)
// 结束绘制
window.addEventListener(mobileStatus ? 'touchend' : 'mouseup',closeDraw)
// 取消(清除画布)
cbtn.addEventListener('click',cancel)
// 保存图片
sbtn.addEventListener('click',save)
</script>
</html>
将前端传来的图片保存到/file下,并把图片的全名传到load方法下
@RequestMapping("save-signature")
public void canvas(@RequestParam("signature") MultipartFile file,HttpServletRequest request) throws IOException {
String name=file.getOriginalFilename();
String path=request.getServletContext().getRealPath("/file");
File filePath=new File(path);
if(!filePath.exists()){
filePath.mkdir();
}
file.transferTo(new File(path+"/"+name));
new PdfTest().load(name);
}
load方法中可以在data中加上一些文字,利用PDF表单键值匹配的方法,将data中的文字和图片添加到最后的PDF中,这里设置的是"name","pwd","picture"三个,inputUrl,outputURL和imageUrl必须是准确的路径,表单域的名称必须和代码中data的key值一样
public void load(String name) {
//模板路径
String inputUrl = "D:/workplace/untitled2/target/untitled2/file/pdf表单.pdf";
//生成的文件路径
String outputUrl = "D:/workplace/untitled2/target/untitled2/file/pdf表单(1).pdf";
Map<String, Object> data = new HashMap<>();
data.put("name", "张山");
data.put("pwd", "100001");
//图片地址
String imageUrl = "D:/workplace/untitled2/target/untitled2/file/"+name;
Map<String, String> templateImageMap = new HashMap<>();
templateImageMap.put("picture", imageUrl);
PdfTest.pdfTemplateInsert(inputUrl, outputUrl, data, templateImageMap);
}
将pdf表单的路径,生成文件的路径和存图片的路径传到pdfTemplateInsert方法中
private static void insertImage(AcroFields form, PdfStamper stamper, String filedName, String url) throws IOException, DocumentException {
int pageNo = form.getFieldPositions(filedName).get(0).page;
Rectangle signRect = form.getFieldPositions(filedName).get(0).position;
float x = signRect.getLeft();
float y = signRect.getBottom();
Image image = Image.getInstance(url);
// 获取操作的页面
PdfContentByte under = stamper.getOverContent(pageNo);
// 根据域的大小缩放图片
image.scaleToFit(signRect.getWidth(), signRect.getHeight());
// 添加图片
image.setAbsolutePosition(x, y);
under.addImage(image);
}
public static Boolean pdfTemplateInsert(String templateUrl, String outputFileUrl, Map<String, Object> templateValueMap, Map<String, String> templateImageMap) {
boolean success = true;
OutputStream os = null;
PdfStamper ps = null;
PdfReader reader = null;
try {
os = Files.newOutputStream(new File(outputFileUrl).toPath());
//读取pdf表单
reader = new PdfReader(templateUrl);
//根据表单生成一个新的pdf文件
ps = new PdfStamper(reader, os);
//获取pdf表单
AcroFields form = ps.getAcroFields();
//给表单中添加中文字体
BaseFont bf = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
form.addSubstitutionFont(bf);
if (null != templateValueMap && !templateValueMap.isEmpty()) {
for (String key : templateValueMap.keySet()) {
form.setField(key, String.valueOf(templateValueMap.get(key)));
}
}
if (null != templateImageMap && !templateImageMap.isEmpty()) {
for (String key : templateImageMap.keySet()) {
insertImage(form, ps, key, templateImageMap.get(key));
}
}
ps.setFormFlattening(true);
} catch (Exception e) {
success = false;
} finally {
try {
ps.close();
reader.close();
os.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return success;
}
在画布内写上签名
实现效果