77namespace OCA \Theming ;
88
99use Imagick ;
10+ use ImagickDraw ;
1011use ImagickPixel ;
1112use OCP \Files \SimpleFS \ISimpleFile ;
1213
@@ -30,17 +31,18 @@ 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 ( ' PNG ' )) {
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 }
43- $ icon ->setImageFormat ('png32 ' );
42+ $ icon ->setImageFormat ('PNG32 ' );
43+
44+ $ favicon = new Imagick ();
45+ $ favicon ->setFormat ('ICO ' );
4446
4547 $ clone = clone $ icon ;
4648 $ clone ->scaleImage (16 , 0 );
@@ -96,7 +98,9 @@ public function getTouchIcon($app) {
9698 * @return Imagick|false
9799 */
98100 public function renderAppIcon ($ app , $ size ) {
99- $ appIcon = $ this ->util ->getAppIcon ($ app );
101+ $ supportSvg = $ this ->imageManager ->canConvert ('SVG ' );
102+ // retrieve app icon
103+ $ appIcon = $ this ->util ->getAppIcon ($ app , $ supportSvg );
100104 if ($ appIcon instanceof ISimpleFile) {
101105 $ appIconContent = $ appIcon ->getContent ();
102106 $ mime = $ appIcon ->getMimeType ();
@@ -111,79 +115,101 @@ public function renderAppIcon($app, $size) {
111115 return false ;
112116 }
113117
114- $ 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+ // if source image is svg but svg not supported, abort
122+ if ($ appIconIsSvg && !$ supportSvg ) {
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+ // calculate final image size and position
160+ $ padding = 0.85 ;
161+ $ original_w = $ appIconFile ->getImageWidth ();
162+ $ original_h = $ appIconFile ->getImageHeight ();
163+ $ contentSize = (int )floor ($ size * $ padding );
164+ $ scale = min ($ contentSize / $ original_w , $ contentSize / $ original_h );
165+ $ new_w = max (1 , (int )floor ($ original_w * $ scale ));
166+ $ new_h = max (1 , (int )floor ($ original_h * $ scale ));
167+ $ offset_w = (int )floor (($ size - $ new_w ) / 2 );
168+ $ offset_h = (int )floor (($ size - $ new_h ) / 2 );
169+ $ cornerRadius = 0.2 * $ size ;
170+ $ color = $ this ->themingDefaults ->getColorPrimary ();
171+ // resize original image
172+ $ appIconFile ->resizeImage ($ new_w , $ new_h , Imagick::FILTER_LANCZOS , 1 );
173+ /**
174+ * invert app icons for bright primary colors
175+ * the default nextcloud logo will not be inverted to black
176+ */
177+ if ($ this ->util ->isBrightColor ($ color )
178+ && !$ appIcon instanceof ISimpleFile
179+ && $ app !== 'core '
180+ ) {
181+ $ appIconFile ->negateImage (false );
182+ }
183+ // construct final image object
184+ try {
185+ // image background
186+ $ finalIconFile = new Imagick ();
187+ $ finalIconFile ->setBackgroundColor (new ImagickPixel ('transparent ' ));
188+ // icon background
189+ $ finalIconFile ->newImage ($ size , $ size , new ImagickPixel ('transparent ' ));
190+ $ draw = new ImagickDraw ();
191+ $ draw ->setFillColor ($ color );
192+ $ draw ->roundRectangle (0 , 0 , $ size - 1 , $ size - 1 , $ cornerRadius , $ cornerRadius );
193+ $ finalIconFile ->drawImage ($ draw );
194+ $ draw ->destroy ();
195+ // overlay icon
196+ $ finalIconFile ->setImageVirtualPixelMethod (Imagick::VIRTUALPIXELMETHOD_TRANSPARENT );
197+ $ finalIconFile ->setImageArtifact ('compose:args ' , '1,0,-0.5,0.5 ' );
198+ $ finalIconFile ->compositeImage ($ appIconFile , Imagick::COMPOSITE_ATOP , $ offset_w , $ offset_h );
199+ $ finalIconFile ->setImageFormat ('PNG32 ' );
200+ if (defined ('Imagick::INTERPOLATE_BICUBIC ' ) === true ) {
201+ $ filter = Imagick::INTERPOLATE_BICUBIC ;
202+ } else {
203+ $ filter = Imagick::FILTER_LANCZOS ;
204+ }
205+ $ finalIconFile ->resizeImage ($ size , $ size , $ filter , 1 , false );
206+
207+ return $ finalIconFile ;
208+ } finally {
209+ unset($ appIconFile );
182210 }
183- $ finalIconFile ->resizeImage ($ size , $ size , $ filter , 1 , false );
184211
185- $ appIconFile ->destroy ();
186- return $ finalIconFile ;
212+ return false ;
187213 }
188214
189215 /**
0 commit comments