@cj-tech-master/excelts 5.1.2 → 5.1.3-canary.20260227071644.5cda867
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/modules/excel/anchor.d.ts +1 -0
- package/dist/browser/modules/excel/anchor.js +3 -0
- package/dist/browser/modules/excel/image.d.ts +1 -0
- package/dist/browser/modules/excel/image.js +16 -0
- package/dist/browser/modules/excel/stream/workbook-writer.browser.js +7 -1
- package/dist/browser/modules/excel/worksheet.js +37 -0
- package/dist/browser/modules/excel/xlsx/xform/core/content-types-xform.js +2 -4
- package/dist/browser/modules/excel/xlsx/xform/sheet/worksheet-xform.js +27 -9
- package/dist/browser/modules/excel/xlsx/xlsx.browser.js +12 -7
- package/dist/cjs/modules/excel/anchor.js +3 -0
- package/dist/cjs/modules/excel/image.js +16 -0
- package/dist/cjs/modules/excel/stream/workbook-writer.browser.js +7 -1
- package/dist/cjs/modules/excel/worksheet.js +37 -0
- package/dist/cjs/modules/excel/xlsx/xform/core/content-types-xform.js +2 -4
- package/dist/cjs/modules/excel/xlsx/xform/sheet/worksheet-xform.js +27 -9
- package/dist/cjs/modules/excel/xlsx/xlsx.browser.js +11 -6
- package/dist/esm/modules/excel/anchor.js +3 -0
- package/dist/esm/modules/excel/image.js +16 -0
- package/dist/esm/modules/excel/stream/workbook-writer.browser.js +7 -1
- package/dist/esm/modules/excel/worksheet.js +37 -0
- package/dist/esm/modules/excel/xlsx/xform/core/content-types-xform.js +2 -4
- package/dist/esm/modules/excel/xlsx/xform/sheet/worksheet-xform.js +27 -9
- package/dist/esm/modules/excel/xlsx/xlsx.browser.js +12 -7
- package/dist/iife/excelts.iife.js +69 -17
- package/dist/iife/excelts.iife.js.map +1 -1
- package/dist/iife/excelts.iife.min.js +5 -5
- package/dist/types/modules/excel/anchor.d.ts +1 -0
- package/dist/types/modules/excel/image.d.ts +1 -0
- package/package.json +1 -1
|
@@ -60,5 +60,6 @@ declare class Image {
|
|
|
60
60
|
constructor(worksheet: Worksheet, model?: ModelInput);
|
|
61
61
|
get model(): Model;
|
|
62
62
|
set model({ type, imageId, range, hyperlinks }: ModelInput);
|
|
63
|
+
clone(worksheet?: Worksheet): Image;
|
|
63
64
|
}
|
|
64
65
|
export { Image, type Model as ImageModel, type ImageModelInput };
|
|
@@ -56,5 +56,21 @@ class Image {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
+
clone(worksheet) {
|
|
60
|
+
const target = worksheet ?? this.worksheet;
|
|
61
|
+
const cloned = new Image(target);
|
|
62
|
+
cloned.type = this.type;
|
|
63
|
+
cloned.imageId = this.imageId;
|
|
64
|
+
if (this.range) {
|
|
65
|
+
cloned.range = {
|
|
66
|
+
tl: this.range.tl.clone(target),
|
|
67
|
+
br: this.range.br ? this.range.br.clone(target) : undefined,
|
|
68
|
+
ext: this.range.ext ? { ...this.range.ext } : undefined,
|
|
69
|
+
editAs: this.range.editAs,
|
|
70
|
+
hyperlinks: this.range.hyperlinks ? { ...this.range.hyperlinks } : undefined
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return cloned;
|
|
74
|
+
}
|
|
59
75
|
}
|
|
60
76
|
export { Image };
|
|
@@ -224,8 +224,14 @@ export class WorkbookWriterBase {
|
|
|
224
224
|
}
|
|
225
225
|
addContentTypes() {
|
|
226
226
|
return new Promise(resolve => {
|
|
227
|
+
const worksheets = this._worksheets.filter(Boolean);
|
|
228
|
+
// In the streaming path, ZIP entries use ws.id which is always sequential.
|
|
229
|
+
// Set fileIndex = id to satisfy the ContentTypesXform contract.
|
|
230
|
+
worksheets.forEach((ws) => {
|
|
231
|
+
ws.fileIndex = ws.id;
|
|
232
|
+
});
|
|
227
233
|
const model = {
|
|
228
|
-
worksheets
|
|
234
|
+
worksheets,
|
|
229
235
|
sharedStrings: this.sharedStrings,
|
|
230
236
|
commentRefs: this.commentRefs,
|
|
231
237
|
media: this.media,
|
|
@@ -492,6 +492,17 @@ class Worksheet {
|
|
|
492
492
|
srcMerges.push(merge);
|
|
493
493
|
}
|
|
494
494
|
}
|
|
495
|
+
// Collect images anchored to the source row before splicing
|
|
496
|
+
// (images whose top-left anchor is on the source row)
|
|
497
|
+
const srcImages = [];
|
|
498
|
+
const srcRow0 = rowNum - 1; // 0-based source row
|
|
499
|
+
for (const image of this._media) {
|
|
500
|
+
if (image.type === "image" && image.range) {
|
|
501
|
+
if (image.range.tl.nativeRow === srcRow0) {
|
|
502
|
+
srcImages.push(image);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
495
506
|
this.spliceRows(rowNum + 1, insert ? 0 : count, ...inserts);
|
|
496
507
|
// now copy styles...
|
|
497
508
|
for (let i = 0; i < count; i++) {
|
|
@@ -523,6 +534,32 @@ class Worksheet {
|
|
|
523
534
|
}
|
|
524
535
|
}
|
|
525
536
|
}
|
|
537
|
+
// Duplicate images from source row into each new row.
|
|
538
|
+
// In overwrite mode, first remove any images anchored to the target rows
|
|
539
|
+
// so they don't coexist with the clones (mirrors merge cleanup above).
|
|
540
|
+
if (!insert) {
|
|
541
|
+
const dstStart0 = rowNum; // first target row, 0-based (1-based rowNum + 1 → 0-based rowNum)
|
|
542
|
+
const dstEnd0 = rowNum + count - 1; // last target row, 0-based
|
|
543
|
+
this._media = this._media.filter(image => {
|
|
544
|
+
if (image.type === "image" && image.range) {
|
|
545
|
+
const row0 = image.range.tl.nativeRow;
|
|
546
|
+
return row0 < dstStart0 || row0 > dstEnd0;
|
|
547
|
+
}
|
|
548
|
+
return true;
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
for (let i = 0; i < count; i++) {
|
|
552
|
+
const rowDelta = i + 1; // offset from source row to target row
|
|
553
|
+
for (const srcImage of srcImages) {
|
|
554
|
+
const cloned = srcImage.clone();
|
|
555
|
+
cloned.range.tl.nativeRow = srcRow0 + rowDelta;
|
|
556
|
+
if (cloned.range.br) {
|
|
557
|
+
const brDelta = srcImage.range.br.nativeRow - srcRow0;
|
|
558
|
+
cloned.range.br.nativeRow = srcRow0 + rowDelta + brDelta;
|
|
559
|
+
}
|
|
560
|
+
this._media.push(cloned);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
526
563
|
}
|
|
527
564
|
/**
|
|
528
565
|
* Cut one or more rows (rows below are shifted up)
|
|
@@ -29,11 +29,9 @@ class ContentTypesXform extends BaseXform {
|
|
|
29
29
|
PartName: toContentTypesPartName(OOXML_PATHS.xlWorkbook),
|
|
30
30
|
ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"
|
|
31
31
|
});
|
|
32
|
-
model.worksheets.forEach((worksheet
|
|
33
|
-
// Use fileIndex if set, otherwise use sequential index (1-based)
|
|
34
|
-
const fileIndex = worksheet.fileIndex || index + 1;
|
|
32
|
+
model.worksheets.forEach((worksheet) => {
|
|
35
33
|
xmlStream.leafNode("Override", {
|
|
36
|
-
PartName: toContentTypesPartName(worksheetPath(fileIndex)),
|
|
34
|
+
PartName: toContentTypesPartName(worksheetPath(worksheet.fileIndex)),
|
|
37
35
|
ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"
|
|
38
36
|
});
|
|
39
37
|
});
|
|
@@ -167,37 +167,55 @@ class WorkSheetXform extends BaseXform {
|
|
|
167
167
|
});
|
|
168
168
|
});
|
|
169
169
|
// prepare comment relationships
|
|
170
|
+
// Use fileIndex (sequential 1-based) for file naming instead of model.id
|
|
171
|
+
// (the workbook-level sheet ID) because model.id can have gaps when sheets
|
|
172
|
+
// have been deleted, causing a mismatch between the relationship targets
|
|
173
|
+
// and the actual ZIP entry paths written by addWorksheets().
|
|
174
|
+
const { fileIndex } = model;
|
|
170
175
|
if (model.comments.length > 0) {
|
|
171
176
|
const comment = {
|
|
172
177
|
Id: nextRid(rels),
|
|
173
178
|
Type: RelType.Comments,
|
|
174
|
-
Target: commentsRelTargetFromWorksheet(
|
|
179
|
+
Target: commentsRelTargetFromWorksheet(fileIndex)
|
|
175
180
|
};
|
|
176
181
|
rels.push(comment);
|
|
177
182
|
const vmlDrawing = {
|
|
178
183
|
Id: nextRid(rels),
|
|
179
184
|
Type: RelType.VmlDrawing,
|
|
180
|
-
Target: vmlDrawingRelTargetFromWorksheet(
|
|
185
|
+
Target: vmlDrawingRelTargetFromWorksheet(fileIndex)
|
|
181
186
|
};
|
|
182
187
|
rels.push(vmlDrawing);
|
|
183
188
|
model.comments.forEach(item => {
|
|
184
189
|
item.refAddress = colCache.decodeAddress(item.ref);
|
|
185
190
|
});
|
|
186
191
|
options.commentRefs.push({
|
|
187
|
-
commentName: `comments${
|
|
188
|
-
vmlDrawing: `vmlDrawing${
|
|
192
|
+
commentName: `comments${fileIndex}`,
|
|
193
|
+
vmlDrawing: `vmlDrawing${fileIndex}`
|
|
189
194
|
});
|
|
190
195
|
}
|
|
191
|
-
// Handle pre-loaded drawing (from file read) that may contain charts or other non-image content
|
|
192
|
-
//
|
|
193
|
-
//
|
|
196
|
+
// Handle pre-loaded drawing (from file read) that may contain charts or other non-image content.
|
|
197
|
+
// Reset anchors and rels so they are rebuilt cleanly from model.media (images) and
|
|
198
|
+
// model.formControls (shapes) below. Without this reset, every read-write cycle would
|
|
199
|
+
// duplicate image anchors because the same images exist in both model.drawing.anchors
|
|
200
|
+
// (preserved for round-trip) and model.media (the canonical image list).
|
|
201
|
+
// For chart drawings, rels are preserved because the raw XML passthrough references
|
|
202
|
+
// original rIds; anchors are still cleared since they are unused for chart drawings.
|
|
194
203
|
if (model.drawing && model.drawing.anchors) {
|
|
195
|
-
// This is a loaded drawing that needs to be added to relationships
|
|
196
204
|
const drawing = model.drawing;
|
|
197
205
|
drawing.rId = nextRid(rels);
|
|
198
206
|
if (!drawing.name) {
|
|
199
207
|
drawing.name = `drawing${++options.drawingsCount}`;
|
|
200
208
|
}
|
|
209
|
+
const hasChartRels = (drawing.rels ?? []).some((rel) => rel.Target && rel.Target.includes("/charts/"));
|
|
210
|
+
// Anchors are always reset: for chart drawings they are unused (raw XML passthrough),
|
|
211
|
+
// for normal drawings they are rebuilt from model.media below.
|
|
212
|
+
drawing.anchors = [];
|
|
213
|
+
if (!hasChartRels) {
|
|
214
|
+
// Non-chart drawings: clear rels so image rels are rebuilt from scratch.
|
|
215
|
+
drawing.rels = [];
|
|
216
|
+
}
|
|
217
|
+
// Chart drawings keep their original rels intact since the raw drawing XML
|
|
218
|
+
// references those rIds directly.
|
|
201
219
|
options.drawings.push(drawing);
|
|
202
220
|
rels.push({
|
|
203
221
|
Id: drawing.rId,
|
|
@@ -325,7 +343,7 @@ class WorkSheetXform extends BaseXform {
|
|
|
325
343
|
rels.push({
|
|
326
344
|
Id: nextRid(rels),
|
|
327
345
|
Type: RelType.VmlDrawing,
|
|
328
|
-
Target: vmlDrawingRelTargetFromWorksheet(
|
|
346
|
+
Target: vmlDrawingRelTargetFromWorksheet(fileIndex)
|
|
329
347
|
});
|
|
330
348
|
}
|
|
331
349
|
// Add hidden DrawingML shapes that bridge to the VML shape ids.
|
|
@@ -33,7 +33,7 @@ import { bufferToString, base64ToUint8Array } from "../../../utils/utils.browser
|
|
|
33
33
|
import { StreamingZip, ZipDeflateFile } from "../../archive/zip/stream.js";
|
|
34
34
|
import { ZipParser } from "../../archive/unzip/zip-parser.js";
|
|
35
35
|
import { PassThrough, concatUint8Arrays } from "../../stream/index.browser.js";
|
|
36
|
-
import { commentsPath, commentsRelTargetFromWorksheetName, ctrlPropPath, drawingPath, drawingRelsPath, OOXML_REL_TARGETS, pivotTableRelTargetFromWorksheetName, pivotCacheDefinitionRelTargetFromWorkbook, getCommentsIndexFromPath, getDrawingNameFromPath, getDrawingNameFromRelsPath, getMediaFilenameFromPath, mediaPath, getPivotCacheDefinitionNameFromPath, getPivotCacheDefinitionNameFromRelsPath, getPivotCacheRecordsNameFromPath, getPivotTableNameFromPath, getPivotTableNameFromRelsPath, pivotCacheDefinitionPath, pivotCacheDefinitionRelsPath, pivotCacheDefinitionRelTargetFromPivotTable, pivotCacheRecordsPath, pivotCacheRecordsRelTarget, pivotTablePath, pivotTableRelsPath, getTableNameFromPath, tablePath, tableRelTargetFromWorksheetName, themePath, getThemeNameFromPath, getVmlDrawingNameFromPath, getWorksheetNoFromWorksheetPath, getWorksheetNoFromWorksheetRelsPath, isBinaryEntryPath, normalizeZipPath, OOXML_PATHS, vmlDrawingRelTargetFromWorksheetName, vmlDrawingPath, worksheetPath, worksheetRelsPath } from "../utils/ooxml-paths.js";
|
|
36
|
+
import { commentsPath, commentsRelTargetFromWorksheetName, ctrlPropPath, drawingPath, drawingRelsPath, OOXML_REL_TARGETS, pivotTableRelTargetFromWorksheetName, pivotCacheDefinitionRelTargetFromWorkbook, getCommentsIndexFromPath, getDrawingNameFromPath, getDrawingNameFromRelsPath, getMediaFilenameFromPath, mediaPath, getPivotCacheDefinitionNameFromPath, getPivotCacheDefinitionNameFromRelsPath, getPivotCacheRecordsNameFromPath, getPivotTableNameFromPath, getPivotTableNameFromRelsPath, pivotCacheDefinitionPath, pivotCacheDefinitionRelsPath, pivotCacheDefinitionRelTargetFromPivotTable, pivotCacheRecordsPath, pivotCacheRecordsRelTarget, pivotTablePath, pivotTableRelsPath, getTableNameFromPath, tablePath, tableRelTargetFromWorksheetName, themePath, getThemeNameFromPath, getVmlDrawingNameFromPath, getWorksheetNoFromWorksheetPath, getWorksheetNoFromWorksheetRelsPath, isBinaryEntryPath, normalizeZipPath, OOXML_PATHS, vmlDrawingRelTargetFromWorksheetName, vmlDrawingPath, worksheetPath, worksheetRelsPath, worksheetRelTarget } from "../utils/ooxml-paths.js";
|
|
37
37
|
import { PassthroughManager } from "../utils/passthrough-manager.js";
|
|
38
38
|
class StreamingZipWriterAdapter {
|
|
39
39
|
constructor(options) {
|
|
@@ -1050,13 +1050,13 @@ class XLSX {
|
|
|
1050
1050
|
});
|
|
1051
1051
|
}
|
|
1052
1052
|
});
|
|
1053
|
-
model.worksheets.forEach((worksheet
|
|
1053
|
+
model.worksheets.forEach((worksheet) => {
|
|
1054
1054
|
worksheet.rId = `rId${count++}`;
|
|
1055
|
-
|
|
1055
|
+
// fileIndex is assigned once in prepareModel() — use it directly
|
|
1056
1056
|
relationships.push({
|
|
1057
1057
|
Id: worksheet.rId,
|
|
1058
1058
|
Type: XLSX.RelType.Worksheet,
|
|
1059
|
-
Target:
|
|
1059
|
+
Target: worksheetRelTarget(worksheet.fileIndex)
|
|
1060
1060
|
});
|
|
1061
1061
|
});
|
|
1062
1062
|
const xform = new RelationshipsXform();
|
|
@@ -1091,8 +1091,8 @@ class XLSX {
|
|
|
1091
1091
|
const commentsXform = new CommentsXform();
|
|
1092
1092
|
const vmlDrawingXform = new VmlDrawingXform();
|
|
1093
1093
|
const ctrlPropXform = new CtrlPropXform();
|
|
1094
|
-
model.worksheets.forEach((worksheet
|
|
1095
|
-
const fileIndex = worksheet
|
|
1094
|
+
model.worksheets.forEach((worksheet) => {
|
|
1095
|
+
const { fileIndex } = worksheet;
|
|
1096
1096
|
let xmlStream = new XmlStream();
|
|
1097
1097
|
worksheetXform.render(xmlStream, worksheet);
|
|
1098
1098
|
zip.append(xmlStream.xml, { name: worksheetPath(fileIndex) });
|
|
@@ -1299,7 +1299,12 @@ class XLSX {
|
|
|
1299
1299
|
worksheetOptions.formControlRefs = model.formControlRefs = [];
|
|
1300
1300
|
let tableCount = 0;
|
|
1301
1301
|
model.tables = [];
|
|
1302
|
-
model.worksheets.forEach((worksheet) => {
|
|
1302
|
+
model.worksheets.forEach((worksheet, index) => {
|
|
1303
|
+
// Assign fileIndex early so that worksheet-xform.prepare() can use it
|
|
1304
|
+
// for comment/VML relationship targets and content type names.
|
|
1305
|
+
// This ensures consistency with addWorksheets() which writes ZIP entries
|
|
1306
|
+
// using the same fileIndex.
|
|
1307
|
+
worksheet.fileIndex = index + 1;
|
|
1303
1308
|
worksheet.tables.forEach((table) => {
|
|
1304
1309
|
tableCount++;
|
|
1305
1310
|
table.target = `table${tableCount}.xml`;
|
|
@@ -59,5 +59,21 @@ class Image {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
+
clone(worksheet) {
|
|
63
|
+
const target = worksheet ?? this.worksheet;
|
|
64
|
+
const cloned = new Image(target);
|
|
65
|
+
cloned.type = this.type;
|
|
66
|
+
cloned.imageId = this.imageId;
|
|
67
|
+
if (this.range) {
|
|
68
|
+
cloned.range = {
|
|
69
|
+
tl: this.range.tl.clone(target),
|
|
70
|
+
br: this.range.br ? this.range.br.clone(target) : undefined,
|
|
71
|
+
ext: this.range.ext ? { ...this.range.ext } : undefined,
|
|
72
|
+
editAs: this.range.editAs,
|
|
73
|
+
hyperlinks: this.range.hyperlinks ? { ...this.range.hyperlinks } : undefined
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return cloned;
|
|
77
|
+
}
|
|
62
78
|
}
|
|
63
79
|
exports.Image = Image;
|
|
@@ -227,8 +227,14 @@ class WorkbookWriterBase {
|
|
|
227
227
|
}
|
|
228
228
|
addContentTypes() {
|
|
229
229
|
return new Promise(resolve => {
|
|
230
|
+
const worksheets = this._worksheets.filter(Boolean);
|
|
231
|
+
// In the streaming path, ZIP entries use ws.id which is always sequential.
|
|
232
|
+
// Set fileIndex = id to satisfy the ContentTypesXform contract.
|
|
233
|
+
worksheets.forEach((ws) => {
|
|
234
|
+
ws.fileIndex = ws.id;
|
|
235
|
+
});
|
|
230
236
|
const model = {
|
|
231
|
-
worksheets
|
|
237
|
+
worksheets,
|
|
232
238
|
sharedStrings: this.sharedStrings,
|
|
233
239
|
commentRefs: this.commentRefs,
|
|
234
240
|
media: this.media,
|
|
@@ -495,6 +495,17 @@ class Worksheet {
|
|
|
495
495
|
srcMerges.push(merge);
|
|
496
496
|
}
|
|
497
497
|
}
|
|
498
|
+
// Collect images anchored to the source row before splicing
|
|
499
|
+
// (images whose top-left anchor is on the source row)
|
|
500
|
+
const srcImages = [];
|
|
501
|
+
const srcRow0 = rowNum - 1; // 0-based source row
|
|
502
|
+
for (const image of this._media) {
|
|
503
|
+
if (image.type === "image" && image.range) {
|
|
504
|
+
if (image.range.tl.nativeRow === srcRow0) {
|
|
505
|
+
srcImages.push(image);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
498
509
|
this.spliceRows(rowNum + 1, insert ? 0 : count, ...inserts);
|
|
499
510
|
// now copy styles...
|
|
500
511
|
for (let i = 0; i < count; i++) {
|
|
@@ -526,6 +537,32 @@ class Worksheet {
|
|
|
526
537
|
}
|
|
527
538
|
}
|
|
528
539
|
}
|
|
540
|
+
// Duplicate images from source row into each new row.
|
|
541
|
+
// In overwrite mode, first remove any images anchored to the target rows
|
|
542
|
+
// so they don't coexist with the clones (mirrors merge cleanup above).
|
|
543
|
+
if (!insert) {
|
|
544
|
+
const dstStart0 = rowNum; // first target row, 0-based (1-based rowNum + 1 → 0-based rowNum)
|
|
545
|
+
const dstEnd0 = rowNum + count - 1; // last target row, 0-based
|
|
546
|
+
this._media = this._media.filter(image => {
|
|
547
|
+
if (image.type === "image" && image.range) {
|
|
548
|
+
const row0 = image.range.tl.nativeRow;
|
|
549
|
+
return row0 < dstStart0 || row0 > dstEnd0;
|
|
550
|
+
}
|
|
551
|
+
return true;
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
for (let i = 0; i < count; i++) {
|
|
555
|
+
const rowDelta = i + 1; // offset from source row to target row
|
|
556
|
+
for (const srcImage of srcImages) {
|
|
557
|
+
const cloned = srcImage.clone();
|
|
558
|
+
cloned.range.tl.nativeRow = srcRow0 + rowDelta;
|
|
559
|
+
if (cloned.range.br) {
|
|
560
|
+
const brDelta = srcImage.range.br.nativeRow - srcRow0;
|
|
561
|
+
cloned.range.br.nativeRow = srcRow0 + rowDelta + brDelta;
|
|
562
|
+
}
|
|
563
|
+
this._media.push(cloned);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
529
566
|
}
|
|
530
567
|
/**
|
|
531
568
|
* Cut one or more rows (rows below are shifted up)
|
|
@@ -32,11 +32,9 @@ class ContentTypesXform extends base_xform_1.BaseXform {
|
|
|
32
32
|
PartName: (0, ooxml_paths_1.toContentTypesPartName)(ooxml_paths_1.OOXML_PATHS.xlWorkbook),
|
|
33
33
|
ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"
|
|
34
34
|
});
|
|
35
|
-
model.worksheets.forEach((worksheet
|
|
36
|
-
// Use fileIndex if set, otherwise use sequential index (1-based)
|
|
37
|
-
const fileIndex = worksheet.fileIndex || index + 1;
|
|
35
|
+
model.worksheets.forEach((worksheet) => {
|
|
38
36
|
xmlStream.leafNode("Override", {
|
|
39
|
-
PartName: (0, ooxml_paths_1.toContentTypesPartName)((0, ooxml_paths_1.worksheetPath)(fileIndex)),
|
|
37
|
+
PartName: (0, ooxml_paths_1.toContentTypesPartName)((0, ooxml_paths_1.worksheetPath)(worksheet.fileIndex)),
|
|
40
38
|
ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"
|
|
41
39
|
});
|
|
42
40
|
});
|
|
@@ -170,37 +170,55 @@ class WorkSheetXform extends base_xform_1.BaseXform {
|
|
|
170
170
|
});
|
|
171
171
|
});
|
|
172
172
|
// prepare comment relationships
|
|
173
|
+
// Use fileIndex (sequential 1-based) for file naming instead of model.id
|
|
174
|
+
// (the workbook-level sheet ID) because model.id can have gaps when sheets
|
|
175
|
+
// have been deleted, causing a mismatch between the relationship targets
|
|
176
|
+
// and the actual ZIP entry paths written by addWorksheets().
|
|
177
|
+
const { fileIndex } = model;
|
|
173
178
|
if (model.comments.length > 0) {
|
|
174
179
|
const comment = {
|
|
175
180
|
Id: nextRid(rels),
|
|
176
181
|
Type: rel_type_1.RelType.Comments,
|
|
177
|
-
Target: (0, ooxml_paths_1.commentsRelTargetFromWorksheet)(
|
|
182
|
+
Target: (0, ooxml_paths_1.commentsRelTargetFromWorksheet)(fileIndex)
|
|
178
183
|
};
|
|
179
184
|
rels.push(comment);
|
|
180
185
|
const vmlDrawing = {
|
|
181
186
|
Id: nextRid(rels),
|
|
182
187
|
Type: rel_type_1.RelType.VmlDrawing,
|
|
183
|
-
Target: (0, ooxml_paths_1.vmlDrawingRelTargetFromWorksheet)(
|
|
188
|
+
Target: (0, ooxml_paths_1.vmlDrawingRelTargetFromWorksheet)(fileIndex)
|
|
184
189
|
};
|
|
185
190
|
rels.push(vmlDrawing);
|
|
186
191
|
model.comments.forEach(item => {
|
|
187
192
|
item.refAddress = col_cache_1.colCache.decodeAddress(item.ref);
|
|
188
193
|
});
|
|
189
194
|
options.commentRefs.push({
|
|
190
|
-
commentName: `comments${
|
|
191
|
-
vmlDrawing: `vmlDrawing${
|
|
195
|
+
commentName: `comments${fileIndex}`,
|
|
196
|
+
vmlDrawing: `vmlDrawing${fileIndex}`
|
|
192
197
|
});
|
|
193
198
|
}
|
|
194
|
-
// Handle pre-loaded drawing (from file read) that may contain charts or other non-image content
|
|
195
|
-
//
|
|
196
|
-
//
|
|
199
|
+
// Handle pre-loaded drawing (from file read) that may contain charts or other non-image content.
|
|
200
|
+
// Reset anchors and rels so they are rebuilt cleanly from model.media (images) and
|
|
201
|
+
// model.formControls (shapes) below. Without this reset, every read-write cycle would
|
|
202
|
+
// duplicate image anchors because the same images exist in both model.drawing.anchors
|
|
203
|
+
// (preserved for round-trip) and model.media (the canonical image list).
|
|
204
|
+
// For chart drawings, rels are preserved because the raw XML passthrough references
|
|
205
|
+
// original rIds; anchors are still cleared since they are unused for chart drawings.
|
|
197
206
|
if (model.drawing && model.drawing.anchors) {
|
|
198
|
-
// This is a loaded drawing that needs to be added to relationships
|
|
199
207
|
const drawing = model.drawing;
|
|
200
208
|
drawing.rId = nextRid(rels);
|
|
201
209
|
if (!drawing.name) {
|
|
202
210
|
drawing.name = `drawing${++options.drawingsCount}`;
|
|
203
211
|
}
|
|
212
|
+
const hasChartRels = (drawing.rels ?? []).some((rel) => rel.Target && rel.Target.includes("/charts/"));
|
|
213
|
+
// Anchors are always reset: for chart drawings they are unused (raw XML passthrough),
|
|
214
|
+
// for normal drawings they are rebuilt from model.media below.
|
|
215
|
+
drawing.anchors = [];
|
|
216
|
+
if (!hasChartRels) {
|
|
217
|
+
// Non-chart drawings: clear rels so image rels are rebuilt from scratch.
|
|
218
|
+
drawing.rels = [];
|
|
219
|
+
}
|
|
220
|
+
// Chart drawings keep their original rels intact since the raw drawing XML
|
|
221
|
+
// references those rIds directly.
|
|
204
222
|
options.drawings.push(drawing);
|
|
205
223
|
rels.push({
|
|
206
224
|
Id: drawing.rId,
|
|
@@ -328,7 +346,7 @@ class WorkSheetXform extends base_xform_1.BaseXform {
|
|
|
328
346
|
rels.push({
|
|
329
347
|
Id: nextRid(rels),
|
|
330
348
|
Type: rel_type_1.RelType.VmlDrawing,
|
|
331
|
-
Target: (0, ooxml_paths_1.vmlDrawingRelTargetFromWorksheet)(
|
|
349
|
+
Target: (0, ooxml_paths_1.vmlDrawingRelTargetFromWorksheet)(fileIndex)
|
|
332
350
|
});
|
|
333
351
|
}
|
|
334
352
|
// Add hidden DrawingML shapes that bridge to the VML shape ids.
|
|
@@ -1053,13 +1053,13 @@ class XLSX {
|
|
|
1053
1053
|
});
|
|
1054
1054
|
}
|
|
1055
1055
|
});
|
|
1056
|
-
model.worksheets.forEach((worksheet
|
|
1056
|
+
model.worksheets.forEach((worksheet) => {
|
|
1057
1057
|
worksheet.rId = `rId${count++}`;
|
|
1058
|
-
|
|
1058
|
+
// fileIndex is assigned once in prepareModel() — use it directly
|
|
1059
1059
|
relationships.push({
|
|
1060
1060
|
Id: worksheet.rId,
|
|
1061
1061
|
Type: XLSX.RelType.Worksheet,
|
|
1062
|
-
Target:
|
|
1062
|
+
Target: (0, ooxml_paths_1.worksheetRelTarget)(worksheet.fileIndex)
|
|
1063
1063
|
});
|
|
1064
1064
|
});
|
|
1065
1065
|
const xform = new relationships_xform_1.RelationshipsXform();
|
|
@@ -1094,8 +1094,8 @@ class XLSX {
|
|
|
1094
1094
|
const commentsXform = new comments_xform_1.CommentsXform();
|
|
1095
1095
|
const vmlDrawingXform = new vml_drawing_xform_1.VmlDrawingXform();
|
|
1096
1096
|
const ctrlPropXform = new ctrl_prop_xform_1.CtrlPropXform();
|
|
1097
|
-
model.worksheets.forEach((worksheet
|
|
1098
|
-
const fileIndex = worksheet
|
|
1097
|
+
model.worksheets.forEach((worksheet) => {
|
|
1098
|
+
const { fileIndex } = worksheet;
|
|
1099
1099
|
let xmlStream = new xml_stream_1.XmlStream();
|
|
1100
1100
|
worksheetXform.render(xmlStream, worksheet);
|
|
1101
1101
|
zip.append(xmlStream.xml, { name: (0, ooxml_paths_1.worksheetPath)(fileIndex) });
|
|
@@ -1302,7 +1302,12 @@ class XLSX {
|
|
|
1302
1302
|
worksheetOptions.formControlRefs = model.formControlRefs = [];
|
|
1303
1303
|
let tableCount = 0;
|
|
1304
1304
|
model.tables = [];
|
|
1305
|
-
model.worksheets.forEach((worksheet) => {
|
|
1305
|
+
model.worksheets.forEach((worksheet, index) => {
|
|
1306
|
+
// Assign fileIndex early so that worksheet-xform.prepare() can use it
|
|
1307
|
+
// for comment/VML relationship targets and content type names.
|
|
1308
|
+
// This ensures consistency with addWorksheets() which writes ZIP entries
|
|
1309
|
+
// using the same fileIndex.
|
|
1310
|
+
worksheet.fileIndex = index + 1;
|
|
1306
1311
|
worksheet.tables.forEach((table) => {
|
|
1307
1312
|
tableCount++;
|
|
1308
1313
|
table.target = `table${tableCount}.xml`;
|
|
@@ -56,5 +56,21 @@ class Image {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
+
clone(worksheet) {
|
|
60
|
+
const target = worksheet ?? this.worksheet;
|
|
61
|
+
const cloned = new Image(target);
|
|
62
|
+
cloned.type = this.type;
|
|
63
|
+
cloned.imageId = this.imageId;
|
|
64
|
+
if (this.range) {
|
|
65
|
+
cloned.range = {
|
|
66
|
+
tl: this.range.tl.clone(target),
|
|
67
|
+
br: this.range.br ? this.range.br.clone(target) : undefined,
|
|
68
|
+
ext: this.range.ext ? { ...this.range.ext } : undefined,
|
|
69
|
+
editAs: this.range.editAs,
|
|
70
|
+
hyperlinks: this.range.hyperlinks ? { ...this.range.hyperlinks } : undefined
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return cloned;
|
|
74
|
+
}
|
|
59
75
|
}
|
|
60
76
|
export { Image };
|
|
@@ -224,8 +224,14 @@ export class WorkbookWriterBase {
|
|
|
224
224
|
}
|
|
225
225
|
addContentTypes() {
|
|
226
226
|
return new Promise(resolve => {
|
|
227
|
+
const worksheets = this._worksheets.filter(Boolean);
|
|
228
|
+
// In the streaming path, ZIP entries use ws.id which is always sequential.
|
|
229
|
+
// Set fileIndex = id to satisfy the ContentTypesXform contract.
|
|
230
|
+
worksheets.forEach((ws) => {
|
|
231
|
+
ws.fileIndex = ws.id;
|
|
232
|
+
});
|
|
227
233
|
const model = {
|
|
228
|
-
worksheets
|
|
234
|
+
worksheets,
|
|
229
235
|
sharedStrings: this.sharedStrings,
|
|
230
236
|
commentRefs: this.commentRefs,
|
|
231
237
|
media: this.media,
|
|
@@ -492,6 +492,17 @@ class Worksheet {
|
|
|
492
492
|
srcMerges.push(merge);
|
|
493
493
|
}
|
|
494
494
|
}
|
|
495
|
+
// Collect images anchored to the source row before splicing
|
|
496
|
+
// (images whose top-left anchor is on the source row)
|
|
497
|
+
const srcImages = [];
|
|
498
|
+
const srcRow0 = rowNum - 1; // 0-based source row
|
|
499
|
+
for (const image of this._media) {
|
|
500
|
+
if (image.type === "image" && image.range) {
|
|
501
|
+
if (image.range.tl.nativeRow === srcRow0) {
|
|
502
|
+
srcImages.push(image);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
495
506
|
this.spliceRows(rowNum + 1, insert ? 0 : count, ...inserts);
|
|
496
507
|
// now copy styles...
|
|
497
508
|
for (let i = 0; i < count; i++) {
|
|
@@ -523,6 +534,32 @@ class Worksheet {
|
|
|
523
534
|
}
|
|
524
535
|
}
|
|
525
536
|
}
|
|
537
|
+
// Duplicate images from source row into each new row.
|
|
538
|
+
// In overwrite mode, first remove any images anchored to the target rows
|
|
539
|
+
// so they don't coexist with the clones (mirrors merge cleanup above).
|
|
540
|
+
if (!insert) {
|
|
541
|
+
const dstStart0 = rowNum; // first target row, 0-based (1-based rowNum + 1 → 0-based rowNum)
|
|
542
|
+
const dstEnd0 = rowNum + count - 1; // last target row, 0-based
|
|
543
|
+
this._media = this._media.filter(image => {
|
|
544
|
+
if (image.type === "image" && image.range) {
|
|
545
|
+
const row0 = image.range.tl.nativeRow;
|
|
546
|
+
return row0 < dstStart0 || row0 > dstEnd0;
|
|
547
|
+
}
|
|
548
|
+
return true;
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
for (let i = 0; i < count; i++) {
|
|
552
|
+
const rowDelta = i + 1; // offset from source row to target row
|
|
553
|
+
for (const srcImage of srcImages) {
|
|
554
|
+
const cloned = srcImage.clone();
|
|
555
|
+
cloned.range.tl.nativeRow = srcRow0 + rowDelta;
|
|
556
|
+
if (cloned.range.br) {
|
|
557
|
+
const brDelta = srcImage.range.br.nativeRow - srcRow0;
|
|
558
|
+
cloned.range.br.nativeRow = srcRow0 + rowDelta + brDelta;
|
|
559
|
+
}
|
|
560
|
+
this._media.push(cloned);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
526
563
|
}
|
|
527
564
|
/**
|
|
528
565
|
* Cut one or more rows (rows below are shifted up)
|
|
@@ -29,11 +29,9 @@ class ContentTypesXform extends BaseXform {
|
|
|
29
29
|
PartName: toContentTypesPartName(OOXML_PATHS.xlWorkbook),
|
|
30
30
|
ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"
|
|
31
31
|
});
|
|
32
|
-
model.worksheets.forEach((worksheet
|
|
33
|
-
// Use fileIndex if set, otherwise use sequential index (1-based)
|
|
34
|
-
const fileIndex = worksheet.fileIndex || index + 1;
|
|
32
|
+
model.worksheets.forEach((worksheet) => {
|
|
35
33
|
xmlStream.leafNode("Override", {
|
|
36
|
-
PartName: toContentTypesPartName(worksheetPath(fileIndex)),
|
|
34
|
+
PartName: toContentTypesPartName(worksheetPath(worksheet.fileIndex)),
|
|
37
35
|
ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"
|
|
38
36
|
});
|
|
39
37
|
});
|