@@ -483,11 +483,11 @@ ipcMain.handle('plantuml:render-svg', async (_, diagram: string, format: 'svg' =
483483 return ;
484484 }
485485
486- const exitDetails = errorOutput . trim ( ) || `Renderer exited with code ${ code } .` ;
486+ const exitDetails = errorOutput . trim ( ) ;
487487 finalize ( {
488488 success : false ,
489- error : 'Local PlantUML renderer failed to produce output.' ,
490- details : exitDetails ,
489+ error : derivePlantumlFriendlyError ( exitDetails , code ?? undefined ) ,
490+ details : exitDetails || ( typeof code === 'number' ? `Renderer exited with code ${ code } .` : undefined ) ,
491491 } ) ;
492492 } ) ;
493493
@@ -514,6 +514,88 @@ let plantumlJarLookupPromise: Promise<string> | null = null;
514514
515515const PLANTUML_JAR_RELATIVE_PATH = path . join ( 'assets' , 'plantuml' , 'plantuml.jar' ) ;
516516
517+ function derivePlantumlFriendlyError ( details ?: string | null , exitCode ?: number ) : string {
518+ if ( details ) {
519+ const normalized = details . toLowerCase ( ) ;
520+
521+ if ( normalized . includes ( 'cannot run program' ) && normalized . includes ( '"dot"' ) ) {
522+ return 'Graphviz (the "dot" executable) is required for the local PlantUML renderer. Install Graphviz or add it to your PATH.' ;
523+ }
524+
525+ if ( normalized . includes ( 'graphviz' ) && normalized . includes ( 'not found' ) ) {
526+ return 'Graphviz binaries were not found. Install Graphviz to enable the local PlantUML renderer.' ;
527+ }
528+
529+ if ( normalized . includes ( 'unsupportedclassversionerror' ) ) {
530+ return 'The bundled PlantUML renderer requires a newer Java runtime. Update Java and try again.' ;
531+ }
532+
533+ if ( normalized . includes ( 'could not find or load main class' ) || normalized . includes ( 'classnotfoundexception' ) ) {
534+ return 'The PlantUML renderer could not start. Ensure assets/plantuml/plantuml.jar is present and accessible.' ;
535+ }
536+
537+ if ( normalized . includes ( 'permission denied' ) ) {
538+ return 'The PlantUML renderer could not be executed because of missing file permissions.' ;
539+ }
540+ }
541+
542+ if ( typeof exitCode === 'number' && exitCode !== 0 ) {
543+ return `Local PlantUML renderer exited with code ${ exitCode } .` ;
544+ }
545+
546+ return 'Local PlantUML renderer failed to produce output.' ;
547+ }
548+
549+ function isPackagedAsarPath ( filePath : string ) : boolean {
550+ return / \. a s a r ( $ | [ \\ / ] ) / . test ( filePath ) ;
551+ }
552+
553+ async function ensurePlantumlJarExtracted ( sourcePath : string ) : Promise < string > {
554+ if ( ! isPackagedAsarPath ( sourcePath ) ) {
555+ return sourcePath ;
556+ }
557+
558+ const tempDir = path . join ( app . getPath ( 'temp' ) , 'docforge-plantuml' ) ;
559+ const destinationPath = path . join ( tempDir , 'plantuml.jar' ) ;
560+
561+ await fs . mkdir ( tempDir , { recursive : true } ) ;
562+
563+ let needsExtraction = true ;
564+ try {
565+ const [ sourceStats , destStats ] = await Promise . all ( [ fs . stat ( sourcePath ) , fs . stat ( destinationPath ) ] ) ;
566+ if ( sourceStats . size === destStats . size ) {
567+ needsExtraction = false ;
568+ }
569+ } catch {
570+ // Either the destination does not exist yet or we could not stat one of the files.
571+ needsExtraction = true ;
572+ }
573+
574+ if ( ! needsExtraction ) {
575+ return destinationPath ;
576+ }
577+
578+ const pipeline = promisify ( stream . pipeline ) ;
579+
580+ try {
581+ await pipeline ( createReadStream ( sourcePath ) , createWriteStream ( destinationPath ) ) ;
582+ } catch ( error ) {
583+ try {
584+ await fs . unlink ( destinationPath ) ;
585+ } catch {
586+ // Ignore cleanup failures; we'll retry extraction later if needed.
587+ }
588+
589+ const message =
590+ error instanceof Error
591+ ? error . message
592+ : 'Failed to extract bundled PlantUML renderer from application archive.' ;
593+ throw new Error ( `Unable to prepare PlantUML renderer: ${ message } ` ) ;
594+ }
595+
596+ return destinationPath ;
597+ }
598+
517599async function resolvePlantUmlJar ( ) : Promise < string > {
518600 if ( cachedPlantumlJarPath ) {
519601 return cachedPlantumlJarPath ;
@@ -539,8 +621,9 @@ async function resolvePlantUmlJar(): Promise<string> {
539621 for ( const candidate of candidates ) {
540622 try {
541623 await fs . access ( candidate ) ;
542- cachedPlantumlJarPath = candidate ;
543- return candidate ;
624+ const usablePath = await ensurePlantumlJarExtracted ( candidate ) ;
625+ cachedPlantumlJarPath = usablePath ;
626+ return usablePath ;
544627 } catch {
545628 // Continue searching
546629 }
0 commit comments