mirror of
https://github.com/kovidgoyal/calibre.git
synced 2026-05-08 10:32:28 +00:00
macOS Tahoe: Have the calibre app icon automatically use a dark look when the system is in dark mode
Uses the new Liquid Glass infrastructure to generate the application icons using actool which means icons have to be generated on a mac, but that is done via a one-time script not per build. I could change it to be done per build, but not needed and doing it that way makes iterating on icon design too slow.
This commit is contained in:
+2
-1
@@ -35,7 +35,8 @@
|
||||
/resources/mozilla-ca-certs.pem
|
||||
/resources/user-agent-data.json
|
||||
/resources/piper-voices.json
|
||||
/icons/icns/*/*.iconset
|
||||
/icons/icns/*.icns
|
||||
/icons/icns/*.car
|
||||
/setup/installer/windows/calibre/build.log
|
||||
/setup/pyqt_enums
|
||||
/tags
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
# Requires installation of XCode 16.2 and
|
||||
# python3 -m pip install certifi html5lib icnsutil
|
||||
# python3 -m pip install certifi html5lib
|
||||
|
||||
vm_name 'macos-calibre'
|
||||
root '/Users/Shared/calibre-build'
|
||||
|
||||
@@ -387,17 +387,14 @@ class Freeze:
|
||||
|
||||
@flush
|
||||
def create_skeleton(self):
|
||||
print('Creating skeleton')
|
||||
c = join(self.build_dir, 'Contents')
|
||||
for x in ('Frameworks', 'MacOS', 'Resources'):
|
||||
os.makedirs(join(c, x))
|
||||
icons = glob.glob(join(CALIBRE_DIR, 'icons', 'icns', 'light', '*.iconset'))
|
||||
if not icons:
|
||||
raise SystemExit('Failed to find icns format icons')
|
||||
for x in icons:
|
||||
output_icns = join(self.resources_dir, basename(x).partition('.')[0] + '.icns')
|
||||
xd = x.replace('/light/', '/dark/')
|
||||
assert x != xd
|
||||
generate_icns(x, xd, output_icns)
|
||||
icns_dir = join(CALIBRE_DIR, 'icons', 'icns')
|
||||
shutil.copy(join(icns_dir, 'Assets.car'), self.resources_dir)
|
||||
for x in glob.glob(join(icns_dir, '*.icns')):
|
||||
shutil.copy(x, self.resources_dir)
|
||||
for helpers in (self.helpers_dir,):
|
||||
os.makedirs(helpers)
|
||||
cdir = dirname(helpers)
|
||||
@@ -421,7 +418,8 @@ class Freeze:
|
||||
LSMinimumSystemVersion=MINIMUM_SYSTEM_VERSION,
|
||||
LSRequiresNativeExecution=True,
|
||||
NSAppleScriptEnabled=False,
|
||||
CFBundleIconFile='',
|
||||
CFBundleIconFile='book.icns',
|
||||
CFBundleIconName='book',
|
||||
)
|
||||
with open(join(cdir, 'Info.plist'), 'wb') as p:
|
||||
plistlib.dump(pl, p)
|
||||
@@ -475,6 +473,7 @@ class Freeze:
|
||||
NSHumanReadableCopyright=time.strftime('Copyright %Y, Kovid Goyal'),
|
||||
CFBundleGetInfoString=('calibre, an E-book management '
|
||||
'application. Visit https://calibre-ebook.com for details.'),
|
||||
CFBundleIconName='calibre',
|
||||
CFBundleIconFile='calibre.icns',
|
||||
NSHighResolutionCapable=True,
|
||||
LSApplicationCategoryType='public.app-category.productivity',
|
||||
@@ -778,6 +777,7 @@ class Freeze:
|
||||
}[launcher]
|
||||
plist['CFBundleExecutable'] = launcher
|
||||
plist['CFBundleIdentifier'] = 'com.calibre-ebook.' + launcher
|
||||
plist['CFBundleIconName'] = launcher
|
||||
plist['CFBundleIconFile'] = launcher + '.icns'
|
||||
e = plist['CFBundleDocumentTypes'][0]
|
||||
e['CFBundleTypeExtensions'] = [x.lower() for x in formats]
|
||||
|
||||
+126
-44
@@ -4,20 +4,77 @@
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import xml.etree.ElementTree as ET
|
||||
from copy import deepcopy
|
||||
|
||||
d, j, a = (getattr(os.path, x) for x in ('dirname', 'join', 'abspath'))
|
||||
base = d(a(__file__))
|
||||
os.chdir(base)
|
||||
|
||||
# To generate this template create an icon using Icon Composer on macOS and in
|
||||
# the saved .icon (which is a folder) look for icon.js
|
||||
icon_settings = {
|
||||
'fill-specializations' : [
|
||||
{
|
||||
'value' : {
|
||||
'automatic-gradient' : 'extended-gray:1.00000,1.00000'
|
||||
}
|
||||
},
|
||||
{
|
||||
'appearance' : 'dark',
|
||||
'value' : {
|
||||
'automatic-gradient' : 'display-p3:0.20500,0.20500,0.20500,1.00000'
|
||||
}
|
||||
}
|
||||
],
|
||||
'groups' : [
|
||||
{
|
||||
'layers' : [
|
||||
{
|
||||
'blend-mode' : 'normal',
|
||||
'glass' : False,
|
||||
'hidden' : False,
|
||||
'image-name' : 'icon.svg',
|
||||
'name' : 'icon',
|
||||
'opacity' : 1,
|
||||
'position' : {
|
||||
'scale' : 0.9,
|
||||
'translation-in-points' : [
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
'shadow' : {
|
||||
'kind' : 'neutral',
|
||||
'opacity' : 0.5
|
||||
},
|
||||
'translucency' : {
|
||||
'enabled' : True,
|
||||
'value' : 0.5
|
||||
}
|
||||
}
|
||||
],
|
||||
'supported-platforms' : {
|
||||
'circles' : [
|
||||
'watchOS'
|
||||
],
|
||||
'squares' : 'shared'
|
||||
}
|
||||
}
|
||||
|
||||
imgsrc = j(d(d(base)), 'imgsrc')
|
||||
sources = {'calibre':j(imgsrc, 'calibre.svg'), 'ebook-edit':j(imgsrc, 'tweak.svg'), 'ebook-viewer':j(imgsrc, 'viewer.svg'), 'book':j(imgsrc, 'book.svg')}
|
||||
if sys.argv[-1] == 'only-logo':
|
||||
sources = {'calibre':sources['calibre']}
|
||||
sources = {
|
||||
'calibre':j(imgsrc, 'calibre.svg'),
|
||||
'ebook-edit':j(imgsrc, 'tweak.svg'),
|
||||
'ebook-viewer':j(imgsrc, 'viewer.svg'),
|
||||
'book':j(imgsrc, 'book.svg')
|
||||
}
|
||||
|
||||
frames = {
|
||||
'light': j(imgsrc, 'frame.svg'),
|
||||
@@ -25,47 +82,72 @@ frames = {
|
||||
}
|
||||
|
||||
|
||||
def render_svg(src, sz, dest):
|
||||
subprocess.check_call(['rsvg-convert', src, '-w', str(sz), '-h', str(sz), '-o', dest])
|
||||
def get_svg_viewbox(file_path: str) -> tuple[float, ...]:
|
||||
tree = ET.parse(file_path)
|
||||
root = tree.getroot()
|
||||
viewbox = root.get('viewBox')
|
||||
if viewbox:
|
||||
return tuple(float(x) for x in viewbox.split())
|
||||
width = root.get('width')
|
||||
height = root.get('height')
|
||||
return [0.0, 0.0, float(width or 0), float(height or 0)]
|
||||
|
||||
|
||||
with tempfile.TemporaryDirectory() as tdir:
|
||||
def create_icon(name: str, svg_path: str, output_path: str) -> str:
|
||||
view_box = get_svg_viewbox(svg_path)
|
||||
sz = view_box[-1]
|
||||
scale = 0.9 * 1024 / sz
|
||||
icon_dir = os.path.join(output_path, f'{name}.icon')
|
||||
if os.path.exists(icon_dir):
|
||||
shutil.rmtree(icon_dir)
|
||||
os.mkdir(icon_dir)
|
||||
s = deepcopy(icon_settings)
|
||||
for group in s['groups']:
|
||||
for layer in group['layers']:
|
||||
layer['image-name'] = os.path.basename(svg_path)
|
||||
layer['name'] = name
|
||||
layer['position']['scale'] = scale
|
||||
with open(os.path.join(icon_dir, 'icon.json'), 'w') as f:
|
||||
json.dump(s, f, indent=2)
|
||||
assets_dir = os.path.join(icon_dir, 'Assets')
|
||||
os.mkdir(assets_dir)
|
||||
shutil.copy(svg_path, assets_dir)
|
||||
return icon_dir
|
||||
|
||||
def render_frame(mode: str, sz: int):
|
||||
f = os.path.join(tdir, f'frame-{mode}-{sz}.png')
|
||||
if not os.path.exists(f):
|
||||
render_svg(frames[mode], sz, f)
|
||||
return f
|
||||
|
||||
def render_framed(mode: str, sz: int, iname: str, shrink_factor: float = 0.75):
|
||||
frame = render_frame(mode, sz)
|
||||
icon = os.path.join(tdir, f'icon-{sz}.png')
|
||||
render_svg(src, int(shrink_factor * sz), icon)
|
||||
subprocess.check_call(f'magick {frame} {icon} -gravity center -compose over -composite {iname}'.split())
|
||||
def create_assets():
|
||||
os.chdir(base)
|
||||
actool = [
|
||||
'xcrun', 'actool', '--warnings', '--platform', 'macosx', '--compile', '.',
|
||||
'--minimum-deployment-target', '15.0', '--output-partial-info-plist', '/dev/stdout',
|
||||
]
|
||||
primary = ''
|
||||
icons = []
|
||||
alternates = []
|
||||
for name, svg in sources.items():
|
||||
if not primary:
|
||||
primary = name
|
||||
icons.append(create_icon(name, svg, os.getcwd()))
|
||||
if name != primary:
|
||||
alternates.extend(('--alternate-app-icon', name))
|
||||
# Generate .icns for backwards compat
|
||||
subprocess.check_call(actool + ['--app-icon', name, icons[-1]])
|
||||
os.remove('Assets.car')
|
||||
subprocess.check_call([
|
||||
'xcrun', 'actool', '--warnings', '--platform', 'macosx', '--compile', '.',
|
||||
'--minimum-deployment-target', '15.0', '--output-partial-info-plist', '/dev/stdout',
|
||||
'--app-icon', primary] + alternates + icons)
|
||||
for x in icons:
|
||||
shutil.rmtree(x)
|
||||
|
||||
for mode in ('light', 'dark'):
|
||||
subdir = mode
|
||||
os.makedirs(subdir, exist_ok=True)
|
||||
for name, src in sources.items():
|
||||
iconset = j(subdir, name + '.iconset')
|
||||
if os.path.exists(iconset):
|
||||
shutil.rmtree(iconset)
|
||||
os.mkdir(iconset)
|
||||
os.chdir(iconset)
|
||||
try:
|
||||
for sz in (16, 32, 64, 128, 256, 512, 1024):
|
||||
iname = f'icon_{sz}x{sz}.png'
|
||||
iname2x = f'icon_{sz // 2}x{sz // 2}@2x.png'
|
||||
if sz < 128:
|
||||
render_svg(src, sz, iname)
|
||||
else:
|
||||
render_framed(mode, sz, iname)
|
||||
subprocess.check_call(['optipng', '-o7', '-strip', 'all', iname])
|
||||
if sz > 16:
|
||||
shutil.copy2(iname, iname2x)
|
||||
if sz > 512 or sz == 64:
|
||||
os.remove(iname)
|
||||
if sz == 128:
|
||||
os.remove(iname2x)
|
||||
finally:
|
||||
os.chdir('../..')
|
||||
|
||||
def main():
|
||||
if 'darwin' in sys.platform.lower():
|
||||
create_assets()
|
||||
else:
|
||||
subprocess.check_call(['ssh', 'ox', 'sh', '-c', '~/bin/update-calibre && python3 ~/calibre-src/icons/icns/make_iconsets.py'])
|
||||
subprocess.check_call(['rsync', '-avz', '--include=*.icns', '--include=*.car', '--exclude=*', 'ox:~/calibre-src/icons/icns/', base + '/'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="1024"
|
||||
height="1024"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg13"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<g
|
||||
filter="url(#a)"
|
||||
id="g2">
|
||||
<rect
|
||||
width="824"
|
||||
height="824"
|
||||
x="100"
|
||||
y="100"
|
||||
fill="#1c1c1e"
|
||||
rx="184"
|
||||
id="rect1" />
|
||||
<rect
|
||||
width="824"
|
||||
height="824"
|
||||
x="100"
|
||||
y="100"
|
||||
fill="url(#b)"
|
||||
rx="184"
|
||||
id="rect2" />
|
||||
</g>
|
||||
<defs
|
||||
id="defs13">
|
||||
<linearGradient
|
||||
id="b"
|
||||
x1="512"
|
||||
x2="512"
|
||||
y1="100"
|
||||
y2="924"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop
|
||||
stop-color="#fff"
|
||||
stop-opacity=".08"
|
||||
id="stop10" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-opacity="0"
|
||||
id="stop11" />
|
||||
</linearGradient>
|
||||
<filter
|
||||
id="a"
|
||||
width="868"
|
||||
height="868"
|
||||
x="78"
|
||||
y="89"
|
||||
color-interpolation-filters="sRGB"
|
||||
filterUnits="userSpaceOnUse">
|
||||
<feFlood
|
||||
flood-opacity="0"
|
||||
result="BackgroundImageFix"
|
||||
id="feFlood11" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
id="feColorMatrix11" />
|
||||
<feOffset
|
||||
dy="11"
|
||||
id="feOffset11" />
|
||||
<feGaussianBlur
|
||||
stdDeviation="11"
|
||||
id="feGaussianBlur11" />
|
||||
<feColorMatrix
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0"
|
||||
id="feColorMatrix12" />
|
||||
<feBlend
|
||||
in2="BackgroundImageFix"
|
||||
result="effect1_dropShadow"
|
||||
id="feBlend12" />
|
||||
<feBlend
|
||||
in="SourceGraphic"
|
||||
in2="effect1_dropShadow"
|
||||
result="shape"
|
||||
id="feBlend13" />
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,83 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="1024"
|
||||
height="1024"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg13"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<g
|
||||
filter="url(#a)"
|
||||
id="g2">
|
||||
<rect
|
||||
width="824"
|
||||
height="824"
|
||||
x="100"
|
||||
y="100"
|
||||
fill="#fff"
|
||||
rx="184"
|
||||
id="rect1" />
|
||||
<rect
|
||||
width="824"
|
||||
height="824"
|
||||
x="100"
|
||||
y="100"
|
||||
fill="url(#b)"
|
||||
rx="184"
|
||||
id="rect2" />
|
||||
</g>
|
||||
<defs
|
||||
id="defs13">
|
||||
<linearGradient
|
||||
id="b"
|
||||
x1="512"
|
||||
x2="512"
|
||||
y1="100"
|
||||
y2="924"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop
|
||||
stop-opacity="0"
|
||||
id="stop10" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-opacity=".2"
|
||||
id="stop11" />
|
||||
</linearGradient>
|
||||
<filter
|
||||
id="a"
|
||||
width="868"
|
||||
height="868"
|
||||
x="78"
|
||||
y="89"
|
||||
color-interpolation-filters="sRGB"
|
||||
filterUnits="userSpaceOnUse">
|
||||
<feFlood
|
||||
flood-opacity="0"
|
||||
result="BackgroundImageFix"
|
||||
id="feFlood11" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
id="feColorMatrix11" />
|
||||
<feOffset
|
||||
dy="11"
|
||||
id="feOffset11" />
|
||||
<feGaussianBlur
|
||||
stdDeviation="11"
|
||||
id="feGaussianBlur11" />
|
||||
<feColorMatrix
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.28 0"
|
||||
id="feColorMatrix12" />
|
||||
<feBlend
|
||||
in2="BackgroundImageFix"
|
||||
result="effect1_dropShadow"
|
||||
id="feBlend12" />
|
||||
<feBlend
|
||||
in="SourceGraphic"
|
||||
in2="effect1_dropShadow"
|
||||
result="shape"
|
||||
id="feBlend13" />
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -36,9 +36,10 @@ subprocess.check_call([sys.executable, j(icons, 'make_ico_files.py'), 'only-logo
|
||||
shutil.copy2(j(icons, 'library.ico'), j(srv, 'common', 'favicon.ico'))
|
||||
shutil.copy2(j(icons, 'library.ico'), j(srv, 'main/static/resources/img', 'favicon.ico'))
|
||||
shutil.copy2(j(icons, 'library.ico'), j(srv, 'open-books/drmfree/static/img', 'favicon.ico'))
|
||||
subprocess.check_call([sys.executable, j(icons, 'icns', 'make_iconsets.py'), 'only-logo'])
|
||||
|
||||
os.chdir(srv)
|
||||
subprocess.check_call(['git', 'commit', '-am', 'Update calibre favicons'])
|
||||
for s in 'main code open-books dl1'.split():
|
||||
subprocess.check_call(['./publish', s, 'update'])
|
||||
|
||||
subprocess.check_call([sys.executable, j(icons, 'icns', 'make_iconsets.py')])
|
||||
|
||||
Reference in New Issue
Block a user