I'm a stickler for including icons for all policies available in Jamf Pro's Self Service app. They help users find items in Self Service, and generally make the app easier to use.
However, I don't like manually extracting icons from apps. It's easy enough with a tool like SAP's Icons app, but if I'm automating package and policy creation with AutoPkg, I should similarly be able to automate icon creation, right?
I created the AppIconExtractor AutoPkg processor to fully automate this task.
At it's core, AppIconExtractor examines an app and exports its icon as a PNG image file.
More technically, it reads the CFBundleIconFile
property from an app's
Info.plist
and saves that image as a PNG file at the path of your choice.
Additionally, ApplIconExtractor
can create icon variations by compositing a
secondary image on top of the app's icon. This makes it simple to automatically
create a version of an icon with a destructive "red X" icon superimposed over
the app icon for use in uninstallation policies, or a version with an "update"
graphic for use in policies that update an app.
Add my recipes and install the Pillow library
First, you'll need my recipe repository available to your local AutoPkg
installation. Add it with autopkg repo-add haircut-recipes
.
AppIconExtractor requires installation of the Pillow Python library.
Pillow is used to convert and composite icons, and can be easily installed on the Mac you use to run AutoPkg. Use this command:
/usr/local/autopkg/python -m pip install --upgrade Pillow
Note that this installs the Pillow library within the path of AutoPkg's Python
framework. This is very important. If you just run pip
or pip3
without
the explicit path to AutoPkg's Python installation, AutoPkg won't be able to
find the library. Recipes will produce an error directing you to install Pillow
using the specifc command above.
With Pillow installed, you're ready to go.
Basic use
Using AppIconExtractor is as simple as including the processor as a step in a
recipe's Process dictionary. Use the shared processor syntax
to call com.github.haircut.processors/AppIconExtractor
.
It requires only one argument: source_app
, which is the path to the .app
from which to extract an icon. If the path to the app points inside a disk
image, that .dmg will be mounted automatically.
By default, the app's icon will be output to the recipe's cache directory as
%NAME%.png%
. You can optionally override this output path (and filename) by
setting the icon_output_path
argument.
A simple example in XML format might look like:
Process
Processor
com.github.haircut.processors/AppIconExtractor
Arguments
source_app
%RECIPE_CACHE_DIR%/CoolApp/CoolApp.app
This will extract the icon from "CoolApp.app" and save it as a 256px square PNG image to the recipe cache directory.
As mentioned, adding the icon_output_path
argument will give you additional
control over the output path and filename. Here's an example in YAML format:
Process:
- Processor: com.github.haircut.processors/AppIconExtractor
Arguments:
source_app: "%RECIPE_CACHE_DIR%/CoolApp/CoolApp.app"
icon_output_path: "%RECIPE_CACHE_DIR%/Icons/Icon-%NAME%.png"
Generating composited variations
Beyond extracting the app's icon, AppIconExtractor can also create variation images by compositing a "template image" on top of the app icon.
The processor can output variations for an "uninstall," "update," and "install" version of the app icon.
To generate a variation, add a processor argument to set an output path for that variation. Use one or more of the following arguments:
composite_install_path
composite_update_path
composite_uninstall_path
Omit any variations you don't want to generate. The processor will only create the variations you request be specify an output path.
If you specify only output paths for variations, AppIconExtractor will use sensible defaults to composite suitable icons. The default templates are glyphs from SF Symbols that will work well in most situations. Each template is 64px in size, and looks nice in the corner.
These templates are encoded within the processor; you don't need to do anything to use these defaults!
Here's an example in YAML format:
Process:
- Processor: com.github.haircut.processors/AppIconExtractor
Arguments:
source_app: "%RECIPE_CACHE_DIR%/CoolApp/CoolApp.app"
icon_output_path: "%RECIPE_CACHE_DIR%/Icons/Icon-%NAME%.png"
composite_update_path: "%RECIPE_CACHE_DIR%/Icons/Update-%NAME%.png"
composite_uninstall_path: ""%RECIPE_CACHE_DIR%/Icons/Uninstall-%NAME%.png"
Notice that we included arguments for the "update" and "uninstall" variations,
but did not set the composite_install_path
argument. This would output the
"bare" app icon as well as variations for "update" and "uninstall" – but no
"install" variation, since we omitted that argument.
Custom templates
If you don't like the default variation templates, you can use your own by
setting composite_install_template
, composite_update_template
and/or
composite_unsinstall_template
. Each argument should be the path to alternative
template image to use for that variation.
AppIconExtractor will calculate the size of the template image at the path you
specify and correctly anchor that template to the composite_position
(see
"Padding and position" below).
Here's an example of using a custom template to generate an "uninstall" variation in XML format:
Process
Processor
com.github.haircut.processors/AppIconExtractor
Arguments
source_app
%RECIPE_CACHE_DIR%/CoolApp/CoolApp.app
composite_uninstall_template
%RECIPE_DIR%/radical-flame.png
composite_uninstall_path
%RECIPE_CACHE_DIR%/delete_%NAME%.png
Padding and position
AppIconExtractor includes a few additional options to customize your composited icon variations.
composite_padding
: sets the number of pixels from the edge of the image the superimposed template image is offset. Defaults to10
pixels.composite_position
: sets the corner to which the superimposed template image for composited variations is anchored. Defaults tobr
for the bottom-right corner. You can change this tobl
(bottom left),ur
(upper right), or ul (upper left) if you prefer.
Combinations of these options are shown below with the padding highlighted in pink:
Clockwise from the upper left, this example shows:
composite_padding
omitted (so it defaults to10
) andcomposite_position
oful
.composite_padding
of20
andcomposite_position
ofur
.composite_padding
of0
andcomposite_position
omitted (so it defaults tobr
).composite_padding
of5
andcomposite_position
ofbl
.
Setting these options applies the same settings to all composited variations. This is an intentional design choice to keep the input arguments – and thus the required code – more manageable.
Output variables
AppIconExtractor sets the path(s) to the extracted app icon, and any composited variations, as output variables during an AutoPkg run. This means you can extract and generate icons, then immediately use those icons in subsequent processors like JamfPolicyUploader.
The following output variables are set if (and only if) the associated variations are requested:
app_icon_path
: path to the extracted, unmodified app icon. Always set.install_icon_path
path to the composited "install" variation. Only set if this variations is requested.update_icon_path
path to the composited "update" variation. Only set if this variations is requested.uninstall_icon_path
path to the composited "uninstall" variation. Only set if this variations is requested.
Example uses in recipes
Here are two examples of using AppIconExtractor in a child recipe or override.
Extract the icon from an available .app bundle
Greg Neagle's recipe for Sublime Text 4 leaves the unarchived
.app available in the recipe cache dir at %RECIPE_CACHE_DIR%/%NAME%/Sublime Text.app
. We'll use this to extract the Sublime Text icon using
AppIconExtractor's default settings.
Process:
- Processor: com.github.haircut.processors/AppIconExtractor
Arguments:
source_app: "%RECIPE_CACHE_DIR%/%NAME%/Sublime Text.app"
- Processor: com.github.grahampugh.jamf-upload.processors/JamfPolicyUploader
Arguments:
policy_template: "%POLICY_TEMPLATE%"
policy_name: "%POLICY_NAME%"
icon: "%app_icon_path%"
replace_icon: True
This extracts Sublime Text's icon without generating any composite variations,
then feeds that extracted icon to the JamfPolicyUploader
processor. We set
replace_icon
to True
to ensure any change to the icon by the vendor is
automatically reflected within our Jamf policy.
Unpacking a .pkg to extract an icon
The recipe for the Google Chrome Enterprise package downloads a
.pkg directly from the vendor, so no repackaging is needed. And while the
AutoPkg recipe unpacks the package to performe code signature verification, it
then runs the PathDeleter
processor to clean up that operation. This means a
child recipe does not have access to a .app from which to extract an icon.
That means we'll need to do a little more work to unpack the package again so that we can get to the app bundle. We'll also generate a custom "uninstall" variations and override the default composition position and padding.
Here's the Process of this more complex example in XML format:
Process
Processor
FlatPkgUnpacker
Arguments
destination_path
%RECIPE_CACHE_DIR%/unpack
flat_pkg_path
%pkg_path%
Processor
PkgPayloadUnpacker
Arguments
destination_path
%RECIPE_CACHE_DIR%/unpack/pkgpayload
pkg_payload_path
%RECIPE_CACHE_DIR%/unpack/GoogleChrome.pkg/Payload
Processor
com.github.haircut.processors/AppIconExtractor
Arguments
composite_padding
20
composite_position
ul
composite_uninstall_path
%RECIPE_CACHE_DIR%/Icon-Uninstall-%NAME%.png
composite_uninstall_template
/Users/haircut/Documents/delete.png
icon_output_path
%RECIPE_CACHE_DIR%/Icon-%NAME%.png
source_app
%RECIPE_CACHE_DIR%/unpack/pkgpayload/Google Chrome.app
Processor
PathDeleter
Arguments
path_list
%RECIPE_CACHE_DIR%/unpack
Processor
com.github.grahampugh.jamf-upload.processors/JamfPolicyUploader
Arguments
icon
%app_icon_path%
policy_name
Install %NAME%
policy_template
Self-Service-Policy.xml
replace_icon
Processor
com.github.grahampugh.jamf-upload.processors/JamfPolicyUploader
Arguments
icon
%uninstall_icon_path%
policy_name
Uninstall %NAME%
policy_template
Uninstall-Policy.xml
replace_icon
This unpacks the Google Chrome enterprise package, extracts the unmodified app icon, and generates an "uninstall" composite version with a custom graphic in the upper left corner with 20px of padding.
The outputs of AppIconExtractor are then used as inputs to
JamfPolicyUploader process runs to set the icons for two different policies.
Setting the replace_icon
argument to True
ensures that any changes to the
icons are reflected on the Jamf Pro policies.
Hopefully this processor will help you extract icons without the manual work, and spiff up those Self Service policies.
Pull requests accepted if you fix a bug or make an improvement!