1+ #!/usr/bin/env python3 
2+ 
3+ """ 
4+ PNG to ICNS Converter 
5+ 
6+ This script converts PNG images to ICNS format, which is used for macOS icons. 
7+ It requires PIL (Pillow) library and supports customizable size ranges. 
8+ 
9+ Usage: 
10+     python3 convert.py input.png output.icns [--min-size SIZE] [--max-size SIZE] 
11+ """ 
12+ 
13+ import  sys 
14+ import  os 
15+ import  argparse 
16+ import  tempfile 
17+ import  shutil 
18+ import  subprocess 
19+ from  PIL  import  Image 
20+ 
21+ def  create_icns (png_path , icns_path , min_size = 16 , max_size = None ):
22+     """ 
23+     Convert a PNG image to ICNS format using iconset method. 
24+      
25+     Args: 
26+         png_path (str): Path to the input PNG file 
27+         icns_path (str): Path for the output ICNS file 
28+         min_size (int): Minimum size for the icon (default: 16) 
29+         max_size (int): Maximum size for the icon (default: original image size) 
30+     """ 
31+     # Open the source image 
32+     img  =  Image .open (png_path )
33+     
34+     # Determine max size if not provided 
35+     if  max_size  is  None :
36+         max_size  =  min (img .width , img .height )
37+     
38+     # Ensure the image is square 
39+     if  img .width  !=  img .height :
40+         print ("Warning: Image is not square. Cropping to square." )
41+         min_dimension  =  min (img .width , img .height )
42+         left  =  (img .width  -  min_dimension ) //  2 
43+         top  =  (img .height  -  min_dimension ) //  2 
44+         right  =  left  +  min_dimension 
45+         bottom  =  top  +  min_dimension 
46+         img  =  img .crop ((left , top , right , bottom ))
47+     
48+     # Create a temporary directory for iconset 
49+     with  tempfile .TemporaryDirectory () as  tmp_dir :
50+         iconset_dir  =  os .path .join (tmp_dir , "iconset.iconset" )
51+         os .makedirs (iconset_dir )
52+         
53+         # Define standard sizes for ICNS (based on Apple's specifications) 
54+         standard_sizes  =  [16 , 32 , 64 , 128 , 256 , 512 , 1024 ]
55+         
56+         # Generate icons for standard sizes within our range 
57+         generated_sizes  =  []
58+         for  size  in  standard_sizes :
59+             if  min_size  <=  size  <=  max_size :
60+                 # Create a copy of the image and resize it 
61+                 resized_img  =  img .copy ().resize ((size , size ), Image .LANCZOS )
62+                 
63+                 # Save as PNG in iconset directory 
64+                 filename  =  f"icon_{ size } { size }  
65+                 resized_img .save (os .path .join (iconset_dir , filename ), "PNG" )
66+                 generated_sizes .append (size )
67+                 print (f"Generated size: { size } { size }  )
68+                 
69+                 # For specific sizes, also generate retina versions 
70+                 retina_pairs  =  {16 : 32 , 32 : 64 , 128 : 256 , 256 : 512 , 512 : 1024 }
71+                 if  size  in  retina_pairs  and  retina_pairs [size ] <=  max_size :
72+                     retina_size  =  retina_pairs [size ]
73+                     retina_img  =  img .copy ().resize ((retina_size , retina_size ), Image .LANCZOS )
74+                     retina_filename  =  f"icon_{ size } { size }  
75+                     retina_img .save (os .path .join (iconset_dir , retina_filename ), "PNG" )
76+                     generated_sizes .append (retina_size )
77+                     print (f"Generated retina size: { retina_size } { retina_size }  )
78+         
79+         # Make sure we include max_size if it's not already included 
80+         if  max_size  not  in generated_sizes :
81+             resized_img  =  img .copy ().resize ((max_size , max_size ), Image .LANCZOS )
82+             filename  =  f"icon_{ max_size } { max_size }  
83+             resized_img .save (os .path .join (iconset_dir , filename ), "PNG" )
84+             generated_sizes .append (max_size )
85+             print (f"Generated max size: { max_size } { max_size }  )
86+         
87+         # Use iconutil to create ICNS file (macOS only) 
88+         try :
89+             subprocess .run (["iconutil" , "-c" , "icns" , iconset_dir , "-o" , icns_path ], check = True )
90+             print (f"Successfully converted { png_path } { icns_path }  )
91+             print (f"Generated sizes: { sorted (set (generated_sizes ))}  )
92+         except  subprocess .CalledProcessError  as  e :
93+             print (f"Error creating ICNS with iconutil: { e }  )
94+             print ("Falling back to Pillow method..." )
95+             # Fallback to Pillow method if iconutil fails 
96+             fallback_method (iconset_dir , icns_path )
97+ 
98+ def  fallback_method (iconset_dir , icns_path ):
99+     """ 
100+     Fallback method using Pillow if iconutil is not available 
101+     """ 
102+     icon_files  =  os .listdir (iconset_dir )
103+     if  not  icon_files :
104+         print ("No icons generated, cannot create ICNS file" )
105+         return 
106+         
107+     # Find the largest icon file to use as main image 
108+     icon_paths  =  [os .path .join (iconset_dir , f ) for  f  in  icon_files ]
109+     largest_icon  =  max (icon_paths , key = lambda  p : Image .open (p ).size [0 ])
110+     
111+     # Open the largest icon as main image 
112+     main_img  =  Image .open (largest_icon )
113+     
114+     # Create list of additional images 
115+     append_images  =  []
116+     for  icon_path  in  icon_paths :
117+         if  icon_path  !=  largest_icon :
118+             append_images .append (Image .open (icon_path ))
119+     
120+     # Save as ICNS 
121+     if  append_images :
122+         main_img .save (
123+             icns_path ,
124+             format = 'ICNS' ,
125+             append_images = append_images 
126+         )
127+     else :
128+         main_img .save (icns_path , format = 'ICNS' )
129+     
130+     print (f"Successfully converted using fallback method to { icns_path }  )
131+ 
132+ def  main ():
133+     parser  =  argparse .ArgumentParser (description = "Convert PNG to ICNS format" )
134+     parser .add_argument ("input" , help = "Input PNG file path" )
135+     parser .add_argument ("output" , help = "Output ICNS file path" )
136+     parser .add_argument ("--min-size" , type = int , default = 16 , help = "Minimum icon size (default: 16)" )
137+     parser .add_argument ("--max-size" , type = int , help = "Maximum icon size (default: original image size)" )
138+     
139+     args  =  parser .parse_args ()
140+     
141+     png_path  =  args .input 
142+     icns_path  =  args .output 
143+     
144+     # Check if input file exists 
145+     if  not  os .path .exists (png_path ):
146+         print (f"Error: Input file '{ png_path }  )
147+         sys .exit (1 )
148+     
149+     # Check if input file is a PNG 
150+     if  not  png_path .lower ().endswith ('.png' ):
151+         print ("Error: Input file must be a PNG image." )
152+         sys .exit (1 )
153+     
154+     try :
155+         create_icns (png_path , icns_path , args .min_size , args .max_size )
156+     except  Exception  as  e :
157+         print (f"Error converting image: { e }  )
158+         sys .exit (1 )
159+ 
160+ if  __name__  ==  "__main__" :
161+     main ()
0 commit comments