Skip to content

Add support for adding images anchored to one cell #746

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 3, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1225,6 +1225,17 @@ ws.addImage(imageId, {
});
```

### Add image to a cell

You can add an image to a cell and then define its width and height in pixels at 96dpi.

```javascript
worksheet.addImage(imageId2, {
tl: { col: 0, row: 0 },
ext: { width: 500, height: 200 }
});
```

## File I/O

### XLSX
Expand Down
7 changes: 6 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,11 @@ export interface ImageRange {
br: { col: number; row: number };
}

export interface ImagePosition {
tl: { col: number; row: number };
ext: { width: number; height: number };
}

export interface Range extends Location {
sheetName: string;

Expand Down Expand Up @@ -1022,7 +1027,7 @@ export interface Worksheet {
* Using the image id from `Workbook.addImage`,
* embed an image within the worksheet to cover a range
*/
addImage(imageId: number, range: string | { editAs?: string; } & ImageRange): void;
addImage(imageId: number, range: string | { editAs?: string; } & ImageRange | { editAs?: string; } & ImagePosition): void;

getImages(): Array<{
type: 'image',
Expand Down
40 changes: 36 additions & 4 deletions lib/xlsx/xform/drawing/drawing-xform.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,23 @@ var XmlStream = require('../../../utils/xml-stream');

var BaseXform = require('../base-xform');
var TwoCellAnchorXform = require('./two-cell-anchor-xform');
var OneCellAnchorXform = require('./one-cell-anchor-xform');

var WorkSheetXform = module.exports = function() {
this.map = {
'xdr:twoCellAnchor': new TwoCellAnchorXform()
'xdr:twoCellAnchor': new TwoCellAnchorXform(),
'xdr:oneCellAnchor': new OneCellAnchorXform()
};
};

function useOneCellAnchor(model) {
return typeof model.range === 'object' && model.range.ext
}

function reconcileOneCellAnchor(model) {
return !!model.ext
}

utils.inherits(WorkSheetXform, BaseXform, {
DRAWING_ATTRIBUTES: {
'xmlns:xdr': 'http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing',
Expand All @@ -29,7 +39,16 @@ utils.inherits(WorkSheetXform, BaseXform, {
prepare: function(model) {
var twoCellAnchorXform = this.map['xdr:twoCellAnchor'];
model.anchors.forEach(function(item, index) {
twoCellAnchorXform.prepare(item, {index: index});
if (!useOneCellAnchor(item)) {
twoCellAnchorXform.prepare(item, {index: index});
}
});

var oneCellAnchorXform = this.map['xdr:oneCellAnchor'];
model.anchors.forEach(function(item, index) {
if (useOneCellAnchor(item)) {
oneCellAnchorXform.prepare(item, {index: index});
}
});
},

Expand All @@ -39,7 +58,16 @@ utils.inherits(WorkSheetXform, BaseXform, {

var twoCellAnchorXform = this.map['xdr:twoCellAnchor'];
model.anchors.forEach(function(item) {
twoCellAnchorXform.render(xmlStream, item);
if (!useOneCellAnchor(item)) {
twoCellAnchorXform.render(xmlStream, item);
}
});

var oneCellAnchorXform = this.map['xdr:oneCellAnchor'];
model.anchors.forEach(function(item) {
if (useOneCellAnchor(item)) {
oneCellAnchorXform.render(xmlStream, item);
}
});

xmlStream.closeNode();
Expand Down Expand Up @@ -92,7 +120,11 @@ utils.inherits(WorkSheetXform, BaseXform, {

reconcile: function(model, options) {
model.anchors.forEach(anchor => {
this.map['xdr:twoCellAnchor'].reconcile(anchor, options);
if (reconcileOneCellAnchor(anchor)) {
this.map['xdr:oneCellAnchor'].reconcile(anchor, options);
} else {
this.map['xdr:twoCellAnchor'].reconcile(anchor, options);
}
});
}
});
52 changes: 52 additions & 0 deletions lib/xlsx/xform/drawing/ext-xform.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Copyright (c) 2016-2017 Guyon Roche
* LICENCE: MIT - please refer to LICENCE file included with this module
* or https://github.com/guyonroche/exceljs/blob/master/LICENSE
*/

'use strict';

var utils = require('../../../utils/utils');
var BaseXform = require('../base-xform');

var ExtXform = module.exports = function(options) {
this.tag = options.tag;
this.map = {
};
};

/** https://en.wikipedia.org/wiki/Office_Open_XML_file_formats#DrawingML */
const EMU_PER_PIXEL_AT_96_DPI = 9525

utils.inherits(ExtXform, BaseXform, {

render: function(xmlStream, model) {
xmlStream.openNode(this.tag);

var width = Math.floor(model.width * EMU_PER_PIXEL_AT_96_DPI);
var height = Math.floor(model.height * EMU_PER_PIXEL_AT_96_DPI);

xmlStream.addAttribute('cx', width);
xmlStream.addAttribute('cy', height);

xmlStream.closeNode();
},

parseOpen: function(node) {
if (node.name == this.tag) {
this.model = {
width: parseInt(node.attributes.cx || '0', 10) / EMU_PER_PIXEL_AT_96_DPI,
height: parseInt(node.attributes.cy || '0', 10) / EMU_PER_PIXEL_AT_96_DPI,
};
return true;
}
return false;
},

parseText: function(text) {
},

parseClose: function(name) {
return false;
}
});
120 changes: 120 additions & 0 deletions lib/xlsx/xform/drawing/one-cell-anchor-xform.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* Copyright (c) 2016-2017 Guyon Roche
* LICENCE: MIT - please refer to LICENCE file included with this module
* or https://github.com/guyonroche/exceljs/blob/master/LICENSE
*/

'use strict';

var utils = require('../../../utils/utils');
var BaseXform = require('../base-xform');
var StaticXform = require('../static-xform');

var CellPositionXform = require('./cell-position-xform');
var ExtXform = require('./ext-xform');
var PicXform = require('./pic-xform');

var OneCellAnchorXform = module.exports = function() {
this.map = {
'xdr:from': new CellPositionXform({tag: 'xdr:from'}),
'xdr:ext': new ExtXform({tag: 'xdr:ext'}),
'xdr:pic': new PicXform(),
'xdr:clientData': new StaticXform({tag: 'xdr:clientData'}),
};
};

utils.inherits(OneCellAnchorXform, BaseXform, {
get tag() { return 'xdr:oneCellAnchor'; },

prepare: function(model, options) {
this.map['xdr:pic'].prepare(model.picture, options);

model.tl = model.range.tl;
model.ext = model.range.ext;
},

render: function(xmlStream, model) {
if (model.range.editAs) {
xmlStream.openNode(this.tag, {editAs: model.range.editAs});
} else {
xmlStream.openNode(this.tag);
}

this.map['xdr:from'].render(xmlStream, model.tl);
this.map['xdr:ext'].render(xmlStream, model.ext);
this.map['xdr:pic'].render(xmlStream, model.picture);
this.map['xdr:clientData'].render(xmlStream, {});

xmlStream.closeNode();
},

parseOpen: function(node) {
if (this.parser) {
this.parser.parseOpen(node);
return true;
}
switch (node.name) {
case this.tag:
this.reset();
this.model = {
editAs: node.attributes.editAs
};
break;
default:
this.parser = this.map[node.name];
if (this.parser) {
this.parser.parseOpen(node);
}
break;
}
return true;
},

parseText: function(text) {
if (this.parser) {
this.parser.parseText(text);
}
},

parseClose: function(name) {
if (this.parser) {
if (!this.parser.parseClose(name)) {
this.parser = undefined;
}
return true;
}
switch (name) {
case this.tag:
this.model = this.model || {};
this.model.tl = this.map['xdr:from'].model;
this.model.ext = this.map['xdr:ext'].model;
this.model.picture = this.map['xdr:pic'].model;
return false;
default:
// could be some unrecognised tags
return true;
}
},

reconcile: function(model, options) {
if (model.picture && model.picture.rId) {
var rel = options.rels[model.picture.rId];
var match = rel.Target.match(/.*\/media\/(.+[.][a-z]{3,4})/);
if (match) {
var name = match[1];
var mediaId = options.mediaIndex[name];
model.medium = options.media[mediaId];
}
}
model.range = {
tl: model.tl,
ext: model.ext,
};
if (model.editAs) {
model.range.editAs = model.editAs;
delete model.editAs;
}
delete model.tl;
delete model.ext;
}
});
41 changes: 41 additions & 0 deletions spec/integration/workbook/images.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,5 +130,46 @@ describe('Workbook', function() {
expect(Buffer.compare(imageData, image.buffer)).to.equal(0);
});
});

it('stores embedded image with one-cell-anchor', function() {
var wb = new Excel.Workbook();
var ws = wb.addWorksheet('blort');
var wb2, ws2;

var imageId = wb.addImage({
filename: IMAGE_FILENAME,
extension: 'jpeg',
});

ws.addImage(imageId, {
tl: { col: 0.1125, row: 0.4 },
ext: { width: 100, height: 100 },
editAs: 'oneCell'
});

return wb.xlsx.writeFile(TEST_XLSX_FILE_NAME)
.then(function() {
wb2 = new Excel.Workbook();
return wb2.xlsx.readFile(TEST_XLSX_FILE_NAME);
})
.then(function() {
ws2 = wb2.getWorksheet('blort');
expect(ws2).to.not.be.undefined();

return fsReadFileAsync(IMAGE_FILENAME);
})
.then(function(imageData) {
const images = ws2.getImages();
expect(images.length).to.equal(1);

const imageDesc = images[0];
expect(imageDesc.range.editAs).to.equal('oneCell');
expect(imageDesc.range.ext.width).to.equal(100);
expect(imageDesc.range.ext.height).to.equal(100);

const image = wb2.getImage(imageDesc.imageId);
expect(Buffer.compare(imageData, image.buffer)).to.equal(0);
});
});
});
});
40 changes: 40 additions & 0 deletions test/test-image-one-cell-anchor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
var fs = require('fs');
var path = require('path');

var HrStopwatch = require('./utils/hr-stopwatch');

var Workbook = require('../excel').Workbook;

var filename = process.argv[2];

var wb = new Workbook();
var ws = wb.addWorksheet('blort');

ws.getCell('B2').value = 'Hello, World!';

var imageId = wb.addImage({
filename: path.join(__dirname, 'data/image2.png'),
extension: 'png',
});
var backgroundId = wb.addImage({
buffer: fs.readFileSync(path.join(__dirname, 'data/bubbles.jpg')),
extension: 'jpeg',
});
ws.addImage(imageId, {
tl: { col: 1, row: 1 },
ext: { width: 100, height: 100 }
});

ws.addBackgroundImage(backgroundId);

var stopwatch = new HrStopwatch();
stopwatch.start();
wb.xlsx.writeFile(filename)
.then(function() {
var micros = stopwatch.microseconds;
console.log('Done.');
console.log('Time taken:', micros)
})
.catch(function(error) {
console.error(error.stack);
});