
这篇只讲功能层 JavaScript 实现。这个工具是我用 Vue 组织页面交互再配合pdf-lib在浏览器端完成 PDF 生成。pdf-lib是公开可用的通用开源库MIT 许可可直接用于浏览器和 Node.js。核心流程是上传图片 - 读取并建模 - 调整顺序/旋转 - 计算页面与布局 - 写入 PDF - 下载在线工具网址https://see-tool.com/image-to-pdf工具截图0先说下 pdf-lib 在这个工具里的角色pdf-lib在这里主要负责三件事创建 PDF 文档、把图片字节嵌入文档、按给定坐标绘制到页面。它的调用模型很直接create/load - addPage/embed - drawImage - save对图片转 PDF 这类场景它有几个很实用的点API 稳定浏览器端可直接运行不依赖后端转换页面坐标和尺寸控制细适合做“边距、缩放、居中、铺满”这类排版同一套逻辑可扩展到“合并现有 PDF、写文本、填表单”等后续功能这个工具里最核心的一步是embedJpg drawImage前者把二进制图片数据放进 PDF后者决定图片在页面中的位置和显示尺寸。只要前面把页面尺寸和布局参数算对最终导出的版式就会稳定可控。1状态模型围绕“图片队列 转换配置”组织工具初始化后维护一个状态对象核心字段如下images当前待转换图片队列settings页面尺寸、方向、布局、边距、文件名isConverting是否正在生成防止重复点击每张图片对象都会保存name、size、format、preview(dataURL)、width、height、rotation。这样后续排序、旋转、预览和生成 PDF 都可以直接复用同一份数据。2文件接入先过滤再读取 DataURL 和尺寸上传与拖拽最后都进入同一入口先只保留图片类型再做数量上限控制。每个文件会经过两步FileReader.readAsDataURL读成 DataURL用Image对象加载拿到naturalWidth/naturalHeight核心读取函数functionreadFileAsDataUrl(file){returnnewPromise(function(resolve,reject){varreadernewFileReader();reader.onloadfunction(event){resolve(String(event.targetevent.target.result?event.target.result:),);};reader.onerrorfunction(){reject(newError(file_read_failed));};reader.readAsDataURL(file);});}读取成功后把图片对象追加进队列页面统计和预览区会立即重渲染。3页面尺寸计算统一转换到 PDF 点单位PDF 里统一使用 pointpt因此先做单位换算MM_TO_PT 72 / 25.4PX_TO_PT 72 / 96固定纸张A4/A3/Letter/Legal先由毫米转 pt如果用户选“原始尺寸”则按图片像素转 pt。方向为auto时会根据图片宽高自动判定横版或竖版。functiongetPageSizePt(pageSize,orientation,imageWidth,imageHeight){varwidthPt;varheightPt;if(pageSizeoriginal){widthPtimageWidth*PX_TO_PT;heightPtimageHeight*PX_TO_PT;}else{varpageSizeValuePAGE_SIZES_MM[pageSize]||PAGE_SIZES_MM.a4;widthPtpageSizeValue.width*MM_TO_PT;heightPtpageSizeValue.height*MM_TO_PT;}// 省略方向交换逻辑return{widthPt:widthPt,heightPt:heightPt,orientation:finalOrientation,};}4布局算法contain / cover / center每页先算内容区contentWidth pageWidth - margin * 2contentHeight pageHeight - margin * 2然后按布局模式计算绘制矩形contain完整显示图片按最小缩放比适配cover铺满内容区按最大缩放比适配center不缩放居中绘制当边距过大导致内容区宽高小于等于 0 时直接抛错转换流程会给出明确提示。5旋转处理先画到 Canvas再统一转 JPEG为了让pdf-lib写入稳定工具会先把当前图片含旋转角度绘制到canvas再导出 JPEG 字节。角度支持 0/90/180/270。functiondrawImageToJpeg(image,rotation){varangleNumber(rotation||0);varswapSideangle90||angle270;varcanvasdocument.createElement(canvas);canvas.widthswapSide?image.naturalHeight:image.naturalWidth;canvas.heightswapSide?image.naturalWidth:image.naturalHeight;varcontextcanvas.getContext(2d,{alpha:false});context.fillStyle#ffffff;context.fillRect(0,0,canvas.width,canvas.height);context.translate(canvas.width/2,canvas.height/2);context.rotate((angle*Math.PI)/180);context.drawImage(image,-image.naturalWidth/2,-image.naturalHeight/2);returnparseDataUrl(canvas.toDataURL(image/jpeg,0.92));}这里先铺白底可以避免透明图在 PDF 中出现黑底。6PDF 生成逐页 addPage embedJpg drawImage转换时按图片队列顺序循环处理每张图先算页面尺寸和布局再写入 PDFasyncfunctionbuildPdfBlobWithPdfLib(pages,PDFDocument){varpdfDocumentawaitPDFDocument.create();for(vari0;ipages.length;i1){varitempages[i];varpagepdfDocument.addPage([item.pageWidthPt,item.pageHeightPt]);varjpgawaitpdfDocument.embedJpg(item.imageBytes);page.drawImage(jpg,{x:item.xPt,y:item.yPt,width:item.drawWidthPt,height:item.drawHeightPt,});}varpdfBytesawaitpdfDocument.save();returnnewBlob([pdfBytes],{type:application/pdf});}最终通过对象 URL 触发浏览器下载文件名会先做非法字符清洗。7交互动作如何影响最终 PDF工具里“上移/下移/拖拽排序/旋转/删除”都直接改images队列因此最终 PDF 页序与图片列表始终一致。排序交换数组元素或拖拽后splice旋转rotation (rotation 90) % 360删除按id过滤由于生成时是按队列顺序遍历交互结果会无缝反映到导出文件。8转换过程控制状态锁 分阶段提示点击“转换”后会先进入isConverting true禁用上传、配置项和操作按钮显示“正在生成第 X / N 页”完成后恢复可操作状态错误按场景给出明确反馈比如无图片、文件名为空、边距过大、生成失败。整条链路是纯前端闭环从读取、排版到导出都在浏览器端完成。