Skip to content

Commit

Permalink
[IMP] XLSX: export images
Browse files Browse the repository at this point in the history
This commit implements the functionality of exporting images into .xlsx file.
The basic idea is to use drawing relationship to connect the actual image
file with the position in .xlsx file. The copying of image file is done in
`spreadsheet_edition` module to avoid wasting bandwith.

task 3125701

closes #1956

Signed-off-by: Rémi Rahir (rar) <[email protected]>
  • Loading branch information
Chenyun Yang committed Mar 7, 2023
1 parent 635a643 commit b8efb5e
Show file tree
Hide file tree
Showing 15 changed files with 1,508 additions and 86 deletions.
7 changes: 6 additions & 1 deletion demo/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,12 @@ topbarMenuRegistry.addChild("xlsx", ["file"], {
const doc = await env.model.exportXLSX();
const zip = new JSZip();
for (const file of doc.files) {
zip.file(file.path, file.content.replaceAll(` xmlns=""`, ""));
if (file.imagePath) {
const fetchedImage = await fetch(file.imagePath).then((response) => response.blob());
zip.file(file.path, fetchedImage);
} else {
zip.file(file.path, file.content.replaceAll(` xmlns=""`, ""));
}
}
zip.generateAsync({ type: "blob" }).then(function (blob) {
saveAs(blob, doc.name);
Expand Down
1 change: 1 addition & 0 deletions src/migrations/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,7 @@ export function createEmptyExcelSheet(sheetId: UID, name: string): ExcelSheetDat
return {
...(createEmptySheet(sheetId, name) as Omit<ExcelSheetData, "charts">),
charts: [],
images: [],
};
}

Expand Down
21 changes: 21 additions & 0 deletions src/plugins/core/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { Image } from "../../types/image";
import {
CommandResult,
CoreCommand,
ExcelWorkbookData,
Figure,
FigureData,
FigureSize,
Pixel,
UID,
Expand Down Expand Up @@ -152,6 +154,25 @@ export class ImagePlugin extends CorePlugin<ImageState> implements ImageState {
}
}

exportForExcel(data: ExcelWorkbookData) {
for (const sheet of data.sheets) {
const figures = this.getters.getFigures(sheet.id);
const images: FigureData<Image>[] = [];
for (const figure of figures) {
if (figure?.tag === "image") {
const image = this.getImage(figure.id);
if (image) {
images.push({
...figure,
data: deepCopy(image),
});
}
}
}
sheet.images = images;
}
}

private getAllImages(): Image[] {
const images: Image[] = [];
for (const sheetId in this.images) {
Expand Down
2 changes: 2 additions & 0 deletions src/types/workbook_data.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { CellValue, Format } from ".";
import { ExcelChartDefinition } from "./chart/chart";
import { ConditionalFormat } from "./conditional_formatting";
import { Image } from "./image";
import { Border, PaneDivision, Pixel, Style, UID } from "./misc";

export interface Dependencies {
Expand Down Expand Up @@ -70,6 +71,7 @@ export interface ExcelCellData extends CellData {
export interface ExcelSheetData extends Omit<SheetData, "figureTables"> {
cells: { [key: string]: ExcelCellData | undefined };
charts: FigureData<ExcelChartDefinition>[];
images: FigureData<Image>[];
filterTables: ExcelFilterTableData[];
}

Expand Down
9 changes: 8 additions & 1 deletion src/types/xlsx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,19 @@ export interface XLSXRelFile {
rels: XLSXRel[];
}

export interface XLSXExportFile {
export type XLSXExportFile = XLSXExportImageFile | XLSXExportXMLFile;

export interface XLSXExportXMLFile {
path: string;
content: string;
contentType?: string;
}

export interface XLSXExportImageFile {
path: string;
imagePath: string;
}

export interface XLSXExport {
name: string;
files: XLSXExportFile[];
Expand Down
1 change: 1 addition & 0 deletions src/xlsx/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const XLSX_RELATION_TYPE = {
theme: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme",
table: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table",
hyperlink: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
image: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
} as const;

export const RELATIONSHIP_NSR =
Expand Down
165 changes: 120 additions & 45 deletions src/xlsx/functions/drawings.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { FIGURE_BORDER_SIZE } from "../../constants";
import { FigureData, HeaderData, SheetData } from "../../types";
import { HeaderData, SheetData } from "../../types";
import { ExcelChartDefinition } from "../../types/chart/chart";
import { XMLAttributes, XMLString } from "../../types/xlsx";
import { DRAWING_NS_A, DRAWING_NS_C, NAMESPACE, RELATIONSHIP_NSR } from "../constants";
import { convertChartId, convertDotValueToEMU } from "../helpers/content_helpers";
import { convertChartId, convertDotValueToEMU, convertImageId } from "../helpers/content_helpers";
import { escapeXml, formatAttributes, joinXmlNodes, parseXML } from "../helpers/xml_helpers";
import { Image } from "./../../types/image";
import { FigureData } from "./../../types/workbook_data";

type FigurePosition = {
to: {
Expand All @@ -22,9 +24,9 @@ type FigurePosition = {
};

export function createDrawing(
chartRelIds: string[],
drawingRelIds: string[],
sheet: SheetData,
figures: FigureData<ExcelChartDefinition>[]
figures: FigureData<ExcelChartDefinition | Image>[]
): XMLDocument {
const namespaces: XMLAttributes = [
["xmlns:xdr", NAMESPACE.drawing],
Expand All @@ -34,46 +36,22 @@ export function createDrawing(
];
const figuresNodes: XMLString[] = [];
for (const [figureIndex, figure] of Object.entries(figures)) {
// position
const { from, to } = convertFigureData(figure, sheet);
const chartId = convertChartId(figure.id);
const cNvPrAttrs: XMLAttributes = [
["id", chartId],
["name", `Chart ${chartId}`],
["title", "Chart"],
];
figuresNodes.push(escapeXml/*xml*/ `
<xdr:twoCellAnchor>
<xdr:from>
<xdr:col>${from.col}</xdr:col>
<xdr:colOff>${from.colOff}</xdr:colOff>
<xdr:row>${from.row}</xdr:row>
<xdr:rowOff>${from.rowOff}</xdr:rowOff>
</xdr:from>
<xdr:to>
<xdr:col>${to.col}</xdr:col>
<xdr:colOff>${to.colOff}</xdr:colOff>
<xdr:row>${to.row}</xdr:row>
<xdr:rowOff>${to.rowOff}</xdr:rowOff>
</xdr:to>
<xdr:graphicFrame>
<xdr:nvGraphicFramePr>
<xdr:cNvPr ${formatAttributes(cNvPrAttrs)} />
<xdr:cNvGraphicFramePr />
</xdr:nvGraphicFramePr>
<xdr:xfrm>
<a:off x="0" y="0"/>
<a:ext cx="0" cy="0"/>
</xdr:xfrm>
<a:graphic>
<a:graphicData uri="${DRAWING_NS_C}">
<c:chart r:id="${chartRelIds[figureIndex]}" />
</a:graphicData>
</a:graphic>
</xdr:graphicFrame>
<xdr:clientData fLocksWithSheet="0"/>
</xdr:twoCellAnchor>
`);
switch (figure?.tag) {
case "chart":
figuresNodes.push(
createChartDrawing(
figure as FigureData<ExcelChartDefinition>,
sheet,
drawingRelIds[figureIndex]
)
);
break;
case "image":
figuresNodes.push(
createImageDrawing(figure as FigureData<Image>, sheet, drawingRelIds[figureIndex])
);
break;
}
}

const xml = escapeXml/*xml*/ `
Expand All @@ -88,7 +66,7 @@ export function createDrawing(
* Returns the coordinates of topLeft (from) and BottomRight (to) of the chart in English Metric Units (EMU)
*/
function convertFigureData(
figure: FigureData<ExcelChartDefinition>,
figure: FigureData<ExcelChartDefinition | Image>,
sheet: SheetData
): FigurePosition {
const { x, y, height, width } = figure;
Expand Down Expand Up @@ -139,3 +117,100 @@ function figureCoordinates(
offset: convertDotValueToEMU(position - currentPosition + FIGURE_BORDER_SIZE),
};
}

function createChartDrawing(
figure: FigureData<ExcelChartDefinition>,
sheet: SheetData,
chartRelId: string
): XMLString {
// position
const { from, to } = convertFigureData(figure, sheet);
const chartId = convertChartId(figure.id);
const cNvPrAttrs: XMLAttributes = [
["id", chartId],
["name", `Chart ${chartId}`],
["title", "Chart"],
];
return escapeXml/*xml*/ `
<xdr:twoCellAnchor>
<xdr:from>
<xdr:col>${from.col}</xdr:col>
<xdr:colOff>${from.colOff}</xdr:colOff>
<xdr:row>${from.row}</xdr:row>
<xdr:rowOff>${from.rowOff}</xdr:rowOff>
</xdr:from>
<xdr:to>
<xdr:col>${to.col}</xdr:col>
<xdr:colOff>${to.colOff}</xdr:colOff>
<xdr:row>${to.row}</xdr:row>
<xdr:rowOff>${to.rowOff}</xdr:rowOff>
</xdr:to>
<xdr:graphicFrame>
<xdr:nvGraphicFramePr>
<xdr:cNvPr ${formatAttributes(cNvPrAttrs)} />
<xdr:cNvGraphicFramePr />
</xdr:nvGraphicFramePr>
<xdr:xfrm>
<a:off x="0" y="0"/>
<a:ext cx="0" cy="0"/>
</xdr:xfrm>
<a:graphic>
<a:graphicData uri="${DRAWING_NS_C}">
<c:chart r:id="${chartRelId}" />
</a:graphicData>
</a:graphic>
</xdr:graphicFrame>
<xdr:clientData fLocksWithSheet="0"/>
</xdr:twoCellAnchor>
`;
}

function createImageDrawing(
figure: FigureData<Image>,
sheet: SheetData,
imageRelId: string
): XMLString {
// position
const { from, to } = convertFigureData(figure, sheet);
const imageId = convertImageId(figure.id);
const cNvPrAttrs: XMLAttributes = [
["id", imageId],
["name", `Image ${imageId}`],
["title", "Image"],
];
return escapeXml/*xml*/ `
<xdr:twoCellAnchor editAs="oneCell">
<xdr:from>
<xdr:col>${from.col}</xdr:col>
<xdr:colOff>${from.colOff}</xdr:colOff>
<xdr:row>${from.row}</xdr:row>
<xdr:rowOff>${from.rowOff}</xdr:rowOff>
</xdr:from>
<xdr:to>
<xdr:col>${to.col}</xdr:col>
<xdr:colOff>${to.colOff}</xdr:colOff>
<xdr:row>${to.row}</xdr:row>
<xdr:rowOff>${to.rowOff}</xdr:rowOff>
</xdr:to>
<xdr:pic>
<xdr:nvPicPr>
<xdr:cNvPr ${formatAttributes(cNvPrAttrs)}/>
<xdr:cNvPicPr preferRelativeResize="0"/>
</xdr:nvPicPr>
<xdr:blipFill>
<a:blip cstate="print" r:embed="${imageRelId}"/>
<a:stretch>
<a:fillRect/>
</a:stretch>
</xdr:blipFill>
<xdr:spPr>
<a:prstGeom prst="rect">
<a:avLst/>
</a:prstGeom>
<a:noFill/>
</xdr:spPr>
</xdr:pic>
<xdr:clientData fLocksWithSheet="0"/>
</xdr:twoCellAnchor>
`;
}
21 changes: 16 additions & 5 deletions src/xlsx/helpers/content_helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,8 @@ export function getCellType(value: number | string | boolean): string {
}
}

/**
* For some reason, Excel will only take the devicePixelRatio (i.e. interface scale on Windows desktop)
* into account for the height.
*/
export function convertHeightToExcel(height: number): number {
return Math.round(HEIGHT_FACTOR * height * window.devicePixelRatio * 100) / 100;
return Math.round(HEIGHT_FACTOR * height * 100) / 100;
}

export function convertWidthToExcel(width: number): number {
Expand Down Expand Up @@ -238,6 +234,21 @@ export function convertChartId(chartId: UID) {
return xlsxId + 1;
}

const imageIds: UID[] = [];

/**
* Convert a image o-spreadsheet id to a xlsx id which
* are unsigned integers (starting from 1).
*/
export function convertImageId(imageId: UID) {
const xlsxId = imageIds.findIndex((id) => id === imageId);
if (xlsxId === -1) {
imageIds.push(imageId);
return imageIds.length;
}
return xlsxId + 1;
}

/**
* Convert a value expressed in dot to EMU.
* EMU = English Metrical Unit
Expand Down
8 changes: 8 additions & 0 deletions src/xlsx/helpers/xlsx_helper.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { XLSXImportFile, XLSXXmlDocuments } from "../../types/xlsx";
import { CONTENT_TYPES_FILE } from "../constants";
import { XLSXExportFile, XLSXExportXMLFile } from "./../../types/xlsx";

/**
* Return all the xmls converted to XLSXImportFile corresponding to the given content type.
Expand All @@ -9,6 +10,13 @@ export function getXLSXFilesOfType(contentType: string, xmls: XLSXXmlDocuments):
return getXlsxFile(paths, xmls);
}

/**
* Return whether an exported file is an XML file or other kinds of file (e.g. image)
*/
export function isXLSXExportXMLFile(file: XLSXExportFile): file is XLSXExportXMLFile {
return "content" in file;
}

/**
* From an array of file path, return the equivalents XLSXFiles. An XLSX File is composed of an XML,
* and optionally of a relationships XML.
Expand Down
11 changes: 3 additions & 8 deletions src/xlsx/helpers/xml_helpers.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import { DEFAULT_FONT_SIZE } from "../../constants";
import { concat } from "../../helpers";
import {
XLSXExportFile,
XLSXStructure,
XMLAttributes,
XMLAttributeValue,
XMLString,
} from "../../types/xlsx";
import { XLSXStructure, XMLAttributes, XMLAttributeValue, XMLString } from "../../types/xlsx";
import { XLSXExportXMLFile } from "./../../types/xlsx";

// -------------------------------------
// XML HELPERS
Expand All @@ -16,7 +11,7 @@ export function createXMLFile(
doc: XMLDocument,
path: string,
contentType?: string
): XLSXExportFile {
): XLSXExportXMLFile {
return {
content: new XMLSerializer().serializeToString(doc),
path,
Expand Down
Loading

0 comments on commit b8efb5e

Please sign in to comment.