docgen-utils 1.0.11 → 1.0.12
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/bundle.js +40084 -5825
- package/dist/bundle.min.js +288 -110
- package/dist/cli.js +23758 -110
- package/dist/packages/docs/import-docx.d.ts.map +1 -1
- package/dist/packages/docs/import-docx.js +397 -7
- package/dist/packages/docs/import-docx.js.map +1 -1
- package/dist/packages/slides/fonts.d.ts +41 -0
- package/dist/packages/slides/fonts.d.ts.map +1 -0
- package/dist/packages/slides/fonts.js +209 -0
- package/dist/packages/slides/fonts.js.map +1 -0
- package/dist/packages/slides/import-pptx.d.ts.map +1 -1
- package/dist/packages/slides/import-pptx.js +583 -120
- package/dist/packages/slides/import-pptx.js.map +1 -1
- package/dist/packages/slides/parse.d.ts.map +1 -1
- package/dist/packages/slides/parse.js +1 -51
- package/dist/packages/slides/parse.js.map +1 -1
- package/dist/packages/slides/transform.d.ts +6 -6
- package/dist/packages/slides/transform.d.ts.map +1 -1
- package/dist/packages/slides/transform.js +13 -44
- package/dist/packages/slides/transform.js.map +1 -1
- package/package.json +3 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"import-docx.d.ts","sourceRoot":"","sources":["../../../packages/docs/import-docx.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"import-docx.d.ts","sourceRoot":"","sources":["../../../packages/docs/import-docx.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA43FH;;;;GAIG;AACH,wBAA8B,UAAU,CACtC,WAAW,EAAE,WAAW,GACvB,OAAO,CAAC,MAAM,CAAC,CA+MjB"}
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* Usage: const html = await importDocx(arrayBuffer);
|
|
6
6
|
*/
|
|
7
7
|
import JSZip from "jszip";
|
|
8
|
+
import { EMFJS } from "rtf.js";
|
|
9
|
+
import { parseHTML } from "linkedom";
|
|
8
10
|
// ============================================================================
|
|
9
11
|
// Constants
|
|
10
12
|
// ============================================================================
|
|
@@ -133,6 +135,332 @@ function getBorderStyleCss(style) {
|
|
|
133
135
|
return styleMap[style] ?? "solid";
|
|
134
136
|
}
|
|
135
137
|
// ============================================================================
|
|
138
|
+
// EMF to SVG Conversion
|
|
139
|
+
// ============================================================================
|
|
140
|
+
// Setup DOM environment for EMFJS (required for SVG creation)
|
|
141
|
+
let emfDomSetup = false;
|
|
142
|
+
function setupEmfDom() {
|
|
143
|
+
if (emfDomSetup)
|
|
144
|
+
return;
|
|
145
|
+
// Create a minimal DOM environment using linkedom
|
|
146
|
+
const { document, window } = parseHTML('<!DOCTYPE html><html><body></body></html>');
|
|
147
|
+
// EMFJS requires document.createElementNS
|
|
148
|
+
if (typeof globalThis.document === 'undefined') {
|
|
149
|
+
globalThis.document = document;
|
|
150
|
+
}
|
|
151
|
+
if (typeof globalThis.window === 'undefined') {
|
|
152
|
+
globalThis.window = window;
|
|
153
|
+
}
|
|
154
|
+
emfDomSetup = true;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Read EMF header to get bounds and frame dimensions
|
|
158
|
+
*/
|
|
159
|
+
function readEmfHeader(buffer) {
|
|
160
|
+
const view = new DataView(buffer);
|
|
161
|
+
// Check if this is a valid EMF (record type should be 0x00000001)
|
|
162
|
+
const recordType = view.getUint32(0, true);
|
|
163
|
+
if (recordType !== 0x00000001) {
|
|
164
|
+
throw new Error('Not a valid EMF file');
|
|
165
|
+
}
|
|
166
|
+
// Bounds rectangle at offset 8-23: left, top, right, bottom (in device units)
|
|
167
|
+
const boundsRight = view.getInt32(16, true);
|
|
168
|
+
const boundsBottom = view.getInt32(20, true);
|
|
169
|
+
// Frame rectangle at offset 24-39: left, top, right, bottom (in 0.01mm units)
|
|
170
|
+
const frameRight = view.getInt32(32, true);
|
|
171
|
+
const frameBottom = view.getInt32(36, true);
|
|
172
|
+
return {
|
|
173
|
+
boundsWidth: Math.max(boundsRight, 1),
|
|
174
|
+
boundsHeight: Math.max(boundsBottom, 1),
|
|
175
|
+
frameWidth: frameRight,
|
|
176
|
+
frameHeight: frameBottom
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
// For backward compatibility
|
|
180
|
+
function readEmfBounds(buffer) {
|
|
181
|
+
const header = readEmfHeader(buffer);
|
|
182
|
+
return { width: header.boundsWidth, height: header.boundsHeight };
|
|
183
|
+
}
|
|
184
|
+
function extractEmfText(buffer) {
|
|
185
|
+
const view = new DataView(buffer);
|
|
186
|
+
const EMR_EXTTEXTOUTW = 0x54;
|
|
187
|
+
const EMR_SETTEXTCOLOR = 0x18;
|
|
188
|
+
const EMR_EXTCREATEFONTINDIRECTW = 0x52;
|
|
189
|
+
const EMR_SELECTOBJECT = 0x25;
|
|
190
|
+
const EMR_DELETEOBJECT = 0x28;
|
|
191
|
+
const EMR_SETTEXTALIGN = 0x16;
|
|
192
|
+
const textRecords = [];
|
|
193
|
+
let currentTextColor = '#000000';
|
|
194
|
+
let currentFontRotation = 0; // in degrees
|
|
195
|
+
let currentFontHeight = 40; // default font height in logical units
|
|
196
|
+
let currentTextAlign = 0; // EMF text alignment flags
|
|
197
|
+
// Font objects map: handle -> { rotation, height }
|
|
198
|
+
const fontObjects = new Map();
|
|
199
|
+
let pos = 0;
|
|
200
|
+
while (pos < buffer.byteLength - 8) {
|
|
201
|
+
const recordType = view.getUint32(pos, true);
|
|
202
|
+
const recordSize = view.getUint32(pos + 4, true);
|
|
203
|
+
if (recordSize < 8 || pos + recordSize > buffer.byteLength)
|
|
204
|
+
break;
|
|
205
|
+
if (recordType === EMR_SETTEXTCOLOR) {
|
|
206
|
+
// Color is at offset 8 (after type and size)
|
|
207
|
+
const r = view.getUint8(pos + 8);
|
|
208
|
+
const g = view.getUint8(pos + 9);
|
|
209
|
+
const b = view.getUint8(pos + 10);
|
|
210
|
+
currentTextColor = '#' + r.toString(16).padStart(2, '0') +
|
|
211
|
+
g.toString(16).padStart(2, '0') +
|
|
212
|
+
b.toString(16).padStart(2, '0');
|
|
213
|
+
}
|
|
214
|
+
// EMR_SETTEXTALIGN - sets text alignment mode
|
|
215
|
+
if (recordType === EMR_SETTEXTALIGN) {
|
|
216
|
+
currentTextAlign = view.getUint32(pos + 8, true);
|
|
217
|
+
}
|
|
218
|
+
// EMR_EXTCREATEFONTINDIRECTW - creates a font with rotation info and height
|
|
219
|
+
if (recordType === EMR_EXTCREATEFONTINDIRECTW) {
|
|
220
|
+
// EMR_EXTCREATEFONTINDIRECTW structure:
|
|
221
|
+
// 0-3: record type
|
|
222
|
+
// 4-7: record size
|
|
223
|
+
// 8-11: ihFonts (handle/index of font object)
|
|
224
|
+
// 12-15: elfw.elfLogFont.lfHeight
|
|
225
|
+
// 16-19: elfw.elfLogFont.lfWidth
|
|
226
|
+
// 20-23: elfw.elfLogFont.lfEscapement (rotation in tenths of a degree)
|
|
227
|
+
// 24-27: elfw.elfLogFont.lfOrientation (also in tenths of a degree)
|
|
228
|
+
const fontHandle = view.getUint32(pos + 8, true);
|
|
229
|
+
const lfHeight = view.getInt32(pos + 12, true);
|
|
230
|
+
const lfEscapement = view.getInt32(pos + 20, true); // Escapement is text rotation
|
|
231
|
+
// Convert from tenths of degrees to degrees, negate for SVG (SVG rotates clockwise, EMF counter-clockwise)
|
|
232
|
+
const rotationDegrees = -lfEscapement / 10;
|
|
233
|
+
// Font height is negative in EMF (cell height), use absolute value
|
|
234
|
+
fontObjects.set(fontHandle, { rotation: rotationDegrees, height: Math.abs(lfHeight) });
|
|
235
|
+
}
|
|
236
|
+
// EMR_SELECTOBJECT - selects a font (and its rotation/height)
|
|
237
|
+
if (recordType === EMR_SELECTOBJECT) {
|
|
238
|
+
const objectHandle = view.getUint32(pos + 8, true);
|
|
239
|
+
// Check if it's a font object we've created
|
|
240
|
+
if (fontObjects.has(objectHandle)) {
|
|
241
|
+
const fontInfo = fontObjects.get(objectHandle);
|
|
242
|
+
currentFontRotation = fontInfo.rotation;
|
|
243
|
+
currentFontHeight = fontInfo.height;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// EMR_DELETEOBJECT - remove font object
|
|
247
|
+
if (recordType === EMR_DELETEOBJECT) {
|
|
248
|
+
const objectHandle = view.getUint32(pos + 8, true);
|
|
249
|
+
fontObjects.delete(objectHandle);
|
|
250
|
+
}
|
|
251
|
+
if (recordType === EMR_EXTTEXTOUTW) {
|
|
252
|
+
// EMR_EXTTEXTOUTW structure:
|
|
253
|
+
// 0-3: record type
|
|
254
|
+
// 4-7: record size
|
|
255
|
+
// 8-23: bounds (16 bytes)
|
|
256
|
+
// 24-27: iGraphicsMode
|
|
257
|
+
// 28-31: exScale
|
|
258
|
+
// 32-35: eyScale
|
|
259
|
+
// 36-39: emrtext.ptlReference.x
|
|
260
|
+
// 40-43: emrtext.ptlReference.y
|
|
261
|
+
// 44-47: emrtext.nChars
|
|
262
|
+
// 48-51: emrtext.offString (offset to string from start of record)
|
|
263
|
+
const x = view.getInt32(pos + 36, true);
|
|
264
|
+
const y = view.getInt32(pos + 40, true);
|
|
265
|
+
const nChars = view.getUint32(pos + 44, true);
|
|
266
|
+
const offString = view.getUint32(pos + 48, true);
|
|
267
|
+
if (nChars > 0 && offString > 0 && pos + offString + nChars * 2 <= buffer.byteLength) {
|
|
268
|
+
// Read UTF-16LE string
|
|
269
|
+
let text = '';
|
|
270
|
+
for (let i = 0; i < nChars; i++) {
|
|
271
|
+
const charCode = view.getUint16(pos + offString + i * 2, true);
|
|
272
|
+
if (charCode === 0)
|
|
273
|
+
break;
|
|
274
|
+
text += String.fromCharCode(charCode);
|
|
275
|
+
}
|
|
276
|
+
if (text.trim()) {
|
|
277
|
+
// Determine SVG text-anchor from EMF text align flags
|
|
278
|
+
// TA_LEFT = 0, TA_RIGHT = 2, TA_CENTER = 6 (bits 1-2)
|
|
279
|
+
const hAlignBits = currentTextAlign & 6;
|
|
280
|
+
let textAnchor = 'start';
|
|
281
|
+
if (hAlignBits === 2) {
|
|
282
|
+
textAnchor = 'end'; // TA_RIGHT
|
|
283
|
+
}
|
|
284
|
+
else if (hAlignBits === 6) {
|
|
285
|
+
textAnchor = 'middle'; // TA_CENTER
|
|
286
|
+
}
|
|
287
|
+
textRecords.push({
|
|
288
|
+
x,
|
|
289
|
+
y,
|
|
290
|
+
text: text.trim(),
|
|
291
|
+
color: currentTextColor,
|
|
292
|
+
rotation: currentFontRotation,
|
|
293
|
+
textAnchor,
|
|
294
|
+
fontHeight: currentFontHeight
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
pos += recordSize;
|
|
300
|
+
}
|
|
301
|
+
return textRecords;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Escape text for SVG
|
|
305
|
+
*/
|
|
306
|
+
function escapeSvgText(text) {
|
|
307
|
+
return text
|
|
308
|
+
.replace(/&/g, '&')
|
|
309
|
+
.replace(/</g, '<')
|
|
310
|
+
.replace(/>/g, '>')
|
|
311
|
+
.replace(/"/g, '"')
|
|
312
|
+
.replace(/'/g, ''');
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Convert EMF buffer to SVG for inline rendering
|
|
316
|
+
*/
|
|
317
|
+
function convertEmfToSvg(buffer) {
|
|
318
|
+
try {
|
|
319
|
+
setupEmfDom();
|
|
320
|
+
const header = readEmfHeader(buffer);
|
|
321
|
+
const { boundsWidth, boundsHeight, frameWidth, frameHeight } = header;
|
|
322
|
+
// Disable EMFJS logging by temporarily overriding console
|
|
323
|
+
const originalLog = console.log;
|
|
324
|
+
console.log = () => { }; // Silence EMFJS debug logs
|
|
325
|
+
try {
|
|
326
|
+
const renderer = new EMFJS.Renderer(buffer);
|
|
327
|
+
const svg = renderer.render({
|
|
328
|
+
width: boundsWidth + 'px',
|
|
329
|
+
height: boundsHeight + 'px',
|
|
330
|
+
wExt: boundsWidth,
|
|
331
|
+
hExt: boundsHeight,
|
|
332
|
+
xExt: boundsWidth,
|
|
333
|
+
yExt: boundsHeight,
|
|
334
|
+
mapMode: 8 // MM_ANISOTROPIC - allows independent scaling of X and Y
|
|
335
|
+
});
|
|
336
|
+
// Get the SVG markup
|
|
337
|
+
let svgMarkup = svg.outerHTML;
|
|
338
|
+
// Extract text from EMF and add to SVG
|
|
339
|
+
const textRecords = extractEmfText(buffer);
|
|
340
|
+
if (textRecords.length > 0) {
|
|
341
|
+
// Calculate scaling from EMF logical units to SVG viewport
|
|
342
|
+
// EMF text coordinates are in the window extent coordinate system
|
|
343
|
+
// EMFJS renders graphics to the viewport extent coordinate system
|
|
344
|
+
// We need to scale text from window extent -> viewport extent (same as graphics)
|
|
345
|
+
const windowExt = readEmfWindowExtent(buffer);
|
|
346
|
+
const viewportExt = readEmfViewportExtent(buffer);
|
|
347
|
+
const scaleX = viewportExt.width / windowExt.width;
|
|
348
|
+
const scaleY = viewportExt.height / windowExt.height;
|
|
349
|
+
// Create text elements
|
|
350
|
+
const textElements = [];
|
|
351
|
+
for (const record of textRecords) {
|
|
352
|
+
const svgX = Math.round(record.x * scaleX);
|
|
353
|
+
const svgY = Math.round(record.y * scaleY);
|
|
354
|
+
// Scale font height from logical units to SVG pixels
|
|
355
|
+
const fontSize = Math.round(record.fontHeight * scaleY);
|
|
356
|
+
// Apply rotation transform if needed
|
|
357
|
+
const transform = record.rotation !== 0
|
|
358
|
+
? ` transform="rotate(${record.rotation}, ${svgX}, ${svgY})"`
|
|
359
|
+
: '';
|
|
360
|
+
// Apply text-anchor for horizontal alignment
|
|
361
|
+
const textAnchorAttr = record.textAnchor !== 'start'
|
|
362
|
+
? ` text-anchor="${record.textAnchor}"`
|
|
363
|
+
: '';
|
|
364
|
+
// SVG uses 'dominant-baseline' for vertical alignment. EMF uses baseline alignment.
|
|
365
|
+
// SVG's default is 'auto' which is similar to baseline, so we don't need to change it.
|
|
366
|
+
textElements.push(`<text x="${svgX}" y="${svgY}" fill="${record.color}" font-size="${fontSize}px" font-family="Arial, sans-serif"${textAnchorAttr}${transform}>${escapeSvgText(record.text)}</text>`);
|
|
367
|
+
}
|
|
368
|
+
// Insert text elements inside the INNER SVG (where graphics are rendered)
|
|
369
|
+
// EMFJS creates a nested SVG structure: outer SVG > inner SVG with graphics
|
|
370
|
+
// We want text in the inner SVG to match the graphics coordinate system
|
|
371
|
+
// Find the first closing </svg> tag (inner SVG)
|
|
372
|
+
const firstSvgClose = svgMarkup.indexOf('</svg>');
|
|
373
|
+
if (firstSvgClose > 0) {
|
|
374
|
+
svgMarkup = svgMarkup.slice(0, firstSvgClose) +
|
|
375
|
+
textElements.join('') +
|
|
376
|
+
svgMarkup.slice(firstSvgClose);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
// Add xmlns attribute if missing (required for inline SVG)
|
|
380
|
+
if (!svgMarkup.includes('xmlns=')) {
|
|
381
|
+
svgMarkup = svgMarkup.replace('<svg ', '<svg xmlns="http://www.w3.org/2000/svg" ');
|
|
382
|
+
}
|
|
383
|
+
// Return raw SVG markup for inline rendering
|
|
384
|
+
return { type: "inlineSvg", svgMarkup };
|
|
385
|
+
}
|
|
386
|
+
finally {
|
|
387
|
+
console.log = originalLog; // Restore console.log
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
catch (err) {
|
|
391
|
+
// If conversion fails, return null (image won't display)
|
|
392
|
+
console.error('EMF to SVG conversion failed:', err);
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Read the window extent from EMF (EMR_SETWINDOWEXTEX record)
|
|
398
|
+
* Returns the last SETWINDOWEXTEX before text rendering starts
|
|
399
|
+
*/
|
|
400
|
+
function readEmfWindowExtent(buffer) {
|
|
401
|
+
const view = new DataView(buffer);
|
|
402
|
+
const EMR_SETWINDOWEXTEX = 0x09;
|
|
403
|
+
let pos = 0;
|
|
404
|
+
let lastWindowExt = { width: 0, height: 0 };
|
|
405
|
+
while (pos < buffer.byteLength - 8) {
|
|
406
|
+
const recordType = view.getUint32(pos, true);
|
|
407
|
+
const recordSize = view.getUint32(pos + 4, true);
|
|
408
|
+
if (recordSize < 8 || pos + recordSize > buffer.byteLength)
|
|
409
|
+
break;
|
|
410
|
+
if (recordType === EMR_SETWINDOWEXTEX) {
|
|
411
|
+
// EMR_SETWINDOWEXTEX:
|
|
412
|
+
// 0-3: record type
|
|
413
|
+
// 4-7: record size
|
|
414
|
+
// 8-11: cx (width)
|
|
415
|
+
// 12-15: cy (height)
|
|
416
|
+
const cx = view.getInt32(pos + 8, true);
|
|
417
|
+
const cy = view.getInt32(pos + 12, true);
|
|
418
|
+
if (cx > 0 && cy > 0) {
|
|
419
|
+
lastWindowExt = { width: cx, height: cy };
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
pos += recordSize;
|
|
423
|
+
}
|
|
424
|
+
// If we found a valid window extent, use it
|
|
425
|
+
if (lastWindowExt.width > 0 && lastWindowExt.height > 0) {
|
|
426
|
+
return lastWindowExt;
|
|
427
|
+
}
|
|
428
|
+
// Fallback to bounds
|
|
429
|
+
const header = readEmfHeader(buffer);
|
|
430
|
+
return { width: header.boundsWidth, height: header.boundsHeight };
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Read the viewport extent from EMF (EMR_SETVIEWPORTEXTEX record)
|
|
434
|
+
* This is used to scale graphics and text to the SVG coordinate space
|
|
435
|
+
*/
|
|
436
|
+
function readEmfViewportExtent(buffer) {
|
|
437
|
+
const view = new DataView(buffer);
|
|
438
|
+
const EMR_SETVIEWPORTEXTEX = 0x0b;
|
|
439
|
+
let pos = 0;
|
|
440
|
+
let lastViewportExt = { width: 0, height: 0 };
|
|
441
|
+
while (pos < buffer.byteLength - 8) {
|
|
442
|
+
const recordType = view.getUint32(pos, true);
|
|
443
|
+
const recordSize = view.getUint32(pos + 4, true);
|
|
444
|
+
if (recordSize < 8 || pos + recordSize > buffer.byteLength)
|
|
445
|
+
break;
|
|
446
|
+
if (recordType === EMR_SETVIEWPORTEXTEX) {
|
|
447
|
+
const cx = view.getInt32(pos + 8, true);
|
|
448
|
+
const cy = view.getInt32(pos + 12, true);
|
|
449
|
+
if (cx > 0 && cy > 0) {
|
|
450
|
+
lastViewportExt = { width: cx, height: cy };
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
pos += recordSize;
|
|
454
|
+
}
|
|
455
|
+
// If we found a valid viewport extent, use it
|
|
456
|
+
if (lastViewportExt.width > 0 && lastViewportExt.height > 0) {
|
|
457
|
+
return lastViewportExt;
|
|
458
|
+
}
|
|
459
|
+
// Fallback to bounds
|
|
460
|
+
const header = readEmfHeader(buffer);
|
|
461
|
+
return { width: header.boundsWidth, height: header.boundsHeight };
|
|
462
|
+
}
|
|
463
|
+
// ============================================================================
|
|
136
464
|
// Parsing Functions
|
|
137
465
|
// ============================================================================
|
|
138
466
|
function parseThemeColors(themeDoc) {
|
|
@@ -1687,11 +2015,56 @@ function renderTableToHtml(table, styleMap, numberingMap, themeColors, imageMap,
|
|
|
1687
2015
|
return html;
|
|
1688
2016
|
}
|
|
1689
2017
|
function renderImageToHtml(img, imageMap) {
|
|
1690
|
-
const
|
|
1691
|
-
if (!
|
|
2018
|
+
const imageData = imageMap.get(img.rId);
|
|
2019
|
+
if (!imageData)
|
|
1692
2020
|
return "";
|
|
1693
2021
|
const width = Math.round(emuToPx(img.width));
|
|
1694
2022
|
const height = Math.round(emuToPx(img.height));
|
|
2023
|
+
// Check if this is an inline SVG (from EMF conversion)
|
|
2024
|
+
if (imageData.type === "inlineSvg") {
|
|
2025
|
+
// Build container styles
|
|
2026
|
+
const containerStyles = [
|
|
2027
|
+
"display:inline-block",
|
|
2028
|
+
`width:${width}px`,
|
|
2029
|
+
`height:${height}px`,
|
|
2030
|
+
"margin:8px 0",
|
|
2031
|
+
];
|
|
2032
|
+
// Build CSS transform for rotation and flip
|
|
2033
|
+
const transforms = [];
|
|
2034
|
+
if (img.rotation) {
|
|
2035
|
+
transforms.push(`rotate(${img.rotation}deg)`);
|
|
2036
|
+
}
|
|
2037
|
+
if (img.flipH) {
|
|
2038
|
+
transforms.push("scaleX(-1)");
|
|
2039
|
+
}
|
|
2040
|
+
if (img.flipV) {
|
|
2041
|
+
transforms.push("scaleY(-1)");
|
|
2042
|
+
}
|
|
2043
|
+
if (transforms.length > 0) {
|
|
2044
|
+
containerStyles.push(`transform:${transforms.join(" ")}`);
|
|
2045
|
+
}
|
|
2046
|
+
// Apply shadow
|
|
2047
|
+
if (img.shadow) {
|
|
2048
|
+
const blurPx = Math.round(emuToPx(img.shadow.blurRadius));
|
|
2049
|
+
const offsetXPx = img.shadow.offsetX ? Math.round(emuToPx(img.shadow.offsetX)) : 0;
|
|
2050
|
+
const offsetYPx = img.shadow.offsetY ? Math.round(emuToPx(img.shadow.offsetY)) : 0;
|
|
2051
|
+
const alpha = img.shadow.alpha / 100;
|
|
2052
|
+
const r = parseInt(img.shadow.color.substring(0, 2), 16);
|
|
2053
|
+
const g = parseInt(img.shadow.color.substring(2, 4), 16);
|
|
2054
|
+
const b = parseInt(img.shadow.color.substring(4, 6), 16);
|
|
2055
|
+
containerStyles.push(`box-shadow:${offsetXPx}px ${offsetYPx}px ${blurPx}px rgba(${r},${g},${b},${alpha})`);
|
|
2056
|
+
}
|
|
2057
|
+
// Apply border
|
|
2058
|
+
if (img.border) {
|
|
2059
|
+
const borderWidthPx = Math.round(emuToPx(img.border.width));
|
|
2060
|
+
containerStyles.push(`border:${borderWidthPx}px solid #${img.border.color}`);
|
|
2061
|
+
}
|
|
2062
|
+
// Inject width/height into the SVG element to ensure proper sizing
|
|
2063
|
+
let styledSvg = imageData.svgMarkup.replace(/<svg /, `<svg style="width:100%;height:100%;display:block;" `);
|
|
2064
|
+
return `<div style="${containerStyles.join(";")}">${styledSvg}</div>`;
|
|
2065
|
+
}
|
|
2066
|
+
// Regular image (dataUri type)
|
|
2067
|
+
const dataUri = imageData.dataUri;
|
|
1695
2068
|
const alt = img.alt ? ` alt="${escapeHtml(img.alt)}"` : "";
|
|
1696
2069
|
const styles = [
|
|
1697
2070
|
"max-width:100%",
|
|
@@ -1810,8 +2183,8 @@ function renderPositionedElements(positionedElements, sectionProps, styleMap, th
|
|
|
1810
2183
|
}
|
|
1811
2184
|
// Handle positioned images
|
|
1812
2185
|
if (el.type === "image" && el.imageRId) {
|
|
1813
|
-
const
|
|
1814
|
-
if (
|
|
2186
|
+
const imageData = imageMap.get(el.imageRId);
|
|
2187
|
+
if (imageData) {
|
|
1815
2188
|
// Build CSS transform for rotation and flip (extracted from XML, not hardcoded)
|
|
1816
2189
|
const transforms = [];
|
|
1817
2190
|
if (el.imageRotation) {
|
|
@@ -1842,7 +2215,15 @@ function renderPositionedElements(positionedElements, sectionProps, styleMap, th
|
|
|
1842
2215
|
const borderWidthPx = Math.round(emuToPx(el.imageBorder.width));
|
|
1843
2216
|
styles.push(`border:${borderWidthPx}px solid #${el.imageBorder.color}`);
|
|
1844
2217
|
}
|
|
1845
|
-
|
|
2218
|
+
// Check if this is an inline SVG (from EMF conversion)
|
|
2219
|
+
if (imageData.type === "inlineSvg") {
|
|
2220
|
+
// Inject width/height into the SVG element to ensure proper sizing
|
|
2221
|
+
const styledSvg = imageData.svgMarkup.replace(/<svg /, `<svg style="width:100%;height:100%;display:block;" `);
|
|
2222
|
+
innerContent = styledSvg;
|
|
2223
|
+
}
|
|
2224
|
+
else {
|
|
2225
|
+
innerContent = `<img src="${imageData.dataUri}" style="width:100%;height:100%;object-fit:cover">`;
|
|
2226
|
+
}
|
|
1846
2227
|
}
|
|
1847
2228
|
}
|
|
1848
2229
|
// Handle frame shape specially - render as border, not solid fill
|
|
@@ -2080,8 +2461,17 @@ export default async function importDocx(arrayBuffer) {
|
|
|
2080
2461
|
const imgFile = zip.file(mediaPath);
|
|
2081
2462
|
if (!imgFile)
|
|
2082
2463
|
continue;
|
|
2083
|
-
const imgData = await imgFile.async("base64");
|
|
2084
2464
|
const ext = mediaPath.split(".").pop()?.toLowerCase() ?? "png";
|
|
2465
|
+
// Handle EMF files specially - convert to SVG for web display
|
|
2466
|
+
if (ext === "emf") {
|
|
2467
|
+
const imgBuffer = await imgFile.async("arraybuffer");
|
|
2468
|
+
const svgData = convertEmfToSvg(imgBuffer);
|
|
2469
|
+
if (svgData) {
|
|
2470
|
+
imageMap.set(rId, svgData);
|
|
2471
|
+
}
|
|
2472
|
+
continue;
|
|
2473
|
+
}
|
|
2474
|
+
const imgData = await imgFile.async("base64");
|
|
2085
2475
|
const mime = ext === "jpg" || ext === "jpeg"
|
|
2086
2476
|
? "image/jpeg"
|
|
2087
2477
|
: ext === "gif"
|
|
@@ -2089,7 +2479,7 @@ export default async function importDocx(arrayBuffer) {
|
|
|
2089
2479
|
: ext === "svg"
|
|
2090
2480
|
? "image/svg+xml"
|
|
2091
2481
|
: `image/${ext}`;
|
|
2092
|
-
imageMap.set(rId, `data:${mime};base64,${imgData}`);
|
|
2482
|
+
imageMap.set(rId, { type: "dataUri", dataUri: `data:${mime};base64,${imgData}` });
|
|
2093
2483
|
}
|
|
2094
2484
|
}
|
|
2095
2485
|
// Parse section properties from document
|