如何使用PHP+MySQL存储Fabric.js画布图片属性实现项目保存
Fabric.js 画布状态持久化实现方案
你之前想的存储元素属性的方向是对的,但不要只存width、height、left、top,Fabric中元素的大小变化本质是缩放属性控制的,额外存储缩放比例、旋转角度、翻转状态才能100%还原画布状态。整体实现分5个部分:改造数据库字段存储元素状态、前端加载时读取已存状态、监听用户操作自动同步状态到数据库、编写后端保存接口、实现画布存图功能。
第一步:改造数据库表结构
先给你的project_letters表新增以下字段,用来存储每个图片元素的状态:
leftDECIMAL(10,2) DEFAULT 10 :元素X轴坐标topDECIMAL(10,2) DEFAULT 10 :元素Y轴坐标scale_xDECIMAL(5,3) DEFAULT 0.1 :元素X轴缩放比例scale_yDECIMAL(5,3) DEFAULT 0.1 :元素Y轴缩放比例flip_xTINYINT(1) DEFAULT 0 :是否水平翻转,1为是0为否flip_yTINYINT(1) DEFAULT 0 :是否垂直翻转,1为是0为否angleDECIMAL(6,2) DEFAULT 0 :元素旋转角度
第二步:改造前端图片加载逻辑
把你原来PHP循环输出addImage函数的部分替换成下面的代码,加载图片时直接读取数据库中已存的状态值,没有历史值时用默认参数:
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/510/fabric.min.js"></script> <canvas id="canvas"> </canvas> <script> // 初始化画布 var canvas = new fabric.Canvas('canvas', { backgroundColor: '#fff' }); <?php // 改用预处理查询,避免SQL注入 $stmt = $Connection->prepare("SELECT * FROM products p, project_letters l WHERE l.product_id =p.product_id and l.project_id = ? ORDER BY l.letter DESC"); $stmt->bind_param("i", $p); $stmt->execute(); $mres = $stmt->get_result(); $NumOfimages = $mres->num_rows; if ($NumOfimages > 0) { while ($row= $mres->fetch_assoc()) { // 读取已存状态,没有则用默认值 $pl_id = $row['pl_id']; $img_path = 'images/products/'.$row['image']; $left = $row['left'] ?? 10; $top = $row['top'] ?? 10; $scaleX = $row['scale_x'] ?? 0.1; $scaleY = $row['scale_y'] ?? 0.1; $flipX = $row['flip_x'] ?? 0; $flipY = $row['flip_y'] ?? 0; $angle = $row['angle'] ?? 0; ?> // 加载图片时直接绑定已存状态 fabric.Image.fromURL('<?php echo $img_path; ?>', function (img) { img.set({ pl_id: <?php echo $pl_id; ?>, // 绑定自定义主键,后续更新时匹配数据库记录 left: <?php echo $left; ?>, top: <?php echo $top; ?>, scaleX: <?php echo $scaleX; ?>, scaleY: <?php echo $scaleY; ?>, flipX: <?php echo $flipX ? 'true' : 'false'; ?>, flipY: <?php echo $flipY ? 'true' : 'false'; ?>, angle: <?php echo $angle; ?> }); canvas.add(img); }, { crossOrigin: 'anonymous' // 防止导出图片时跨域污染 }); <?php }}?> canvas.setWidth(<?php echo $WIDTH ?>); canvas.setHeight(<?php echo $HEIGHT ?>);
第三步:监听元素操作自动保存状态
在画布初始化、所有图片加载完成的代码后面,添加Fabric事件监听,用户结束拖动、缩放、旋转操作时,自动把元素最新状态发送给后端保存:
// 监听元素修改事件(拖动、缩放、旋转结束松开鼠标时触发) canvas.on('object:modified', function(e) { const activeObj = e.target; // 跳过非图片元素 if (!activeObj.pl_id) return; // 收集当前元素状态 const formData = new FormData(); formData.append('pl_id', activeObj.pl_id); formData.append('left', activeObj.left.toFixed(2)); formData.append('top', activeObj.top.toFixed(2)); formData.append('scale_x', activeObj.scaleX.toFixed(3)); formData.append('scale_y', activeObj.scaleY.toFixed(3)); formData.append('flip_x', activeObj.flipX ? 1 : 0); formData.append('flip_y', activeObj.flipY ? 1 : 0); formData.append('angle', activeObj.angle.toFixed(2)); // 发送请求到后端 fetch('save_element_state.php', { method: 'POST', body: formData }).catch(err => console.error('状态保存失败', err)); });
第四步:编写后端状态保存接口
新建save_element_state.php文件,接收前端传的元素参数,更新到数据库:
<?php require_once '你的数据库连接文件路径.php'; if ($_SERVER['REQUEST_METHOD'] === 'POST') { // 参数类型校验 $pl_id = intval($_POST['pl_id']); $left = floatval($_POST['left']); $top = floatval($_POST['top']); $scale_x = floatval($_POST['scale_x']); $scale_y = floatval($_POST['scale_y']); $flip_x = intval($_POST['flip_x']); $flip_y = intval($_POST['flip_y']); $angle = floatval($_POST['angle']); // 预处理更新,防SQL注入 $stmt = $Connection->prepare("UPDATE project_letters SET `left` = ?, `top` = ?, scale_x = ?, scale_y = ?, flip_x = ?, flip_y = ?, angle = ? WHERE pl_id = ?"); $stmt->bind_param("dddddddi", $left, $top, $scale_x, $scale_y, $flip_x, $flip_y, $angle, $pl_id); $stmt->execute(); echo 'ok'; } ?>
第五步:画布导出为图片
你原有的手动下载图片逻辑可以正常使用,如果需要把画布图片自动保存到服务器,添加对应逻辑即可:
// 原有手动下载功能保留 var link = document.createElement('a'); link.innerHTML = '导出为图片'; link.addEventListener('click', function(ev) { link.href = canvas.toDataURL(); link.download = "<?php echo $project['project_title'] ?>.png"; }, false); document.body.appendChild(link); // 新增:保存画布到服务器 var saveServerBtn = document.createElement('a'); saveServerBtn.innerHTML = '保存画布到服务器'; saveServerBtn.addEventListener('click', function(ev) { ev.preventDefault(); const base64 = canvas.toDataURL('image/png'); const formData = new FormData(); formData.append('img_base64', base64); formData.append('project_id', <?php echo $p; ?>); fetch('save_canvas_img.php', { method: 'POST', body: formData }).then(()=>alert('画布保存成功')); }, false); document.body.appendChild(saveServerBtn); </script>
对应的save_canvas_img.php后端代码:
<?php require_once '你的数据库连接文件路径.php'; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $project_id = intval($_POST['project_id']); $base64 = $_POST['img_base64']; // 解析base64图片数据 $base64 = str_replace('data:image/png;base64,', '', $base64); $base64 = str_replace(' ', '+', $base64); $imgData = base64_decode($base64); // 生成保存路径 $savePath = 'uploads/canvas/'.$project_id.'_'.time().'.png'; file_put_contents($savePath, $imgData); // 可把路径存到项目表的封面字段 $stmt = $Connection->prepare("UPDATE projects SET cover_path = ? WHERE project_id = ?"); $stmt->bind_param("si", $savePath, $project_id); $stmt->execute(); echo 'ok'; } ?>
注意事项
- 不要单独存储width/height属性:Fabric会保留图片原始宽高,所有尺寸变化通过缩放属性控制,只存宽高会丢失旋转、翻转、等比缩放的状态,还原时容易变形
- 不要用高频触发事件做保存:
object:moving/object:scaling这类事件会在用户拖动过程中几十毫秒触发一次,会产生大量无效请求,用object:modified事件只会在用户操作结束松开鼠标时触发一次,性能最优 - 所有数据库操作用预处理语句:你原有代码中直接把
$p变量拼接到SQL里存在SQL注入风险,所有用户传入的参数必须通过bind_param绑定后再执行查询 - 注意图片跨域问题:如果后续用到外链图片,需要给图片服务器配置跨域头,同时在fromURL时加
crossOrigin: 'anonymous'参数,否则导出图片时会出现画布污染报错
内容的提问来源于stack exchange,提问作者Danny Mak
相关产品推荐
相关产品推荐

