@@ -317,6 +317,7 @@ async function fetchMermaidPNG(code) {
317317 return fs . readFileSync ( pngCachePath ) ;
318318 }
319319
320+ // Try with htmlLabels:false (SVG text labels, needed for sharp/librsvg rasterization)
320321 const krokiSource = toKrokiMermaidSource ( code ) ;
321322 const body = Buffer . from ( krokiSource , 'utf8' ) ;
322323 const { statusCode, body : resp } = await krokiPost ( '/mermaid/svg' , body ) ;
@@ -328,17 +329,25 @@ async function fetchMermaidPNG(code) {
328329 } catch ( e ) {
329330 console . warn ( ` ⚠ Mermaid SVG rasterize failed (${ e . message } ); falling back to Kroki PNG` ) ;
330331 }
331- } else if ( statusCode !== 200 ) {
332- console . warn ( ` ⚠ Kroki SVG ${ statusCode } : ${ resp . toString ( 'utf8' ) . slice ( 0 , 120 ) } ` ) ;
333332 }
334333
335334 const pngFallback = await krokiPost ( '/mermaid/png' , body ) ;
336335 if ( pngFallback . statusCode === 200 && pngFallback . body . length ) {
337336 fs . writeFileSync ( pngCachePath , pngFallback . body ) ;
338337 return pngFallback . body ;
339338 }
340- if ( pngFallback . statusCode !== 200 ) {
341- console . warn ( ` ⚠ Kroki PNG ${ pngFallback . statusCode } : ${ pngFallback . body . toString ( 'utf8' ) . slice ( 0 , 120 ) } ` ) ;
339+
340+ // htmlLabels:false can break diagrams with \n in labels on some Kroki versions.
341+ // Fallback: request PNG with the original code (Kroki's PNG uses a headless browser
342+ // that renders foreignObject / HTML labels correctly).
343+ const originalBody = Buffer . from ( code , 'utf8' ) ;
344+ const originalPng = await krokiPost ( '/mermaid/png' , originalBody ) ;
345+ if ( originalPng . statusCode === 200 && originalPng . body . length ) {
346+ fs . writeFileSync ( pngCachePath , originalPng . body ) ;
347+ return originalPng . body ;
348+ }
349+ if ( originalPng . statusCode !== 200 ) {
350+ console . warn ( ` ⚠ Kroki PNG (all attempts) ${ originalPng . statusCode } : ${ originalPng . body . toString ( 'utf8' ) . slice ( 0 , 120 ) } ` ) ;
342351 }
343352 return null ;
344353}
@@ -1466,24 +1475,19 @@ function drawCoverCollage(doc, pw, ph, imgPaths) {
14661475 return ;
14671476 }
14681477
1469- // n >= 4 — full-width hero + lower mosaic (cycles through remaining headers)
1470- const heroH = Math . round ( ph * 0.44 ) ;
1471- drawCoverImageCell ( doc , imgPaths [ 0 ] , 0 , 0 , pw , heroH ) ;
1472-
1473- const galleryTop = heroH + g ;
1474- const galleryH = ph - galleryTop ;
1475- const rest = n - 1 ;
1476- const cols = Math . min ( 7 , Math . max ( 3 , Math . ceil ( Math . sqrt ( rest * ( pw / Math . max ( galleryH , 1 ) ) * 1.15 ) ) ) ) ;
1477- const rows = Math . ceil ( rest / cols ) ;
1478+ // n >= 4 — uniform grid, all cells roughly the same size
1479+ const aspect = pw / ph ;
1480+ const cols = Math . min ( 8 , Math . max ( 3 , Math . round ( Math . sqrt ( n * aspect ) ) ) ) ;
1481+ const rows = Math . ceil ( n / cols ) ;
14781482 const cellW = ( pw - g * ( cols - 1 ) ) / cols ;
1479- const cellH = ( galleryH - g * ( rows - 1 ) ) / rows ;
1483+ const cellH = ( ph - g * ( rows - 1 ) ) / rows ;
14801484
14811485 let cell = 0 ;
14821486 for ( let row = 0 ; row < rows ; row ++ ) {
14831487 for ( let col = 0 ; col < cols ; col ++ ) {
1484- const imgIdx = 1 + ( cell % rest ) ;
1488+ const imgIdx = cell % n ;
14851489 const x = col * ( cellW + g ) ;
1486- const y = galleryTop + row * ( cellH + g ) ;
1490+ const y = row * ( cellH + g ) ;
14871491 drawCoverImageCell ( doc , imgPaths [ imgIdx ] , x , y , cellW , cellH ) ;
14881492 cell += 1 ;
14891493 }
@@ -1496,15 +1500,15 @@ function drawCoverCollage(doc, pw, ph, imgPaths) {
14961500 * without the directional-light noise of the old 3-gradient version.
14971501 */
14981502function drawCoverAtmosphericOverlay ( doc , pw , ph ) {
1499- // Main tonal wash — top slightly lighter than bottom for depth .
1503+ // Main tonal wash — lighter so the collage shows through as visible texture .
15001504 const wash = doc . linearGradient ( 0 , 0 , 0 , ph )
1501- . stop ( 0 , '#0b1220' , 0.86 )
1502- . stop ( 1 , '#020617' , 0.94 ) ;
1505+ . stop ( 0 , '#0b1220' , 0.55 )
1506+ . stop ( 1 , '#020617' , 0.72 ) ;
15031507 doc . rect ( 0 , 0 , pw , ph ) . fill ( wash ) ;
15041508
15051509 // Soft left-column shadow to seat the typography without forcing a hard edge.
15061510 const shadow = doc . linearGradient ( 0 , 0 , pw * 0.55 , 0 )
1507- . stop ( 0 , '#020617' , 0.45 )
1511+ . stop ( 0 , '#020617' , 0.40 )
15081512 . stop ( 1 , '#020617' , 0 ) ;
15091513 doc . rect ( 0 , 0 , pw * 0.55 , ph ) . fill ( shadow ) ;
15101514}
0 commit comments