UNPKG

officegen-2

Version:

Office Open XML Generator using Node.js streams. Supporting Microsoft Office 2007 and later Word (docx), PowerPoint (pptx,ppsx) and Excel (xlsx). This module is for all frameworks and environments. No need for any commandline tool - this module is doing everything inside it.

477 lines (421 loc) 17.8 kB
### MS Excel 2007 Creater v0.0.1 Author : chuanyi.zheng@gmail.com History: 2012/11/07 first created ### fs = require 'fs' path = require 'path' exec = require 'child_process' xml = require 'xmlbuilder' existsSync = fs.existsSync || path.existsSync tool = i2a : (i) -> return 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.charAt(i-1) copy : (origin, target) -> if existsSync(origin) fs.mkdirSync(target, 755) if not existsSync(target) files = fs.readdirSync(origin) if files for f in files oCur = origin + '/' + f tCur = target + '/' + f s = fs.statSync(oCur) if s.isFile() fs.writeFileSync(tCur,fs.readFileSync(oCur,''),'') else if s.isDirectory() tool.copy oCur, tCur opt = tmpl_path : __dirname class ContentTypes constructor: (@book)-> toxml:()-> types = xml.create('Types',{version:'1.0',encoding:'UTF-8',standalone:true}) types.att('xmlns','http://schemas.openxmlformats.org/package/2006/content-types') types.ele('Override',{PartName:'/xl/theme/theme1.xml',ContentType:'application/vnd.openxmlformats-officedocument.theme+xml'}) types.ele('Override',{PartName:'/xl/styles.xml',ContentType:'application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml'}) types.ele('Default',{Extension:'rels',ContentType:'application/vnd.openxmlformats-package.relationships+xml'}) types.ele('Default',{Extension:'xml',ContentType:'application/xml'}) types.ele('Override',{PartName:'/xl/workbook.xml',ContentType:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml'}) types.ele('Override',{PartName:'/docProps/app.xml',ContentType:'application/vnd.openxmlformats-officedocument.extended-properties+xml'}) for i in [1..@book.sheets.length] types.ele('Override',{PartName:'/xl/worksheets/sheet'+i+'.xml',ContentType:'application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml'}) types.ele('Override',{PartName:'/xl/sharedStrings.xml',ContentType:'application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml'}) types.ele('Override',{PartName:'/docProps/core.xml',ContentType:'application/vnd.openxmlformats-package.core-properties+xml'}) return types.end() class DocPropsApp constructor: (@book)-> toxml: ()-> props = xml.create('Properties',{version:'1.0',encoding:'UTF-8',standalone:true}) props.att('xmlns','http://schemas.openxmlformats.org/officeDocument/2006/extended-properties') props.att('xmlns:vt','http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes') props.ele('Application','Microsoft Excel') props.ele('DocSecurity','0') props.ele('ScaleCrop','false') tmp = props.ele('HeadingPairs').ele('vt:vector',{size:2,baseType:'variant'}) tmp.ele('vt:variant').ele('vt:lpstr','工作表') tmp.ele('vt:variant').ele('vt:i4',''+@book.sheets.length) tmp = props.ele('TitlesOfParts').ele('vt:vector',{size:@book.sheets.length,baseType:'lpstr'}) for i in [1..@book.sheets.length] tmp.ele('vt:lpstr',@book.sheets[i-1].name) props.ele('Company') props.ele('LinksUpToDate','false') props.ele('SharedDoc','false') props.ele('HyperlinksChanged','false') props.ele('AppVersion','12.0000') return props.end() class XlWorkbook constructor: (@book)-> toxml: ()-> wb = xml.create('workbook',{version:'1.0',encoding:'UTF-8',standalone:true}) wb.att('xmlns','http://schemas.openxmlformats.org/spreadsheetml/2006/main') wb.att('xmlns:r','http://schemas.openxmlformats.org/officeDocument/2006/relationships') wb.ele('fileVersion ',{appName:'xl',lastEdited:'4',lowestEdited:'4',rupBuild:'4505'}) wb.ele('workbookPr',{filterPrivacy:'1',defaultThemeVersion:'124226'}) wb.ele('bookViews').ele('workbookView ',{xWindow:'0',yWindow:'90',windowWidth:'19200',windowHeight:'11640'}) tmp = wb.ele('sheets') for i in [1..@book.sheets.length] tmp.ele('sheet',{name:@book.sheets[i-1].name,sheetId:''+i,'r:id':'rId'+i}) wb.ele('calcPr',{calcId:'124519'}) return wb.end() class XlRels constructor: (@book)-> toxml: ()-> rs = xml.create('Relationships',{version:'1.0',encoding:'UTF-8',standalone:true}) rs.att('xmlns','http://schemas.openxmlformats.org/package/2006/relationships') for i in [1..@book.sheets.length] rs.ele('Relationship',{Id:'rId'+i,Type:'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet',Target:'worksheets/sheet'+i+'.xml'}) rs.ele('Relationship',{Id:'rId'+(@book.sheets.length+1),Type:'http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme',Target:'theme/theme1.xml'}) rs.ele('Relationship',{Id:'rId'+(@book.sheets.length+2),Type:'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles',Target:'styles.xml'}) rs.ele('Relationship',{Id:'rId'+(@book.sheets.length+3),Type:'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings',Target:'sharedStrings.xml'}) return rs.end() class SharedStrings constructor: ()-> @cache = {} @arr = [] str2id: (s)-> id = @cache[s] if id return id else @arr.push s @cache[s] = @arr.length return @arr.length toxml: ()-> sst = xml.create('sst',{version:'1.0',encoding:'UTF-8',standalone:true}) sst.att('xmlns','http://schemas.openxmlformats.org/spreadsheetml/2006/main') sst.att('count',''+@arr.length) sst.att('uniqueCount',''+@arr.length) for i in [0...@arr.length] si = sst.ele('si') si.ele('t',@arr[i]) si.ele('phoneticPr',{fontId:1,type:'noConversion'}) return sst.end() class Sheet constructor: (@book, @name, @cols, @rows) -> @data = {} for i in [1..@rows] @data[i] = {} for j in [1..@cols] @data[i][j] = {v:0, dataType: 'string'} @merges = [] @col_wd = [] @row_ht = {} @styles = {} set: (col, row, value) -> if (typeof value) is "string" #console.log "Found string " + value @data[row][col].v = @book.ss.str2id(''+value) if value? and value isnt '' #console.log "id=" + @data[row][col].v @data[row][col].dataType = 'string' else @data[row][col].v = value @data[row][col].dataType = 'number' merge: (from_cell, to_cell) -> @merges.push({from:from_cell, to:to_cell}) width: (col, wd) -> @col_wd.push {c:col,cw:wd} height: (row, ht) -> @row_ht[row] = ht font: (col, row, font_s)-> @styles['font_'+col+'_'+row] = @book.st.font2id(font_s) fill: (col, row, fill_s)-> @styles['fill_'+col+'_'+row] = @book.st.fill2id(fill_s) border: (col, row, bder_s)-> @styles['bder_'+col+'_'+row] = @book.st.bder2id(bder_s) align: (col, row, align_s)-> @styles['algn_'+col+'_'+row] = align_s valign: (col, row, valign_s)-> @styles['valgn_'+col+'_'+row] = valign_s rotate: (col, row, textRotation)-> @styles['rotate_'+col+'_'+row] = textRotation wrap: (col, row, wrap_s)-> @styles['wrap_'+col+'_'+row] = wrap_s style_id: (col, row) -> inx = '_'+col+'_'+row style = { font_id:@styles['font'+inx], fill_id:@styles['fill'+inx], bder_id:@styles['bder'+inx], align:@styles['algn'+inx], valign:@styles['valgn'+inx], rotate:@styles['rotate'+inx], wrap:@styles['wrap'+inx] } id = @book.st.style2id(style) return id toxml: () -> ws = xml.create('worksheet', {version:'1.0',encoding:'UTF-8',standalone:true}) ws.att('xmlns','http://schemas.openxmlformats.org/spreadsheetml/2006/main') ws.att('xmlns:r', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships') ws.ele('dimension',{ref:'A1'}) ws.ele('sheetViews').ele('sheetView',{workbookViewId:'0'}) ws.ele('sheetFormatPr',{defaultRowHeight:'13.5'}) if @col_wd.length > 0 cols = ws.ele('cols') for cw in @col_wd cols.ele('col',{min:''+cw.c,max:''+cw.c,width:cw.cw,customWidth:'1'}) sd = ws.ele('sheetData') for i in [1..@rows] r = sd.ele('row',{r:''+i,spans:'1:'+@cols}) ht = @row_ht[i] if ht r.att('ht',ht) r.att('customHeight','1') for j in [1..@cols] ix = @data[i][j] sid = @style_id(j, i) if (ix.v isnt 0) or (sid isnt 1) c = r.ele('c',{r:''+tool.i2a(j)+i}) c.att('s',''+(sid-1)) if sid isnt 1 if ix.v isnt 0 # comment out for test generating number #console.log @data[i][j] if ix.dataType is "string" c.att('t','s') c.ele('v', '' + (ix.v-1)) else if ix.dataType is "number" c.ele('v', '' + ix.v) else c.ele('v', '' + ix.v) if @merges.length > 0 mc = ws.ele('mergeCells',{count:@merges.length}) for m in @merges mc.ele('mergeCell', {ref:(''+tool.i2a(m.from.col)+m.from.row+':'+tool.i2a(m.to.col)+m.to.row)}) ws.ele('phoneticPr',{fontId:'1',type:'noConversion'}) ws.ele('pageMargins',{left:'0.7',right:'0.7',top:'0.75',bottom:'0.75',header:'0.3',footer:'0.3'}) ws.ele('pageSetup',{paperSize:'9',orientation:'portrait',horizontalDpi:'200',verticalDpi:'200'}) return ws.end() class Style constructor: (@book)-> @cache = {} @mfonts = [] # font style @mfills = [] # fill style @mbders = [] # border style @mstyle = [] # cell style<ref-font,ref-fill,ref-border,align> @with_default() with_default:()-> @def_font_id = @font2id(null) @def_fill_id = @fill2id(null) @def_bder_id = @bder2id(null) @def_align = '-' @def_valign = '-' @def_rotate = '-' @def_wrap = '-' @def_style_id = @style2id({ font_id:@def_font_id, fill_id:@def_fill_id, bder_id:@def_bder_id, align:@def_align, valign:@def_valign, rotate:@def_rotate}) font2id: (font)-> font or= {} font.bold or= '-' font.iter or= '-' font.sz or= '11' font.color or= '-' font.name or= 'Arial' font.scheme or='minor' font.family or= '2' k = 'font_'+font.bold+font.iter+font.sz+font.color+font.name+font.scheme+font.family id = @cache[k] if id return id else @mfonts.push font @cache[k] = @mfonts.length return @mfonts.length fill2id: (fill)-> fill or= {} fill.type or= 'none' fill.bgColor or= '-' fill.fgColor or= '-' k = 'fill_' + fill.type + fill.bgColor + fill.fgColor id = @cache[k] if id return id else @mfills.push fill @cache[k] = @mfills.length return @mfills.length bder2id: (bder)-> bder or= {} bder.left or= '-' bder.right or= '-' bder.top or= '-' bder.bottom or= '-' k = 'bder_'+bder.left+'_'+bder.right+'_'+bder.top+'_'+bder.bottom id = @cache[k] if id return id else @mbders.push bder @cache[k] = @mbders.length return @mbders.length style2id:(style)-> style.align or= @def_align style.valign or= @def_valign style.rotate or= @def_rotate style.wrap or= @def_wrap style.font_id or= @def_font_id style.fill_id or= @def_fill_id style.bder_id or= @def_bder_id k = 's_' + style.font_id + '_' + style.fill_id + '_' + style.bder_id + '_' + style.align + '_' + style.valign + '_' + style.wrap + '_' + style.rotate id = @cache[k] if id return id else @mstyle.push style @cache[k] = @mstyle.length return @mstyle.length toxml: ()-> ss = xml.create('styleSheet',{version:'1.0',encoding:'UTF-8',standalone:true}) ss.att('xmlns','http://schemas.openxmlformats.org/spreadsheetml/2006/main') fonts = ss.ele('fonts',{count:@mfonts.length}) for o in @mfonts e = fonts.ele('font') e.ele('b') if o.bold isnt '-' e.ele('i') if o.iter isnt '-' e.ele('sz',{val:o.sz}) e.ele('color',{theme:o.color}) if o.color isnt '-' e.ele('name',{val:o.name}) e.ele('family',{val:o.family}) e.ele('charset',{val:'134'}) e.ele('scheme',{val:'minor'}) if o.scheme isnt '-' fills = ss.ele('fills',{count:@mfills.length}) for o in @mfills e = fills.ele('fill') es = e.ele('patternFill',{patternType:o.type}) es.ele('fgColor',{theme:'8',tint:'0.79998168889431442'}) if o.fgColor isnt '-' es.ele('bgColor',{indexed:o.bgColor}) if o.bgColor isnt '-' bders = ss.ele('borders',{count:@mbders.length}) for o in @mbders e = bders.ele('border') if o.left isnt '-' then e.ele('left',{style:o.left}).ele('color',{auto:'1'}) else e.ele('left') if o.right isnt '-' then e.ele('right',{style:o.right}).ele('color',{auto:'1'}) else e.ele('right') if o.top isnt '-' then e.ele('top',{style:o.top}).ele('color',{auto:'1'}) else e.ele('top') if o.bottom isnt '-' then e.ele('bottom',{style:o.bottom}).ele('color',{auto:'1'}) else e.ele('bottom') e.ele('diagonal') ss.ele('cellStyleXfs',{count:'1'}).ele('xf',{numFmtId:'0',fontId:'0',fillId:'0',borderId:'0'}).ele('alignment',{vertical:'center'}) cs = ss.ele('cellXfs',{count:@mstyle.length}) for o in @mstyle e = cs.ele('xf',{numFmtId:'0',fontId:(o.font_id-1),fillId:(o.fill_id-1),borderId:(o.bder_id-1),xfId:'0'}) e.att('applyFont','1') if o.font_id isnt 1 e.att('applyFill','1') if o.fill_id isnt 1 e.att('applyBorder','1') if o.bder_id isnt 1 if o.align isnt '-' or o.valign isnt '-' or o.wrap isnt '-' e.att('applyAlignment','1') ea = e.ele('alignment',{textRotation:(if o.rotate is '-' then '0' else o.rotate),horizontal:(if o.align is '-' then 'left' else o.align), vertical:(if o.valign is '-' then 'top' else o.valign)}) ea.att('wrapText','1') if o.wrap isnt '-' ss.ele('cellStyles',{count:'1'}).ele('cellStyle',{name:'常规',xfId:'0',builtinId:'0'}) ss.ele('dxfs',{count:'0'}) ss.ele('tableStyles',{count:'0',defaultTableStyle:'TableStyleMedium9',defaultPivotStyle:'PivotStyleLight16'}) return ss.end() class Workbook constructor: (@fpath, @fname) -> @id = ''+parseInt(Math.random()*9999999) # create temp folder & copy template data target = path.join(path.resolve(@fpath),@id) fs.rmdirSync(target) if existsSync(target) tool.copy (opt.tmpl_path + '/tmpl'),target # init @sheets = [] @ss = new SharedStrings @ct = new ContentTypes(@) @da = new DocPropsApp(@) @wb = new XlWorkbook(@) @re = new XlRels(@) @st = new Style(@) createSheet: (name, cols, rows) -> sheet = new Sheet(@,name,cols,rows) @sheets.push sheet return sheet save: (cb) => target = path.join(path.resolve(@fpath),@id) # 1 - build [Content_Types].xml if not fs.existsSync(target) fs.mkdirSync target, (e) -> return if !e or (e and e.code is 'EEXIST') console.log path + 'created' else console.log(e) fs.writeFileSync(target+'\\[Content_Types].xml',@ct.toxml(),'utf8') # 2 - build docProps/app.xml if not fs.existsSync(path.join(target,'docProps')) fs.mkdirSync path.join(target,'docProps'), (e) -> return if !e or (e and e.code is 'EEXIST') console.log path + 'created' else console.log(e) fs.writeFileSync(target+'\\docProps\\app.xml',@da.toxml(),'utf8') # 3 - build xl/workbook.xml if not fs.existsSync(path.join(target,'xl')) fs.mkdirSync path.join(target,'xl'), (e) -> return if !e or (e and e.code is 'EEXIST') console.log path + 'created' else console.log(e) fs.writeFileSync(target+'\\xl\\workbook.xml',@wb.toxml(),'utf8') # 4 - build xl/sharedStrings.xml fs.writeFileSync(target+'\\xl\\sharedStrings.xml',@ss.toxml(),'utf8') # 5 - build xl/_rels/workbook.xml.rels fs.mkdirSync path.join(target,path.join('xl','_rels')), (e) -> return if !e or (e and e.code is 'EEXIST') console.log path + 'created' else console.log(e) fs.writeFileSync(target+'\\xl\\_rels\\workbook.xml.rels',@re.toxml(),'utf8') # 6 - build xl/worksheets/sheet(1-N).xml fs.mkdirSync path.join(target,path.join('xl', 'worksheets')), (e) -> return if !e or (e and e.code is 'EEXIST') console.log path + 'created' else console.log(e) for i in [0...@sheets.length] fs.writeFileSync(target+'\\xl\\worksheets\\sheet'+(i+1)+'.xml',@sheets[i].toxml(),'utf8') # 7 - build xl/styles.xml fs.writeFileSync(target+'\\xl\\styles.xml',@st.toxml(),'utf8') # 8 - compress temp folder to target file args = ' a -tzip "' + path.join(path.resolve(@fpath),@fname) + '" "*"' opts = {cwd:target} exec.exec '"'+opt.tmpl_path+'\\tool\\7za.exe"' + args, opts, (err,stdout,stderr)-> # 9 - delete temp folder exec.exec 'rmdir "' + target + '" /q /s',()-> console.log(err) cb err cancel: () -> #target = path.join(path.resolve(@fpath),@id) # delete temp folder fs.rmdirSync target module.exports = createWorkbook: (fpath, fname)-> return new Workbook(fpath, fname)