77namespace OCA \Theming ;
88
99use Imagick ;
10+ use ImagickDraw ;
1011use ImagickPixel ;
1112use OCP \Files \SimpleFS \ISimpleFile ;
1213
@@ -30,18 +31,19 @@ public function __construct(
3031 * @return string|false image blob
3132 */
3233 public function getFavicon ($ app ) {
33- if (!$ this ->imageManager ->shouldReplaceIcons ( )) {
34+ if (!$ this ->imageManager ->canConvert ( ' ICO ' )) {
3435 return false ;
3536 }
3637 try {
37- $ favicon = new Imagick ();
38- $ favicon ->setFormat ('ico ' );
3938 $ icon = $ this ->renderAppIcon ($ app , 128 );
4039 if ($ icon === false ) {
4140 return false ;
4241 }
4342 $ icon ->setImageFormat ('png32 ' );
4443
44+ $ favicon = new Imagick ();
45+ $ favicon ->setFormat ('ico ' );
46+
4547 $ clone = clone $ icon ;
4648 $ clone ->scaleImage (16 , 0 );
4749 $ favicon ->addImage ($ clone );
@@ -111,79 +113,108 @@ public function renderAppIcon($app, $size) {
111113 return false ;
112114 }
113115
116+ $ padding = 0.15 ;
114117 $ color = $ this ->themingDefaults ->getColorPrimary ();
118+ $ appIconFile = null ;
119+ $ appIconIsSvg = ($ mime === 'image/svg+xml ' || substr ($ appIconContent , 0 , 4 ) === '<svg ' );
115120
116- // generate background image with rounded corners
117- $ cornerRadius = 0.2 * $ size ;
118- $ background = '<?xml version="1.0" encoding="UTF-8"?> '
119- . '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:cc="http://creativecommons.org/ns#" width=" ' . $ size . '" height=" ' . $ size . '" xmlns:xlink="http://www.w3.org/1999/xlink"> '
120- . '<rect x="0" y="0" rx=" ' . $ cornerRadius . '" ry=" ' . $ cornerRadius . '" width=" ' . $ size . '" height=" ' . $ size . '" style="fill: ' . $ color . ';" /> '
121- . '</svg> ' ;
122- // resize svg magic as this seems broken in Imagemagick
123- if ($ mime === 'image/svg+xml ' || substr ($ appIconContent , 0 , 4 ) === '<svg ' ) {
124- if (substr ($ appIconContent , 0 , 5 ) !== '<?xml ' ) {
125- $ svg = '<?xml version="1.0"?> ' . $ appIconContent ;
126- } else {
127- $ svg = $ appIconContent ;
128- }
129- $ tmp = new Imagick ();
130- $ tmp ->setBackgroundColor (new ImagickPixel ('transparent ' ));
131- $ tmp ->setResolution (72 , 72 );
132- $ tmp ->readImageBlob ($ svg );
133- $ x = $ tmp ->getImageWidth ();
134- $ y = $ tmp ->getImageHeight ();
135- $ tmp ->destroy ();
136-
137- // convert svg to resized image
121+ // determine if SVG support is available
122+ if ($ appIconIsSvg && !$ this ->imageManager ->canConvert ('SVG ' )) {
123+ return false ;
124+ }
125+
126+ try {
127+ // construct original image object
138128 $ appIconFile = new Imagick ();
139- $ resX = (int )(72 * $ size / $ x );
140- $ resY = (int )(72 * $ size / $ y );
141- $ appIconFile ->setResolution ($ resX , $ resY );
142129 $ appIconFile ->setBackgroundColor (new ImagickPixel ('transparent ' ));
143- $ appIconFile ->readImageBlob ($ svg );
144-
145- /**
146- * invert app icons for bright primary colors
147- * the default nextcloud logo will not be inverted to black
148- */
149- if ($ this ->util ->isBrightColor ($ color )
150- && !$ appIcon instanceof ISimpleFile
151- && $ app !== 'core '
152- ) {
153- $ appIconFile ->negateImage (false );
130+
131+ if ($ appIconIsSvg ) {
132+ // handle SVG images
133+ // ensure proper XML declaration
134+ if (substr ($ appIconContent , 0 , 5 ) !== '<?xml ' ) {
135+ $ svg = '<?xml version="1.0"?> ' . $ appIconContent ;
136+ } else {
137+ $ svg = $ appIconContent ;
138+ }
139+ // get dimensions for resolution calculation
140+ $ tmp = new Imagick ();
141+ $ tmp ->setBackgroundColor (new ImagickPixel ('transparent ' ));
142+ $ tmp ->setResolution (72 , 72 );
143+ $ tmp ->readImageBlob ($ svg );
144+ $ x = $ tmp ->getImageWidth ();
145+ $ y = $ tmp ->getImageHeight ();
146+ $ tmp ->destroy ();
147+ // set resolution for proper scaling
148+ $ resX = (int )(72 * $ size / $ x );
149+ $ resY = (int )(72 * $ size / $ y );
150+ $ appIconFile ->setResolution ($ resX , $ resY );
151+ $ appIconFile ->readImageBlob ($ svg );
152+ } else {
153+ // handle non-SVG images
154+ $ appIconFile ->readImageBlob ($ appIconContent );
154155 }
155- } else {
156- $ appIconFile = new Imagick ();
157- $ appIconFile ->setBackgroundColor (new ImagickPixel ('transparent ' ));
158- $ appIconFile ->readImageBlob ($ appIconContent );
156+ } catch (\ImagickException $ e ) {
157+ return false ;
159158 }
160- // offset for icon positioning
161- $ padding = 0.15 ;
162- $ border_w = (int )($ appIconFile ->getImageWidth () * $ padding );
163- $ border_h = (int )($ appIconFile ->getImageHeight () * $ padding );
164- $ innerWidth = ($ appIconFile ->getImageWidth () - $ border_w * 2 );
165- $ innerHeight = ($ appIconFile ->getImageHeight () - $ border_h * 2 );
166- $ appIconFile ->adaptiveResizeImage ($ innerWidth , $ innerHeight );
167- // center icon
168- $ offset_w = (int )($ size / 2 - $ innerWidth / 2 );
169- $ offset_h = (int )($ size / 2 - $ innerHeight / 2 );
170-
171- $ finalIconFile = new Imagick ();
172- $ finalIconFile ->setBackgroundColor (new ImagickPixel ('transparent ' ));
173- $ finalIconFile ->readImageBlob ($ background );
174- $ finalIconFile ->setImageVirtualPixelMethod (Imagick::VIRTUALPIXELMETHOD_TRANSPARENT );
175- $ finalIconFile ->setImageArtifact ('compose:args ' , '1,0,-0.5,0.5 ' );
176- $ finalIconFile ->compositeImage ($ appIconFile , Imagick::COMPOSITE_ATOP , $ offset_w , $ offset_h );
177- $ finalIconFile ->setImageFormat ('png24 ' );
178- if (defined ('Imagick::INTERPOLATE_BICUBIC ' ) === true ) {
179- $ filter = Imagick::INTERPOLATE_BICUBIC ;
180- } else {
181- $ filter = Imagick::FILTER_LANCZOS ;
159+ /**
160+ * invert app icons for bright primary colors
161+ * the default nextcloud logo will not be inverted to black
162+ */
163+ if ($ this ->util ->isBrightColor ($ color )
164+ && !$ appIcon instanceof ISimpleFile
165+ && $ app !== 'core '
166+ ) {
167+ $ appIconFile ->negateImage (false );
168+ }
169+
170+ // calculate final image size and position
171+ $ original_w = $ appIconFile ->getImageWidth ();
172+ $ original_h = $ appIconFile ->getImageHeight ();
173+ $ contentBox = (int )floor ($ size * (1 - 2 * $ padding ));
174+ if ($ contentBox < 1 ) {
175+ $ contentBox = $ size ; // fallback safety
176+ }
177+ $ scaleX = $ contentBox / $ original_w ;
178+ $ scaleY = $ contentBox / $ original_h ;
179+ $ scale = min ($ scaleX , $ scaleY );
180+ $ new_w = max (1 , (int )floor ($ original_w * $ scale ));
181+ $ new_h = max (1 , (int )floor ($ original_h * $ scale ));
182+ $ offset_w = (int )floor (($ size - $ new_w ) / 2 );
183+ $ offset_h = (int )floor (($ size - $ new_h ) / 2 );
184+ // resize original image
185+ $ appIconFile ->resizeImage ($ new_w , $ new_h , Imagick::FILTER_LANCZOS , 1 );
186+ // construct final image object
187+ try {
188+ // image background
189+ $ finalIconFile = new Imagick ();
190+ $ finalIconFile ->setBackgroundColor (new ImagickPixel ('transparent ' ));
191+ // icon background
192+ $ finalIconFile ->newImage ($ size , $ size , new ImagickPixel ('transparent ' ));
193+ $ finalIconFile ->setImageFormat ('PNG32 ' );
194+ $ draw = new ImagickDraw ();
195+ $ draw ->setFillColor ($ color );
196+ $ cornerRadius = 0.2 * $ size ;
197+ $ draw ->roundRectangle (0 , 0 , $ size - 1 , $ size - 1 , $ cornerRadius , $ cornerRadius );
198+ $ finalIconFile ->drawImage ($ draw );
199+ $ draw ->destroy ();
200+ // overlay icon
201+ $ finalIconFile ->setImageVirtualPixelMethod (Imagick::VIRTUALPIXELMETHOD_TRANSPARENT );
202+ $ finalIconFile ->setImageArtifact ('compose:args ' , '1,0,-0.5,0.5 ' );
203+ $ finalIconFile ->compositeImage ($ appIconFile , Imagick::COMPOSITE_ATOP , $ offset_w , $ offset_h );
204+ $ finalIconFile ->setImageFormat ('PNG32 ' );
205+ if (defined ('Imagick::INTERPOLATE_BICUBIC ' ) === true ) {
206+ $ filter = Imagick::INTERPOLATE_BICUBIC ;
207+ } else {
208+ $ filter = Imagick::FILTER_LANCZOS ;
209+ }
210+ $ finalIconFile ->resizeImage ($ size , $ size , $ filter , 1 , false );
211+
212+ return $ finalIconFile ;
213+ } finally {
214+ unset($ appIconFile );
182215 }
183- $ finalIconFile ->resizeImage ($ size , $ size , $ filter , 1 , false );
184216
185- $ appIconFile ->destroy ();
186- return $ finalIconFile ;
217+ return false ;
187218 }
188219
189220 /**
0 commit comments