* Parse a gradient fill into a CSS gradient string.
( gradFill: SafeXmlNode, ctx: RenderContext, placeholderColorNode?: SafeXmlNode )
| 429 | * Parse a gradient fill into a CSS gradient string. |
| 430 | */ |
| 431 | function resolveGradient( |
| 432 | gradFill: SafeXmlNode, |
| 433 | ctx: RenderContext, |
| 434 | placeholderColorNode?: SafeXmlNode |
| 435 | ): string { |
| 436 | // Parse gradient stops |
| 437 | const gsLst = gradFill.child('gsLst') |
| 438 | const stops: { position: number; color: string }[] = [] |
| 439 | |
| 440 | for (const gs of gsLst.children('gs')) { |
| 441 | const pos = gs.numAttr('pos') ?? 0 |
| 442 | const posPercent = pctToDecimal(pos) * 100 |
| 443 | const { color, alpha } = resolveColorWithPlaceholder(gs, ctx, placeholderColorNode) |
| 444 | stops.push({ position: posPercent, color: toCssColor(color, alpha) }) |
| 445 | } |
| 446 | |
| 447 | if (stops.length === 0) { |
| 448 | return '' |
| 449 | } |
| 450 | |
| 451 | // Sort stops by position |
| 452 | stops.sort((a, b) => a.position - b.position) |
| 453 | |
| 454 | const stopsStr = stops.map((s) => `${s.color} ${s.position.toFixed(1)}%`).join(', ') |
| 455 | |
| 456 | // Determine gradient type |
| 457 | const lin = gradFill.child('lin') |
| 458 | if (lin.exists()) { |
| 459 | const angle = angleToDeg(lin.numAttr('ang') ?? 0) |
| 460 | // OOXML angle 0 = top-to-bottom in the gradient coordinate system |
| 461 | // CSS angle 0 = bottom-to-top, so we need to adjust |
| 462 | const cssAngle = (angle + 90) % 360 |
| 463 | return `linear-gradient(${cssAngle.toFixed(1)}deg, ${stopsStr})` |
| 464 | } |
| 465 | |
| 466 | const path = gradFill.child('path') |
| 467 | if (path.exists()) { |
| 468 | const pathType = path.attr('path') |
| 469 | if (pathType === 'circle' || pathType === 'shape' || pathType === 'rect') { |
| 470 | // OOXML path gradients: stop pos=0 = fillToRect center, pos=100000 = shape edge. |
| 471 | // CSS radial-gradient: 0% = center, 100% = edge. |
| 472 | // Conventions match — no reversal needed. |
| 473 | |
| 474 | // Resolve fillToRect center point |
| 475 | const ftr = path.child('fillToRect') |
| 476 | let cx = 50 |
| 477 | let cy = 50 |
| 478 | if (ftr.exists()) { |
| 479 | const l = (ftr.numAttr('l') ?? 0) / 100000 |
| 480 | const t = (ftr.numAttr('t') ?? 0) / 100000 |
| 481 | const r = (ftr.numAttr('r') ?? 0) / 100000 |
| 482 | const b = (ftr.numAttr('b') ?? 0) / 100000 |
| 483 | cx = ((l + (1 - r)) / 2) * 100 |
| 484 | cy = ((t + (1 - b)) / 2) * 100 |
| 485 | } |
| 486 | |
| 487 | if (pathType === 'rect') { |
| 488 | // Rectangular gradient (L∞ norm / Chebyshev distance): creates cross/X contour |
no test coverage detected