javascript: convert HTML files to PDF
convert HTML files to PDF
https://ekoopmans.github.io/html2pdf.js/
https://github.com/eKoopmans/html2pdf.js
https://www.nutrient.io/blog/top-ten-ways-to-convert-html-to-pdf/
Nutrient Document Engine — Enterprise-grade HTML-to-PDF with modern CSS support, form conversion, and workflow automation.
wkhtmltopdf — CLI powered by WebKit; handles complex HTML/CSS and JavaScript.
Puppeteer — Node.js automation for Chrome/Chromium; great for JavaScript‑heavy pages.
Playwright — Cross‑browser automation (Chromium, Firefox, WebKit).
jsPDF — Lightweight client‑side PDF creation for simple layouts.
html2pdf.js — Combines html2canvas + jsPDF for browser‑side export.
WeasyPrint — Python engine that renders HTML/CSS precisely.
pdfmake — Declarative JSON schema for PDFs in Node and browser.
PDFKit — Programmatic Node.js PDF generation with fine layout control.
Dompdf — PHP library for HTML/CSS to PDF.
https://github.com/niklasvh/html2canvas
https://html2canvas.hertzen.com/
https://github.com/MrRio/jsPDF
<script src="jspdf.min.js"></script> <script src="html2canvas.min.js"></script> <script src="html2pdf.min.js"></script>
<%@ Page Language="C#" AutoEventWireup="true" ValidateRequest="false" CodeBehind="WebForm11.aspx.cs" Inherits="ModalPopup.WebForm11" %>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
<title>TinyMCE 图片上传示例</title>
<meta name="author" content="geovindu,Geovin Du,塗聚文,涂聚文" />
<!-- TinyMCE 7.0 CDN -->
<script src="tinymce/7.6.1/tinymce.min.js" referrerpolicy="origin"></script>
<!-- PDF导出库 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<!-- mammoth.js 用于Word文档解析 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/mammoth/1.6.0/mammoth.browser.min.js"></script>
<!-- html-docx-js 用于Word导出 -->
<script src="https://cdn.jsdelivr.net/npm/html-docx-js@0.3.1/dist/html-docx.min.js"></script>
<style>
.upload-progress {
display: none;
margin-top: 10px;
}
.upload-success {
color: green;
margin-top: 10px;
}
.upload-error {
color: red;
margin-top: 10px;
}
.progress-container {
width: 300px;
height: 20px;
background-color: #e0e0e0;
border-radius: 10px;
overflow: hidden;
margin-top: 5px;
}
.progress-bar {
height: 100%;
background-color: #4CAF50;
text-align: center;
line-height: 20px;
color: white;
transition: width 0.3s ease;
width: 0%;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.editor-section {
margin-bottom: 30px;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
background-color: #f9f9f9;
}
.editor-section h3 {
margin-top: 0;
color: #333;
margin-bottom: 15px;
}
.button-group {
margin-top: 20px;
text-align: center;
}
.button-group input[type="button"] {
margin: 0 5px;
padding: 8px 16px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.button-group input[type="button"]:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<form id="form1" runat="server">
<div class="container">
<h2>TinyMCE 7.0 多编辑器演示</h2>
<!-- 编辑器1 -->
<div class="editor-section">
<h3>编辑器 1</h3>
<asp:TextBox ID="txtContent1" runat="server" TextMode="MultiLine" Rows="8" Columns="60"></asp:TextBox>
<div class="upload-progress" id="uploadProgress1">
<asp:Label ID="lblProgress1" runat="server" Text="上传中..."></asp:Label>
<div class="progress-container">
<div class="progress-bar" id="progressBar1">0%</div>
</div>
</div>
<div id="uploadResult1" class="upload-success" style="display: none;">
<asp:Label ID="lblSuccess1" runat="server" Text=""></asp:Label>
</div>
<div id="uploadError1" class="upload-error" style="display: none;">
<asp:Label ID="lblError1" runat="server" Text=""></asp:Label>
</div>
</div>
<!-- 编辑器2 -->
<div class="editor-section">
<h3>编辑器 2</h3>
<asp:TextBox ID="txtContent2" runat="server" TextMode="MultiLine" Rows="8" Columns="60"></asp:TextBox>
<div class="upload-progress" id="uploadProgress2">
<asp:Label ID="lblProgress2" runat="server" Text="上传中..."></asp:Label>
<div class="progress-container">
<div class="progress-bar" id="progressBar2">0%</div>
</div>
</div>
<div id="uploadResult2" class="upload-success" style="display: none;">
<asp:Label ID="lblSuccess2" runat="server" Text=""></asp:Label>
</div>
<div id="uploadError2" class="upload-error" style="display: none;">
<asp:Label ID="lblError2" runat="server" Text=""></asp:Label>
</div>
</div>
<!-- 编辑器3 -->
<div class="editor-section">
<h3>编辑器 3</h3>
<asp:TextBox ID="txtContent3" runat="server" TextMode="MultiLine" Rows="8" Columns="60"></asp:TextBox>
<div class="upload-progress" id="uploadProgress3">
<asp:Label ID="lblProgress3" runat="server" Text="上传中..."></asp:Label>
<div class="progress-container">
<div class="progress-bar" id="progressBar3">0%</div>
</div>
</div>
<div id="uploadResult3" class="upload-success" style="display: none;">
<asp:Label ID="lblSuccess3" runat="server" Text=""></asp:Label>
</div>
<div id="uploadError3" class="upload-error" style="display: none;">
<asp:Label ID="lblError3" runat="server" Text=""></asp:Label>
</div>
</div>
<div class="button-group">
<asp:Button ID="btnSaveAll" runat="server" Text="保存所有内容" OnClick="btnSaveAll_Click" />
<asp:Button ID="btnSave1" runat="server" Text="保存编辑器1" OnClick="btnSave1_Click" />
<asp:Button ID="btnSave2" runat="server" Text="保存编辑器2" OnClick="btnSave2_Click" />
<asp:Button ID="btnSave3" runat="server" Text="保存编辑器3" OnClick="btnSave3_Click" />
</div>
</div>
</form>
<script>
// 全局变量用于存储每个编辑器的上传回调
var editorCallbacks = {};
// 初始化编辑器的函数
function initEditor(editorId, progressId, resultId, errorId, successLabelId, errorLabelId) {
console.log('初始化编辑器:', editorId);
tinymce.init({
selector: '#' + editorId,
plugins: 'image media link preview code',
toolbar: 'undo redo | bold italic | alignleft aligncenter alignright | link image media | preview code | export_pdf export_word import_word',
height: 300,
language: 'zh_CN',
language_url: 'https://cdn.tiny.cloud/1/no-api-key/tinymce/7/langs/zh_CN.js',
// 支持图片和视频上传
file_picker_types: 'image media',
// 自定义按钮配置
setup: function (editor) {
// 导出为PDF按钮
editor.ui.registry.addButton('export_pdf', {
text: '导出PDF',
icon: 'document-properties',
tooltip: '导出为PDF文件',
onAction: function () {
exportToPDF(editor);
}
});
// 导出为Word按钮
editor.ui.registry.addButton('export_word', {
text: '导出Word',
icon: 'newdocument',
tooltip: '导出为Word文件',
onAction: function () {
exportToWord(editor);
}
});
// 导入Word按钮
editor.ui.registry.addButton('import_word', {
text: '导入Word',
icon: 'upload',
tooltip: '从Word文件导入',
onAction: function () {
importFromWord(editor);
}
});
},
file_picker_callback: function (callback, value, meta) {
console.log('编辑器', editorId, '的文件选择器被调用');
// 存储该编辑器的回调函数
editorCallbacks[editorId] = callback;
// 根据meta.type判断是图片还是视频
var isImage = meta.filetype === 'image';
var isMedia = meta.filetype === 'media';
console.log('文件类型:', meta.filetype, 'isImage:', isImage, 'isMedia:', isMedia);
// 创建文件输入框
var input = document.createElement('input');
input.setAttribute('type', 'file');
// 设置文件类型过滤
if (isImage) {
input.setAttribute('accept', 'image/*');
} else if (isMedia) {
input.setAttribute('accept', 'video/*');
} else {
input.setAttribute('accept', 'image/*,video/*');
}
input.onchange = function () {
var file = this.files[0];
if (!file) {
console.log('未选择文件');
return;
}
console.log('选择的文件:', file.name, file.size);
// 显示该编辑器的上传进度
document.getElementById(progressId).style.display = 'block';
document.getElementById(resultId).style.display = 'none';
document.getElementById(errorId).style.display = 'none';
// 创建表单数据
var formData = new FormData();
formData.append('file', file);
// 创建XMLHttpRequest
var xhr = new XMLHttpRequest();
xhr.open('POST', '/TinyMCEWebForm/TinyMCEUploadHandler.ashx');
// 监听上传进度
xhr.upload.addEventListener('progress', function (e) {
if (e.lengthComputable) {
var percentComplete = Math.round((e.loaded / e.total) * 100);
var progressBar = document.getElementById(progressId.replace('Progress', 'Bar'));
if (progressBar) {
progressBar.style.width = percentComplete + '%';
progressBar.textContent = percentComplete + '%';
}
}
});
// 处理响应
xhr.onload = function () {
document.getElementById(progressId).style.display = 'none';
if (xhr.status === 200) {
try {
var response = JSON.parse(xhr.responseText);
console.log('上传响应:', response);
if (response && response.success) {
// 上传成功
document.getElementById(resultId).style.display = 'block';
document.getElementById(successLabelId).textContent = isMedia ? '视频上传成功!' : '图片上传成功!';
// 调用该编辑器的回调函数插入文件
if (typeof editorCallbacks[editorId] === 'function') {
console.log('调用回调函数插入文件:', response.location);
// 根据文件类型设置不同的参数
if (isMedia) {
// 视频文件
editorCallbacks[editorId](response.location, {
source2: response.location,
poster: '',
type: 'video/mp4'
});
} else {
// 图片文件
editorCallbacks[editorId](response.location, { title: file.name });
}
delete editorCallbacks[editorId]; // 清空回调
} else {
console.error('回调函数不存在');
}
} else {
// 上传失败
document.getElementById(errorId).style.display = 'block';
document.getElementById(errorLabelId).textContent = response ? (response.error || '上传失败') : '无效响应';
delete editorCallbacks[editorId];
}
} catch (parseError) {
console.error('JSON解析错误:', parseError);
document.getElementById(errorId).style.display = 'block';
document.getElementById(errorLabelId).textContent = '响应格式错误: ' + parseError.message;
delete editorCallbacks[editorId];
}
} else {
console.error('HTTP错误,状态码:', xhr.status);
document.getElementById(errorId).style.display = 'block';
document.getElementById(errorLabelId).textContent = '上传失败,状态码: ' + xhr.status;
delete editorCallbacks[editorId];
}
};
// 处理错误
xhr.onerror = function () {
console.error('网络错误');
document.getElementById(progressId).style.display = 'none';
document.getElementById(errorId).style.display = 'block';
document.getElementById(errorLabelId).textContent = '网络错误,请重试';
delete editorCallbacks[editorId];
};
// 发送请求
console.log('开始上传文件...');
xhr.send(formData);
};
// 触发文件选择
input.click();
}
});
}
// 页面加载完成后初始化所有编辑器
document.addEventListener('DOMContentLoaded', function() {
// 初始化编辑器1
initEditor(
'<%= txtContent1.ClientID %>',
'uploadProgress1',
'uploadResult1',
'uploadError1',
'<%= lblSuccess1.ClientID %>',
'<%= lblError1.ClientID %>'
);
// 初始化编辑器2
initEditor(
'<%= txtContent2.ClientID %>',
'uploadProgress2',
'uploadResult2',
'uploadError2',
'<%= lblSuccess2.ClientID %>',
'<%= lblError2.ClientID %>'
);
// 初始化编辑器3
initEditor(
'<%= txtContent3.ClientID %>',
'uploadProgress3',
'uploadResult3',
'uploadError3',
'<%= lblSuccess3.ClientID %>',
'<%= lblError3.ClientID %>'
);
console.log('所有编辑器初始化完成');
});
// PDF导出功能
function exportToPDF(editor) {
console.log('开始导出PDF...');
try {
// 获取编辑器内容
var content = editor.getContent();
var editorName = editor.id;
// 创建临时容器
var tempContainer = document.createElement('div');
tempContainer.innerHTML = content;
tempContainer.style.width = '794px'; // A4宽度
tempContainer.style.padding = '40px';
tempContainer.style.position = 'absolute';
tempContainer.style.left = '-9999px';
document.body.appendChild(tempContainer);
// 使用html2canvas将内容转换为图片
html2canvas(tempContainer, {
scale: 2,
useCORS: true,
allowTaint: true
}).then(function(canvas) {
// 创建PDF
const { jsPDF } = window.jspdf;
var pdf = new jsPDF('p', 'px', 'a4');
var imgData = canvas.toDataURL('image/png');
var imgWidth = 794;
var pageHeight = 1123;
var imgHeight = canvas.height * imgWidth / canvas.width;
var heightLeft = imgHeight;
var position = 0;
// 添加第一页
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
// 添加更多页面(如果内容超过一页)
while (heightLeft >= 0) {
position = heightLeft - imgHeight;
pdf.addPage();
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
}
// 保存PDF
var fileName = '文档_' + editorName + '_' + new Date().getTime() + '.pdf';
pdf.save(fileName);
// 清理临时容器
document.body.removeChild(tempContainer);
console.log('PDF导出成功:', fileName);
alert('PDF导出成功!');
}).catch(function(error) {
console.error('PDF导出失败:', error);
alert('PDF导出失败:' + error.message);
document.body.removeChild(tempContainer);
});
} catch (error) {
console.error('PDF导出过程中发生错误:', error);
alert('PDF导出失败:' + error.message);
}
}
// 使用html-docx-js实现Word导出功能
function exportToWord(editor) {
console.log('开始使用html-docx-js导出Word文档...');
try {
// 检查html-docx-js是否加载成功
if (typeof htmlDocx === 'undefined') {
console.error('html-docx-js库未正确加载');
alert('Word导出失败:html-docx-js库未正确加载,请检查网络连接!');
return;
}
// 获取编辑器内容
var content = editor.getContent();
var editorName = editor.id;
// 构建完整的HTML文档(包含样式)
var htmlContent = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body { font-family: "Microsoft YaHei", SimHei, Arial, sans-serif; font-size: 12pt; line-height: 1.5; }
h1 { font-size: 24pt; font-weight: bold; margin: 20px 0; }
h2 { font-size: 20pt; font-weight: bold; margin: 18px 0; }
h3 { font-size: 16pt; font-weight: bold; margin: 16px 0; }
p { margin: 10px 0; }
img { max-width: 100%; height: auto; }
table { border-collapse: collapse; width: 100%; margin: 10px 0; }
table td, table th { border: 1px solid #000; padding: 5px; }
ul, ol { margin: 10px 0; padding-left: 20px; }
.align-left { text-align: left; }
.align-center { text-align: center; }
.align-right { text-align: right; }
.bold { font-weight: bold; }
.italic { font-style: italic; }
.underline { text-decoration: underline; }
</style>
</head>
<body>
${content}
</body>
</html>
`;
console.log('生成HTML内容,开始转换为DOCX...');
// 使用html-docx-js转换为docx
var converted = htmlDocx.asBlob(htmlContent);
// 生成文件名
var fileName = '文档_' + editorName + '_' + new Date().getTime() + '.docx';
// 创建下载链接并触发下载
var link = document.createElement('a');
link.href = URL.createObjectURL(converted);
link.download = fileName;
document.body.appendChild(link);
link.click();
// 清理
document.body.removeChild(link);
URL.revokeObjectURL(link.href);
console.log('Word导出成功:', fileName);
alert('Word文档导出成功!\n文件名:' + fileName);
} catch (error) {
console.error('Word导出过程中发生错误:', error);
alert('导出Word失败:' + error.message + '\n\n请检查浏览器控制台获取更多详细信息。');
}
}
// Word导入功能 - 使用mammoth.js实现
function importFromWord(editor) {
console.log('开始导入Word...');
try {
// 检查mammoth库是否正确加载
if (typeof window.mammoth === 'undefined') {
console.error('mammoth.js库未正确加载');
alert('Word导入失败:mammoth.js库未正确加载');
return;
}
// 创建文件输入框
var input = document.createElement('input');
input.setAttribute('type', 'file');
input.setAttribute('accept', '.docx');
input.onchange = function() {
var file = this.files[0];
if (!file) {
console.log('未选择文件');
return;
}
console.log('选择的Word文件:', file.name, file.size);
// 创建文件读取器
var reader = new FileReader();
reader.onload = function(e) {
try {
console.log('文件读取成功,开始解析...');
// 使用mammoth.js解析Word文档
var arrayBuffer = e.target.result;
mammoth.convertToHtml({ arrayBuffer: arrayBuffer })
.then(function(result) {
console.log('Word文档解析成功');
// 获取解析后的HTML内容
var html = result.value;
var messages = result.messages;
// 显示解析消息(如果有)
if (messages && messages.length > 0) {
console.log('解析消息:', messages);
}
// 将内容插入编辑器
editor.setContent(html);
console.log('Word内容已成功导入到编辑器');
alert('Word文件导入成功!');
})
.catch(function(error) {
console.error('Word文档解析失败:', error);
alert('Word文档解析失败:' + error.message);
});
} catch (error) {
console.error('文件处理失败:', error);
alert('文件处理失败:' + error.message);
}
};
reader.onerror = function() {
console.error('文件读取失败');
alert('文件读取失败,请重试');
};
// 读取文件
reader.readAsArrayBuffer(file);
};
// 触发文件选择
input.click();
} catch (error) {
console.error('Word导入过程中发生错误:', error);
alert('Word导入失败:' + error.message);
}
}
</script>
</body>
</html>
浙公网安备 33010602011771号