diff --git a/package.json b/package.json index 466e422..4d1c247 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@mlightcad/mtext-parser", - "version": "1.4.0", + "version": "1.4.1", "description": "AutoCAD MText parser written in TypeScript", "type": "module", "main": "dist/parser.js", diff --git a/src/parser.test.ts b/src/parser.test.ts index a9921a4..eb26d76 100644 --- a/src/parser.test.ts +++ b/src/parser.test.ts @@ -1863,4 +1863,35 @@ describe('MTextColor', () => { expect(color.rgb).toEqual([1, 2, 3]); expect(copy.rgb).toEqual([4, 5, 6]); }); + + it('fromCssColor parses hex and rgb()', () => { + const hex = MTextColor.fromCssColor('#ff0000'); + expect(hex).not.toBeNull(); + expect(hex!.rgb).toEqual([255, 0, 0]); + + const shortHex = MTextColor.fromCssColor('#0f0'); + expect(shortHex).not.toBeNull(); + expect(shortHex!.rgb).toEqual([0, 255, 0]); + + const rgb = MTextColor.fromCssColor('rgb(0, 128, 255)'); + expect(rgb).not.toBeNull(); + expect(rgb!.rgb).toEqual([0, 128, 255]); + }); + + it('fromCssColor handles rgba() and rejects transparent', () => { + const rgba = MTextColor.fromCssColor('rgba(10, 20, 30, 0.5)'); + expect(rgba).not.toBeNull(); + expect(rgba!.rgb).toEqual([10, 20, 30]); + + const transparent = MTextColor.fromCssColor('transparent'); + expect(transparent).toBeNull(); + }); + + it('toCssColor returns hex for RGB and null for ACI', () => { + const rgb = new MTextColor([255, 0, 0]); + expect(rgb.toCssColor()).toBe('#ff0000'); + + const aci = new MTextColor(1); + expect(aci.toCssColor()).toBeNull(); + }); }); diff --git a/src/parser.ts b/src/parser.ts index 95ef7fb..b2d04c7 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -229,6 +229,75 @@ export function int2rgb(value: number): RGB { return [r, g, b]; } +function clampColorChannel(value: number): number { + return Math.max(0, Math.min(255, Math.round(value))); +} + +function normalizeColorNumber(color: number): number { + return Math.max(0, Math.min(0xffffff, Math.round(color))); +} + +function colorNumberToHex(color: number | null): string | null { + if (color === null) return null; + return `#${normalizeColorNumber(color).toString(16).padStart(6, '0')}`; +} + +function normalizeHexColor(value: string | null | undefined): string | null { + if (!value) return null; + const normalized = value.trim().toLowerCase(); + if (/^#[0-9a-f]{6}$/.test(normalized)) return normalized; + if (/^[0-9a-f]{6}$/.test(normalized)) return `#${normalized}`; + if (/^#[0-9a-f]{3}$/.test(normalized)) { + const r = normalized[1]; + const g = normalized[2]; + const b = normalized[3]; + return `#${r}${r}${g}${g}${b}${b}`; + } + if (/^[0-9a-f]{3}$/.test(normalized)) { + const r = normalized[0]; + const g = normalized[1]; + const b = normalized[2]; + return `#${r}${r}${g}${g}${b}${b}`; + } + return null; +} + +function cssColorToRgbValue(value: string | null | undefined): number | null { + if (!value) return null; + const raw = value.trim().toLowerCase(); + if (raw === 'transparent') return null; + + const hex = normalizeHexColor(raw); + if (hex) { + return normalizeColorNumber(Number.parseInt(hex.slice(1), 16)); + } + + const fnMatch = raw.match(/^rgba?\((.*)\)$/); + if (!fnMatch) return null; + + const parts = fnMatch[1] + .replace(/\s*\/\s*/g, ' ') + .split(/[,\s]+/) + .map(p => p.trim()) + .filter(Boolean); + + if (parts.length < 3) return null; + + const toChannel = (token: string): number => { + if (token.endsWith('%')) { + const percent = Number.parseFloat(token.slice(0, -1)); + return clampColorChannel((percent / 100) * 255); + } + const num = Number.parseFloat(token); + return clampColorChannel(num); + }; + + const r = toChannel(parts[0]); + const g = toChannel(parts[1]); + const b = toChannel(parts[2]); + return rgb2int([r, g, b]); +} + /** * Escape DXF line endings * @param text - Text to escape @@ -1642,6 +1711,29 @@ export class MTextColor { return { aci: this._aci, rgb: this.rgb, rgbValue: this._rgbValue }; } + /** + * Convert the current color to a CSS hex color string (#rrggbb). + * Returns null if the color is ACI-based and has no RGB value. + */ + toCssColor(): string | null { + if (this._rgbValue !== null) { + return colorNumberToHex(this._rgbValue); + } + return null; + } + + /** + * Create an MTextColor from a CSS color string. + * Supports #rgb, #rrggbb, rgb(...), rgba(...). Returns null if invalid or transparent. + */ + static fromCssColor(value: string | null | undefined): MTextColor | null { + const rgbValue = cssColorToRgbValue(value); + if (rgbValue === null) return null; + const color = new MTextColor(); + color.rgbValue = rgbValue; + return color; + } + /** * Equality check for color. * @param other The other MTextColor to compare.