├── requirements.in ├── .gitignore ├── screenshot.png ├── requirements.txt ├── LICENSE ├── README.md ├── templates └── index.html └── viewer.py /requirements.in: -------------------------------------------------------------------------------- 1 | flask 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Asset-Package* 2 | __MACOSX 3 | 4 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexwlchan/aws-architecture-icon-browser/main/screenshot.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile 6 | # 7 | click==8.0.1 8 | # via flask 9 | flask==2.0.1 10 | # via -r requirements.in 11 | itsdangerous==2.0.1 12 | # via flask 13 | jinja2==3.0.1 14 | # via flask 15 | markupsafe==2.0.1 16 | # via jinja2 17 | werkzeug==2.0.1 18 | # via flask 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Alex Chan 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 17 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 18 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 19 | OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aws-architecture-icon-browser 2 | 3 | AWS publish the [AWS Architecture Icons][icons], a collection of product and service icons that you can use in architecture diagrams. 4 | I normally download the asset package, which contains PNG and SVG icons. 5 | 6 | This is a small web app that lets me look for icons by name, rather than poking through files and folders: 7 | 8 | ![A screenshot of the browser. There are three headings 'Architecture' 'Category' and 'Resource', and a filter applied for the keyword 'network', which shows a collection of red, orange and purple icons. Icons are shown on the left-hand side, with their name and a list of sizes on the right.](screenshot.png) 9 | 10 | Features: 11 | 12 | - Display all the icons in a list 13 | - Show me all the different sizes of icon 14 | - Let me search for icons, and see all the matching icons together 15 | 16 | [icons]: https://aws.amazon.com/architecture/icons/ 17 | 18 | 19 | 20 | ## Motivation 21 | 22 | I'm sure there are better ways to look at an icon collection if you use professional diagramming software, but I don't have much experience in those tools. 23 | I'm more used to making simple diagrams as hand-written SVGs, or dropping a few icons into a slide deck. 24 | (I use OmniGraffle, but only about 1% of what it can do.) 25 | 26 | I threw this together in a pinch while trying to find icons for a blog post, so the code is a bit slapdash. 27 | Use accordingly. 28 | (Apparently there's no icon for CloudWatch Metrics?) 29 | 30 | 31 | 32 | ## Usage 33 | 34 | This tool needs Python 3. 35 | Once you have Python 3 installed, to run this app: 36 | 37 | ``` 38 | # Clone the repo 39 | git clone https://github.com/alexwlchan/aws-architecture-icon-browser.git 40 | cd aws-architecture-icon-browser 41 | 42 | # Install requirements 43 | pip3 install -r requirements.txt 44 | 45 | # Run the app 46 | python3 viewer.py 47 | ``` 48 | 49 | The app will automatically download a copy of the AWS Architecture Icons to your computer, then you'll then see the app running at . 50 | 51 | I did consider providing a hosted version, but I'm not sure if anybody else will even find this useful – and there might be copyright issues with serving the icons from my website that I don't want to think about right now. 52 | 53 | 54 | 55 | ## License 56 | 57 | MIT. 58 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | AWS Architecture icon browser 2 | 3 | 67 | 68 |
69 | 102 | 103 |

A browser for the AWS Architecture Icons.

104 | 105 | 106 | 107 |
108 | 109 |
110 | 111 | {% for title, icon_collection in [ 112 | ("Architecture", architecture_icons), 113 | ("Category", category_icons), 114 | ("Resource", resource_icons) 115 | ] %} 116 |

{{ title }}

117 | 118 | 147 | {% endfor %} 148 | -------------------------------------------------------------------------------- /viewer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import collections 4 | import colorsys 5 | import os 6 | import re 7 | import urllib.request 8 | import zipfile 9 | 10 | from flask import abort, Flask, render_template, send_file 11 | 12 | 13 | # From https://aws.amazon.com/architecture/icons/ 14 | ASSET_PACKAGE_URL = "https://d1.awsstatic.com/webteam/architecture-icons/q3-2021/Asset-Package_07302021.533e2e5c12d0759fd00ce35fa70d8418a26f4a90.zip" 15 | 16 | 17 | app = Flask(__name__) 18 | 19 | 20 | def ensure_asset_package_downloaded(): 21 | """ 22 | Ensures an asset package is downloaded and unpacker in the 23 | current directory. 24 | 25 | Returns the name of the asset package directory. 26 | """ 27 | asset_package_dirname = os.path.basename(ASSET_PACKAGE_URL).split(".")[0] 28 | if os.path.isdir(asset_package_dirname): 29 | return asset_package_dirname 30 | 31 | filename = os.path.basename(ASSET_PACKAGE_URL) 32 | print(f"Downloading asset package to {filename}") 33 | urllib.request.urlretrieve(ASSET_PACKAGE_URL, filename) 34 | 35 | with zipfile.ZipFile(filename) as zf: 36 | zf.extractall() 37 | 38 | return asset_package_dirname 39 | 40 | 41 | def get_file_paths_under(root="."): 42 | """Generates the paths to every file under ``root``.""" 43 | if not os.path.isdir(root): 44 | raise ValueError(f"Cannot find files under non-existent directory: {root!r}") 45 | 46 | for dirpath, _, filenames in os.walk(root): 47 | for f in filenames: 48 | if f == ".DS_Store": 49 | continue 50 | 51 | if os.path.isfile(os.path.join(dirpath, f)): 52 | yield os.path.join(dirpath, f) 53 | 54 | 55 | @app.template_filter("description") 56 | def asset_description(path): 57 | """ 58 | Given the path to an asset, return a short description. 59 | """ 60 | # Filenames are of the form 61 | # 62 | # Arch_AWS-App-Mesh_48.svg 63 | # Arch_AWS-App-Mesh_64@5x.png 64 | # Res_AWS-App-Mesh-Mesh_48_Light.png 65 | # 66 | filename = os.path.basename(path) 67 | 68 | size = filename.split("_")[-1].split(".")[0] 69 | image_format = filename.split(".")[-1] 70 | 71 | return f"{image_format} ({size})" 72 | 73 | 74 | @app.template_filter("by_size") 75 | def assets_by_size(assets): 76 | """ 77 | Given a list of assets, return a list of them by format and size. 78 | """ 79 | asset_filenames = {path: os.path.basename(path) for path in assets} 80 | 81 | # Filenames are of the form 82 | # 83 | # Arch_AWS-App-Mesh_48.svg 84 | # Arch_AWS-App-Mesh_64@5x.png 85 | # Res_AWS-App-Mesh-Mesh_48_Light.png 86 | # 87 | assets_by_size = { 88 | path: int( 89 | filename.replace("_Light", "") 90 | .replace("_Dark", "") 91 | .split("_")[-1] 92 | .split(".")[0] 93 | .replace("64@5x", "320") 94 | ) 95 | for path, filename in asset_filenames.items() 96 | } 97 | 98 | for path in assets_by_size: 99 | if path.endswith(".svg"): 100 | assets_by_size[path] += 1 101 | 102 | return sorted(assets_by_size, key=lambda path: assets_by_size[path], reverse=True) 103 | 104 | 105 | @app.template_filter("highest_res") 106 | def highest_res_asset(assets): 107 | """ 108 | Given a list of assets, return the one which is highest resolution. 109 | """ 110 | return assets_by_size(assets)[0] 111 | 112 | 113 | @app.route("/") 114 | def index(): 115 | paths = list(get_file_paths_under(asset_package_dirname)) 116 | 117 | architecture_icons = collections.defaultdict(list) 118 | category_icons = collections.defaultdict(list) 119 | resource_icons = collections.defaultdict(list) 120 | 121 | for p in paths: 122 | name = ( 123 | os.path.basename(p) 124 | .replace("_Light", "") 125 | .replace("_Dark", "") 126 | .rsplit("_", 1)[0] 127 | .replace("-", " ") 128 | ) 129 | 130 | if "/Architecture-" in p: 131 | architecture_icons[name.replace("Arch_", "")].append(p) 132 | 133 | if "/Category-" in p: 134 | category_icons[name.replace("Arch Category_", "")].append(p) 135 | 136 | if "/Resource-" in p: 137 | resource_icons[name.replace("Res_", "").replace("_", ": ")].append(p) 138 | 139 | return render_template( 140 | "index.html", 141 | paths=paths, 142 | architecture_icons=architecture_icons, 143 | category_icons=category_icons, 144 | resource_icons=resource_icons, 145 | ) 146 | 147 | 148 | @app.route("/") 149 | def serve_asset(path): 150 | if path.startswith("Asset-Package_"): 151 | return send_file(path) 152 | else: 153 | return abort(404) 154 | 155 | 156 | @app.template_filter("tint_color") 157 | def choose_color_from_path(assets): 158 | """ 159 | Makes a rough guess about the colour of this icon. 160 | """ 161 | path = next(p for p in assets if p.endswith(".svg")) 162 | 163 | with open(path) as infile: 164 | hex_strings = set(re.findall(r"#[A-F0-9a-f]{6}", infile.read())) 165 | 166 | colors = { 167 | ( 168 | int(hs[1:3], 16) / 255, 169 | int(hs[3:5], 16) / 255, 170 | int(hs[5:7], 16) / 255, 171 | ): hs # red-green-blue 172 | for hs in hex_strings 173 | } 174 | 175 | colors_to_hls = {(r, g, b): colorsys.rgb_to_hls(r, g, b) for (r, g, b) in colors} 176 | 177 | # Remove any colors which are too light to have sufficient contrast 178 | usable_colors = { 179 | (r, g, b): (h, lightness, s) 180 | for (r, g, b), (h, lightness, s) in colors_to_hls.items() 181 | if lightness <= 0.8 182 | } 183 | 184 | try: 185 | most_saturated_color = max(usable_colors, key=lambda c: usable_colors[c][2]) 186 | except ValueError: 187 | # no usable color, just use black 188 | return "#000" 189 | 190 | return colors[most_saturated_color] 191 | 192 | 193 | if __name__ == "__main__": 194 | asset_package_dirname = ensure_asset_package_downloaded() 195 | 196 | app.run(debug=True, port=2520) 197 | --------------------------------------------------------------------------------