曾經(jīng)花了很大的精力做了一個在線的方案制作工具父泳,類似“稿定設計”。當然直接使用已經(jīng)成熟的工具也可以解決問題但是考慮到后續(xù)定制化的需求店雅,以及對于自己定制化資源的整合還是決定自己來實現(xiàn)一套面褐。目前這套系統(tǒng)已經(jīng)穩(wěn)定運行了1年多了纬黎,產(chǎn)出了很多優(yōu)質的方案也提升了整個公司的效率。
這套系統(tǒng)在制作過程中遇到了很多的技術難點闽晦,其中一個就是對于PDF文件的解析扳碍,因為有很多的已經(jīng)完成的線下PDF方案,為了能把這些方案導入系統(tǒng)就會涉及到對于PDF文件的解析和結構轉換仙蛉。思路大致如此:
讀取PDF文件笋敞,解析文件結構,解析每頁數(shù)據(jù)荠瘪,提取每頁文件中的組件夯巷,并把組件結構轉換為自己系統(tǒng)可用結構,生成頁面哀墓,并添加新組建生成方案趁餐。
這里面有兩個技術點需要解決:
1、PDF文件結構解析
對于PDF文件的結構篮绰,有一篇文章PDF文件解析與PDF惡代分析中的一些坑說的很清楚后雷。如果按照這個思路走,當然也可以,但是單獨就解析這塊就可以做一個龐大的系統(tǒng)了喷面,另尋他法星瘾。考慮到系統(tǒng)是基于nodejs搭建的惧辈,找到兩個可以使用的方案:
-
pdf2json
可以提取文件中的文本信息琳状,圖形和圖形提取不出來,依賴于nodejs環(huán)境 -
pdfjs
可以提取所有信息盒齿,依賴于瀏覽器環(huán)境
看起來pdfjs更合適一點念逞,就是文檔資源少一點,看起來有點費勁边翁。
研究下來發(fā)現(xiàn)pdfjs有3點可以利用
1翎承、page.getTextContent,提前每頁中的文本信息
2符匾、PDFJS.SVGGraphics叨咖,頁面渲染為SVG
3、page.render啊胶,通過canvas渲染為圖片
如果把頁面直接渲染為圖片是最簡單辦法甸各,當然轉化之后所有的組件和文字都不能單獨編輯了,目前看來唯一可行的就是通過pdfjs吧PDF文件每頁解析為svg焰坪,然后再把svg文件拆分趣倾,提起所有可用組件,文字部分通過getTextContent提取某饰,獨立解析儒恋。思路如下:
SVG文件拆解
生成SVG文件
let document = await PDFJS.getDocument(new Uint8Array(await sourceFile.arrayBuffer()));
let page = await document.getPage(0);
var viewport = page.getViewport({ scale: 1 });
let scale = Math.min(viewBox.width / viewport.width, viewBox.height / viewport.height);
let opList = await page.getOperatorList();
var svgGfx = new PDFJS.SVGGraphics(page.commonObjs, page.objs);
let svg = null;
try {
svg = await svgGfx.getSVG(opList, page.getViewport({ scale: scale }));
svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
} catch (error) {
svg = this.createTag('svg');
svg.setAttribute('viewbox', "0 0 "+ viewBox.width +" " + viewBox.height);
}
提取頁面文本信息
let textContent = await page.getTextContent({});
let texts = textContent.items.map(text => {
let fontFamily = textContent.styles[text.fontName].fontFamily;
text.fontFamily = fontFamily;
if(fontFamily.toLowerCase().indexOf('bold') != -1) {
text.bold = true;
}else {
text.bold = false;
}
return text;
})
拆解SVG頁面元素為平行結構
async makeNodesOfSVG(svg, svgNodes) {
let tags = new Set(['tspan', 'circle', 'ellipse', 'image', 'line', 'mesh', 'path', 'polygon', 'polyline', 'rect', 'use']);
let withoutTags = new Set(['clipPath', 'defs', 'hatch', 'linearGradient', 'marker', 'mask', 'meshgradient', 'metadata', 'pattern', 'radialGradient', 'script', 'style', 'symbol', 'title']);
for (let i = 0; i < svg.childNodes.length; i++) {
const node = svg.childNodes[i];
let tagName = node.tagName || '';
tagName = tagName.replace('svg:', '');
if (withoutTags.has(tagName)) {
continue;
}
if (tags.has(tagName)) {
let fill = (node.attributes['fill'] || {})['nodeValue'];
if (fill == 'none') {
continue;
}
if(tagName == 'tspan' && !fill) {
continue;
}
let nodes = [node.cloneNode(true)];
while (node.parentNode) {
if (node.parentNode.tagName == 'svg') {
break;
}
nodes.splice(0, 0, node.parentNode.cloneNode(false))
node = node.parentNode;
}
for (let i = 0; i < nodes.length - 1; i++) {
const node = nodes[i];
node.appendChild(nodes[i + 1]);
}
svgNodes.push(nodes[0]);
} else {
await this.makeNodesOfSVG(node, svgNodes);
}
}
}
獲取最內(nèi)層需要渲染的元素
把最內(nèi)層元素拆解為獨立元素
之前的操作,所有需要渲染的元素外層都包裹著幾層結構黔漂,這幾層結構都是元素的transform诫尽,我們需要把這幾層結構合并為一個transform,并把元素獨立出來炬守。
把拆解的元素渲染到網(wǎng)頁箱锐。
紅色框標記的位置是我們真正需要提取的元素。
// 提取需要的元素
getNodeOfSVG(svg) {
let tags = new Set(['tspan', 'circle', 'ellipse', 'image', 'line', 'mesh', 'path', 'polygon', 'polyline', 'rect', 'use']);
let noTags = new Set(['clipPath', 'defs', 'hatch', 'linearGradient', 'marker', 'mask', 'meshgradient', 'metadata', 'pattern', 'radialGradient', 'script', 'style', 'symbol', 'title']);
let tagName = svg.tagName || '';
tagName = tagName.replace('svg:', '');
if (tags.has(tagName)) {
return svg;
}
for (let i = 0; i < svg.childNodes.length; i++) {
const node = svg.childNodes[i];
tagName = node.tagName || '';
tagName = tagName.replace('svg:', '');
if (noTags.has(tagName)) {
continue;
}
if (tags.has(tagName)) {
return node;
} else {
return this.getNodeOfSVG(node);
}
}
}
// 獲取元素的transform
for (let j = 0; j < nodes.length; j++) {
const node = nodes[j];
let bound = node.getBoundingClientRect();
// 換算轉換矩陣
let point = svg.createSVGPoint();
point.x = bound.x;
point.y = bound.y;
let inode = pptgen.getItemOfSVG(node);
let transform = inode.getCTM();
let rotate = pptgen.decomposeMatrix(transform).rotateZ;
transform = (new DOMMatrix([1, 0, 0, 1, -bound.x, -bound.y])).multiply(transform);
let cnode = inode.cloneNode(true);
page.items.push({
node: cnode,
bound: bound,
transform: transform,
rotate: rotate
});
// 位置標注
let markDiv = document.createElement('div');
markDiv.style.position = 'absolute';
markDiv.style.left = bound.x + 'px';
markDiv.style.top = bound.y + 'px';
markDiv.style.width = bound.width + 'px';
markDiv.style.height = bound.height + 'px';
markDiv.style.border = '1px solid #ff0000';
svgContent.appendChild(markDiv);
}
到這里我們已經(jīng)提取到我們需要的基本元素劳较,接下來就是把這些元素轉換成需要的結構化數(shù)據(jù)驹止。
2、組件結構轉換
文本框
if (onode.tagName == 'tspan') {
let fontSize = page.scale * page.texts[txtIndex].transform[0];
let fontFamily = page.texts[txtIndex].fontFamily;
let bold = page.texts[txtIndex].bold;
let color = '';
if (onode.attributes['fill']) {
let rgb = onode.attributes['fill']['nodeValue'];
if(onode.attributes['stroke-width']) {
bold = true;
}
color = this._rgb2hex(rgb);
}
let text = '';
let str = page.texts[txtIndex].str;
text = text + str;
txtIndex++;
svgs.push({
type: 'text',
text: text,
x: bound.x,
y: bound.y,
w: bound.width,
h: bound.height,
bold: bold,
fontSize: fontSize,
fontFamily: fontFamily,
color: color
})
}
圖片
if (onode.tagName == 'image') {
let url = onode.attributes['xlink:href']['nodeValue'];
let blob = await this._url2blob(url);
let ext = blob.type == 'image/png' ? 'png' : 'jpg';
var file = new File([blob], "image." + ext, { type: blob.type });
let hash = md5(new Uint8Array(await file.arrayBuffer()))
if (imageCache[hash]) {
// 緩存
url = imageCache[hash];
} else {
// 存儲
let uploadParams = await this._tokenInfo(ext);
url = await this._upload(file, uploadParams.key, uploadParams.token);
imageCache[hash] = url;
}
svgs.push({
type: 'image',
x: bound.x,
y: bound.y,
w: bound.width,
h: bound.height,
url: url
})
}
其他形狀元素
else {
let svgNode = this.createTag('svg', { 'width': bound.width + 'px', 'height': bound.height + 'px', 'viewbox': '0 0 ' + bound.width + ' ' + bound.height });
let matrixItems = [item.transform.a, item.transform.b, item.transform.c, item.transform.d, item.transform.e, item.transform.f];
item.node.setAttribute('transform', 'matrix(' + matrixItems.join(' ') + ')');
svgNode.appendChild(item.node);
let svgString = svgNode.outerHTML;
if(svgString.length > 2000) {
// 超規(guī)格文件
let blob = new Blob([svgString]);
let ext = 'svg';
var file = new File([blob], "image." + ext, { type: 'image/svg+xml' });
let url = '';
let hash = md5(new Uint8Array(await file.arrayBuffer()))
if (imageCache[hash]) {
// 緩存
url = imageCache[hash];
} else {
// 存儲
let uploadParams = await this._tokenInfo(ext);
url = await this._upload(file, uploadParams.key, uploadParams.token);
imageCache[hash] = url;
}
svgs.push({
type: 'svg',
x: bound.x,
y: bound.y,
w: bound.width,
h: bound.height,
url: url
})
}else {
svgs.push({
type: 'svg',
x: bound.x,
y: bound.y,
w: bound.width,
h: bound.height,
url: "data:image/svg+xml;base64," + base64Encode(svgString)
})
}
}
到這里核心的部分基本就完成了观蜗,當然為了讓解析出來的結構更清晰一點還需要涉及到文本元素的合并臊恋,行文本合并、列文本合并墓捻,把結構相同位置相近的文本框合并為一個抖仅;形狀的合并坊夫,比如表格、組合圖形撤卢;圖片的形變环凿,比如旋轉、切變放吩、裁剪等智听。