@@ -75,6 +75,21 @@ class GridsetProcessor extends BaseProcessor {
7575 // Helper function to ensure color has alpha channel (Grid3 format)
7676 private ensureAlphaChannel ( color : string | undefined ) : string {
7777 if ( ! color ) return '#FFFFFFFF' ;
78+
79+ // Handle rgb() and rgba() formats
80+ const rgbMatch = color . match ( / r g b a ? \( ( \d + ) , \s * ( \d + ) , \s * ( \d + ) (?: , \s * ( [ \d . ] + ) ) ? \) / ) ;
81+ if ( rgbMatch ) {
82+ const r = parseInt ( rgbMatch [ 1 ] ) ;
83+ const g = parseInt ( rgbMatch [ 2 ] ) ;
84+ const b = parseInt ( rgbMatch [ 3 ] ) ;
85+ const a = rgbMatch [ 4 ] !== undefined ? parseFloat ( rgbMatch [ 4 ] ) : 1.0 ;
86+ const alphaHex = Math . round ( a * 255 )
87+ . toString ( 16 )
88+ . toUpperCase ( )
89+ . padStart ( 2 , '0' ) ;
90+ return `#${ r . toString ( 16 ) . padStart ( 2 , '0' ) } ${ g . toString ( 16 ) . padStart ( 2 , '0' ) } ${ b . toString ( 16 ) . padStart ( 2 , '0' ) } ${ alphaHex } ` ;
91+ }
92+
7893 // If already 8 digits (with alpha), return as is
7994 if ( color . match ( / ^ # [ 0 - 9 A - F a - f ] { 8 } $ / ) ) return color ;
8095 // If 6 digits (no alpha), add FF for fully opaque
@@ -90,6 +105,42 @@ class GridsetProcessor extends BaseProcessor {
90105 return '#FFFFFFFF' ;
91106 }
92107
108+ /**
109+ * Calculate appropriate font color (black or white) based on background brightness
110+ * Uses WCAG relative luminance formula to determine contrast
111+ */
112+ private getContrastFontColor ( backgroundColor : string | undefined ) : string {
113+ if ( ! backgroundColor ) return '#FF000000FF' ; // Default to black
114+
115+ // Parse color from various formats
116+ let r = 255 ,
117+ g = 255 ,
118+ b = 255 ;
119+
120+ // Handle hex colors
121+ const hexMatch = backgroundColor . match ( / # ? ( [ 0 - 9 A - F a - f ] { 2 } ) ( [ 0 - 9 A - F a - f ] { 2 } ) ( [ 0 - 9 A - F a - f ] { 2 } ) / ) ;
122+ if ( hexMatch ) {
123+ r = parseInt ( hexMatch [ 1 ] , 16 ) ;
124+ g = parseInt ( hexMatch [ 2 ] , 16 ) ;
125+ b = parseInt ( hexMatch [ 3 ] , 16 ) ;
126+ } else {
127+ // Handle rgb() format
128+ const rgbMatch = backgroundColor . match ( / r g b a ? \( ( \d + ) , \s * ( \d + ) , \s * ( \d + ) / ) ;
129+ if ( rgbMatch ) {
130+ r = parseInt ( rgbMatch [ 1 ] ) ;
131+ g = parseInt ( rgbMatch [ 2 ] ) ;
132+ b = parseInt ( rgbMatch [ 3 ] ) ;
133+ }
134+ }
135+
136+ // Calculate relative luminance using WCAG formula
137+ const luminance = ( 0.299 * r + 0.587 * g + 0.114 * b ) / 255 ;
138+
139+ // Use white text for dark backgrounds (luminance < 0.5), black for light backgrounds
140+ // Return 6-digit hex (ensureAlphaChannel will add FF for alpha)
141+ return luminance < 0.5 ? '#FFFFFF' : '#000000' ;
142+ }
143+
93144 /**
94145 * Extract words from Grid3 WordList structure
95146 */
@@ -1798,7 +1849,10 @@ class GridsetProcessor extends BaseProcessor {
17981849 // For "None" surround, just use BackColour for the fill (no TileColour)
17991850 BackColour : this . ensureAlphaChannel ( style . backgroundColor ) ,
18001851 BorderColour : this . ensureAlphaChannel ( style . borderColor ) ,
1801- FontColour : this . ensureAlphaChannel ( style . fontColor ) ,
1852+ // Calculate font color based on background if not explicitly set
1853+ FontColour : this . ensureAlphaChannel (
1854+ style . fontColor || this . getContrastFontColor ( style . backgroundColor )
1855+ ) ,
18021856 FontName : style . fontFamily || 'Arial' ,
18031857 FontSize : style . fontSize ?. toString ( ) || '16' ,
18041858 } ;
@@ -1867,30 +1921,58 @@ class GridsetProcessor extends BaseProcessor {
18671921 imageExt = imageMatch [ 1 ] . toLowerCase ( ) ;
18681922 }
18691923
1870- // Grid3 dynamically constructs image filenames by prepending cell coordinates
1871- // The XML should only contain the suffix: -0-text-0.{ext}
1872- // Grid3 automatically adds the X-Y prefix based on the Cell's position
1873- captionAndImage . Image = `-0-text-0.${ imageExt } ` ;
1874-
18751924 // Extract image data from button parameters if available
18761925 // (AstericsGridProcessor stores it there during loadIntoTree)
1926+ // Also handle data URLs from OBZ conversion
18771927 let imageData = Buffer . alloc ( 0 ) ;
1928+ let hasImageData = false ;
1929+
18781930 if (
18791931 button . parameters &&
18801932 button . parameters . imageData &&
18811933 Buffer . isBuffer ( button . parameters . imageData )
18821934 ) {
18831935 imageData = button . parameters . imageData as any ;
1936+ hasImageData = imageData . length > 0 ;
1937+ } else if (
1938+ button . image &&
1939+ typeof button . image === 'string' &&
1940+ button . image . startsWith ( 'data:image' )
1941+ ) {
1942+ // Convert data URL to Buffer (for OBZ → Grid3 conversion)
1943+ try {
1944+ const matches = button . image . match ( / ^ d a t a : i m a g e \/ ( \w + ) ; b a s e 6 4 , ( .+ ) $ / ) ;
1945+ if ( matches ) {
1946+ const extension = matches [ 1 ] ; // e.g., 'png', 'jpeg', 'gif'
1947+ const base64Data = matches [ 2 ] ;
1948+ imageData = Buffer . from ( base64Data , 'base64' ) ;
1949+ imageExt = extension ; // Override the detected extension
1950+ hasImageData = imageData . length > 0 ;
1951+ }
1952+ } catch ( err ) {
1953+ console . warn (
1954+ `[Grid3] Failed to convert data URL to Buffer for button ${ button . id } :` ,
1955+ err
1956+ ) ;
1957+ }
18841958 }
18851959
1886- // Store image data for later writing to ZIP
1887- buttonImages . set ( button . id , {
1888- imageData : imageData ,
1889- ext : imageExt ,
1890- pageName : page . name || page . id ,
1891- x : position . x ,
1892- y : position . y + yOffset ,
1893- } ) ;
1960+ // Only add image reference if we have actual image data
1961+ if ( hasImageData ) {
1962+ // Grid3 dynamically constructs image filenames by prepending cell coordinates
1963+ // The XML should only contain the suffix: -0-text-0.{ext}
1964+ // Grid3 automatically adds the X-Y prefix based on the Cell's position
1965+ captionAndImage . Image = `-0-text-0.${ imageExt } ` ;
1966+
1967+ // Store image data for later writing to ZIP
1968+ buttonImages . set ( button . id , {
1969+ imageData : imageData ,
1970+ ext : imageExt ,
1971+ pageName : page . name || page . id ,
1972+ x : position . x ,
1973+ y : position . y + yOffset ,
1974+ } ) ;
1975+ }
18941976 }
18951977
18961978 const cellData : Record < string , unknown > = {
@@ -1927,9 +2009,11 @@ class GridsetProcessor extends BaseProcessor {
19272009 if ( button . style ?. borderColor ) {
19282010 styleObj . BorderColour = this . ensureAlphaChannel ( button . style . borderColor ) ;
19292011 }
1930- if ( button . style ?. fontColor ) {
1931- styleObj . FontColour = this . ensureAlphaChannel ( button . style . fontColor ) ;
1932- }
2012+ // Always add font color inline - either from button style or calculated from background
2013+ const fontColor =
2014+ button . style ?. fontColor ||
2015+ this . getContrastFontColor ( button . style ?. backgroundColor ) ;
2016+ styleObj . FontColour = this . ensureAlphaChannel ( fontColor ) ;
19332017 if ( button . style ?. fontFamily ) {
19342018 styleObj . FontName = button . style . fontFamily ;
19352019 }
0 commit comments