diff --git a/public/static/js/phone-image.js b/public/static/js/phone-image.js
index f25623d..3b69e04 100644
--- a/public/static/js/phone-image.js
+++ b/public/static/js/phone-image.js
@@ -100,7 +100,7 @@ var PhoneImageEngine = (function () {
/**
* 渲染排版预览 - 生成分页后的HTML
- * 两遍遍历: 先算总页数, 再带页码生成
+ * 管线: preprocess -> parse -> renderContentFlow -> measure -> paginate -> generate -> renderThumbnails
* @returns {jQuery Deferred} resolves with pages array
*/
function render() {
@@ -113,36 +113,41 @@ var PhoneImageEngine = (function () {
var cleanHtml = preprocessContent(postData.content_html);
var blocks = parseHtmlToBlocks(cleanHtml);
- // 封面页
- pages.push(generateCoverPage(sizeConfig));
-
- // 内容分页
- var contentPages = paginateContent(blocks, contentAreaHeight, sizeConfig);
- pages = pages.concat(contentPages);
-
- // 尾页
- pages.push(generateSummaryPage(sizeConfig, pages.length));
-
- // 两遍遍历: 现在知道了总页数, 给每页补上 N/M 页码
- var totalPages = pages.length;
- for (var i = 0; i < pages.length; i++) {
- if (pages[i].type === 'content') {
- // 在页脚中添加 N/M 格式页码
- pages[i].html = pages[i].html.replace(
- '' + pages[i].pageNum + '',
- '' + (i + 1) + '/' + totalPages + ''
- );
- }
- }
-
- // 渲染内容流(中间栏)- 同步
+ // 先渲染内容流(DOM) - 用于实测高度
renderContentFlow(blocks);
- // 渲染缩略图(右侧)- 异步截图
- renderThumbnails(sizeConfig).then(function () {
- deferred.resolve(pages);
- }).catch(function (err) {
- deferred.reject(err);
+ // DOM实测高度 - 等待一帧确保渲染完成
+ requestAnimationFrame(function () {
+ measureBlockHeights(blocks);
+
+ // 封面页
+ pages.push(generateCoverPage(sizeConfig));
+
+ // 内容分页(使用实测高度)
+ var contentPages = paginateContent(blocks, contentAreaHeight, sizeConfig);
+ pages = pages.concat(contentPages);
+
+ // 尾页
+ pages.push(generateSummaryPage(sizeConfig, pages.length));
+
+ // 两遍遍历: 现在知道了总页数, 给每页补上 N/M 页码
+ var totalPages = pages.length;
+ for (var i = 0; i < pages.length; i++) {
+ if (pages[i].type === 'content') {
+ // 在页脚中添加 N/M 格式页码
+ pages[i].html = pages[i].html.replace(
+ '' + pages[i].pageNum + '',
+ '' + (i + 1) + '/' + totalPages + ''
+ );
+ }
+ }
+
+ // 渲染缩略图(右侧)- 异步截图
+ renderThumbnails(sizeConfig).then(function () {
+ deferred.resolve(pages);
+ }).catch(function (err) {
+ deferred.reject(err);
+ });
});
return deferred.promise();
@@ -158,15 +163,28 @@ var PhoneImageEngine = (function () {
// 清除空段落
html = html.replace(/
]*>\s*<\/p>/gi, '');
- // 处理图片: 清除内联样式, 强制 max-width:100%
+ // 处理图片: 保留原始尺寸到data属性, 强制 max-width:100%
html = html.replace(/
]*?)>/gi, function (match, attrs) {
- // 移除 style, width, height 内联属性
+ // 移除内联style
attrs = attrs.replace(/\s*style\s*=\s*"[^"]*"/gi, '');
attrs = attrs.replace(/\s*style\s*=\s*'[^']*'/gi, '');
+ // 提取原始width/height值, 保存到data属性
+ var widthVal = '';
+ var heightVal = '';
+ var widthMatch = attrs.match(/\swidth\s*=\s*"([^"]*)"/i) ||
+ attrs.match(/\swidth\s*=\s*'([^']*)'/i);
+ var heightMatch = attrs.match(/\sheight\s*=\s*"([^"]*)"/i) ||
+ attrs.match(/\sheight\s*=\s*'([^']*)'/i);
+ if (widthMatch) widthVal = widthMatch[1];
+ if (heightMatch) heightVal = heightMatch[1];
+ // 移除原始width/height属性
attrs = attrs.replace(/\s*width\s*=\s*"[^"]*"/gi, '');
attrs = attrs.replace(/\s*width\s*=\s*'[^']*'/gi, '');
attrs = attrs.replace(/\s*height\s*=\s*"[^"]*"/gi, '');
attrs = attrs.replace(/\s*height\s*=\s*'[^']*'/gi, '');
+ // 添加data属性保存原始尺寸(用于比例计算)
+ if (widthVal) attrs += ' data-original-width="' + widthVal + '"';
+ if (heightVal) attrs += ' data-original-height="' + heightVal + '"';
return '
';
});
@@ -252,10 +270,20 @@ var PhoneImageEngine = (function () {
case 'table':
block.type = 'table';
block.html = $el[0].outerHTML;
- block.estimatedHeight = estimateTableHeight($el);
+ block.estimatedHeight = 0;
+ block.needsMeasurement = true;
break;
case 'img':
- block.estimatedHeight = estimateImageHeight($el);
+ // 尝试从data属性计算比例高度, 否则标记需要DOM测量
+ var imgW = parseInt($el.attr('data-original-width'), 10);
+ var imgH = parseInt($el.attr('data-original-height'), 10);
+ if (imgW > 0 && imgH > 0) {
+ var imgRatio = getContentWidth() / imgW;
+ block.estimatedHeight = Math.round(imgH * imgRatio) + 20;
+ } else {
+ block.estimatedHeight = 0;
+ block.needsMeasurement = true;
+ }
block.$img = $el;
break;
case 'ul':
@@ -347,11 +375,12 @@ var PhoneImageEngine = (function () {
}
/**
- * 估算表格高度
+ * 估算表格高度 (已废弃 - DOM实测替代)
+ * @deprecated 使用 measureBlockHeights() 替代
*/
function estimateTableHeight($table) {
- var rows = $table.find('tr').length;
- return rows * 36 + 20; // 36px per row (35px content + 1px border) + 20px margin
+ // DOM实测将覆盖此值,返回0作为占位
+ return 0;
}
// ===== 分页核心算法 =====
@@ -833,18 +862,22 @@ var PhoneImageEngine = (function () {
}
/**
- * 渲染到隐藏区域 → 截图 → 显示缩略图
- * @param {Object} sizeConfig
- * @returns {jQuery Deferred} resolves with pages array
+ * 共享截图核心逻辑 - 将pages渲染到staging并逐页截图
+ * @param {Object} opts - { scale, outputCanvas, quality, sizeConfig }
+ * scale: html2canvas缩放 (1=缩略图, 2=保存)
+ * outputCanvas: true=输出canvas对象, false=输出dataURL字符串
+ * quality: JPEG质量 (0-1)
+ * sizeConfig: 尺寸配置(用于纯图片页canvas渲染)
+ * @returns {jQuery Deferred} resolves with array of dataURL strings or canvas objects
*/
- function renderThumbnails(sizeConfig) {
+ function doCapturePages(opts) {
var deferred = $.Deferred();
ensureStaging();
var $staging = $('#render-staging');
$staging.empty();
- // 渲染所有页面到隐藏区域(使用wrapper以便html2canvas正确渲染)
+ // 渲染所有页面到隐藏区域
var sizeClass = 'size-' + config.size;
var $wrapper = $('
');
for (var i = 0; i < pages.length; i++) {
@@ -852,118 +885,25 @@ var PhoneImageEngine = (function () {
}
$staging.append($wrapper);
- // 等待一帧确保 DOM 渲染完成
- requestAnimationFrame(function () {
- // html2canvas会跳过visibility:hidden的元素,临时切换为可见
- $staging.css({ visibility: 'visible' });
- // 先转换代码块为图片
- convertCodeBlocks().then(function () {
- return convertTables();
- }).then(function () {
- var $pageElems = $staging.find('.phone-image-page');
- if ($pageElems.length === 0) {
- $staging.css({ visibility: 'hidden' });
- displayThumbnails([], sizeConfig);
- deferred.resolve(pages);
- return;
- }
+ // 等待字体和DOM就绪
+ var fontsReady = (typeof document.fonts !== 'undefined' && document.fonts.ready)
+ ? document.fonts.ready
+ : $.Deferred().resolve().promise();
- // 串行截图每一页
- var thumbnailDataUrls = [];
- var idx = 0;
- var total = $pageElems.length;
-
- function captureNext() {
- if (idx >= total) {
- $staging.empty();
- $staging.css({ visibility: 'hidden' });
- displayThumbnails(thumbnailDataUrls, sizeConfig);
- deferred.resolve(pages);
- return;
- }
-
- var $elem = $pageElems.eq(idx);
- // 纯图片页优化: 跳过html2canvas,直接用原图src
- if (isPureImagePage($elem)) {
- var pureSrc = getPureImageSrc($elem);
- if (pureSrc) {
- thumbnailDataUrls.push(pureSrc);
- idx++;
- captureNext();
- return;
- }
- }
- html2canvas($elem[0], {
- scale: 1,
- useCORS: true,
- backgroundColor: '#ffffff',
- width: $elem.outerWidth(),
- height: $elem.outerHeight(),
- logging: false
- }).then(function (canvas) {
- thumbnailDataUrls.push(canvas.toDataURL('image/jpeg', 0.85));
- idx++;
- captureNext();
- }).catch(function (err) {
- $staging.css({ visibility: 'hidden' });
- deferred.reject('截图失败(第' + (idx + 1) + '页): ' + err);
+ fontsReady.then(function () {
+ requestAnimationFrame(function () {
+ // html2canvas会跳过visibility:hidden的元素,临时切换为可见
+ $staging.css({ visibility: 'visible' });
+ // 先转换代码块为图片
+ convertCodeBlocks().then(function () {
+ return convertTables();
+ }).then(function () {
+ runCaptureLoop($staging, opts, deferred);
+ }).catch(function () {
+ // 代码块转换失败,继续转换表格并截图流程
+ convertTables().then(function () {
+ runCaptureLoop($staging, opts, deferred);
});
- }
-
- captureNext();
- }).catch(function () {
- // 代码块转换失败,继续转换表格并截图流程
- convertTables().then(function () {
- var $pageElems = $staging.find('.phone-image-page');
- if ($pageElems.length === 0) {
- $staging.css({ visibility: 'hidden' });
- displayThumbnails([], sizeConfig);
- deferred.resolve(pages);
- return;
- }
-
- var thumbnailDataUrls = [];
- var idx = 0;
- var total = $pageElems.length;
-
- function captureNext() {
- if (idx >= total) {
- $staging.empty();
- $staging.css({ visibility: 'hidden' });
- displayThumbnails(thumbnailDataUrls, sizeConfig);
- deferred.resolve(pages);
- return;
- }
-
- var $elem = $pageElems.eq(idx);
- // 纯图片页优化: 跳过html2canvas,直接用原图src
- if (isPureImagePage($elem)) {
- var pureSrc = getPureImageSrc($elem);
- if (pureSrc) {
- thumbnailDataUrls.push(pureSrc);
- idx++;
- captureNext();
- return;
- }
- }
- html2canvas($elem[0], {
- scale: 1,
- useCORS: true,
- backgroundColor: '#ffffff',
- width: $elem.outerWidth(),
- height: $elem.outerHeight(),
- logging: false
- }).then(function (canvas) {
- thumbnailDataUrls.push(canvas.toDataURL('image/jpeg', 0.85));
- idx++;
- captureNext();
- }).catch(function (err) {
- $staging.css({ visibility: 'hidden' });
- deferred.reject('截图失败(第' + (idx + 1) + '页): ' + err);
- });
- }
-
- captureNext();
});
});
});
@@ -971,6 +911,117 @@ var PhoneImageEngine = (function () {
return deferred.promise();
}
+ /**
+ * 执行串行截图循环
+ * @param {jQuery} $staging - staging元素
+ * @param {Object} opts - 截图选项
+ * @param {jQuery Deferred} deferred - 外部deferred
+ */
+ function runCaptureLoop($staging, opts, deferred) {
+ var $pageElems = $staging.find('.phone-image-page');
+ if ($pageElems.length === 0) {
+ $staging.css({ visibility: 'hidden' });
+ if (opts.outputCanvas) {
+ deferred.reject('没有可渲染的页面');
+ } else {
+ deferred.resolve([]);
+ }
+ return;
+ }
+
+ var results = [];
+ var idx = 0;
+ var total = $pageElems.length;
+
+ function captureNext() {
+ if (idx >= total) {
+ $staging.empty();
+ $staging.css({ visibility: 'hidden' });
+ deferred.resolve(results);
+ return;
+ }
+
+ var $elem = $pageElems.eq(idx);
+
+ // 纯图片页优化
+ if (isPureImagePage($elem)) {
+ var pureSrc = getPureImageSrc($elem);
+ if (pureSrc) {
+ if (opts.outputCanvas) {
+ // 保存模式: 高质量绘制原图到canvas
+ renderPureImageToCanvas(pureSrc, opts.sizeConfig.width, opts.sizeConfig.height).then(function (canvas) {
+ results.push(canvas);
+ idx++;
+ captureNext();
+ }).catch(function () {
+ // 加载失败,回退到html2canvas
+ capturePageViaHtml2Canvas($elem, $staging, opts, results, idx, total, deferred, captureNext);
+ });
+ return;
+ } else {
+ // 缩略图模式: 直接用src
+ results.push(pureSrc);
+ idx++;
+ captureNext();
+ return;
+ }
+ }
+ }
+
+ capturePageViaHtml2Canvas($elem, $staging, opts, results, idx, total, deferred, captureNext);
+ }
+
+ captureNext();
+ }
+
+ /**
+ * 用html2canvas截图单页
+ */
+ function capturePageViaHtml2Canvas($elem, $staging, opts, results, idx, total, deferred, captureNext) {
+ html2canvas($elem[0], {
+ scale: opts.scale,
+ useCORS: true,
+ backgroundColor: '#ffffff',
+ width: $elem.outerWidth(),
+ height: $elem.outerHeight(),
+ logging: false
+ }).then(function (canvas) {
+ if (opts.outputCanvas) {
+ results.push(canvas);
+ } else {
+ results.push(canvas.toDataURL('image/jpeg', opts.quality));
+ }
+ idx++;
+ captureNext();
+ }).catch(function (err) {
+ $staging.css({ visibility: 'hidden' });
+ deferred.reject('截图失败(第' + (idx + 1) + '页): ' + err);
+ });
+ }
+
+ /**
+ * 渲染到隐藏区域 -> 截图 -> 显示缩略图
+ * @param {Object} sizeConfig
+ * @returns {jQuery Deferred} resolves with pages array
+ */
+ function renderThumbnails(sizeConfig) {
+ var deferred = $.Deferred();
+
+ doCapturePages({
+ scale: 1,
+ outputCanvas: false,
+ quality: 0.85,
+ sizeConfig: sizeConfig
+ }).then(function (dataUrls) {
+ displayThumbnails(dataUrls, sizeConfig);
+ deferred.resolve(pages);
+ }).catch(function (err) {
+ deferred.reject(err);
+ });
+
+ return deferred.promise();
+ }
+
/**
* 在右侧显示缩略图
* @param {Array} dataUrls - base64 图片数据
@@ -1021,171 +1072,35 @@ var PhoneImageEngine = (function () {
* @returns {jQuery Deferred} resolves with canvas array
*/
function capturePagesFromStaging() {
- var deferred = $.Deferred();
- ensureStaging();
-
- var $staging = $('#render-staging');
- $staging.empty();
-
var sizeConfig = config.sizes[config.size] || config.sizes.xiaohongshu;
-
- // 添加 size class wrapper
- var sizeClass = 'size-' + config.size;
- var $wrapper = $('
');
- for (var i = 0; i < pages.length; i++) {
- $wrapper.append(pages[i].html);
- }
- $staging.append($wrapper);
-
- requestAnimationFrame(function () {
- // html2canvas会跳过visibility:hidden的元素,临时切换为可见
- $staging.css({ visibility: 'visible' });
- // 先转换代码块为图片
- convertCodeBlocks().then(function () {
- return convertTables();
- }).then(function () {
- var $pageElems = $staging.find('.phone-image-page');
- if ($pageElems.length === 0) {
- $staging.css({ visibility: 'hidden' });
- deferred.reject('没有可渲染的页面');
- return;
- }
-
- var canvases = [];
- var idx = 0;
- var total = $pageElems.length;
-
- function captureNext() {
- if (idx >= total) {
- $staging.empty();
- $staging.css({ visibility: 'hidden' });
- deferred.resolve(canvases);
- return;
- }
-
- var $elem = $pageElems.eq(idx);
- // 纯图片页优化: 直接绘制原图到canvas
- if (isPureImagePage($elem)) {
- var pureSrc = getPureImageSrc($elem);
- if (pureSrc) {
- renderPureImageToCanvas(pureSrc, sizeConfig.width, sizeConfig.height).then(function (canvas) {
- canvases.push(canvas);
- idx++;
- captureNext();
- }).catch(function () {
- // 加载失败,回退到html2canvas
- html2canvas($elem[0], {
- scale: 2,
- useCORS: true,
- backgroundColor: '#ffffff',
- width: $elem.outerWidth(),
- height: $elem.outerHeight(),
- logging: false
- }).then(function (canvas) {
- canvases.push(canvas);
- idx++;
- captureNext();
- }).catch(function (err) {
- $staging.css({ visibility: 'hidden' });
- deferred.reject('截图失败(第' + (idx + 1) + '页): ' + err);
- });
- });
- return;
- }
- }
- html2canvas($elem[0], {
- scale: 2,
- useCORS: true,
- backgroundColor: '#ffffff',
- width: $elem.outerWidth(),
- height: $elem.outerHeight(),
- logging: false
- }).then(function (canvas) {
- canvases.push(canvas);
- idx++;
- captureNext();
- }).catch(function (err) {
- $staging.css({ visibility: 'hidden' });
- deferred.reject('截图失败(第' + (idx + 1) + '页): ' + err);
- });
- }
-
- captureNext();
- }).catch(function () {
- // 代码块转换失败,继续转换表格并截图流程
- convertTables().then(function () {
- var $pageElems = $staging.find('.phone-image-page');
- if ($pageElems.length === 0) {
- $staging.css({ visibility: 'hidden' });
- deferred.reject('没有可渲染的页面');
- return;
- }
-
- var canvases = [];
- var idx = 0;
- var total = $pageElems.length;
-
- function captureNext() {
- if (idx >= total) {
- $staging.empty();
- $staging.css({ visibility: 'hidden' });
- deferred.resolve(canvases);
- return;
- }
-
- var $elem = $pageElems.eq(idx);
- // 纯图片页优化: 直接绘制原图到canvas
- if (isPureImagePage($elem)) {
- var pureSrc = getPureImageSrc($elem);
- if (pureSrc) {
- renderPureImageToCanvas(pureSrc, sizeConfig.width, sizeConfig.height).then(function (canvas) {
- canvases.push(canvas);
- idx++;
- captureNext();
- }).catch(function () {
- // 加载失败,回退到html2canvas
- html2canvas($elem[0], {
- scale: 2,
- useCORS: true,
- backgroundColor: '#ffffff',
- width: $elem.outerWidth(),
- height: $elem.outerHeight(),
- logging: false
- }).then(function (canvas) {
- canvases.push(canvas);
- idx++;
- captureNext();
- }).catch(function (err) {
- $staging.css({ visibility: 'hidden' });
- deferred.reject('截图失败(第' + (idx + 1) + '页): ' + err);
- });
- });
- return;
- }
- }
- html2canvas($elem[0], {
- scale: 2,
- useCORS: true,
- backgroundColor: '#ffffff',
- width: $elem.outerWidth(),
- height: $elem.outerHeight(),
- logging: false
- }).then(function (canvas) {
- canvases.push(canvas);
- idx++;
- captureNext();
- }).catch(function (err) {
- $staging.css({ visibility: 'hidden' });
- deferred.reject('截图失败(第' + (idx + 1) + '页): ' + err);
- });
- }
-
- captureNext();
- });
- });
+ return doCapturePages({
+ scale: 2,
+ outputCanvas: true,
+ quality: 0.92,
+ sizeConfig: sizeConfig
});
+ }
- return deferred.promise();
+ /**
+ * DOM实测高度 - 从#content-flow中读取每个块的实际渲染高度
+ * @param {Array} blocks - parseHtmlToBlocks 返回的块数组
+ */
+ function measureBlockHeights(blocks) {
+ var $flow = $('#content-flow');
+ if (!$flow.length) return;
+
+ for (var i = 0; i < blocks.length; i++) {
+ var block = blocks[i];
+ if (block.type === 'page-break') continue;
+
+ var $blockEl = $flow.find('.content-flow-block[data-index="' + i + '"]');
+ if ($blockEl.length) {
+ var actualHeight = Math.round($blockEl[0].getBoundingClientRect().height);
+ if (actualHeight > 0) {
+ block.estimatedHeight = actualHeight;
+ }
+ }
+ }
}
/**