Skip to content

Commit

Permalink
Merge pull request #66 from eladxxx/master
Browse files Browse the repository at this point in the history
Style/theme implementation
  • Loading branch information
SheetJSDev committed May 29, 2014
2 parents ba1ff17 + 5e9bca7 commit c91e94d
Show file tree
Hide file tree
Showing 9 changed files with 510 additions and 4 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ that does not start with `!` corresponds to a cell (using `A-1` notation).
- `.c` : comments associated with the cell
- `.z` : the number format string associated with the cell (if requested)
- `.l` : the hyperlink of the cell (.Target holds link, .tooltip is tooltip)
- `.s` : the style/theme of the cell (if applicable)

For dates, `.v` holds the raw date code from the sheet and `.w` holds the text

Expand All @@ -222,6 +223,7 @@ The exported `read` and `readFile` functions accept an options argument:
| cellFormula | true | Save formulae to the .f field |
| cellHTML | true | Parse rich text and save HTML to the .h field |
| cellNF | false | Save number format string to the .z field |
| cellStyles | false | Save style/theme info to the .s field |
| sheetStubs | false | Create cell objects for stub cells |
| sheetRows | 0 | If >0, read the first `sheetRows` rows ** |
| bookDeps | false | If true, parse calculation chains |
Expand Down
2 changes: 2 additions & 0 deletions bits/56_stycommon.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
var styles = {}; // shared styles

var themes = {}; // shared themes

51 changes: 50 additions & 1 deletion bits/57_styxml.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,48 @@
/* 18.8.21 fills CT_Fills */
function parse_fills(t, opts) {
styles.Fills = [];
var fill = {};
t[0].match(/<[^>]*>/g).forEach(function(x) {
var y = parsexmltag(x);
switch(y[0]) {
case '<fills': case '<fills>': case '</fills>': break;

/* 18.8.20 fill CT_Fill */
case '<fill>': break;
case '</fill>': styles.Fills.push(fill); fill = {}; break;

/* 18.8.32 patternFill CT_PatternFill */
case '<patternFill':
if(y.patternType) fill.patternType = y.patternType;
break;
case '<patternFill/>': break;

/* 18.8.3 bgColor CT_Color */
case '<bgColor':
if(!fill.bgColor) fill.bgColor = {};
if(y.indexed) fill.bgColor.indexed = parseInt(y.indexed);
if(y.theme) fill.bgColor.theme = parseInt(y.theme);
if(y.tint) fill.bgColor.tint = Number(y.tint);
/* Excel uses 8 character RGB strings? */
if(y.rgb) fill.bgColor.rgb = y.rgb.substring(y.rgb.length - 6);
break;
case '</bgColor>': break;

/* 18.8.19 fgColor CT_Color */
case '<fgColor':
if(!fill.fgColor) fill.fgColor = {};
if(y.theme) fill.fgColor.theme = parseInt(y.theme);
if(y.tint) fill.fgColor.tint = Number(y.tint);
/* Excel uses 8 character RGB strings? */
if(y.rgb) fill.fgColor.rgb = y.rgb.substring(y.rgb.length - 6);
break;
case '</fgColor>': break;

default: if(opts.WTF) throw 'unrecognized ' + y[0] + ' in fills';
}
});
}

/* 18.8.31 numFmts CT_NumFmts */
function parse_numFmts(t, opts) {
styles.NumberFmt = [];
Expand Down Expand Up @@ -38,6 +83,7 @@ function parse_cellXfs(t, opts) {
/* 18.8.45 xf CT_Xf */
case '<xf': delete y[0];
if(y.numFmtId) y.numFmtId = parseInt(y.numFmtId, 10);
if(y.fillId) y.fillId = parseInt(y.fillId, 10);
styles.CellXf.push(y); break;
case '</xf>': break;

Expand Down Expand Up @@ -73,7 +119,10 @@ function parse_sty_xml(data, opts) {
if((t=data.match(/<numFmts([^>]*)>.*<\/numFmts>/))) parse_numFmts(t, opts);

/* fonts CT_Fonts ? */
/* fills CT_Fills ? */

/* fills CT_Fills */
if((t=data.match(/<fills([^>]*)>.*<\/fills>/))) parse_fills(t, opts);

/* borders CT_Borders ? */
/* cellStyleXfs CT_CellStyleXfs ? */

Expand Down
182 changes: 182 additions & 0 deletions bits/59_theme.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,185 @@
RELS.THEME = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme";

/* Various RGB/HSL utility functions - might want to put these elsewhere. */
/* From http://www.javascripter.net/faq/hextorgb.htm, usage: var X = hexToX('FFFFFF') */
function cutHex(h) {return (h.charAt(0)=="#") ? h.substring(1,7):h}
function hexToR(h) {return parseInt((cutHex(h)).substring(0,2),16)}
function hexToG(h) {return parseInt((cutHex(h)).substring(2,4),16)}
function hexToB(h) {return parseInt((cutHex(h)).substring(4,6),16)}
/* From http://www.javascripter.net/faq/rgbtohex.htm, usage: var RGB = rgbToHex(R, G, B) */
function toHex(n) {
n = parseInt(n,10);
if (isNaN(n)) return "00";
n = Math.max(0,Math.min(n,255));
return "0123456789ABCDEF".charAt((n-n%16)/16)
+ "0123456789ABCDEF".charAt(n%16);
}
function rgbToHex(R,G,B) {return toHex(R)+toHex(G)+toHex(B)}
/* From the specification. */
var HLSMAX = 255;
/* From https://gist.github.com/mjackson/5311256 via http://stackoverflow.com/a/9493060 */
/**
* Converts an RGB color value to HSL. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes r, g, and b are contained in the set [0, 255] and
* returns h, s, and l in the set [0, 1].
*
* @param Number r The red color value
* @param Number g The green color value
* @param Number b The blue color value
* @return Array The HSL representation
*/
function rgbToHsl(r, g, b){
r /= 255, g /= 255, b /= 255;
var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;

if(max == min){
h = s = 0; // achromatic
}else{
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch(max){
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}

return [h, s, l];
}
/**
* Converts an HSL color value to RGB. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes h, s, and l are contained in the set [0, 1] and
* returns r, g, and b in the set [0, 255].
*
* @param Number h The hue
* @param Number s The saturation
* @param Number l The lightness
* @return Array The RGB representation
*/
function hslToRgb(h, s, l){
var r, g, b;

if(s == 0){
r = g = b = l; // achromatic
}else{
function hue2rgb(p, q, t){
if(t < 0) t += 1;
if(t > 1) t -= 1;
if(t < 1/6) return p + (q - p) * 6 * t;
if(t < 1/2) return q;
if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
}

var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}

return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}
/* Utility function to apply tint to an RGB color. */
function rgb_tint(rgb, tint) {
var r = hexToR(rgb),
g = hexToG(rgb),
b = hexToB(rgb),
hsl = rgbToHsl(r, g, b);

/* Apply tint as described in pages 1757-1758 of the ECMA Office Open XML specification. */
/* NOTE: This is totally messed up... see http://social.msdn.microsoft.com/Forums/en-US/e9d8c136-6d62-4098-9b1b-dac786149f43/excel-color-tint-algorithm-incorrect */
if (tint < 0) {
hsl[2] = hsl[2] * (1.0 + tint);
} else if (tint > 0) {
hsl[2] = hsl[2] * (1.0 + tint);

// XXX This doesn't work...
//hsl[2] = hsl[2] * (1.0 - tint) + (HLSMAX - HLSMAX * (1.0 - tint));
}

rgb = hslToRgb(hsl[0], hsl[1], hsl[2]);

return rgbToHex(rgb[0], rgb[1], rgb[2]);
}

function parse_clrScheme(t, opts) {
themes.themeElements.clrScheme = [];
var color = {};
t[0].match(/<[^>]*>/g).forEach(function(x) {
var y = parsexmltag(x);
switch(y[0]) {
case '<a:clrScheme': case '<<a:clrScheme>': case '</<a:clrScheme>': break;

/* 20.1.2.3.32 srgbClr CT_SRgbColor */
case '<a:srgbClr': color.rgb = y.val; break;

/* 20.1.2.3.33 sysClr CT_SystemColor */
case '<a:sysClr': color.rgb = y.lastClr; break;

/* 20.1.4.1.9 dk1 (Dark 1) */
case '<a:dk1>':
case '</a:dk1>':
/* 20.1.4.1.10 dk2 (Dark 2) */
case '<a:dk2>':
case '</a:dk2>':
/* 20.1.4.1.22 lt1 (Light 1) */
case '<a:lt1>':
case '</a:lt1>':
/* 20.1.4.1.23 lt2 (Light 2) */
case '<a:lt2>':
case '</a:lt2>':
/* 20.1.4.1.1 accent1 (Accent 1) */
case '<a:accent1>':
case '</a:accent1>':
/* 20.1.4.1.2 accent2 (Accent 2) */
case '<a:accent2>':
case '</a:accent2>':
/* 20.1.4.1.3 accent3 (Accent 3) */
case '<a:accent3>':
case '</a:accent3>':
/* 20.1.4.1.4 accent4 (Accent 4) */
case '<a:accent4>':
case '</a:accent4>':
/* 20.1.4.1.5 accent5 (Accent 5) */
case '<a:accent5>':
case '</a:accent5>':
/* 20.1.4.1.6 accent6 (Accent 6) */
case '<a:accent6>':
case '</a:accent6>':
/* 20.1.4.1.19 hlink (Hyperlink) */
case '<a:hlink>':
case '</a:hlink>':
/* 20.1.4.1.15 folHlink (Followed Hyperlink) */
case '<a:folHlink>':
case '</a:folHlink>':
if (y[0][1] === '/') {
themes.themeElements.clrScheme.push(color);
color = {};
} else {
color.name = y[0].substring(3, y[0].length - 1);
}
break;

default: if(opts.WTF) throw 'unrecognized ' + y[0] + ' in clrScheme';
}
});
}

/* 14.2.7 Theme Part */
function parse_theme_xml(data, opts) {
themes.themeElements = {};

var t;

/* clrScheme */
if((t=data.match(/<a:clrScheme([^>]*)>.*<\/a:clrScheme>/))) parse_clrScheme(t, opts);

return themes;
}

function write_theme() { return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Office Theme"><a:themeElements><a:clrScheme name="Office"><a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1><a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1><a:dk2><a:srgbClr val="1F497D"/></a:dk2><a:lt2><a:srgbClr val="EEECE1"/></a:lt2><a:accent1><a:srgbClr val="4F81BD"/></a:accent1><a:accent2><a:srgbClr val="C0504D"/></a:accent2><a:accent3><a:srgbClr val="9BBB59"/></a:accent3><a:accent4><a:srgbClr val="8064A2"/></a:accent4><a:accent5><a:srgbClr val="4BACC6"/></a:accent5><a:accent6><a:srgbClr val="F79646"/></a:accent6><a:hlink><a:srgbClr val="0000FF"/></a:hlink><a:folHlink><a:srgbClr val="800080"/></a:folHlink></a:clrScheme><a:fontScheme name="Office"><a:majorFont><a:latin typeface="Cambria"/><a:ea typeface=""/><a:cs typeface=""/><a:font script="Jpan" typeface="MS Pゴシック"/><a:font script="Hang" typeface="맑은 고딕"/><a:font script="Hans" typeface="宋体"/><a:font script="Hant" typeface="新細明體"/><a:font script="Arab" typeface="Times New Roman"/><a:font script="Hebr" typeface="Times New Roman"/><a:font script="Thai" typeface="Tahoma"/><a:font script="Ethi" typeface="Nyala"/><a:font script="Beng" typeface="Vrinda"/><a:font script="Gujr" typeface="Shruti"/><a:font script="Khmr" typeface="MoolBoran"/><a:font script="Knda" typeface="Tunga"/><a:font script="Guru" typeface="Raavi"/><a:font script="Cans" typeface="Euphemia"/><a:font script="Cher" typeface="Plantagenet Cherokee"/><a:font script="Yiii" typeface="Microsoft Yi Baiti"/><a:font script="Tibt" typeface="Microsoft Himalaya"/><a:font script="Thaa" typeface="MV Boli"/><a:font script="Deva" typeface="Mangal"/><a:font script="Telu" typeface="Gautami"/><a:font script="Taml" typeface="Latha"/><a:font script="Syrc" typeface="Estrangelo Edessa"/><a:font script="Orya" typeface="Kalinga"/><a:font script="Mlym" typeface="Kartika"/><a:font script="Laoo" typeface="DokChampa"/><a:font script="Sinh" typeface="Iskoola Pota"/><a:font script="Mong" typeface="Mongolian Baiti"/><a:font script="Viet" typeface="Times New Roman"/><a:font script="Uigh" typeface="Microsoft Uighur"/><a:font script="Geor" typeface="Sylfaen"/></a:majorFont><a:minorFont><a:latin typeface="Calibri"/><a:ea typeface=""/><a:cs typeface=""/><a:font script="Jpan" typeface="MS Pゴシック"/><a:font script="Hang" typeface="맑은 고딕"/><a:font script="Hans" typeface="宋体"/><a:font script="Hant" typeface="新細明體"/><a:font script="Arab" typeface="Arial"/><a:font script="Hebr" typeface="Arial"/><a:font script="Thai" typeface="Tahoma"/><a:font script="Ethi" typeface="Nyala"/><a:font script="Beng" typeface="Vrinda"/><a:font script="Gujr" typeface="Shruti"/><a:font script="Khmr" typeface="DaunPenh"/><a:font script="Knda" typeface="Tunga"/><a:font script="Guru" typeface="Raavi"/><a:font script="Cans" typeface="Euphemia"/><a:font script="Cher" typeface="Plantagenet Cherokee"/><a:font script="Yiii" typeface="Microsoft Yi Baiti"/><a:font script="Tibt" typeface="Microsoft Himalaya"/><a:font script="Thaa" typeface="MV Boli"/><a:font script="Deva" typeface="Mangal"/><a:font script="Telu" typeface="Gautami"/><a:font script="Taml" typeface="Latha"/><a:font script="Syrc" typeface="Estrangelo Edessa"/><a:font script="Orya" typeface="Kalinga"/><a:font script="Mlym" typeface="Kartika"/><a:font script="Laoo" typeface="DokChampa"/><a:font script="Sinh" typeface="Iskoola Pota"/><a:font script="Mong" typeface="Mongolian Baiti"/><a:font script="Viet" typeface="Arial"/><a:font script="Uigh" typeface="Microsoft Uighur"/><a:font script="Geor" typeface="Sylfaen"/></a:minorFont></a:fontScheme><a:fmtScheme name="Office"><a:fillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="50000"/><a:satMod val="300000"/></a:schemeClr></a:gs><a:gs pos="35000"><a:schemeClr val="phClr"><a:tint val="37000"/><a:satMod val="300000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:tint val="15000"/><a:satMod val="350000"/></a:schemeClr></a:gs></a:gsLst><a:lin ang="16200000" scaled="1"/></a:gradFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="100000"/><a:shade val="100000"/><a:satMod val="130000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:tint val="50000"/><a:shade val="100000"/><a:satMod val="350000"/></a:schemeClr></a:gs></a:gsLst><a:lin ang="16200000" scaled="0"/></a:gradFill></a:fillStyleLst><a:lnStyleLst><a:ln w="9525" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"><a:shade val="95000"/><a:satMod val="105000"/></a:schemeClr></a:solidFill><a:prstDash val="solid"/></a:ln><a:ln w="25400" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/></a:ln><a:ln w="38100" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/></a:ln></a:lnStyleLst><a:effectStyleLst><a:effectStyle><a:effectLst><a:outerShdw blurRad="40000" dist="20000" dir="5400000" rotWithShape="0"><a:srgbClr val="000000"><a:alpha val="38000"/></a:srgbClr></a:outerShdw></a:effectLst></a:effectStyle><a:effectStyle><a:effectLst><a:outerShdw blurRad="40000" dist="23000" dir="5400000" rotWithShape="0"><a:srgbClr val="000000"><a:alpha val="35000"/></a:srgbClr></a:outerShdw></a:effectLst></a:effectStyle><a:effectStyle><a:effectLst><a:outerShdw blurRad="40000" dist="23000" dir="5400000" rotWithShape="0"><a:srgbClr val="000000"><a:alpha val="35000"/></a:srgbClr></a:outerShdw></a:effectLst><a:scene3d><a:camera prst="orthographicFront"><a:rot lat="0" lon="0" rev="0"/></a:camera><a:lightRig rig="threePt" dir="t"><a:rot lat="0" lon="0" rev="1200000"/></a:lightRig></a:scene3d><a:sp3d><a:bevelT w="63500" h="25400"/></a:sp3d></a:effectStyle></a:effectStyleLst><a:bgFillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="40000"/><a:satMod val="350000"/></a:schemeClr></a:gs><a:gs pos="40000"><a:schemeClr val="phClr"><a:tint val="45000"/><a:shade val="99000"/><a:satMod val="350000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:shade val="20000"/><a:satMod val="255000"/></a:schemeClr></a:gs></a:gsLst><a:path path="circle"><a:fillToRect l="50000" t="-80000" r="50000" b="180000"/></a:path></a:gradFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="80000"/><a:satMod val="300000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:shade val="30000"/><a:satMod val="200000"/></a:schemeClr></a:gs></a:gsLst><a:path path="circle"><a:fillToRect l="50000" t="50000" r="50000" b="50000"/></a:path></a:gradFill></a:bgFillStyleLst></a:fmtScheme></a:themeElements><a:objectDefaults><a:spDef><a:spPr/><a:bodyPr/><a:lstStyle/><a:style><a:lnRef idx="1"><a:schemeClr val="accent1"/></a:lnRef><a:fillRef idx="3"><a:schemeClr val="accent1"/></a:fillRef><a:effectRef idx="2"><a:schemeClr val="accent1"/></a:effectRef><a:fontRef idx="minor"><a:schemeClr val="lt1"/></a:fontRef></a:style></a:spDef><a:lnDef><a:spPr/><a:bodyPr/><a:lstStyle/><a:style><a:lnRef idx="2"><a:schemeClr val="accent1"/></a:lnRef><a:fillRef idx="0"><a:schemeClr val="accent1"/></a:fillRef><a:effectRef idx="1"><a:schemeClr val="accent1"/></a:effectRef><a:fontRef idx="minor"><a:schemeClr val="tx1"/></a:fontRef></a:style></a:lnDef></a:objectDefaults><a:extraClrSchemeLst/></a:theme>'; }
12 changes: 11 additions & 1 deletion bits/72_wsxml.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,24 @@ function parse_ws_xml(data, opts, rels) {
}

/* formatting */
var fmtid = 0;
var fmtid = 0, fillid = 0;
if(cell.s && styles.CellXf) {
var cf = styles.CellXf[cell.s];
if(cf && cf.numFmtId) fmtid = cf.numFmtId;
if(opts.cellStyles && cf && cf.fillId) fillid = cf.fillId;
}
try {
p.w = SSF.format(fmtid,p.v,_ssfopts);
if(opts.cellNF) p.z = SSF._table[fmtid];
if(fillid) {
p.s = styles.Fills[fillid];
if (p.s.fgColor && p.s.fgColor.theme) {
p.s.fgColor.rgb = rgb_tint(themes.themeElements.clrScheme[p.s.fgColor.theme].rgb, p.s.fgColor.tint || 0);
}
if (p.s.bgColor && p.s.bgColor.theme) {
p.s.bgColor.rgb = rgb_tint(themes.themeElements.clrScheme[p.s.bgColor.theme].rgb, p.s.bgColor.tint || 0);
}
}
} catch(e) { if(opts.WTF) throw e; }
s[cell.r] = p;
});
Expand Down
4 changes: 4 additions & 0 deletions bits/79_xmlbin.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ function parse_sty(data, name, opts) {
return (name.substr(-4)===".bin" ? parse_sty_bin : parse_sty_xml)(data, opts);
}

function parse_theme(data, name, opts) {
return parse_theme_xml(data, opts);
}

function parse_sst(data, name, opts) {
return (name.substr(-4)===".bin" ? parse_sst_bin : parse_sst_xml)(data, opts);
}
Expand Down
1 change: 1 addition & 0 deletions bits/84_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ var fix_read_opts = fix_opts([
['cellNF', false], /* emit cell number format string as .z */
['cellHTML', true], /* emit html string as .h */
['cellFormula', true], /* emit formulae as .f */
['cellStyles', false], /* emits style/theme as .s */

['sheetStubs', false], /* emit empty cells */
['sheetRows', 0, 'n'], /* read n rows (0 = read all rows) */
Expand Down
4 changes: 4 additions & 0 deletions bits/85_parsezip.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ function parse_zip(zip, opts) {

styles = {};
if(dir.style) styles = parse_sty(getzipdata(zip, dir.style.replace(/^\//,'')),dir.style, opts);

themes = {};
if(opts.cellStyles && dir.themes) themes = parse_theme(getzipdata(zip, dir.themes[0].replace(/^\//,'')),dir.themes[0], opts);
}

var wb = parse_wb(getzipdata(zip, dir.workbooks[0].replace(/^\//,'')), dir.workbooks[0], opts);
Expand Down Expand Up @@ -105,6 +108,7 @@ function parse_zip(zip, opts) {
SheetNames: props.SheetNames,
Strings: strs,
Styles: styles,
Themes: themes,
SSF: SSF.get_table()
};
if(opts.bookFiles) {
Expand Down

0 comments on commit c91e94d

Please sign in to comment.