PDF文件解析

曾經(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文件解析

讀取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)
        })
    }
}

到這里核心的部分基本就完成了观蜗,當然為了讓解析出來的結構更清晰一點還需要涉及到文本元素的合并臊恋,行文本合并、列文本合并墓捻,把結構相同位置相近的文本框合并為一個抖仅;形狀的合并坊夫,比如表格、組合圖形撤卢;圖片的形變环凿,比如旋轉、切變放吩、裁剪等智听。


最終呈現(xiàn)
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市渡紫,隨后出現(xiàn)的幾起案子到推,更是在濱河造成了極大的恐慌,老刑警劉巖惕澎,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件莉测,死亡現(xiàn)場離奇詭異,居然都是意外死亡唧喉,警方通過查閱死者的電腦和手機捣卤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來八孝,“玉大人董朝,你說我怎么就攤上這事∷舭ⅲ” “怎么了益涧?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵锈锤,是天一觀的道長驯鳖。 經(jīng)常有香客問我,道長久免,這世上最難降的妖魔是什么浅辙? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮阎姥,結果婚禮上记舆,老公的妹妹穿的比我還像新娘。我一直安慰自己呼巴,他們只是感情好泽腮,可當我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著衣赶,像睡著了一般诊赊。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上府瞄,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天碧磅,我揣著相機與錄音,去河邊找鬼。 笑死鲸郊,一個胖子當著我的面吹牛丰榴,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播秆撮,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼四濒,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了像吻?” 一聲冷哼從身側響起峻黍,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎拨匆,沒想到半個月后姆涩,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡惭每,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年骨饿,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片台腥。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡宏赘,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出黎侈,到底是詐尸還是另有隱情察署,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布峻汉,位于F島的核電站贴汪,受9級特大地震影響,放射性物質發(fā)生泄漏休吠。R本人自食惡果不足惜扳埂,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望瘤礁。 院中可真熱鬧阳懂,春花似錦、人聲如沸柜思。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽赡盘。三九已至号枕,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間亡脑,已是汗流浹背堕澄。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工邀跃, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蛙紫。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓拍屑,卻偏偏與公主長得像,于是被迫代替她去往敵國和親坑傅。 傳聞我的和親對象是個殘疾皇子僵驰,可洞房花燭夜當晚...
    茶點故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容