背景:合同模板場(chǎng)景,css中l(wèi)inear-gradient漸變實(shí)現(xiàn)的信紙格式(每行內(nèi)容不管是否占滿,底線都到行末),因在wkhtmltopdf工具把網(wǎng)頁(yè)轉(zhuǎn)pdf時(shí)有各種兼容和效果問題,最后只能用js強(qiáng)行拆分行,每行加下劃線效果。
問題描述
起初,漢字按1em寬度計(jì)算,其他非漢字類按半個(gè)漢字(0.5em)計(jì)算,應(yīng)用了不少合同模板html,沒出現(xiàn)問題。直到海南安居房合同,要求打印替換后的內(nèi)容也要加粗后。即:信紙格式里的內(nèi)容,不加粗時(shí),顯示正常,加粗時(shí),顯示就超過原本計(jì)算的一行,自動(dòng)換行到下一行了。見下圖:


也就是說:宋體,加粗前后,字符的實(shí)際寬度變了。
字體實(shí)際寬度調(diào)研
僅對(duì)宋體,因?yàn)橹剖胶贤际撬误w為字體的。針對(duì)中文、英文、數(shù)字、特殊-英(特殊字符-半角)、特殊-中(特殊字符-全角)這些字符,做了加粗前后的對(duì)比。
-
win10系統(tǒng)(windows系統(tǒng))下效果:
字符寬度-win10.png -
win11系統(tǒng)(windows系統(tǒng))下效果:
字符寬度-win11.png -
macOS系統(tǒng)(蘋果系統(tǒng))下效果:
字符寬度-蘋果系統(tǒng).png
對(duì)比了一下,發(fā)現(xiàn):
- 在windows系統(tǒng)下,不加粗時(shí),符合非漢字字符是半個(gè)漢字字符的寬度。在蘋果系統(tǒng)下,就比較雜亂了。非漢字字符,有超過半個(gè)漢字寬度的,有小于半個(gè)漢字寬度。
- 加粗后,不管是windows系統(tǒng)下,還是蘋果系統(tǒng)下,,都比較亂。
結(jié)論
綜上所述,加粗前后都無法精準(zhǔn)的算每行字符的實(shí)際長(zhǎng)度。仍舊只能采用臨時(shí)調(diào)整寬度,比如:每行26個(gè)漢字,js計(jì)算按26漢字計(jì)算,每行樣式臨時(shí)改成26.4em的寬度,以保證正常顯示。
附上調(diào)研源碼:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
<title>宋體加粗前后的字符實(shí)際寬度</title>
<style>
table {
margin: 0 30px;
font-size: 14px;
font-family: '宋體';
border: 1px solid #aaa;
border-collapse: collapse;
}
th,
td {
border: 1px solid #aaa;
padding: 5px;
}
</style>
</head>
<body>
<div>
<table>
<thead>
<tr>
<th>類型</th>
<th>字符</th>
<th>實(shí)際大小<br>(20px為參考)</th>
<th>em<br>實(shí)際大小/20)</th>
<th>加粗字符</th>
<th>加粗后實(shí)際大小<br>(20px為參考)</th>
<th>em<br>(實(shí)際大小/20)</th>
<th>加粗前后對(duì)比<br>(加粗后width/加粗前width)</th>
</tr>
</thead>
<tbody>
<!-- <tr>
<td>中文</td>
<td>漢</td>
<td>20px</td>
<td>1</td>
<td style="font-weight: bold;">漢</td>
<td>20px</td>
<td>1</td>
<td>1</td>
</tr> -->
</tbody>
</table>
</div>
<script>
function measureTextWidth(text, font) {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
context.font = font;
return context.measureText(text).width;
}
// 使用示例
// const width = measureTextWidth('中', 'bold 16px SimSun'); // SimSun 是宋體
// console.log(`加粗后的字符寬度為 ${width}px`);
var charObj = {
'中文': '黎妃',
'英文': 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
'數(shù)字': '0123456789',
'特殊-英': '\,.;$^*""*()-+=|<>&%#@!~',
'特殊-中': ',。、;?!()【】:“”‘’《》¥·',
};
var tbody = document.querySelector('tbody');
var strHtml = '';
Object.keys(charObj).forEach(function (key) {
var str = charObj[key];
str.split('').forEach(function (char) {
var width = measureTextWidth(char, '20px 宋體');
var widthBold = measureTextWidth(char, 'bold 20px 宋體');
var percent = width / 20;
var percent2 = widthBold / 20;
var percent3 = widthBold / width;
strHtml +=`<tr>
<td>${key}</td>
<td>${char}</td>
<td>${width}</td>
<td>${percent}</td>
<td style="font-weight: bold;">${char}</td>
<td>${widthBold}</td>
<td>${percent2}</td>
<td>${percent3}</td>
</tr>`;
})
});
tbody.innerHTML = strHtml;
</script>
</body>
</html>
優(yōu)化方案
原理:逐字測(cè)量寬度,當(dāng)寬度超過容器時(shí),強(qiáng)制截?cái)嗖趲聞澗€的 div 中。
效果圖:

源碼:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JS 實(shí)現(xiàn)信紙線格式演示</title>
<style>
body {
font-family: "SimSun", "Songti SC", serif; /* 自定義宋體,更像文檔 */
background-color: #f0f2f5;
padding: 50px;
}
.container {
width: 800px;
margin: 0 auto;
background: white;
padding: 60px;
box-shadow: 0 4px 15px rgba(0,0,0,0.05);
}
h2 {
text-align: center;
border-bottom: 2px solid #333;
padding-bottom: 15px;
margin-bottom: 30px;
}
.tips {
background: #e6f7ff;
border: 1px solid #91d5ff;
padding: 15px;
border-radius: 4px;
margin-bottom: 30px;
font-size: 14px;
color: #0050b3;
line-height: 1.6;
}
/* 原始樣式:未處理前 */
.origin-text {
display: inline-block;
position: relative;
text-indent: 0;
border: none;
min-width: 2em;
text-align: left;
vertical-align: top;
line-height: 28px;
background: linear-gradient(transparent, transparent 27px, #000 1px, #000) 0% 0% / 100% 28px;
}
/* 處理后的通用容器樣式 */
.js-letter-paper {
width: 100%;
/* 字體設(shè)置稍微大點(diǎn)方便觀察 */
font-size: 18px;
line-height: 32px; /* 固定行高非常重要 */
color: #333;
}
/* 核心:JS 生成的每一行 */
.js-line-row {
display: block; /* 必須獨(dú)占一行 */
width: 100%;
height: 32px; /* 高度等于容器行高 */
line-height: 32px;
border-bottom: 1px solid #000; /* 實(shí)線邊框 */
box-sizing: border-box; /* 邊框算入高度 */
/* 文字內(nèi)容溢出處理(防止撐開高度) */
white-space: nowrap;
overflow: hidden;
}
/* 模擬空行占位 */
.js-line-row:empty::before {
content: "\00a0";
}
.btn-action {
display: block;
width: 200px;
margin: 20px auto;
padding: 10px;
background: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
text-align: center;
}
.btn-action:hover {
background: #40a9ff;
}
</style>
</head>
<body>
<div class="container">
<h2>JS 暴力計(jì)算斷行實(shí)現(xiàn)信紙線</h2>
<div class="tips">
<strong>場(chǎng)景說明:</strong><br>
當(dāng) CSS3 `background` 方案在某些舊版打印引擎(如 wkhtmltopdf, 老舊IE)失效時(shí),或者需要對(duì)每一行文字進(jìn)行精確控制(如行尾對(duì)齊、最后一行補(bǔ)全)時(shí),使用 JS 暴力計(jì)算斷行是最穩(wěn)妥的方案。<br>
<strong>原理:</strong> 逐字測(cè)量寬度,當(dāng)寬度超過容器時(shí),強(qiáng)制截?cái)嗖趲聞澗€的 `div` 中。
</div>
<h3>示例一:多行文本自動(dòng)分割</h3>
<p>原始文本區(qū)域(點(diǎn)擊下方按鈕轉(zhuǎn)換):</p>
<!-- 待處理的容器 -->
<div id="target-content" class="origin-text" style="width: 26em;font-size:18px;">
乙方占比70%,萬寧市人民政府占比30%,乙方持有住房滿10年后,持有年限每增加1年,可獲贈(zèng)6%的政府持有增值收益份額;乙方持有住房滿15年后,乙方獲得該住房100%產(chǎn)權(quán)。(詳見附件九《安居房共有產(chǎn)權(quán)協(xié)議》)
</div>
<button class="btn-action" onclick="transformToLetterPaper()">執(zhí)行轉(zhuǎn)換 (JS)</button>
<p>轉(zhuǎn)換結(jié)果:</p>
<div id="result-container" class="js-letter-paper" style="width: 26em;font-size:18px;">
<!-- 結(jié)果將生成在這里 -->
<div class="js-line-row" style="color:#999; border-bottom:1px dashed #ccc;">(等待生成...)</div>
</div>
</div>
<script>
/**
* 核心轉(zhuǎn)換函數(shù)
* 將一段純文本轉(zhuǎn)換為帶下劃線的行結(jié)構(gòu)
* @param {string} text - 原始文本
* @param {HTMLElement} container - 目標(biāo)容器(用于獲取寬度和樣式)
*/
function splitTextToRows(text, container,sourceDiv) {
// 1. 獲取樣式的參考對(duì)象
// 測(cè)量的關(guān)鍵:字體大小、粗細(xì)、字體家族必須與顯示時(shí)完全一致,否則切割會(huì)錯(cuò)位。
// 優(yōu)先使用 sourceDiv (原始內(nèi)容) 的樣式,如果沒有則使用 container
const styleRefElement = sourceDiv || container;
const computedStyle = window.getComputedStyle(styleRefElement);
// 2. 獲取容器寬度 (這是限制每一行長(zhǎng)度的物理邊界)
// 注意:必須使用內(nèi)容盒(content-box)寬度,需減去 padding
const containerStyle = window.getComputedStyle(container);
const paddingLeft = parseFloat(containerStyle.paddingLeft) || 0;
const paddingRight = parseFloat(containerStyle.paddingRight) || 0;
const containerWidth = container.clientWidth - paddingLeft - paddingRight;
// 3. 創(chuàng)建測(cè)寬尺
const ruler = document.createElement('span');
// 復(fù)制所有可能會(huì)影響文字寬度的 CSS 屬性
ruler.style.fontFamily = computedStyle.fontFamily;
ruler.style.fontSize = computedStyle.fontSize;
ruler.style.fontWeight = "bold"||computedStyle.fontWeight; // 核心:獲取并應(yīng)用 font-weight
ruler.style.letterSpacing = computedStyle.letterSpacing;
ruler.style.fontStyle = computedStyle.fontStyle;
ruler.style.visibility = 'hidden';
ruler.style.whiteSpace = 'nowrap';
ruler.style.position = 'absolute';
ruler.style.top = '-9999px'; // 移出視口
document.body.appendChild(ruler);
const rows = [];
let currentLine = '';
// 4. 預(yù)處理文本
// 將換行符轉(zhuǎn)換為空格,避免測(cè)量干擾(視需求而定,信紙通常是將長(zhǎng)文本連續(xù)排版)
const cleanText = text.replace(/[\r\n]+/g, '');
const chars = cleanText.split('');
// 5. 逐字計(jì)算
for (let i = 0; i < chars.length; i++) {
const char = chars[i];
// 試探性加入字符
ruler.innerText = currentLine + char;
// 如果寬度超過容器 (預(yù)留 2px Buffer 防止瀏覽器渲染差異)
if (ruler.offsetWidth > containerWidth - 2) {
rows.push(currentLine);
currentLine = char;
} else {
currentLine += char;
}
}
if (currentLine) {
rows.push(currentLine);
}
// 6. 清理
document.body.removeChild(ruler);
// 7. 補(bǔ)全空白行
while (rows.length <= 0) {
rows.push('');
}
console.log('0206-rows:',rows);
// 返回結(jié)果同時(shí)也返回檢測(cè)到的樣式,以便渲染時(shí)應(yīng)用
return {
rows: rows,
styles: {
fontWeight: 'bold'||computedStyle.fontWeight,
fontSize: computedStyle.fontSize,
fontFamily: computedStyle.fontFamily
}
};
}
/**
* 觸發(fā)轉(zhuǎn)換動(dòng)作
*/
function transformToLetterPaper() {
const sourceDiv = document.getElementById('target-content');
const resultDiv = document.getElementById('result-container');
// 優(yōu)化:同步寬度,確保結(jié)果容器的內(nèi)容寬度與源容器一致
const sourceComputedStyle = window.getComputedStyle(sourceDiv);
// 注意:getComputedStyle 返回的 width 通常是像素值 (px)
// 賦值給 resultDiv 確保兩者的換行基準(zhǔn)一致
resultDiv.style.width = sourceComputedStyle.width;
const rawText = sourceDiv.innerText; // 獲取純文本
// 執(zhí)行分割計(jì)算
// minLines 設(shè)置為 0,表示行數(shù)完全由內(nèi)容決定,不強(qiáng)制填充空白行
const result = splitTextToRows(rawText, resultDiv, sourceDiv);
const lines = result.rows;
const detectedStyles = result.styles;
// 渲染 DOM
let html = '';
lines.forEach(line => {
const content = line ? line : '';
// 將檢測(cè)到的 font-weight 等樣式應(yīng)用到每一行,確保視覺一致
html += `<div class="js-line-row" style="font-weight:${detectedStyles.fontWeight}">${escapeHtml(content)}</div>`;
});
resultDiv.innerHTML = html;
}
// 簡(jiǎn)單的 HTML 轉(zhuǎn)義防止 XSS
function escapeHtml(text) {
if (!text) return '';
return text
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
</script>
</body>
</html>
應(yīng)用在模板html里的核心代碼(es5語(yǔ)法):
function splitLineForPdf(text, sourceDiv, isbold) {
var resultHtml = '';
// 1. 獲取樣式的參考對(duì)象
// 測(cè)量的關(guān)鍵:字體大小、粗細(xì)、字體家族必須與顯示時(shí)完全一致,否則切割會(huì)錯(cuò)位。
var computedStyle = window.getComputedStyle(sourceDiv);
// 2. 獲取容器寬度 (這是限制每一行長(zhǎng)度的物理邊界)
// 注意:必須使用內(nèi)容盒(content-box)寬度,需減去 padding
var containerStyle = window.getComputedStyle(sourceDiv);
var paddingLeft = parseFloat(containerStyle.paddingLeft) || 0;
var paddingRight = parseFloat(containerStyle.paddingRight) || 0;
var containerWidth = sourceDiv.clientWidth - paddingLeft - paddingRight;
// 3. 創(chuàng)建測(cè)寬尺
var ruler = document.createElement('span');
// 復(fù)制所有可能會(huì)影響文字寬度的 CSS 屬性
ruler.style.fontFamily = computedStyle.fontFamily;
ruler.style.fontSize = computedStyle.fontSize;
//因?yàn)榧哟趾?,不同類型字符(中文、英文、特殊符?hào)等等),不同系統(tǒng)對(duì)應(yīng)的字符寬度完全不一樣,沒有規(guī)律了
ruler.style.fontWeight = isbold ? "bolder" : "normal";
ruler.style.letterSpacing = computedStyle.letterSpacing;
ruler.style.fontStyle = computedStyle.fontStyle;
ruler.style.visibility = 'hidden';
ruler.style.whiteSpace = 'nowrap';
ruler.style.position = 'absolute';
ruler.style.top = '-9999px'; // 移出視口
document.body.appendChild(ruler);
var rows = [];
var currentLine = '';
// 4. 預(yù)處理文本
// 將換行符轉(zhuǎn)換為空格,避免測(cè)量干擾(視需求而定,信紙通常是將長(zhǎng)文本連續(xù)排版)
var cleanText = text.replace(/[\r\n]+/g, '');
var chars = cleanText.split('');
// 5. 逐字計(jì)算
for (var i = 0; i < chars.length; i++) {
var char = chars[i];
// 試探性加入字符
ruler.innerText = currentLine + char;
// 如果寬度超過容器 (預(yù)留 2px Buffer 防止瀏覽器渲染差異)
if (ruler.offsetWidth > containerWidth - 2) {
rows.push(currentLine);
currentLine = char;
} else {
currentLine += char;
}
}
if (currentLine) {
rows.push(currentLine);
}
// 6. 清理
document.body.removeChild(ruler);
// 7. 補(bǔ)全空白行
while (rows.length <= 0) {
rows.push('');
}
console.log('信紙格式-rows:',rows);
$.each(rows, function (index, item) {
resultHtml += '<i class="js-row">' + item + '</i>';
});
return resultHtml;
}
/**
* 轉(zhuǎn)pdf前,替換成js的斷行,保證pdf的多下劃線顯示正常。
*/
function showMultlineToPdf() {
//html結(jié)構(gòu): <span class="mf-letterline" style="width: 20em;" data-words="20" data-isbold="1"></span>
// 優(yōu)化后:data-words屬性不需要了
$('.mf-letterline').each(function () {
var isbold = ($(this).attr('data-isbold')==1) ? true : false;//是否加粗字體
var curStr = ($(this).text() || '').trim();
var sourceEl = $(this).get(0);//jquery對(duì)象轉(zhuǎn)原生dom元素
var replaceStr = splitLineForPdf(curStr,sourceEl,isbold);
$(this).html(replaceStr);//用js斷行后的內(nèi)容替換原來的文本
$(this).addClass('topdf');//避免網(wǎng)頁(yè)版打印替換后,樣式背景線和js斷行背景線同時(shí)出現(xiàn)
});
}


