├── .devcontainer ├── Dockerfile ├── devcontainer.json └── library-scripts │ ├── common-debian.sh │ └── node-debian.sh ├── .github └── workflows │ └── swift.yml ├── .gitignore ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── Silica │ ├── CGAffineTransform.swift │ ├── CGBitmapInfo.swift │ ├── CGColor.swift │ ├── CGColorRenderingIntent.swift │ ├── CGColorSpace.swift │ ├── CGContext.swift │ ├── CGDrawingMode.swift │ ├── CGFont.swift │ ├── CGImage.swift │ ├── CGImageDestination.swift │ ├── CGImageSource.swift │ ├── CGImageSourcePNG.swift │ ├── CGLineCap.swift │ ├── CGLineJoin.swift │ ├── CGPath.swift │ ├── CairoConvertible.swift │ └── UIKit │ ├── NSStringDrawing.swift │ ├── UIBezierPath.swift │ ├── UIColor.swift │ ├── UIFont.swift │ ├── UIGraphics.swift │ ├── UIImage.swift │ ├── UIRectCorner.swift │ └── UIViewContentMode.swift └── Tests └── SilicaTests ├── FontTests.swift ├── StyleKitTests.swift └── Utilities ├── Define.swift ├── TestAssets.swift ├── TestStyleKit.swift └── URLClient.swift /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # [Choice] Swift version: 2 | ARG VARIANT=6.0.3-jammy 3 | FROM swift:${VARIANT} 4 | 5 | # [Option] Install zsh 6 | ARG INSTALL_ZSH="true" 7 | # [Option] Upgrade OS packages to their latest versions 8 | ARG UPGRADE_PACKAGES="false" 9 | 10 | # Install needed packages and setup non-root user. Use a separate RUN statement to add your own dependencies. 11 | ARG USERNAME=vscode 12 | ARG USER_UID=1000 13 | ARG USER_GID=$USER_UID 14 | COPY library-scripts/common-debian.sh /tmp/library-scripts/ 15 | RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 16 | && /bin/bash /tmp/library-scripts/common-debian.sh "${INSTALL_ZSH}" "${USERNAME}" "${USER_UID}" "${USER_GID}" "${UPGRADE_PACKAGES}" "true" "true" \ 17 | && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* && rm -rf /tmp/library-scripts 18 | 19 | # Install Cairo 20 | RUN export DEBIAN_FRONTEND=noninteractive \ 21 | && apt-get update \ 22 | && apt-get install -y libcairo2-dev \ 23 | libfontconfig-dev \ 24 | libfreetype6-dev \ 25 | && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/swift 3 | { 4 | "name": "Swift", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | "args": { 8 | // Update the VARIANT arg to pick a Swift version 9 | "VARIANT": "6.0.3-jammy", 10 | // Options 11 | "NODE_VERSION": "lts/*", 12 | "UPGRADE_PACKAGES": "true" 13 | } 14 | }, 15 | "runArgs": [ 16 | "--cap-add=SYS_PTRACE", 17 | "--security-opt", 18 | "seccomp=unconfined" 19 | ], 20 | 21 | // Configure tool-specific properties. 22 | "customizations": { 23 | // Configure properties specific to VS Code. 24 | "vscode": { 25 | // Set *default* container specific settings.json values on container create. 26 | "settings": { 27 | "lldb.library": "/usr/lib/liblldb.so" 28 | }, 29 | 30 | // Add the IDs of extensions you want installed when the container is created. 31 | "extensions": [ 32 | "sswg.swift-lang" 33 | ] 34 | } 35 | }, 36 | 37 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 38 | // "forwardPorts": [], 39 | 40 | // Use 'postCreateCommand' to run commands after the container is created. 41 | // "postCreateCommand": "", 42 | 43 | // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 44 | "remoteUser": "vscode" 45 | } 46 | -------------------------------------------------------------------------------- /.devcontainer/library-scripts/common-debian.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | #------------------------------------------------------------------------------------------------------------- 3 | # Copyright (c) Microsoft Corporation. All rights reserved. 4 | # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. 5 | #------------------------------------------------------------------------------------------------------------- 6 | # 7 | # Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/common.md 8 | # Maintainer: The VS Code and Codespaces Teams 9 | # 10 | # Syntax: ./common-debian.sh [install zsh flag] [username] [user UID] [user GID] [upgrade packages flag] [install Oh My Zsh! flag] [Add non-free packages] 11 | 12 | set -e 13 | 14 | INSTALL_ZSH=${1:-"true"} 15 | USERNAME=${2:-"automatic"} 16 | USER_UID=${3:-"automatic"} 17 | USER_GID=${4:-"automatic"} 18 | UPGRADE_PACKAGES=${5:-"true"} 19 | INSTALL_OH_MYS=${6:-"true"} 20 | ADD_NON_FREE_PACKAGES=${7:-"false"} 21 | SCRIPT_DIR="$(cd $(dirname "${BASH_SOURCE[0]}") && pwd)" 22 | MARKER_FILE="/usr/local/etc/vscode-dev-containers/common" 23 | 24 | if [ "$(id -u)" -ne 0 ]; then 25 | echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' 26 | exit 1 27 | fi 28 | 29 | # Ensure that login shells get the correct path if the user updated the PATH using ENV. 30 | rm -f /etc/profile.d/00-restore-env.sh 31 | echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh 32 | chmod +x /etc/profile.d/00-restore-env.sh 33 | 34 | # If in automatic mode, determine if a user already exists, if not use vscode 35 | if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then 36 | USERNAME="" 37 | POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") 38 | for CURRENT_USER in ${POSSIBLE_USERS[@]}; do 39 | if id -u ${CURRENT_USER} > /dev/null 2>&1; then 40 | USERNAME=${CURRENT_USER} 41 | break 42 | fi 43 | done 44 | if [ "${USERNAME}" = "" ]; then 45 | USERNAME=vscode 46 | fi 47 | elif [ "${USERNAME}" = "none" ]; then 48 | USERNAME=root 49 | USER_UID=0 50 | USER_GID=0 51 | fi 52 | 53 | # Load markers to see which steps have already run 54 | if [ -f "${MARKER_FILE}" ]; then 55 | echo "Marker file found:" 56 | cat "${MARKER_FILE}" 57 | source "${MARKER_FILE}" 58 | fi 59 | 60 | # Ensure apt is in non-interactive to avoid prompts 61 | export DEBIAN_FRONTEND=noninteractive 62 | 63 | # Function to call apt-get if needed 64 | apt_get_update_if_needed() 65 | { 66 | if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then 67 | echo "Running apt-get update..." 68 | apt-get update 69 | else 70 | echo "Skipping apt-get update." 71 | fi 72 | } 73 | 74 | # Run install apt-utils to avoid debconf warning then verify presence of other common developer tools and dependencies 75 | if [ "${PACKAGES_ALREADY_INSTALLED}" != "true" ]; then 76 | 77 | package_list="apt-utils \ 78 | openssh-client \ 79 | gnupg2 \ 80 | dirmngr \ 81 | iproute2 \ 82 | procps \ 83 | lsof \ 84 | htop \ 85 | net-tools \ 86 | psmisc \ 87 | curl \ 88 | wget \ 89 | rsync \ 90 | ca-certificates \ 91 | unzip \ 92 | zip \ 93 | nano \ 94 | vim-tiny \ 95 | less \ 96 | jq \ 97 | lsb-release \ 98 | apt-transport-https \ 99 | dialog \ 100 | libc6 \ 101 | libgcc1 \ 102 | libkrb5-3 \ 103 | libgssapi-krb5-2 \ 104 | libicu[0-9][0-9] \ 105 | liblttng-ust[0-9] \ 106 | libstdc++6 \ 107 | zlib1g \ 108 | locales \ 109 | sudo \ 110 | ncdu \ 111 | man-db \ 112 | strace \ 113 | manpages \ 114 | manpages-dev \ 115 | init-system-helpers" 116 | 117 | # Needed for adding manpages-posix and manpages-posix-dev which are non-free packages in Debian 118 | if [ "${ADD_NON_FREE_PACKAGES}" = "true" ]; then 119 | # Bring in variables from /etc/os-release like VERSION_CODENAME 120 | . /etc/os-release 121 | sed -i -E "s/deb http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME} main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME} main contrib non-free/" /etc/apt/sources.list 122 | sed -i -E "s/deb-src http:\/\/(deb|httredir)\.debian\.org\/debian ${VERSION_CODENAME} main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME} main contrib non-free/" /etc/apt/sources.list 123 | sed -i -E "s/deb http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME}-updates main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME}-updates main contrib non-free/" /etc/apt/sources.list 124 | sed -i -E "s/deb-src http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME}-updates main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME}-updates main contrib non-free/" /etc/apt/sources.list 125 | sed -i "s/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main contrib non-free/" /etc/apt/sources.list 126 | sed -i "s/deb-src http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main contrib non-free/" /etc/apt/sources.list 127 | sed -i "s/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main contrib non-free/" /etc/apt/sources.list 128 | sed -i "s/deb-src http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main contrib non-free/" /etc/apt/sources.list 129 | # Handle bullseye location for security https://www.debian.org/releases/bullseye/amd64/release-notes/ch-information.en.html 130 | sed -i "s/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main contrib non-free/" /etc/apt/sources.list 131 | sed -i "s/deb-src http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main contrib non-free/" /etc/apt/sources.list 132 | echo "Running apt-get update..." 133 | apt-get update 134 | package_list="${package_list} manpages-posix manpages-posix-dev" 135 | else 136 | apt_get_update_if_needed 137 | fi 138 | 139 | # Install libssl1.1 if available 140 | if [[ ! -z $(apt-cache --names-only search ^libssl1.1$) ]]; then 141 | package_list="${package_list} libssl1.1" 142 | fi 143 | 144 | # Install appropriate version of libssl1.0.x if available 145 | libssl_package=$(dpkg-query -f '${db:Status-Abbrev}\t${binary:Package}\n' -W 'libssl1\.0\.?' 2>&1 || echo '') 146 | if [ "$(echo "$LIlibssl_packageBSSL" | grep -o 'libssl1\.0\.[0-9]:' | uniq | sort | wc -l)" -eq 0 ]; then 147 | if [[ ! -z $(apt-cache --names-only search ^libssl1.0.2$) ]]; then 148 | # Debian 9 149 | package_list="${package_list} libssl1.0.2" 150 | elif [[ ! -z $(apt-cache --names-only search ^libssl1.0.0$) ]]; then 151 | # Ubuntu 18.04, 16.04, earlier 152 | package_list="${package_list} libssl1.0.0" 153 | fi 154 | fi 155 | 156 | echo "Packages to verify are installed: ${package_list}" 157 | apt-get -y install --no-install-recommends ${package_list} 2> >( grep -v 'debconf: delaying package configuration, since apt-utils is not installed' >&2 ) 158 | 159 | # Install git if not already installed (may be more recent than distro version) 160 | if ! type git > /dev/null 2>&1; then 161 | apt-get -y install --no-install-recommends git 162 | fi 163 | 164 | PACKAGES_ALREADY_INSTALLED="true" 165 | fi 166 | 167 | # Get to latest versions of all packages 168 | if [ "${UPGRADE_PACKAGES}" = "true" ]; then 169 | apt_get_update_if_needed 170 | apt-get -y upgrade --no-install-recommends 171 | apt-get autoremove -y 172 | fi 173 | 174 | # Ensure at least the en_US.UTF-8 UTF-8 locale is available. 175 | # Common need for both applications and things like the agnoster ZSH theme. 176 | if [ "${LOCALE_ALREADY_SET}" != "true" ] && ! grep -o -E '^\s*en_US.UTF-8\s+UTF-8' /etc/locale.gen > /dev/null; then 177 | echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen 178 | locale-gen 179 | LOCALE_ALREADY_SET="true" 180 | fi 181 | 182 | # Create or update a non-root user to match UID/GID. 183 | group_name="${USERNAME}" 184 | if id -u ${USERNAME} > /dev/null 2>&1; then 185 | # User exists, update if needed 186 | if [ "${USER_GID}" != "automatic" ] && [ "$USER_GID" != "$(id -g $USERNAME)" ]; then 187 | group_name="$(id -gn $USERNAME)" 188 | groupmod --gid $USER_GID ${group_name} 189 | usermod --gid $USER_GID $USERNAME 190 | fi 191 | if [ "${USER_UID}" != "automatic" ] && [ "$USER_UID" != "$(id -u $USERNAME)" ]; then 192 | usermod --uid $USER_UID $USERNAME 193 | fi 194 | else 195 | # Create user 196 | if [ "${USER_GID}" = "automatic" ]; then 197 | groupadd $USERNAME 198 | else 199 | groupadd --gid $USER_GID $USERNAME 200 | fi 201 | if [ "${USER_UID}" = "automatic" ]; then 202 | useradd -s /bin/bash --gid $USERNAME -m $USERNAME 203 | else 204 | useradd -s /bin/bash --uid $USER_UID --gid $USERNAME -m $USERNAME 205 | fi 206 | fi 207 | 208 | # Add sudo support for non-root user 209 | if [ "${USERNAME}" != "root" ] && [ "${EXISTING_NON_ROOT_USER}" != "${USERNAME}" ]; then 210 | echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME 211 | chmod 0440 /etc/sudoers.d/$USERNAME 212 | EXISTING_NON_ROOT_USER="${USERNAME}" 213 | fi 214 | 215 | # ** Shell customization section ** 216 | if [ "${USERNAME}" = "root" ]; then 217 | user_rc_path="/root" 218 | else 219 | user_rc_path="/home/${USERNAME}" 220 | fi 221 | 222 | # Restore user .bashrc defaults from skeleton file if it doesn't exist or is empty 223 | if [ ! -f "${user_rc_path}/.bashrc" ] || [ ! -s "${user_rc_path}/.bashrc" ] ; then 224 | cp /etc/skel/.bashrc "${user_rc_path}/.bashrc" 225 | fi 226 | 227 | # Restore user .profile defaults from skeleton file if it doesn't exist or is empty 228 | if [ ! -f "${user_rc_path}/.profile" ] || [ ! -s "${user_rc_path}/.profile" ] ; then 229 | cp /etc/skel/.profile "${user_rc_path}/.profile" 230 | fi 231 | 232 | # .bashrc/.zshrc snippet 233 | rc_snippet="$(cat << 'EOF' 234 | 235 | if [ -z "${USER}" ]; then export USER=$(whoami); fi 236 | if [[ "${PATH}" != *"$HOME/.local/bin"* ]]; then export PATH="${PATH}:$HOME/.local/bin"; fi 237 | 238 | # Display optional first run image specific notice if configured and terminal is interactive 239 | if [ -t 1 ] && [[ "${TERM_PROGRAM}" = "vscode" || "${TERM_PROGRAM}" = "codespaces" ]] && [ ! -f "$HOME/.config/vscode-dev-containers/first-run-notice-already-displayed" ]; then 240 | if [ -f "/usr/local/etc/vscode-dev-containers/first-run-notice.txt" ]; then 241 | cat "/usr/local/etc/vscode-dev-containers/first-run-notice.txt" 242 | elif [ -f "/workspaces/.codespaces/shared/first-run-notice.txt" ]; then 243 | cat "/workspaces/.codespaces/shared/first-run-notice.txt" 244 | fi 245 | mkdir -p "$HOME/.config/vscode-dev-containers" 246 | # Mark first run notice as displayed after 10s to avoid problems with fast terminal refreshes hiding it 247 | ((sleep 10s; touch "$HOME/.config/vscode-dev-containers/first-run-notice-already-displayed") &) 248 | fi 249 | 250 | # Set the default git editor if not already set 251 | if [ -z "$(git config --get core.editor)" ] && [ -z "${GIT_EDITOR}" ]; then 252 | if [ "${TERM_PROGRAM}" = "vscode" ]; then 253 | if [[ -n $(command -v code-insiders) && -z $(command -v code) ]]; then 254 | export GIT_EDITOR="code-insiders --wait" 255 | else 256 | export GIT_EDITOR="code --wait" 257 | fi 258 | fi 259 | fi 260 | 261 | EOF 262 | )" 263 | 264 | # code shim, it fallbacks to code-insiders if code is not available 265 | cat << 'EOF' > /usr/local/bin/code 266 | #!/bin/sh 267 | 268 | get_in_path_except_current() { 269 | which -a "$1" | grep -A1 "$0" | grep -v "$0" 270 | } 271 | 272 | code="$(get_in_path_except_current code)" 273 | 274 | if [ -n "$code" ]; then 275 | exec "$code" "$@" 276 | elif [ "$(command -v code-insiders)" ]; then 277 | exec code-insiders "$@" 278 | else 279 | echo "code or code-insiders is not installed" >&2 280 | exit 127 281 | fi 282 | EOF 283 | chmod +x /usr/local/bin/code 284 | 285 | # systemctl shim - tells people to use 'service' if systemd is not running 286 | cat << 'EOF' > /usr/local/bin/systemctl 287 | #!/bin/sh 288 | set -e 289 | if [ -d "/run/systemd/system" ]; then 290 | exec /bin/systemctl "$@" 291 | else 292 | echo '\n"systemd" is not running in this container due to its overhead.\nUse the "service" command to start services instead. e.g.: \n\nservice --status-all' 293 | fi 294 | EOF 295 | chmod +x /usr/local/bin/systemctl 296 | 297 | # Codespaces bash and OMZ themes - partly inspired by https://github.com/ohmyzsh/ohmyzsh/blob/master/themes/robbyrussell.zsh-theme 298 | codespaces_bash="$(cat \ 299 | <<'EOF' 300 | 301 | # Codespaces bash prompt theme 302 | __bash_prompt() { 303 | local userpart='`export XIT=$? \ 304 | && [ ! -z "${GITHUB_USER}" ] && echo -n "\[\033[0;32m\]@${GITHUB_USER} " || echo -n "\[\033[0;32m\]\u " \ 305 | && [ "$XIT" -ne "0" ] && echo -n "\[\033[1;31m\]➜" || echo -n "\[\033[0m\]➜"`' 306 | local gitbranch='`\ 307 | if [ "$(git config --get codespaces-theme.hide-status 2>/dev/null)" != 1 ]; then \ 308 | export BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || git rev-parse --short HEAD 2>/dev/null); \ 309 | if [ "${BRANCH}" != "" ]; then \ 310 | echo -n "\[\033[0;36m\](\[\033[1;31m\]${BRANCH}" \ 311 | && if git ls-files --error-unmatch -m --directory --no-empty-directory -o --exclude-standard ":/*" > /dev/null 2>&1; then \ 312 | echo -n " \[\033[1;33m\]✗"; \ 313 | fi \ 314 | && echo -n "\[\033[0;36m\]) "; \ 315 | fi; \ 316 | fi`' 317 | local lightblue='\[\033[1;34m\]' 318 | local removecolor='\[\033[0m\]' 319 | PS1="${userpart} ${lightblue}\w ${gitbranch}${removecolor}\$ " 320 | unset -f __bash_prompt 321 | } 322 | __bash_prompt 323 | 324 | EOF 325 | )" 326 | 327 | codespaces_zsh="$(cat \ 328 | <<'EOF' 329 | # Codespaces zsh prompt theme 330 | __zsh_prompt() { 331 | local prompt_username 332 | if [ ! -z "${GITHUB_USER}" ]; then 333 | prompt_username="@${GITHUB_USER}" 334 | else 335 | prompt_username="%n" 336 | fi 337 | PROMPT="%{$fg[green]%}${prompt_username} %(?:%{$reset_color%}➜ :%{$fg_bold[red]%}➜ )" # User/exit code arrow 338 | PROMPT+='%{$fg_bold[blue]%}%(5~|%-1~/…/%3~|%4~)%{$reset_color%} ' # cwd 339 | PROMPT+='$([ "$(git config --get codespaces-theme.hide-status 2>/dev/null)" != 1 ] && git_prompt_info)' # Git status 340 | PROMPT+='%{$fg[white]%}$ %{$reset_color%}' 341 | unset -f __zsh_prompt 342 | } 343 | ZSH_THEME_GIT_PROMPT_PREFIX="%{$fg_bold[cyan]%}(%{$fg_bold[red]%}" 344 | ZSH_THEME_GIT_PROMPT_SUFFIX="%{$reset_color%} " 345 | ZSH_THEME_GIT_PROMPT_DIRTY=" %{$fg_bold[yellow]%}✗%{$fg_bold[cyan]%})" 346 | ZSH_THEME_GIT_PROMPT_CLEAN="%{$fg_bold[cyan]%})" 347 | __zsh_prompt 348 | 349 | EOF 350 | )" 351 | 352 | # Add RC snippet and custom bash prompt 353 | if [ "${RC_SNIPPET_ALREADY_ADDED}" != "true" ]; then 354 | echo "${rc_snippet}" >> /etc/bash.bashrc 355 | echo "${codespaces_bash}" >> "${user_rc_path}/.bashrc" 356 | echo 'export PROMPT_DIRTRIM=4' >> "${user_rc_path}/.bashrc" 357 | if [ "${USERNAME}" != "root" ]; then 358 | echo "${codespaces_bash}" >> "/root/.bashrc" 359 | echo 'export PROMPT_DIRTRIM=4' >> "/root/.bashrc" 360 | fi 361 | chown ${USERNAME}:${group_name} "${user_rc_path}/.bashrc" 362 | RC_SNIPPET_ALREADY_ADDED="true" 363 | fi 364 | 365 | # Optionally install and configure zsh and Oh My Zsh! 366 | if [ "${INSTALL_ZSH}" = "true" ]; then 367 | if ! type zsh > /dev/null 2>&1; then 368 | apt_get_update_if_needed 369 | apt-get install -y zsh 370 | fi 371 | if [ "${ZSH_ALREADY_INSTALLED}" != "true" ]; then 372 | echo "${rc_snippet}" >> /etc/zsh/zshrc 373 | ZSH_ALREADY_INSTALLED="true" 374 | fi 375 | 376 | # Adapted, simplified inline Oh My Zsh! install steps that adds, defaults to a codespaces theme. 377 | # See https://github.com/ohmyzsh/ohmyzsh/blob/master/tools/install.sh for official script. 378 | oh_my_install_dir="${user_rc_path}/.oh-my-zsh" 379 | if [ ! -d "${oh_my_install_dir}" ] && [ "${INSTALL_OH_MYS}" = "true" ]; then 380 | template_path="${oh_my_install_dir}/templates/zshrc.zsh-template" 381 | user_rc_file="${user_rc_path}/.zshrc" 382 | umask g-w,o-w 383 | mkdir -p ${oh_my_install_dir} 384 | git clone --depth=1 \ 385 | -c core.eol=lf \ 386 | -c core.autocrlf=false \ 387 | -c fsck.zeroPaddedFilemode=ignore \ 388 | -c fetch.fsck.zeroPaddedFilemode=ignore \ 389 | -c receive.fsck.zeroPaddedFilemode=ignore \ 390 | "https://github.com/ohmyzsh/ohmyzsh" "${oh_my_install_dir}" 2>&1 391 | echo -e "$(cat "${template_path}")\nDISABLE_AUTO_UPDATE=true\nDISABLE_UPDATE_PROMPT=true" > ${user_rc_file} 392 | sed -i -e 's/ZSH_THEME=.*/ZSH_THEME="codespaces"/g' ${user_rc_file} 393 | 394 | mkdir -p ${oh_my_install_dir}/custom/themes 395 | echo "${codespaces_zsh}" > "${oh_my_install_dir}/custom/themes/codespaces.zsh-theme" 396 | # Shrink git while still enabling updates 397 | cd "${oh_my_install_dir}" 398 | git repack -a -d -f --depth=1 --window=1 399 | # Copy to non-root user if one is specified 400 | if [ "${USERNAME}" != "root" ]; then 401 | cp -rf "${user_rc_file}" "${oh_my_install_dir}" /root 402 | chown -R ${USERNAME}:${group_name} "${user_rc_path}" 403 | fi 404 | fi 405 | fi 406 | 407 | # Persist image metadata info, script if meta.env found in same directory 408 | meta_info_script="$(cat << 'EOF' 409 | #!/bin/sh 410 | . /usr/local/etc/vscode-dev-containers/meta.env 411 | 412 | # Minimal output 413 | if [ "$1" = "version" ] || [ "$1" = "image-version" ]; then 414 | echo "${VERSION}" 415 | exit 0 416 | elif [ "$1" = "release" ]; then 417 | echo "${GIT_REPOSITORY_RELEASE}" 418 | exit 0 419 | elif [ "$1" = "content" ] || [ "$1" = "content-url" ] || [ "$1" = "contents" ] || [ "$1" = "contents-url" ]; then 420 | echo "${CONTENTS_URL}" 421 | exit 0 422 | fi 423 | 424 | #Full output 425 | echo 426 | echo "Development container image information" 427 | echo 428 | if [ ! -z "${VERSION}" ]; then echo "- Image version: ${VERSION}"; fi 429 | if [ ! -z "${DEFINITION_ID}" ]; then echo "- Definition ID: ${DEFINITION_ID}"; fi 430 | if [ ! -z "${VARIANT}" ]; then echo "- Variant: ${VARIANT}"; fi 431 | if [ ! -z "${GIT_REPOSITORY}" ]; then echo "- Source code repository: ${GIT_REPOSITORY}"; fi 432 | if [ ! -z "${GIT_REPOSITORY_RELEASE}" ]; then echo "- Source code release/branch: ${GIT_REPOSITORY_RELEASE}"; fi 433 | if [ ! -z "${BUILD_TIMESTAMP}" ]; then echo "- Timestamp: ${BUILD_TIMESTAMP}"; fi 434 | if [ ! -z "${CONTENTS_URL}" ]; then echo && echo "More info: ${CONTENTS_URL}"; fi 435 | echo 436 | EOF 437 | )" 438 | if [ -f "${SCRIPT_DIR}/meta.env" ]; then 439 | mkdir -p /usr/local/etc/vscode-dev-containers/ 440 | cp -f "${SCRIPT_DIR}/meta.env" /usr/local/etc/vscode-dev-containers/meta.env 441 | echo "${meta_info_script}" > /usr/local/bin/devcontainer-info 442 | chmod +x /usr/local/bin/devcontainer-info 443 | fi 444 | 445 | # Write marker file 446 | mkdir -p "$(dirname "${MARKER_FILE}")" 447 | echo -e "\ 448 | PACKAGES_ALREADY_INSTALLED=${PACKAGES_ALREADY_INSTALLED}\n\ 449 | LOCALE_ALREADY_SET=${LOCALE_ALREADY_SET}\n\ 450 | EXISTING_NON_ROOT_USER=${EXISTING_NON_ROOT_USER}\n\ 451 | RC_SNIPPET_ALREADY_ADDED=${RC_SNIPPET_ALREADY_ADDED}\n\ 452 | ZSH_ALREADY_INSTALLED=${ZSH_ALREADY_INSTALLED}" > "${MARKER_FILE}" 453 | 454 | echo "Done!" 455 | -------------------------------------------------------------------------------- /.devcontainer/library-scripts/node-debian.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #------------------------------------------------------------------------------------------------------------- 3 | # Copyright (c) Microsoft Corporation. All rights reserved. 4 | # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. 5 | #------------------------------------------------------------------------------------------------------------- 6 | # 7 | # Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/node.md 8 | # Maintainer: The VS Code and Codespaces Teams 9 | # 10 | # Syntax: ./node-debian.sh [directory to install nvm] [node version to install (use "none" to skip)] [non-root user] [Update rc files flag] [install node-gyp deps] 11 | 12 | export NVM_DIR=${1:-"/usr/local/share/nvm"} 13 | export NODE_VERSION=${2:-"lts"} 14 | USERNAME=${3:-"automatic"} 15 | UPDATE_RC=${4:-"true"} 16 | INSTALL_TOOLS_FOR_NODE_GYP="${5:-true}" 17 | export NVM_VERSION="0.38.0" 18 | 19 | set -e 20 | 21 | if [ "$(id -u)" -ne 0 ]; then 22 | echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' 23 | exit 1 24 | fi 25 | 26 | # Ensure that login shells get the correct path if the user updated the PATH using ENV. 27 | rm -f /etc/profile.d/00-restore-env.sh 28 | echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh 29 | chmod +x /etc/profile.d/00-restore-env.sh 30 | 31 | # Determine the appropriate non-root user 32 | if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then 33 | USERNAME="" 34 | POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") 35 | for CURRENT_USER in ${POSSIBLE_USERS[@]}; do 36 | if id -u ${CURRENT_USER} > /dev/null 2>&1; then 37 | USERNAME=${CURRENT_USER} 38 | break 39 | fi 40 | done 41 | if [ "${USERNAME}" = "" ]; then 42 | USERNAME=root 43 | fi 44 | elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then 45 | USERNAME=root 46 | fi 47 | 48 | updaterc() { 49 | if [ "${UPDATE_RC}" = "true" ]; then 50 | echo "Updating /etc/bash.bashrc and /etc/zsh/zshrc..." 51 | if [[ "$(cat /etc/bash.bashrc)" != *"$1"* ]]; then 52 | echo -e "$1" >> /etc/bash.bashrc 53 | fi 54 | if [ -f "/etc/zsh/zshrc" ] && [[ "$(cat /etc/zsh/zshrc)" != *"$1"* ]]; then 55 | echo -e "$1" >> /etc/zsh/zshrc 56 | fi 57 | fi 58 | } 59 | 60 | # Function to run apt-get if needed 61 | apt_get_update_if_needed() 62 | { 63 | if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then 64 | echo "Running apt-get update..." 65 | apt-get update 66 | else 67 | echo "Skipping apt-get update." 68 | fi 69 | } 70 | 71 | # Checks if packages are installed and installs them if not 72 | check_packages() { 73 | if ! dpkg -s "$@" > /dev/null 2>&1; then 74 | apt_get_update_if_needed 75 | apt-get -y install --no-install-recommends "$@" 76 | fi 77 | } 78 | 79 | # Ensure apt is in non-interactive to avoid prompts 80 | export DEBIAN_FRONTEND=noninteractive 81 | 82 | # Install dependencies 83 | check_packages apt-transport-https curl ca-certificates tar gnupg2 dirmngr 84 | 85 | # Install yarn 86 | if type yarn > /dev/null 2>&1; then 87 | echo "Yarn already installed." 88 | else 89 | # Import key safely (new method rather than deprecated apt-key approach) and install 90 | curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor > /usr/share/keyrings/yarn-archive-keyring.gpg 91 | echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/yarn-archive-keyring.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list 92 | apt-get update 93 | apt-get -y install --no-install-recommends yarn 94 | fi 95 | 96 | # Adjust node version if required 97 | if [ "${NODE_VERSION}" = "none" ]; then 98 | export NODE_VERSION= 99 | elif [ "${NODE_VERSION}" = "lts" ]; then 100 | export NODE_VERSION="lts/*" 101 | fi 102 | 103 | # Create a symlink to the installed version for use in Dockerfile PATH statements 104 | export NVM_SYMLINK_CURRENT=true 105 | 106 | # Install the specified node version if NVM directory already exists, then exit 107 | if [ -d "${NVM_DIR}" ]; then 108 | echo "NVM already installed." 109 | if [ "${NODE_VERSION}" != "" ]; then 110 | su ${USERNAME} -c ". $NVM_DIR/nvm.sh && nvm install ${NODE_VERSION} && nvm clear-cache" 111 | fi 112 | exit 0 113 | fi 114 | 115 | # Create nvm group, nvm dir, and set sticky bit 116 | if ! cat /etc/group | grep -e "^nvm:" > /dev/null 2>&1; then 117 | groupadd -r nvm 118 | fi 119 | umask 0002 120 | usermod -a -G nvm ${USERNAME} 121 | mkdir -p ${NVM_DIR} 122 | chown :nvm ${NVM_DIR} 123 | chmod g+s ${NVM_DIR} 124 | su ${USERNAME} -c "$(cat << EOF 125 | set -e 126 | umask 0002 127 | # Do not update profile - we'll do this manually 128 | export PROFILE=/dev/null 129 | ls -lah /home/${USERNAME}/.nvs || : 130 | curl -so- https://raw.githubusercontent.com/nvm-sh/nvm/v${NVM_VERSION}/install.sh | bash 131 | source ${NVM_DIR}/nvm.sh 132 | if [ "${NODE_VERSION}" != "" ]; then 133 | nvm alias default ${NODE_VERSION} 134 | fi 135 | nvm clear-cache 136 | EOF 137 | )" 2>&1 138 | # Update rc files 139 | if [ "${UPDATE_RC}" = "true" ]; then 140 | updaterc "$(cat < /dev/null 2>&1; then 153 | to_install="${to_install} make" 154 | fi 155 | if ! type gcc > /dev/null 2>&1; then 156 | to_install="${to_install} gcc" 157 | fi 158 | if ! type g++ > /dev/null 2>&1; then 159 | to_install="${to_install} g++" 160 | fi 161 | if ! type python3 > /dev/null 2>&1; then 162 | to_install="${to_install} python3-minimal" 163 | fi 164 | if [ ! -z "${to_install}" ]; then 165 | apt_get_update_if_needed 166 | apt-get -y install ${to_install} 167 | fi 168 | fi 169 | 170 | echo "Done!" 171 | -------------------------------------------------------------------------------- /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Swift 2 | on: [push] 3 | jobs: 4 | 5 | macos: 6 | name: macOS 7 | runs-on: macos-latest 8 | steps: 9 | - name: Install Swift 10 | uses: slashmo/install-swift@v0.3.0 11 | with: 12 | version: 6.0.3 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | - name: Swift Version 16 | run: swift --version 17 | - name: Install dependencies 18 | run: brew install cairo fontconfig freetype 19 | - name: Build (Debug) 20 | run: swift build -c debug 21 | - name: Build (Release) 22 | run: swift build -c release 23 | - name: Test (Debug) 24 | run: swift test -c debug 25 | 26 | linux: 27 | name: Linux 28 | strategy: 29 | matrix: 30 | swift: [6.0.3] 31 | runs-on: ubuntu-20.04 32 | steps: 33 | - name: Install Swift 34 | uses: slashmo/install-swift@v0.3.0 35 | with: 36 | version: ${{ matrix.swift }} 37 | - name: Checkout 38 | uses: actions/checkout@v2 39 | - name: Swift Version 40 | run: swift --version 41 | - name: Install dependencies 42 | run: sudo apt-get install -y libcairo2-dev libfreetype6-dev libfontconfig-dev 43 | - name: Build (Debug) 44 | run: swift build -c debug 45 | - name: Build (Release) 46 | run: swift build -c release 47 | - name: Test (Debug) 48 | run: swift test -c debug 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | Packages/ 38 | Package.pins 39 | Package.resolved 40 | .build/ 41 | .swiftpm 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | 51 | # Carthage 52 | # 53 | 54 | Carthage/* 55 | 56 | # fastlane 57 | # 58 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 59 | # screenshots whenever they are needed. 60 | # For more information about the recommended setup visit: 61 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 62 | 63 | fastlane/report.xml 64 | fastlane/Preview.html 65 | fastlane/screenshots 66 | fastlane/test_output 67 | 68 | # Jazzy 69 | docs 70 | 71 | # VS Code 72 | .vscode 73 | 74 | # Finder 75 | .DS_Store 76 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:6.0 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "Silica", 6 | products: [ 7 | .library( 8 | name: "Silica", 9 | targets: ["Silica"] 10 | ) 11 | ], 12 | dependencies: [ 13 | .package( 14 | url: "https://github.com/PureSwift/Cairo.git", 15 | branch: "master" 16 | ), 17 | .package( 18 | url: "https://github.com/PureSwift/FontConfig.git", 19 | branch: "master" 20 | ) 21 | ], 22 | targets: [ 23 | .target( 24 | name: "Silica", 25 | dependencies: [ 26 | "Cairo", 27 | "FontConfig" 28 | ] 29 | ), 30 | .testTarget( 31 | name: "SilicaTests", 32 | dependencies: ["Silica"] 33 | ) 34 | ] 35 | ) 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Silica 2 | Pure Swift CoreGraphics (Quartz2D) implementation 3 | 4 | This library is a compatibility layer over Cairo for porting UIKit (and CoreGraphics) drawing code to other platforms. -------------------------------------------------------------------------------- /Sources/Silica/CGAffineTransform.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AffineTransform.swift 3 | // Silica 4 | // 5 | // Created by Alsey Coleman Miller on 5/8/16. 6 | // Copyright © 2016 PureSwift. All rights reserved. 7 | // 8 | 9 | import Cairo 10 | import CCairo 11 | import Foundation 12 | 13 | #if os(macOS) 14 | 15 | import struct CoreGraphics.CGAffineTransform 16 | public typealias CGAffineTransform = CoreGraphics.CGAffineTransform 17 | 18 | #else 19 | 20 | /// Affine Transform 21 | public struct CGAffineTransform { 22 | 23 | // MARK: - Properties 24 | 25 | public var a, b, c, d, tx, ty: CGFloat 26 | 27 | // MARK: - Initialization 28 | 29 | public init(a: CGFloat, b: CGFloat, c: CGFloat, d: CGFloat, tx: CGFloat, ty: CGFloat) { 30 | 31 | self.a = a 32 | self.b = b 33 | self.c = c 34 | self.d = d 35 | self.tx = tx 36 | self.ty = ty 37 | } 38 | } 39 | 40 | #endif 41 | 42 | public extension CGAffineTransform { 43 | 44 | static var identity: CGAffineTransform { CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0) } 45 | } 46 | 47 | // MARK: - Geometry Math 48 | 49 | // Immutable math 50 | 51 | public protocol CGAffineTransformMath { 52 | 53 | func applying(_ transform: CGAffineTransform) -> Self 54 | } 55 | 56 | // Mutable versions 57 | 58 | public extension CGAffineTransformMath { 59 | 60 | @inline(__always) 61 | mutating func apply(_ transform: CGAffineTransform) { 62 | 63 | self = self.applying(transform) 64 | } 65 | } 66 | 67 | // Implementations 68 | 69 | extension CGPoint: CGAffineTransformMath { 70 | 71 | @inline(__always) 72 | public func applying(_ t: CGAffineTransform) -> CGPoint { 73 | 74 | return CGPoint(x: t.a * x + t.c * y + t.tx, 75 | y: t.b * x + t.d * y + t.ty) 76 | } 77 | } 78 | 79 | extension CGSize: CGAffineTransformMath { 80 | 81 | @inline(__always) 82 | public func applying( _ transform: CGAffineTransform) -> CGSize { 83 | 84 | var newSize = CGSize(width: transform.a * width + transform.c * height, 85 | height: transform.b * width + transform.d * height) 86 | 87 | if newSize.width < 0 { newSize.width = -newSize.width } 88 | if newSize.height < 0 { newSize.height = -newSize.height } 89 | 90 | return newSize 91 | } 92 | } 93 | 94 | // MARK: - Cairo Conversion 95 | 96 | extension CGAffineTransform: CairoConvertible { 97 | 98 | public typealias CairoType = Cairo.Matrix 99 | 100 | @inline(__always) 101 | public init(cairo matrix: CairoType) { 102 | 103 | self.init(a: CGFloat(matrix.xx), 104 | b: CGFloat(matrix.xy), 105 | c: CGFloat(matrix.yx), 106 | d: CGFloat(matrix.yy), 107 | tx: CGFloat(matrix.x0), 108 | ty: CGFloat(matrix.y0)) 109 | } 110 | 111 | @inline(__always) 112 | public func toCairo() -> CairoType { 113 | 114 | var matrix = Matrix() 115 | 116 | matrix.xx = Double(a) 117 | matrix.xy = Double(b) 118 | matrix.yx = Double(c) 119 | matrix.yy = Double(d) 120 | matrix.x0 = Double(tx) 121 | matrix.y0 = Double(ty) 122 | 123 | return matrix 124 | } 125 | } 126 | 127 | -------------------------------------------------------------------------------- /Sources/Silica/CGBitmapInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BitmapInfo.swift 3 | // Silica 4 | // 5 | // Created by Alsey Coleman Miller on 5/11/16. 6 | // Copyright © 2016 PureSwift. All rights reserved. 7 | // 8 | 9 | /// Component information for a bitmap image. 10 | /// 11 | /// Applications that store pixel data in memory using ARGB format must take care in how they read data. 12 | /// If the code is not written correctly, it’s possible to misread the data which leads to colors or alpha that appear wrong. 13 | /// The byte order constants specify the byte ordering of pixel formats. 14 | public struct CGBitmapInfo { 15 | 16 | /// The components of a bitmap are floating-point values. 17 | public var floatComponents: Bool 18 | 19 | /// Alpha information that specifies whether a bitmap contains an alpha channel 20 | /// and how the alpha channel is generated. 21 | public var alpha: Alpha 22 | 23 | /// /// The byte ordering of pixel formats. 24 | public var byteOrder: ByteOrder 25 | 26 | public init(floatComponents: Bool = false, alpha: Alpha = .none, byteOrder: ByteOrder = .default) { 27 | 28 | self.floatComponents = floatComponents 29 | self.alpha = alpha 30 | self.byteOrder = byteOrder 31 | } 32 | } 33 | 34 | // MARK: - Equatable 35 | 36 | extension CGBitmapInfo: Equatable { 37 | 38 | public static func == (lhs: CGBitmapInfo, rhs: CGBitmapInfo) -> Bool { 39 | 40 | return lhs.floatComponents == rhs.floatComponents 41 | && lhs.alpha == rhs.alpha 42 | && lhs.byteOrder == rhs.byteOrder 43 | } 44 | } 45 | 46 | // MARK: - Supporting Types 47 | 48 | public extension CGBitmapInfo { 49 | 50 | /// Alpha information that specifies whether a bitmap contains an alpha channel 51 | /// and how the alpha channel is generated. 52 | /// 53 | /// Specifies (1) whether a bitmap contains an alpha channel, 54 | /// (2) where the alpha bits are located in the image data, 55 | /// and (3) whether the alpha value is premultiplied. 56 | /// 57 | /// Alpha blending is accomplished by combining the color components of the source image 58 | /// with the color components of the destination image using the linear interpolation formula, 59 | /// where “source” is one color component of one pixel of the new paint 60 | /// and “destination” is one color component of the background image. 61 | /// 62 | /// - Note: Silica supports premultiplied alpha only for images. 63 | /// You should not premultiply any other color values specified in Silica. 64 | enum Alpha { 65 | 66 | /// There is no alpha channel. 67 | case none 68 | 69 | /// The alpha component is stored in the least significant bits of each pixel and the color components 70 | /// have already been multiplied by this alpha value. For example, premultiplied RGBA. 71 | case premultipliedLast 72 | 73 | /// The alpha component is stored in the most significant bits of each pixel and the color components 74 | /// have already been multiplied by this alpha value. For example, premultiplied ARGB. 75 | case premultipliedFirst 76 | 77 | /// The alpha component is stored in the least significant bits of each pixel. For example, non-premultiplied RGBA. 78 | case last 79 | 80 | /// The alpha component is stored in the most significant bits of each pixel. For example, non-premultiplied ARGB. 81 | case first 82 | 83 | /// There is no alpha channel. 84 | case noneSkipLast 85 | 86 | /// There is no alpha channel. 87 | /// If the total size of the pixel is greater than the space required for the number of color components 88 | /// in the color space, the most significant bits are ignored. 89 | case noneSkipFirst 90 | 91 | /// There is no color data, only an alpha channel. 92 | case alphaOnly 93 | } 94 | 95 | /// The byte ordering of pixel formats. 96 | enum ByteOrder { 97 | 98 | /// The default byte order. 99 | case `default` 100 | 101 | /// 16-bit, big endian format. 102 | case big16 103 | 104 | /// 16-bit, little endian format. 105 | case little16 106 | 107 | /// 32-bit, big endian format. 108 | case big32 109 | 110 | /// 32-bit, little endian format. 111 | case little32 112 | } 113 | } 114 | 115 | -------------------------------------------------------------------------------- /Sources/Silica/CGColor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Color.swift 3 | // Silica 4 | // 5 | // Created by Alsey Coleman Miller on 5/9/16. 6 | // Copyright © 2016 PureSwift. All rights reserved. 7 | // 8 | 9 | import struct Foundation.CGFloat 10 | import Cairo 11 | 12 | public struct CGColor: Equatable { 13 | 14 | // MARK: - Properties 15 | 16 | public var red: CGFloat 17 | 18 | public var green: CGFloat 19 | 20 | public var blue: CGFloat 21 | 22 | public var alpha: CGFloat 23 | 24 | // MARK: - Initialization 25 | 26 | public init(red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat = 1.0) { 27 | 28 | self.red = red 29 | self.green = green 30 | self.blue = blue 31 | self.alpha = alpha 32 | } 33 | 34 | public init(grey: CGFloat, alpha: CGFloat = 1.0) { 35 | 36 | self.red = grey 37 | self.green = grey 38 | self.blue = grey 39 | self.alpha = alpha 40 | } 41 | 42 | // MARK: - Singletons 43 | 44 | public static var clear: CGColor { CGColor(red: 0, green: 0, blue: 0, alpha: 0) } 45 | 46 | public static var black: CGColor { CGColor(red: 0, green: 0, blue: 0) } 47 | 48 | public static var white: CGColor { CGColor(red: 1, green: 1, blue: 1) } 49 | 50 | public static var red: CGColor { CGColor(red: 1, green: 0, blue: 0) } 51 | 52 | public static var green: CGColor { CGColor(red: 0, green: 1, blue: 0) } 53 | 54 | public static var blue: CGColor { CGColor(red: 0, green: 0, blue: 1) } 55 | } 56 | 57 | // MARK: - Equatable 58 | 59 | public func == (lhs: CGColor, rhs: CGColor) -> Bool { 60 | 61 | return lhs.red == rhs.red 62 | && lhs.green == rhs.green 63 | && lhs.blue == rhs.blue 64 | && lhs.alpha == rhs.alpha 65 | } 66 | 67 | // MARK: - Internal Cairo Conversion 68 | 69 | internal extension Cairo.Pattern { 70 | 71 | convenience init(color: CGColor) { 72 | 73 | self.init(color: (Double(color.red), 74 | Double(color.green), 75 | Double(color.blue), 76 | Double(color.alpha))) 77 | 78 | assert(status.rawValue == 0, "Error creating Cairo.Pattern from Silica.Color: \(status)") 79 | } 80 | } 81 | 82 | // MARK: - CoreGraphics API 83 | 84 | public func CGColorCreateGenericGray(_ grey: CGFloat, _ alpha: CGFloat) -> CGColor { 85 | 86 | return CGColor(grey: grey, alpha: alpha) 87 | } 88 | 89 | 90 | -------------------------------------------------------------------------------- /Sources/Silica/CGColorRenderingIntent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorRenderingIntent.swift 3 | // Silica 4 | // 5 | // Created by Alsey Coleman Miller on 6/2/17. 6 | // 7 | // 8 | 9 | /// The rendering intent specifies how Silica should handle colors 10 | /// that are not located within the gamut of the destination color space of a graphics context. 11 | /// It determines the exact method used to map colors from one color space to another. 12 | /// 13 | /// If you do not explicitly set the rendering intent, 14 | /// the graphics context uses the relative colorimetric rendering intent, 15 | /// except when drawing sampled images. 16 | public enum ColorRenderingIntent { 17 | 18 | /// The default rendering intent for the graphics context. 19 | case `default` 20 | 21 | /// Map colors outside of the gamut of the output device 22 | /// to the closest possible match inside the gamut of the output device. 23 | /// This can produce a clipping effect, where two different color values 24 | /// in the gamut of the graphics context are mapped to the same color value 25 | /// in the output device’s gamut. 26 | /// 27 | /// Unlike the relative colorimetric, absolute colorimetric does not modify colors inside the gamut of the output device. 28 | case absoluteColorimetric 29 | 30 | /// Map colors outside of the gamut of the output device 31 | /// to the closest possible match inside the gamut of the output device. 32 | /// This can produce a clipping effect, where two different color values 33 | /// in the gamut of the graphics context are mapped to the same color value 34 | /// in the output device’s gamut. 35 | /// 36 | /// The relative colorimetric shifts all colors (including those within the gamut) 37 | /// to account for the difference between the white point of the graphics context 38 | /// and the white point of the output device. 39 | case relativeColorimetric 40 | 41 | /// Preserve the visual relationship between colors by compressing the gamut of the graphics context 42 | /// to fit inside the gamut of the output device. 43 | /// Perceptual intent is good for photographs and other complex, detailed images. 44 | case perceptual 45 | 46 | /// Preserve the relative saturation value of the colors when converting into the gamut of the output device. 47 | /// The result is an image with bright, saturated colors. 48 | /// Saturation intent is good for reproducing images with low detail, such as presentation charts and graphs. 49 | case saturation 50 | } 51 | -------------------------------------------------------------------------------- /Sources/Silica/CGColorSpace.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorSpace.swift 3 | // Silica 4 | // 5 | // Created by Alsey Coleman Miller on 6/2/17. 6 | // 7 | // 8 | /* 9 | import struct Foundation.Data 10 | import CLCMS 11 | import LittleCMS 12 | 13 | /// A profile that specifies how to interpret a color value for display. 14 | public final class ColorSpace { 15 | 16 | // MARK: - Properties 17 | 18 | /// The underlying LittleCMS profile 19 | public let profile: LittleCMS.Profile 20 | 21 | // MARK: - Initialization 22 | 23 | public required init(profile: LittleCMS.Profile) { 24 | 25 | self.profile = profile 26 | } 27 | 28 | public init?(data: Data) { 29 | 30 | guard let profile = Profile(data: data) 31 | else { return nil } 32 | 33 | self.profile = profile 34 | } 35 | 36 | // MARK: - Accessors 37 | 38 | public var numberOfComponents: UInt { 39 | 40 | return profile.signature.numberOfComponents 41 | } 42 | 43 | public var model: Model { 44 | 45 | let signature = profile.signature 46 | 47 | switch signature { 48 | 49 | case cmsSigGrayData: 50 | return .monochrome 51 | case cmsSigRgbData: 52 | return .rgb 53 | case cmsSigCmykData: 54 | return .cmyk 55 | case cmsSigLabData: 56 | return .lab 57 | default: 58 | return .unknown 59 | } 60 | } 61 | } 62 | 63 | // MARK: - Singletons 64 | 65 | public extension ColorSpace { 66 | 67 | /// Device-independent RGB color space. 68 | static let genericRGB: ColorSpace = ColorSpace(profile: Profile(sRGB: nil)!) 69 | } 70 | 71 | // MARK: - Supporting Types 72 | 73 | public extension ColorSpace { 74 | 75 | // CoreGraphics API 76 | 77 | /// Models for color spaces. 78 | public enum Model { 79 | 80 | /// An unknown color space model. 81 | case unknown 82 | 83 | /// A monochrome color space model. 84 | case monochrome 85 | 86 | /// An RGB color space model. 87 | case rgb 88 | 89 | /// A CMYK color space model. 90 | case cmyk 91 | 92 | /// A Lab color space model. 93 | case lab 94 | 95 | /// A DeviceN color space model. 96 | case deviceN 97 | 98 | /// An indexed color space model. 99 | case indexed 100 | 101 | /// A pattern color space model. 102 | case pattern 103 | } 104 | } 105 | */ 106 | -------------------------------------------------------------------------------- /Sources/Silica/CGContext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Context.swift 3 | // Silica 4 | // 5 | // Created by Alsey Coleman Miller on 5/8/16. 6 | // Copyright © 2016 PureSwift. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import Darwin.C.math 11 | #elseif os(Linux) 12 | import Glibc 13 | #endif 14 | 15 | import Cairo 16 | import CCairo 17 | import Foundation 18 | 19 | public final class CGContext { 20 | 21 | // MARK: - Properties 22 | 23 | public let surface: Cairo.Surface 24 | 25 | public let size: CGSize 26 | 27 | public var textMatrix = CGAffineTransform.identity 28 | 29 | // MARK: - Private Properties 30 | 31 | private let internalContext: Cairo.Context 32 | 33 | private var internalState: State = State() 34 | 35 | // MARK: - Initialization 36 | 37 | public init(surface: Cairo.Surface, size: CGSize) throws(CairoError) { 38 | 39 | let context = Cairo.Context(surface: surface) 40 | 41 | if let error = CairoError(context.status) { 42 | throw error 43 | } 44 | 45 | // Cairo defaults to line width 2.0 46 | context.lineWidth = 1.0 47 | 48 | self.size = size 49 | self.internalContext = context 50 | self.surface = surface 51 | } 52 | 53 | // MARK: - Accessors 54 | 55 | /// Returns the current transformation matrix. 56 | public var currentTransform: CGAffineTransform { 57 | return CGAffineTransform(cairo: internalContext.matrix) 58 | } 59 | 60 | public var currentPoint: CGPoint? { 61 | 62 | guard let point = internalContext.currentPoint 63 | else { return nil } 64 | 65 | return CGPoint(x: CGFloat(point.x), y: CGFloat(point.y)) 66 | } 67 | 68 | public var shouldAntialias: Bool { 69 | 70 | get { return internalContext.antialias != CAIRO_ANTIALIAS_NONE } 71 | 72 | set { internalContext.antialias = newValue ? CAIRO_ANTIALIAS_DEFAULT : CAIRO_ANTIALIAS_NONE } 73 | } 74 | 75 | public var lineWidth: CGFloat { 76 | 77 | get { return CGFloat(internalContext.lineWidth) } 78 | 79 | set { internalContext.lineWidth = Double(newValue) } 80 | } 81 | 82 | public var lineJoin: CGLineJoin { 83 | 84 | get { return CGLineJoin(cairo: internalContext.lineJoin) } 85 | 86 | set { internalContext.lineJoin = newValue.toCairo() } 87 | } 88 | 89 | public var lineCap: CGLineCap { 90 | 91 | get { return CGLineCap(cairo: internalContext.lineCap) } 92 | 93 | set { internalContext.lineCap = newValue.toCairo() } 94 | } 95 | 96 | public var miterLimit: CGFloat { 97 | 98 | get { return CGFloat(internalContext.miterLimit) } 99 | 100 | set { internalContext.miterLimit = Double(newValue) } 101 | } 102 | 103 | public var lineDash: (phase: CGFloat, lengths: [CGFloat]) { 104 | 105 | get { 106 | let cairoValue = internalContext.lineDash 107 | 108 | return (CGFloat(cairoValue.phase), cairoValue.lengths.map({ CGFloat($0) })) 109 | } 110 | 111 | set { 112 | internalContext.lineDash = (Double(newValue.phase), newValue.lengths.map({ Double($0) })) } 113 | } 114 | 115 | @inline(__always) 116 | public func setLineDash(phase: CGFloat, lengths: [CGFloat]) { 117 | 118 | self.lineDash = (phase, lengths) 119 | } 120 | 121 | public var tolerance: CGFloat { 122 | 123 | get { return CGFloat(internalContext.tolerance) } 124 | 125 | set { internalContext.tolerance = Double(newValue) } 126 | } 127 | 128 | /// Returns a `Path` built from the current path information in the graphics context. 129 | public var path: CGPath { 130 | 131 | var path = CGPath() 132 | 133 | let cairoPath = internalContext.copyPath() 134 | 135 | var index = 0 136 | 137 | while index < cairoPath.count { 138 | 139 | let header = cairoPath[index].header 140 | 141 | let length = Int(header.length) 142 | 143 | let data = Array(cairoPath.data[index + 1 ..< length]) 144 | 145 | let element: PathElement 146 | 147 | switch header.type { 148 | 149 | case CAIRO_PATH_MOVE_TO: 150 | 151 | let point = CGPoint(x: CGFloat(data[0].point.x), 152 | y: CGFloat(data[0].point.y)) 153 | 154 | element = PathElement.moveToPoint(point) 155 | 156 | case CAIRO_PATH_LINE_TO: 157 | 158 | let point = CGPoint(x: CGFloat(data[0].point.x), 159 | y: CGFloat(data[0].point.y)) 160 | 161 | element = PathElement.addLineToPoint(point) 162 | 163 | case CAIRO_PATH_CURVE_TO: 164 | 165 | let control1 = CGPoint(x: CGFloat(data[0].point.x), 166 | y: CGFloat(data[0].point.y)) 167 | let control2 = CGPoint(x: CGFloat(data[1].point.x), 168 | y: CGFloat(data[1].point.y)) 169 | let destination = CGPoint(x: CGFloat(data[2].point.x), 170 | y: CGFloat(data[2].point.y)) 171 | 172 | element = PathElement.addCurveToPoint(control1, control2, destination) 173 | 174 | case CAIRO_PATH_CLOSE_PATH: 175 | 176 | element = PathElement.closeSubpath 177 | 178 | default: fatalError("Unknown Cairo Path data: \(header.type.rawValue)") 179 | } 180 | 181 | path.elements.append(element) 182 | 183 | // increment 184 | index += length 185 | } 186 | 187 | return path 188 | } 189 | 190 | public var fillColor: CGColor { 191 | 192 | get { return internalState.fill?.color ?? CGColor.black } 193 | 194 | set { internalState.fill = (newValue, Cairo.Pattern(color: newValue)) } 195 | } 196 | 197 | public var strokeColor: CGColor { 198 | 199 | get { return internalState.stroke?.color ?? CGColor.black } 200 | 201 | set { internalState.stroke = (newValue, Cairo.Pattern(color: newValue)) } 202 | } 203 | 204 | @inline(__always) 205 | public func setAlpha(_ alpha: CGFloat) { 206 | self.alpha = alpha 207 | } 208 | 209 | public var alpha: CGFloat { 210 | 211 | get { return CGFloat(internalState.alpha) } 212 | 213 | set { 214 | 215 | // store new value 216 | internalState.alpha = newValue 217 | 218 | // update stroke 219 | if var stroke = internalState.stroke { 220 | 221 | stroke.color.alpha = newValue 222 | stroke.pattern = Pattern(color: stroke.color) 223 | 224 | internalState.stroke = stroke 225 | } 226 | 227 | // update fill 228 | if var fill = internalState.fill { 229 | 230 | fill.color.alpha = newValue 231 | fill.pattern = Pattern(color: fill.color) 232 | 233 | internalState.fill = fill 234 | } 235 | } 236 | } 237 | 238 | public var fontSize: CGFloat { 239 | 240 | get { return internalState.fontSize } 241 | 242 | set { internalState.fontSize = newValue } 243 | } 244 | 245 | public var characterSpacing: CGFloat { 246 | 247 | get { return internalState.characterSpacing } 248 | 249 | set { internalState.characterSpacing = newValue } 250 | } 251 | 252 | @inline(__always) 253 | public func setTextDrawingMode(_ newValue: CGTextDrawingMode) { 254 | 255 | self.textDrawingMode = newValue 256 | } 257 | 258 | public var textDrawingMode: CGTextDrawingMode { 259 | 260 | get { return internalState.textMode } 261 | 262 | set { internalState.textMode = newValue } 263 | } 264 | 265 | public var textPosition: CGPoint { 266 | 267 | get { return CGPoint(x: textMatrix.tx, y: textMatrix.ty) } 268 | 269 | set { 270 | textMatrix.tx = newValue.x 271 | textMatrix.ty = newValue.y 272 | } 273 | } 274 | 275 | // MARK: - Methods 276 | 277 | // MARK: Defining Pages 278 | 279 | public func beginPage() { 280 | 281 | internalContext.copyPage() 282 | } 283 | 284 | public func endPage() { 285 | 286 | internalContext.showPage() 287 | } 288 | 289 | // MARK: Transforming the Coordinate Space 290 | 291 | public func scaleBy(x: CGFloat, y: CGFloat) { 292 | 293 | internalContext.scale(x: Double(x), y: Double(y)) 294 | } 295 | 296 | public func translateBy(x: CGFloat, y: CGFloat) { 297 | 298 | internalContext.translate(x: Double(x), y: Double(y)) 299 | } 300 | 301 | public func rotateBy(_ angle: CGFloat) { 302 | 303 | internalContext.rotate(Double(angle)) 304 | } 305 | 306 | /// Transforms the user coordinate system in a context using a specified matrix. 307 | public func concatenate(_ transform: CGAffineTransform) { 308 | 309 | internalContext.transform(transform.toCairo()) 310 | } 311 | 312 | // MARK: Saving and Restoring the Graphics State 313 | 314 | public func save() throws(CairoError) { 315 | internalContext.save() 316 | if let error = CairoError(internalContext.status) { 317 | throw error 318 | } 319 | let newState = internalState.copy 320 | newState.next = internalState 321 | internalState = newState 322 | } 323 | 324 | /// Pushes a copy of the current graphics state onto the graphics state stack for the context. 325 | public func saveGState() { 326 | try! save() 327 | } 328 | 329 | public func restore() throws(CairoError) { 330 | 331 | guard let restoredState = internalState.next 332 | else { throw .invalidRestore } 333 | 334 | internalContext.restore() 335 | 336 | if let error = CairoError(internalContext.status) { 337 | throw error 338 | } 339 | 340 | // success 341 | 342 | internalState = restoredState 343 | } 344 | 345 | /// Sets the current graphics state to the state most recently saved. 346 | public func restoreGState() { 347 | try! restore() 348 | } 349 | 350 | // MARK: Setting Graphics State Attributes 351 | 352 | public func setShadow(offset: CGSize, radius: CGFloat, color: CGColor) { 353 | 354 | let colorPattern = Pattern(color: color) 355 | 356 | internalState.shadow = (offset: offset, radius: radius, color: color, pattern: colorPattern) 357 | } 358 | 359 | // MARK: Constructing Paths 360 | 361 | /// Creates a new empty path in a graphics context. 362 | public func beginPath() { 363 | 364 | internalContext.newPath() 365 | } 366 | 367 | /// Closes and terminates the current path’s subpath. 368 | public func closePath() { 369 | 370 | internalContext.closePath() 371 | } 372 | 373 | /// Begins a new subpath at the specified point. 374 | public func move(to point: CGPoint) { 375 | 376 | internalContext.move(to: (x: Double(point.x), y: Double(point.y))) 377 | } 378 | 379 | /// Appends a straight line segment from the current point to the specified point. 380 | public func addLine(to point: CGPoint) { 381 | 382 | internalContext.line(to: (x: Double(point.x), y: Double(point.y))) 383 | } 384 | 385 | /// Adds a cubic Bézier curve to the current path, with the specified end point and control points. 386 | func addCurve(to end: CGPoint, control1: CGPoint, control2: CGPoint) { 387 | 388 | internalContext.curve(to: ((x: Double(control1.x), y: Double(control1.y)), 389 | (x: Double(control2.x), y: Double(control2.y)), 390 | (x: Double(end.x), y: Double(end.y)))) 391 | } 392 | 393 | /// Adds a quadratic Bézier curve to the current path, with the specified end point and control point. 394 | public func addQuadCurve(to end: CGPoint, control point: CGPoint) { 395 | 396 | let currentPoint = self.currentPoint ?? CGPoint() 397 | 398 | let first = CGPoint(x: (currentPoint.x / 3.0) + (2.0 * point.x / 3.0), 399 | y: (currentPoint.y / 3.0) + (2.0 * point.y / 3.0)) 400 | 401 | let second = CGPoint(x: (2.0 * currentPoint.x / 3.0) + (end.x / 3.0), 402 | y: (2.0 * currentPoint.y / 3.0) + (end.y / 3.0)) 403 | 404 | addCurve(to: end, control1: first, control2: second) 405 | } 406 | 407 | /// Adds an arc of a circle to the current path, specified with a radius and angles. 408 | public func addArc(center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, clockwise: Bool) { 409 | 410 | internalContext.addArc(center: (x: Double(center.x), y: Double(center.y)), 411 | radius: Double(radius), 412 | angle: (Double(startAngle), Double(endAngle)), 413 | negative: clockwise) 414 | } 415 | 416 | /// Adds an arc of a circle to the current path, specified with a radius and two tangent lines. 417 | public func addArc(tangent1End: CGPoint, tangent2End: CGPoint, radius: CGFloat) { 418 | 419 | let points: (CGPoint, CGPoint) = (tangent1End, tangent2End) 420 | 421 | let currentPoint = self.currentPoint ?? CGPoint() 422 | 423 | // arguments 424 | let x0 = currentPoint.x.native 425 | let y0 = currentPoint.y.native 426 | let x1 = points.0.x.native 427 | let y1 = points.0.y.native 428 | let x2 = points.1.x.native 429 | let y2 = points.1.y.native 430 | 431 | // calculated 432 | let dx0 = x0 - x1 433 | let dy0 = y0 - y1 434 | let dx2 = x2 - x1 435 | let dy2 = y2 - y1 436 | let xl0 = sqrt((dx0 * dx0) + (dy0 * dy0)) 437 | 438 | guard xl0 != 0 else { return } 439 | 440 | let xl2 = sqrt((dx2 * dx2) + (dy2 * dy2)) 441 | let san = (dx2 * dy0) - (dx0 * dy2) 442 | 443 | guard san != 0 else { 444 | 445 | addLine(to: points.0) 446 | return 447 | } 448 | 449 | let n0x: CGFloat.NativeType 450 | let n0y: CGFloat.NativeType 451 | let n2x: CGFloat.NativeType 452 | let n2y: CGFloat.NativeType 453 | 454 | if san < 0 { 455 | n0x = -dy0 / xl0 456 | n0y = dx0 / xl0 457 | n2x = dy2 / xl2 458 | n2y = -dx2 / xl2 459 | 460 | } else { 461 | n0x = dy0 / xl0 462 | n0y = -dx0 / xl0 463 | n2x = -dy2 / xl2 464 | n2y = dx2 / xl2 465 | } 466 | 467 | let t = (dx2*n2y - dx2*n0y - dy2*n2x + dy2*n0x) / san 468 | 469 | let center = CGPoint(x: CGFloat(x1 + radius.native * (t * dx0 + n0x)), 470 | y: CGFloat(y1 + radius.native * (t * dy0 + n0y))) 471 | let angle = (start: atan2(-n0y, -n0x), end: atan2(-n2y, -n2x)) 472 | 473 | self.addArc(center: center, 474 | radius: radius, 475 | startAngle: CGFloat(angle.start), 476 | endAngle: CGFloat(angle.end), 477 | clockwise: (san < 0)) 478 | } 479 | 480 | /// Adds a rectangular path to the current path. 481 | public func addRect(_ rect: CGRect) { 482 | 483 | internalContext.addRectangle(x: Double(rect.origin.x), 484 | y: Double(rect.origin.y), 485 | width: Double(rect.size.width), 486 | height: Double(rect.size.height)) 487 | } 488 | 489 | /// Adds a previously created path object to the current path in a graphics context. 490 | public func addPath(_ path: CGPath) { 491 | 492 | for element in path.elements { 493 | 494 | switch element { 495 | 496 | case let .moveToPoint(point): move(to: point) 497 | 498 | case let .addLineToPoint(point): addLine(to: point) 499 | 500 | case let .addQuadCurveToPoint(control, destination): addQuadCurve(to: destination, control: control) 501 | 502 | case let .addCurveToPoint(control1, control2, destination): addCurve(to: destination, control1: control1, control2: control2) 503 | 504 | case .closeSubpath: closePath() 505 | } 506 | } 507 | } 508 | 509 | // MARK: - Painting Paths 510 | 511 | /// Paints a line along the current path. 512 | public func strokePath() { 513 | 514 | if internalState.shadow != nil { 515 | 516 | startShadow() 517 | } 518 | 519 | internalContext.source = internalState.stroke?.pattern ?? .default 520 | 521 | internalContext.stroke() 522 | 523 | if internalState.shadow != nil { 524 | 525 | endShadow() 526 | } 527 | } 528 | 529 | /// Paints the area within the current path, as determined by the specified fill rule. 530 | public func fillPath(using rule: CGPathFillRule = .winding) { 531 | 532 | let evenOdd: Bool 533 | 534 | switch rule { 535 | case .evenOdd: evenOdd = true 536 | case .winding: evenOdd = false 537 | } 538 | 539 | try! fillPath(evenOdd: evenOdd, preserve: false) 540 | } 541 | 542 | public func clear() { 543 | 544 | internalContext.source = internalState.fill?.pattern ?? .default 545 | 546 | internalContext.clip() 547 | internalContext.clipPreserve() 548 | } 549 | 550 | public func drawPath(using mode: CGDrawingMode = CGDrawingMode()) { 551 | 552 | switch mode { 553 | case .fill: try! fillPath(evenOdd: false, preserve: false) 554 | case .evenOddFill: try! fillPath(evenOdd: true, preserve: false) 555 | case .fillStroke: try! fillPath(evenOdd: false, preserve: true) 556 | case .evenOddFillStroke: try! fillPath(evenOdd: true, preserve: true) 557 | case .stroke: strokePath() 558 | } 559 | } 560 | 561 | public func clip(evenOdd: Bool = false) { 562 | 563 | if evenOdd { 564 | 565 | internalContext.fillRule = CAIRO_FILL_RULE_EVEN_ODD 566 | } 567 | 568 | internalContext.clip() 569 | 570 | if evenOdd { 571 | 572 | internalContext.fillRule = CAIRO_FILL_RULE_WINDING 573 | } 574 | } 575 | 576 | @inline(__always) 577 | public func clip(to rect: CGRect) { 578 | 579 | beginPath() 580 | addRect(rect) 581 | clip() 582 | } 583 | 584 | // MARK: - Using Transparency Layers 585 | 586 | public func beginTransparencyLayer(in rect: CGRect? = nil, auxiliaryInfo: [String: Any]? = nil) { 587 | 588 | // in case we clip (for the rect) 589 | internalContext.save() 590 | 591 | if let rect = rect { 592 | 593 | internalContext.newPath() 594 | addRect(rect) 595 | internalContext.clip() 596 | } 597 | 598 | saveGState() 599 | alpha = 1.0 600 | internalState.shadow = nil 601 | 602 | internalContext.pushGroup() 603 | } 604 | 605 | public func endTransparencyLayer() { 606 | 607 | let group = internalContext.popGroup() 608 | 609 | // undo change to alpha and shadow state 610 | restoreGState() 611 | 612 | // paint contents 613 | internalContext.source = group 614 | internalContext.paint(alpha: Double(internalState.alpha)) 615 | 616 | // undo clipping (if any) 617 | internalContext.restore() 618 | } 619 | 620 | // MARK: - Drawing an Image to a Graphics Context 621 | 622 | /// Draws an image into a graphics context. 623 | public func draw(_ image: CGImage, in rect: CGRect) { 624 | 625 | internalContext.save() 626 | 627 | let imageSurface = image.surface 628 | 629 | let sourceRect = CGRect(x: 0, y: 0, width: CGFloat(image.width), height: CGFloat(image.height)) 630 | 631 | let pattern = Pattern(surface: imageSurface) 632 | 633 | var patternMatrix = Matrix.identity 634 | 635 | patternMatrix.translate(x: Double(rect.origin.x), 636 | y: Double(rect.origin.y)) 637 | 638 | patternMatrix.scale(x: Double(rect.size.width / sourceRect.size.width), 639 | y: Double(rect.size.height / sourceRect.size.height)) 640 | 641 | patternMatrix.scale(x: 1, y: -1) 642 | 643 | patternMatrix.translate(x: 0, y: Double(-sourceRect.size.height)) 644 | 645 | patternMatrix.invert() 646 | 647 | pattern.matrix = patternMatrix 648 | 649 | pattern.extend = .pad 650 | 651 | internalContext.operator = CAIRO_OPERATOR_OVER 652 | 653 | internalContext.source = pattern 654 | 655 | internalContext.addRectangle(x: Double(rect.origin.x), 656 | y: Double(rect.origin.y), 657 | width: Double(rect.size.width), 658 | height: Double(rect.size.height)) 659 | 660 | internalContext.fill() 661 | 662 | internalContext.restore() 663 | } 664 | 665 | // MARK: - Drawing Text 666 | 667 | public func setFont(_ font: CGFont) { 668 | 669 | internalContext.fontFace = font.scaledFont.face 670 | internalState.font = font 671 | } 672 | 673 | /// Uses the Cairo toy text API. 674 | public func show(toyText text: String) { 675 | 676 | let oldPoint = internalContext.currentPoint 677 | 678 | internalContext.move(to: (0, 0)) 679 | 680 | // calculate text matrix 681 | 682 | var cairoTextMatrix = Matrix.identity 683 | 684 | cairoTextMatrix.scale(x: Double(fontSize), y: Double(fontSize)) 685 | 686 | cairoTextMatrix.multiply(a: cairoTextMatrix, b: textMatrix.toCairo()) 687 | 688 | internalContext.setFont(matrix: cairoTextMatrix) 689 | 690 | internalContext.source = internalState.fill?.pattern ?? .default 691 | 692 | internalContext.show(text: text) 693 | 694 | let distance = internalContext.currentPoint ?? (0, 0) 695 | 696 | textPosition = CGPoint(x: textPosition.x + CGFloat(distance.x), y: textPosition.y + CGFloat(distance.y)) 697 | 698 | if let oldPoint = oldPoint { 699 | 700 | internalContext.move(to: oldPoint) 701 | 702 | } else { 703 | 704 | internalContext.newPath() 705 | } 706 | } 707 | 708 | public func show(text: String) { 709 | 710 | guard let font = internalState.font?.scaledFont, 711 | fontSize > 0.0 && text.isEmpty == false 712 | else { return } 713 | 714 | let glyphs = text.unicodeScalars.map { font[UInt($0.value)] } 715 | 716 | show(glyphs: glyphs) 717 | } 718 | 719 | public func show(glyphs: [FontIndex]) { 720 | 721 | guard let font = internalState.font, 722 | fontSize > 0.0 && glyphs.isEmpty == false 723 | else { return } 724 | 725 | let advances = font.advances(for: glyphs, fontSize: fontSize, textMatrix: textMatrix, characterSpacing: characterSpacing) 726 | 727 | show(glyphs: unsafeBitCast(glyphs.merge(advances), to: [(glyph: FontIndex, advance: CGSize)].self)) 728 | } 729 | 730 | public func show(glyphs glyphAdvances: [(glyph: FontIndex, advance: CGSize)]) { 731 | 732 | guard let font = internalState.font, 733 | fontSize > 0.0 && glyphAdvances.isEmpty == false 734 | else { return } 735 | 736 | let advances = glyphAdvances.map { $0.advance } 737 | let glyphs = glyphAdvances.map { $0.glyph } 738 | let positions = font.positions(for: advances, textMatrix: textMatrix) 739 | 740 | // render 741 | show(glyphs: unsafeBitCast(glyphs.merge(positions), to: [(glyph: FontIndex, position: CGPoint)].self)) 742 | 743 | // advance text position 744 | advances.forEach { 745 | textPosition.x += $0.width 746 | textPosition.y += $0.height 747 | } 748 | } 749 | 750 | public func show(glyphs glyphPositions: [(glyph: FontIndex, position: CGPoint)]) { 751 | 752 | guard let font = internalState.font?.scaledFont, 753 | fontSize > 0.0 && glyphPositions.isEmpty == false 754 | else { return } 755 | 756 | // actual rendering 757 | 758 | let cairoGlyphs: [cairo_glyph_t] = glyphPositions.indexedMap { (index, element) in 759 | 760 | var cairoGlyph = cairo_glyph_t() 761 | 762 | cairoGlyph.index = UInt(element.glyph) 763 | 764 | let userSpacePoint = element.position.applying(textMatrix) 765 | 766 | cairoGlyph.x = Double(userSpacePoint.x) 767 | 768 | cairoGlyph.y = Double(userSpacePoint.y) 769 | 770 | return cairoGlyph 771 | } 772 | 773 | var cairoTextMatrix = Matrix.identity 774 | 775 | cairoTextMatrix.scale(x: Double(fontSize), y: Double(fontSize)) 776 | 777 | let ascender = (Double(font.ascent) * Double(fontSize)) / Double(font.unitsPerEm) 778 | 779 | let silicaTextMatrix = Matrix(a: Double(textMatrix.a), 780 | b: Double(textMatrix.b), 781 | c: Double(textMatrix.c), 782 | d: Double(textMatrix.d), 783 | t: (0, ascender)) 784 | 785 | cairoTextMatrix.multiply(a: cairoTextMatrix, b: silicaTextMatrix) 786 | 787 | internalContext.setFont(matrix: cairoTextMatrix) 788 | 789 | internalContext.source = internalState.fill?.pattern ?? .default 790 | 791 | // show glyphs 792 | cairoGlyphs.forEach { internalContext.show(glyph: $0) } 793 | } 794 | 795 | // MARK: - Private Functions 796 | 797 | private func fillPath(evenOdd: Bool, preserve: Bool) throws(CairoError) { 798 | 799 | if internalState.shadow != nil { 800 | 801 | startShadow() 802 | } 803 | 804 | internalContext.source = internalState.fill?.pattern ?? .default 805 | 806 | internalContext.fillRule = evenOdd ? CAIRO_FILL_RULE_EVEN_ODD : CAIRO_FILL_RULE_WINDING 807 | 808 | internalContext.fillPreserve() 809 | 810 | if preserve == false { 811 | 812 | internalContext.newPath() 813 | } 814 | 815 | if internalState.shadow != nil { 816 | 817 | endShadow() 818 | } 819 | 820 | if let error = CairoError(internalContext.status) { 821 | throw error 822 | } 823 | } 824 | 825 | private func startShadow() { 826 | 827 | internalContext.pushGroup() 828 | } 829 | 830 | private func endShadow() { 831 | 832 | let pattern = internalContext.popGroup() 833 | 834 | internalContext.save() 835 | 836 | let radius = internalState.shadow!.radius 837 | 838 | let alphaSurface = try! Surface.Image(format: .a8, 839 | width: Int(ceil(size.width + 2 * radius)), 840 | height: Int(ceil(size.height + 2 * radius))) 841 | 842 | let alphaContext = Cairo.Context(surface: alphaSurface) 843 | 844 | alphaContext.source = pattern 845 | 846 | alphaContext.paint() 847 | 848 | alphaSurface.flush() 849 | 850 | internalContext.source = internalState.shadow!.pattern 851 | 852 | internalContext.mask(surface: alphaSurface, 853 | at: (Double(internalState.shadow!.offset.width), 854 | Double(internalState.shadow!.offset.height))) 855 | 856 | // draw content 857 | internalContext.source = pattern 858 | internalContext.paint() 859 | 860 | internalContext.restore() 861 | } 862 | } 863 | 864 | // MARK: - Private 865 | 866 | /// Default black pattern 867 | internal extension Cairo.Pattern { 868 | 869 | nonisolated(unsafe) static var `default`: Cairo.Pattern = Cairo.Pattern(color: (red: 0, green: 0, blue: 0)) 870 | } 871 | 872 | internal extension Silica.CGContext { 873 | 874 | /// To save non-Cairo state variables 875 | fileprivate final class State { 876 | 877 | var next: State? 878 | var alpha: CGFloat = 1.0 879 | var fill: (color: CGColor, pattern: Cairo.Pattern)? 880 | var stroke: (color: CGColor, pattern: Cairo.Pattern)? 881 | var shadow: (offset: CGSize, radius: CGFloat, color: CGColor, pattern: Cairo.Pattern)? 882 | var font: CGFont? 883 | var fontSize: CGFloat = 0.0 884 | var characterSpacing: CGFloat = 0.0 885 | var textMode = CGTextDrawingMode() 886 | 887 | init() { } 888 | 889 | var copy: State { 890 | 891 | let copy = State() 892 | 893 | copy.next = next 894 | copy.alpha = alpha 895 | copy.fill = fill 896 | copy.stroke = stroke 897 | copy.shadow = shadow 898 | copy.font = font 899 | copy.fontSize = fontSize 900 | copy.characterSpacing = characterSpacing 901 | copy.textMode = textMode 902 | 903 | return copy 904 | } 905 | } 906 | } 907 | 908 | // MARK: - Internal Extensions 909 | 910 | internal extension Collection { 911 | 912 | func indexedMap(_ transform: (Index, Iterator.Element) throws -> T) rethrows -> [T] { 913 | 914 | let count = self.count 915 | if count == 0 { 916 | return [] 917 | } 918 | 919 | var result = ContiguousArray() 920 | result.reserveCapacity(count) 921 | 922 | var i = self.startIndex 923 | 924 | for _ in 0.. 935 | (_ other: C) -> [(Iterator.Element, T)] 936 | where C.Iterator.Element == T, C.Index == Index { 937 | 938 | precondition(self.count == other.count, "The collection to merge must be of the same size") 939 | 940 | return self.indexedMap { ($1, other[$0]) } 941 | } 942 | } 943 | 944 | #if os(macOS) && Xcode 945 | 946 | import Foundation 947 | import AppKit 948 | 949 | public extension Silica.CGContext { 950 | 951 | @objc(debugQuickLookObject) 952 | var debugQuickLookObject: AnyObject { 953 | return surface.debugQuickLookObject 954 | } 955 | } 956 | 957 | #endif 958 | -------------------------------------------------------------------------------- /Sources/Silica/CGDrawingMode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DrawingMode.swift 3 | // Silica 4 | // 5 | // Created by Alsey Coleman Miller on 5/9/16. 6 | // Copyright © 2016 PureSwift. All rights reserved. 7 | // 8 | 9 | /// Options for rendering text. 10 | public enum CGTextDrawingMode: CInt, Sendable { 11 | 12 | case fill 13 | case stroke 14 | case fillStroke 15 | case invisible 16 | case fillClip 17 | case strokeClip 18 | case fillStrokeClip 19 | case clip 20 | 21 | public init() { self = .fill } 22 | } 23 | 24 | /// Options for rendering a path. 25 | public enum CGDrawingMode { 26 | 27 | /// Render the area contained within the path using the non-zero winding number rule. 28 | case fill 29 | 30 | /// Render the area within the path using the even-odd rule. 31 | case evenOddFill 32 | 33 | 34 | /// Render a line along the path. 35 | case stroke 36 | 37 | /// First fill and then stroke the path, using the nonzero winding number rule. 38 | case fillStroke 39 | 40 | /// First fill and then stroke the path, using the even-odd rule. 41 | case evenOddFillStroke 42 | 43 | // Source compatibility 44 | 45 | /// kCGPathEOFill 46 | public static var eoFill: CGDrawingMode { .evenOddFill } 47 | 48 | /// kCGPathEOFillStroke 49 | public static var eoFillStroke: CGDrawingMode { .evenOddFillStroke } 50 | 51 | public init() { self = .fill } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/Silica/CGFont.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Font.swift 3 | // Silica 4 | // 5 | // Created by Alsey Coleman Miller on 5/9/16. 6 | // Copyright © 2016 PureSwift. All rights reserved. 7 | // 8 | 9 | import Cairo 10 | import CCairo 11 | import FontConfig 12 | import struct Foundation.CGFloat 13 | import struct Foundation.CGSize 14 | import struct Foundation.CGPoint 15 | 16 | /// Silica's `Font` type. 17 | public struct CGFont { 18 | 19 | /// Private font cache. 20 | internal nonisolated(unsafe) static var cache = [String: CGFont]() 21 | 22 | // MARK: - Properties 23 | 24 | /// Same as full name. 25 | public let name: String 26 | 27 | /// Font family name. 28 | public let family: String 29 | 30 | // MARK: - Internal Properties 31 | 32 | public let scaledFont: Cairo.ScaledFont 33 | 34 | // MARK: - Initialization 35 | 36 | public init?(name: String, configuration: FontConfiguration = .current) { 37 | 38 | if let cachedFont = CGFont.cache[name] { 39 | 40 | self = cachedFont 41 | 42 | } else { 43 | 44 | // create new font 45 | guard let pattern = FontConfig.Pattern(cgFont: name, configuration: configuration), 46 | let family = pattern.family 47 | else { return nil } 48 | 49 | let face = FontFace(pattern: pattern) 50 | 51 | let options = FontOptions() 52 | options.hintMetrics = .off 53 | options.hintStyle = CAIRO_HINT_STYLE_NONE 54 | 55 | self.name = name 56 | self.family = family 57 | do { 58 | self.scaledFont = try ScaledFont( 59 | face: face, 60 | matrix: .identity, 61 | currentTransformation: .identity, 62 | options: options 63 | ) 64 | } 65 | catch .noMemory { 66 | assertionFailure("Insufficient memory to perform the operation.") 67 | return nil 68 | } 69 | catch { 70 | return nil 71 | } 72 | 73 | // Default font is Verdana, make sure the name is correct 74 | let defaultFontName = "Verdana" 75 | 76 | guard name == defaultFontName || scaledFont.fullName != defaultFontName 77 | else { return nil } 78 | 79 | // cache 80 | CGFont.cache[name] = self 81 | } 82 | } 83 | } 84 | 85 | // MARK: - Equatable 86 | 87 | extension CGFont: Equatable { 88 | 89 | public static func == (lhs: CGFont, rhs: CGFont) -> Bool { 90 | lhs.name == rhs.name 91 | } 92 | } 93 | 94 | // MARK: - Hashable 95 | 96 | extension CGFont: Hashable { 97 | 98 | public func hash(into hasher: inout Hasher) { 99 | name.hash(into: &hasher) 100 | } 101 | } 102 | 103 | // MARK: - Identifiable 104 | 105 | extension CGFont: Identifiable { 106 | 107 | public var id: String { name } 108 | } 109 | 110 | // MARK: - Text Math 111 | 112 | public extension CGFont { 113 | 114 | func advances(for glyphs: [FontIndex], fontSize: CGFloat, textMatrix: CGAffineTransform = .identity, characterSpacing: CGFloat = 0.0) -> [CGSize] { 115 | 116 | // only horizontal layout is supported 117 | 118 | // calculate advances 119 | let glyphSpaceToTextSpace = fontSize / CGFloat(scaledFont.unitsPerEm) 120 | 121 | return scaledFont.advances(for: glyphs).map { CGSize(width: (CGFloat($0) * glyphSpaceToTextSpace) + characterSpacing, height: 0).applying(textMatrix) } 122 | } 123 | 124 | func positions(for advances: [CGSize], textMatrix: CGAffineTransform = .identity) -> [CGPoint] { 125 | 126 | var glyphPositions = [CGPoint](repeating: CGPoint(), count: advances.count) 127 | 128 | // first position is {0, 0} 129 | for i in 1 ..< glyphPositions.count { 130 | 131 | let textSpaceAdvance = advances[i-1].applying(textMatrix) 132 | 133 | glyphPositions[i] = CGPoint(x: glyphPositions[i-1].x + textSpaceAdvance.width, 134 | y: glyphPositions[i-1].y + textSpaceAdvance.height) 135 | } 136 | 137 | return glyphPositions 138 | } 139 | 140 | func singleLineWidth(text: String, fontSize: CGFloat, textMatrix: CGAffineTransform = .identity) -> CGFloat { 141 | 142 | let glyphs = text.unicodeScalars.map { scaledFont[UInt($0.value)] } 143 | 144 | let textWidth = advances(for: glyphs, fontSize: fontSize, textMatrix: textMatrix).reduce(CGFloat(0), { $0 + $1.width }) 145 | 146 | return textWidth 147 | } 148 | } 149 | 150 | // MARK: - Font Config Pattern 151 | 152 | internal extension FontConfig.Pattern { 153 | 154 | convenience init?(cgFont name: String, configuration: FontConfiguration = .current) { 155 | self.init() 156 | 157 | let separator: Character = "-" 158 | 159 | let traits: String? 160 | 161 | let family: String 162 | 163 | let components = name.split(separator: separator, maxSplits: 2, omittingEmptySubsequences: true) 164 | 165 | if components.count == 2 { 166 | family = String(components[0]) 167 | traits = String(components[1]) 168 | } else { 169 | family = name 170 | traits = nil 171 | } 172 | 173 | self.family = family 174 | assert(self.family == family) 175 | 176 | // FontConfig assumes Medium Roman Regular, add / replace additional traits 177 | if let traits = traits { 178 | 179 | if traits.contains("Bold") { 180 | self.weight = .bold 181 | } 182 | 183 | if traits.contains("Italic") { 184 | self.slant = .italic 185 | } 186 | 187 | if traits.contains("Oblique") { 188 | self.slant = .oblique 189 | } 190 | 191 | if traits.contains("Condensed") { 192 | self.width = .condensed 193 | } 194 | } 195 | 196 | guard configuration.substitute(pattern: self, kind: FcMatchPattern) 197 | else { return nil } 198 | 199 | self.defaultSubstitutions() 200 | 201 | guard configuration.match(self) != nil 202 | else { return nil } 203 | 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /Sources/Silica/CGImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Image.swift 3 | // Silica 4 | // 5 | // Created by Alsey Coleman Miller on 5/11/16. 6 | // Copyright © 2016 PureSwift. All rights reserved. 7 | // 8 | 9 | import struct Foundation.Data 10 | import Cairo 11 | 12 | /// Represents bitmap images and bitmap image masks, based on sample data that you supply. 13 | /// A bitmap (or sampled) image is a rectangular array of pixels, 14 | /// with each pixel representing a single sample or data point in a source image. 15 | public final class CGImage { 16 | 17 | // MARK: - Properties 18 | 19 | public var width: Int { 20 | 21 | return surface.width 22 | } 23 | 24 | public var height: Int { 25 | 26 | return surface.height 27 | } 28 | 29 | /// The cached Cairo surface for this image. 30 | internal let surface: Cairo.Surface.Image 31 | 32 | // MARK: - Initialization 33 | 34 | internal init(surface: Cairo.Surface.Image) { 35 | 36 | self.surface = surface 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/Silica/CGImageDestination.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageDestination.swift 3 | // Silica 4 | // 5 | // Created by Alsey Coleman Miller on 6/2/17. 6 | // 7 | // 8 | 9 | import struct Foundation.Data 10 | 11 | /// This object abstracts the data-writing task. 12 | /// An image source can write image data to `Data`. 13 | public protocol ImageDestination: AnyObject, RandomAccessCollection, MutableCollection { 14 | 15 | 16 | } 17 | -------------------------------------------------------------------------------- /Sources/Silica/CGImageSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageSource.swift 3 | // Silica 4 | // 5 | // Created by Alsey Coleman Miller on 6/1/17. 6 | // 7 | // 8 | 9 | import struct Foundation.Data 10 | 11 | /// This object abstracts the data-reading task. 12 | /// An image source can read image data from a `Data` instance. 13 | public protocol CGImageSource: AnyObject { 14 | 15 | static var typeIdentifier: String { get } 16 | 17 | init?(data: Data) 18 | 19 | func createImage(at index: Int) -> CGImage? 20 | } 21 | 22 | // MARK: - CoreGraphics 23 | 24 | public enum CGImageSourceOption: String { 25 | 26 | case typeIdentifierHint = "kCGImageSourceTypeIdentifierHint" 27 | case shouldAllowFloat = "kCGImageSourceShouldAllowFloat" 28 | case shouldCache = "kCGImageSourceShouldCache" 29 | case createThumbnailFromImageIfAbsent = "kCGImageSourceCreateThumbnailFromImageIfAbsent" 30 | case createThumbnailFromImageAlways = "kCGImageSourceCreateThumbnailFromImageAlways" 31 | case createThumbnailWithTransform = "kCGImageSourceCreateThumbnailWithTransform" 32 | case thumbnailMaxPixelSize = "kCGImageSourceThumbnailMaxPixelSize" 33 | } 34 | 35 | public func CGImageSourceCreateWithData(_ data: Data, _ options: [CGImageSourceOption: Any]?) { 36 | 37 | 38 | } 39 | 40 | @inline(__always) 41 | public func CGImageSourceGetType(_ imageSource: T) -> String { 42 | T.typeIdentifier 43 | } 44 | 45 | public func CGImageSourceCopyTypeIdentifiers() -> [String] { 46 | [CGImageSourcePNG.typeIdentifier] 47 | } 48 | -------------------------------------------------------------------------------- /Sources/Silica/CGImageSourcePNG.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageSourcePNG.swift 3 | // Silica 4 | // 5 | // Created by Alsey Coleman Miller on 5/11/16. 6 | // Copyright © 2016 PureSwift. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import Darwin.C.math 11 | #elseif canImport(Glibc) 12 | import Glibc 13 | #endif 14 | 15 | import struct Foundation.Data 16 | import Cairo 17 | 18 | public final class CGImageSourcePNG: CGImageSource { 19 | 20 | // MARK: - Class Properties 21 | 22 | public static var typeIdentifier: String { "public.png" } 23 | 24 | // MARK: - Properties 25 | 26 | public let surface: Cairo.Surface.Image 27 | 28 | // MARK: - Initialization 29 | 30 | public init?(data: Data) { 31 | 32 | guard let surface = try? Cairo.Surface.Image(png: data) 33 | else { return nil } 34 | 35 | self.surface = surface 36 | } 37 | 38 | // MARK: - Methods 39 | 40 | public func createImage(at index: Int) -> CGImage? { 41 | let image = CGImage(surface: surface) 42 | return image 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/Silica/CGLineCap.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LineCap.swift 3 | // Silica 4 | // 5 | // Created by Alsey Coleman Miller on 5/9/16. 6 | // Copyright © 2016 PureSwift. All rights reserved. 7 | // 8 | 9 | import Cairo 10 | import CCairo 11 | 12 | public enum CGLineCap: UInt32 { 13 | 14 | case butt 15 | case round 16 | case square 17 | 18 | public init() { self = .butt } 19 | } 20 | 21 | // MARK: - Cairo Conversion 22 | 23 | extension CGLineCap: CairoConvertible { 24 | 25 | public typealias CairoType = cairo_line_cap_t 26 | 27 | public init(cairo: CairoType) { 28 | 29 | self.init(rawValue: cairo.rawValue)! 30 | } 31 | 32 | public func toCairo() -> CairoType { 33 | 34 | return CairoType(rawValue: rawValue) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/Silica/CGLineJoin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LineJoin.swift 3 | // Silica 4 | // 5 | // Created by Alsey Coleman Miller on 5/9/16. 6 | // Copyright © 2016 PureSwift. All rights reserved. 7 | // 8 | 9 | import Cairo 10 | import CCairo 11 | 12 | public enum CGLineJoin: UInt32 { 13 | 14 | case miter 15 | case round 16 | case bevel 17 | 18 | public init() { self = .miter } 19 | } 20 | 21 | // MARK: - Cairo Conversion 22 | 23 | extension CGLineJoin: CairoConvertible { 24 | 25 | public typealias CairoType = cairo_line_join_t 26 | 27 | public init(cairo: CairoType) { 28 | 29 | self.init(rawValue: cairo.rawValue)! 30 | } 31 | 32 | public func toCairo() -> CairoType { 33 | 34 | return CairoType(rawValue: rawValue) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/Silica/CGPath.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Path.swift 3 | // Silica 4 | // 5 | // Created by Alsey Coleman Miller on 5/8/16. 6 | // Copyright © 2016 PureSwift. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A graphics path is a mathematical description of a series of shapes or lines. 12 | public struct CGPath { 13 | 14 | public typealias Element = PathElement 15 | 16 | public var elements: [Element] 17 | 18 | public init(elements: [Element] = []) { 19 | 20 | self.elements = elements 21 | } 22 | } 23 | 24 | // MARK: - Supporting Types 25 | 26 | /// A path element. 27 | public enum PathElement { 28 | 29 | /// The path element that starts a new subpath. The element holds a single point for the destination. 30 | case moveToPoint(CGPoint) 31 | 32 | /// The path element that adds a line from the current point to a new point. 33 | /// The element holds a single point for the destination. 34 | case addLineToPoint(CGPoint) 35 | 36 | /// The path element that adds a quadratic curve from the current point to the specified point. 37 | /// The element holds a control point and a destination point. 38 | case addQuadCurveToPoint(CGPoint, CGPoint) 39 | 40 | /// The path element that adds a cubic curve from the current point to the specified point. 41 | /// The element holds two control points and a destination point. 42 | case addCurveToPoint(CGPoint, CGPoint, CGPoint) 43 | 44 | /// The path element that closes and completes a subpath. The element does not contain any points. 45 | case closeSubpath 46 | } 47 | 48 | // MARK: - Constructing a Path 49 | 50 | public extension CGPath { 51 | 52 | mutating func addRect(_ rect: CGRect) { 53 | 54 | let newElements: [Element] = [.moveToPoint(CGPoint(x: rect.minX, y: rect.minY)), 55 | .addLineToPoint(CGPoint(x: rect.maxX, y: rect.minY)), 56 | .addLineToPoint(CGPoint(x: rect.maxX, y: rect.maxY)), 57 | .addLineToPoint(CGPoint(x: rect.minX, y: rect.maxY)), 58 | .closeSubpath] 59 | 60 | elements.append(contentsOf: newElements) 61 | } 62 | 63 | mutating func addEllipse(in rect: CGRect) { 64 | 65 | var p = CGPoint() 66 | var p1 = CGPoint() 67 | var p2 = CGPoint() 68 | 69 | let hdiff = rect.width / 2 * KAPPA 70 | let vdiff = rect.height / 2 * KAPPA 71 | 72 | p = CGPoint(x: rect.origin.x + rect.width / 2, y: rect.origin.y + rect.height) 73 | elements.append(.moveToPoint(p)) 74 | 75 | p = CGPoint(x: rect.origin.x, y: rect.origin.y + rect.height / 2) 76 | p1 = CGPoint(x: rect.origin.x + rect.width / 2 - hdiff, y: rect.origin.y + rect.height) 77 | p2 = CGPoint(x: rect.origin.x, y: rect.origin.y + rect.height / 2 + vdiff) 78 | elements.append(.addCurveToPoint(p1, p2, p)) 79 | 80 | p = CGPoint(x: rect.origin.x + rect.size.width / 2, y: rect.origin.y) 81 | p1 = CGPoint(x: rect.origin.x, y: rect.origin.y + rect.size.height / 2 - vdiff) 82 | p2 = CGPoint(x: rect.origin.x + rect.size.width / 2 - hdiff, y: rect.origin.y) 83 | elements.append(.addCurveToPoint(p1, p2, p)) 84 | 85 | p = CGPoint(x: rect.origin.x + rect.size.width, y: rect.origin.y + rect.size.height / 2) 86 | p1 = CGPoint(x: rect.origin.x + rect.size.width / 2 + hdiff, y: rect.origin.y) 87 | p2 = CGPoint(x: rect.origin.x + rect.size.width, y: rect.origin.y + rect.size.height / 2 - vdiff) 88 | elements.append(.addCurveToPoint(p1, p2, p)) 89 | 90 | p = CGPoint(x: rect.origin.x + rect.size.width / 2, y: rect.origin.y + rect.size.height) 91 | p1 = CGPoint(x: rect.origin.x + rect.size.width, y: rect.origin.y + rect.size.height / 2 + vdiff) 92 | p2 = CGPoint(x: rect.origin.x + rect.size.width / 2 + hdiff, y: rect.origin.y + rect.size.height) 93 | elements.append(.addCurveToPoint(p1, p2, p)) 94 | } 95 | 96 | mutating func move(to point: CGPoint) { 97 | 98 | elements.append(.moveToPoint(point)) 99 | } 100 | 101 | mutating func addLine(to point: CGPoint) { 102 | 103 | elements.append(.addLineToPoint(point)) 104 | } 105 | 106 | mutating func addCurve(to endPoint: CGPoint, control1: CGPoint, control2: CGPoint) { 107 | 108 | elements.append(.addCurveToPoint(control1, control2, endPoint)) 109 | } 110 | 111 | mutating func addQuadCurve(to endPoint: CGPoint, control: CGPoint) { 112 | 113 | elements.append(.addQuadCurveToPoint(control, endPoint)) 114 | } 115 | 116 | mutating func closeSubpath() { 117 | 118 | elements.append(.closeSubpath) 119 | } 120 | } 121 | 122 | // This magic number is 4 *(sqrt(2) -1)/3 123 | private let KAPPA: CGFloat = 0.5522847498 124 | 125 | // MARK: - CoreGraphics API 126 | 127 | public struct CGPathElement { 128 | 129 | public var type: CGPathElementType 130 | 131 | public var points: (CGPoint, CGPoint, CGPoint) 132 | 133 | public init(type: CGPathElementType, points: (CGPoint, CGPoint, CGPoint)) { 134 | 135 | self.type = type 136 | self.points = points 137 | } 138 | } 139 | 140 | /// Rules for determining which regions are interior to a path. 141 | /// 142 | /// When filling a path, regions that a fill rule defines as interior to the path are painted. 143 | /// When clipping with a path, regions interior to the path remain visible after clipping. 144 | public enum CGPathFillRule: Int { 145 | 146 | /// A rule that considers a region to be interior to a path based on the number of times it is enclosed by path elements. 147 | case evenOdd 148 | 149 | /// A rule that considers a region to be interior to a path if the winding number for that region is nonzero. 150 | case winding 151 | } 152 | 153 | /// The type of element found in a path. 154 | public enum CGPathElementType { 155 | 156 | /// The path element that starts a new subpath. The element holds a single point for the destination. 157 | case moveToPoint 158 | 159 | /// The path element that adds a line from the current point to a new point. 160 | /// The element holds a single point for the destination. 161 | case addLineToPoint 162 | 163 | /// The path element that adds a quadratic curve from the current point to the specified point. 164 | /// The element holds a control point and a destination point. 165 | case addQuadCurveToPoint 166 | 167 | /// The path element that adds a cubic curve from the current point to the specified point. 168 | /// The element holds two control points and a destination point. 169 | case addCurveToPoint 170 | 171 | /// The path element that closes and completes a subpath. The element does not contain any points. 172 | case closeSubpath 173 | } 174 | 175 | // MARK: - Silica Conversion 176 | 177 | public extension CGPathElement { 178 | 179 | init(_ element: Silica.PathElement) { 180 | 181 | switch element { 182 | 183 | case let .moveToPoint(point): 184 | 185 | self.type = .moveToPoint 186 | self.points = (point, CGPoint(), CGPoint()) 187 | 188 | case let .addLineToPoint(point): 189 | 190 | self.type = .addLineToPoint 191 | self.points = (point, CGPoint(), CGPoint()) 192 | 193 | case let .addQuadCurveToPoint(control, destination): 194 | 195 | self.type = .addQuadCurveToPoint 196 | self.points = (control, destination, CGPoint()) 197 | 198 | case let .addCurveToPoint(control1, control2, destination): 199 | 200 | self.type = .addCurveToPoint 201 | self.points = (control1, control2, destination) 202 | 203 | case .closeSubpath: 204 | 205 | self.type = .closeSubpath 206 | self.points = (CGPoint(), CGPoint(), CGPoint()) 207 | } 208 | } 209 | } 210 | 211 | public extension Silica.PathElement { 212 | 213 | init(_ element: CGPathElement) { 214 | 215 | switch element.type { 216 | 217 | case .moveToPoint: self = .moveToPoint(element.points.0) 218 | 219 | case .addLineToPoint: self = .addLineToPoint(element.points.0) 220 | 221 | case .addQuadCurveToPoint: self = .addQuadCurveToPoint(element.points.0, element.points.1) 222 | 223 | case .addCurveToPoint: self = .addCurveToPoint(element.points.0, element.points.1, element.points.2) 224 | 225 | case .closeSubpath: self = .closeSubpath 226 | } 227 | } 228 | } 229 | 230 | -------------------------------------------------------------------------------- /Sources/Silica/CairoConvertible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CairoConvertible.swift 3 | // Silica 4 | // 5 | // Created by Alsey Coleman Miller on 5/9/16. 6 | // Copyright © 2016 PureSwift. All rights reserved. 7 | // 8 | 9 | public protocol CairoConvertible { 10 | 11 | associatedtype CairoType 12 | 13 | init(cairo: CairoType) 14 | 15 | func toCairo() -> CairoType 16 | } -------------------------------------------------------------------------------- /Sources/Silica/UIKit/NSStringDrawing.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSStringDrawing.swift 3 | // Cacao 4 | // 5 | // Created by Alsey Coleman Miller on 5/30/16. 6 | // Copyright © 2016 PureSwift. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public typealias NSParagraphStyle = NSMutableParagraphStyle 12 | public typealias NSStringDrawingContext = Void 13 | 14 | /// Encapsulates the paragraph or ruler attributes. 15 | public final class NSMutableParagraphStyle { 16 | 17 | // MARK: - Properties 18 | 19 | /// The text alignment 20 | public var alignment = NSTextAlignment() 21 | 22 | // MARK: - Initialization 23 | 24 | public init() { } 25 | 26 | public static func `default`() -> NSMutableParagraphStyle { 27 | return NSMutableParagraphStyle() 28 | } 29 | } 30 | 31 | extension NSMutableParagraphStyle { 32 | 33 | public func toCacao() -> ParagraphStyle { 34 | 35 | var paragraphStyle = ParagraphStyle() 36 | 37 | paragraphStyle.alignment = alignment.toCacao() 38 | 39 | return paragraphStyle 40 | } 41 | } 42 | 43 | public enum NSTextAlignment: Int { 44 | 45 | case left 46 | case center 47 | case right 48 | case justified 49 | case natural 50 | 51 | public init() { self = .left } 52 | } 53 | 54 | extension NSTextAlignment { 55 | 56 | public func toCacao() -> TextAlignment { 57 | 58 | switch self { 59 | 60 | case .left: return .left 61 | case .center: return .center 62 | case .right: return .right 63 | 64 | default: return .left 65 | } 66 | } 67 | } 68 | 69 | public enum NSLineBreakMode: Int { 70 | 71 | /// Wrap at word boundaries, default 72 | case byWordWrapping = 0 73 | case byCharWrapping 74 | case byClipping 75 | case byTruncatingHead 76 | case byTruncatingTail 77 | case byTruncatingMiddle 78 | 79 | public init() { self = .byWordWrapping } 80 | } 81 | 82 | /* 83 | extension NSLineBreakMode: CacaoConvertible { 84 | 85 | 86 | }*/ 87 | 88 | /// Rendering options for a string when it is drawn. 89 | public struct NSStringDrawingOptions: OptionSet, ExpressibleByIntegerLiteral { 90 | 91 | public var rawValue: Int 92 | 93 | public init(rawValue: Int) { 94 | self.rawValue = rawValue 95 | } 96 | 97 | public init(integerLiteral value: Int) { 98 | self.rawValue = value 99 | } 100 | 101 | public init() { 102 | self = .usesLineFragmentOrigin 103 | } 104 | } 105 | 106 | public extension NSStringDrawingOptions { 107 | 108 | static var usesLineFragmentOrigin: NSStringDrawingOptions { NSStringDrawingOptions(rawValue: (1 << 0)) } 109 | static var usesFontLeading: NSStringDrawingOptions { NSStringDrawingOptions(rawValue: (1 << 1)) } 110 | static var usesDeviceMetrics: NSStringDrawingOptions { NSStringDrawingOptions(rawValue: (1 << 3)) } 111 | static var truncatesLastVisibleLine: NSStringDrawingOptions { NSStringDrawingOptions(rawValue: (1 << 5)) } 112 | 113 | } 114 | 115 | /// Expects `UIFont` value. 116 | public let NSFontAttributeName = "NSFontAttributeName" 117 | 118 | /// Expects `UIColor` value. 119 | public let NSForegroundColorAttributeName = "NSForegroundColorAttributeName" 120 | 121 | /// Expects `NSMutableParagraphStyle` value. 122 | public let NSParagraphStyleAttributeName = "NSParagraphStyleAttributeName" 123 | 124 | public extension String { 125 | 126 | /// UIKit compatility drawing 127 | func draw(in rect: CGRect, withAttributes attributes: [String: Any]) { 128 | 129 | guard let context = UIGraphicsGetCurrentContext() 130 | else { return } 131 | 132 | // get values from attributes 133 | let textAttributes = TextAttributes(UIKit: attributes) 134 | 135 | self.draw(in: rect, context: context, attributes: textAttributes) 136 | } 137 | 138 | func boundingRect(with size: CGSize, options: NSStringDrawingOptions = NSStringDrawingOptions(), attributes: [String: Any], context: NSStringDrawingContext? = nil) -> CGRect { 139 | 140 | guard let context = UIGraphicsGetCurrentContext() 141 | else { return CGRect.zero } 142 | 143 | let textAttributes = TextAttributes(UIKit: attributes) 144 | 145 | var textFrame = self.contentFrame(for: CGRect(origin: CGPoint(), size: size), textMatrix: context.textMatrix, attributes: textAttributes) 146 | 147 | let font = textAttributes.font 148 | 149 | let descender = (CGFloat(font.cgFont.scaledFont.descent) * font.pointSize) / CGFloat(font.cgFont.scaledFont.unitsPerEm) 150 | 151 | 152 | textFrame.size.height = textFrame.size.height - descender 153 | //textFrame.size.height -= descender // Swift 3 error 154 | 155 | return textFrame 156 | } 157 | 158 | func draw(in rect: CGRect, context: Silica.CGContext, attributes: TextAttributes = TextAttributes()) { 159 | 160 | // set context values 161 | context.setTextAttributes(attributes) 162 | 163 | // render 164 | let textRect = self.contentFrame(for: rect, textMatrix: context.textMatrix, attributes: attributes) 165 | 166 | context.textPosition = textRect.origin 167 | 168 | context.show(text: self) 169 | } 170 | 171 | func contentFrame(for bounds: CGRect, textMatrix: CGAffineTransform = .identity, attributes: TextAttributes = TextAttributes()) -> CGRect { 172 | 173 | // assume horizontal layout (not rendering non-latin languages) 174 | 175 | // calculate frame 176 | 177 | let textWidth = attributes.font.cgFont.singleLineWidth(text: self, fontSize: attributes.font.pointSize, textMatrix: textMatrix) 178 | 179 | let lines: CGFloat = 1.0 180 | 181 | let textHeight = attributes.font.pointSize * lines 182 | 183 | var textRect = CGRect(x: bounds.origin.x, 184 | y: bounds.origin.y, 185 | width: textWidth, 186 | height: textHeight) // height == font.size 187 | 188 | switch attributes.paragraphStyle.alignment { 189 | 190 | case .left: break // always left by default 191 | 192 | case .center: textRect.origin.x = (bounds.width - textRect.width) / 2 193 | 194 | case .right: textRect.origin.x = bounds.width - textRect.width 195 | } 196 | 197 | return textRect 198 | } 199 | } 200 | 201 | // MARK: - Supporting Types 202 | 203 | public struct TextAttributes { 204 | 205 | public init() { } 206 | 207 | public var font = UIFont(name: "Helvetica", size: 17)! 208 | 209 | public var color = UIColor.black 210 | 211 | public var paragraphStyle = ParagraphStyle() 212 | } 213 | 214 | public extension TextAttributes { 215 | 216 | init(UIKit attributes: [String: Any]) { 217 | 218 | var textAttributes = TextAttributes() 219 | 220 | if let font = attributes[NSFontAttributeName] as? UIFont { 221 | 222 | textAttributes.font = font 223 | } 224 | 225 | if let textColor = (attributes[NSForegroundColorAttributeName] as? UIColor) { 226 | 227 | textAttributes.color = textColor 228 | } 229 | 230 | if let paragraphStyle = attributes[NSParagraphStyleAttributeName] as? NSParagraphStyle { 231 | 232 | textAttributes.paragraphStyle = paragraphStyle.toCacao() 233 | } 234 | 235 | self = textAttributes 236 | } 237 | } 238 | 239 | public struct ParagraphStyle { 240 | 241 | public init() { } 242 | 243 | public var alignment = TextAlignment() 244 | } 245 | 246 | public enum TextAlignment { 247 | 248 | public init() { self = .left } 249 | 250 | case left 251 | case center 252 | case right 253 | } 254 | 255 | // MARK: - Extensions 256 | 257 | public extension CGContext { 258 | 259 | func setTextAttributes(_ attributes: TextAttributes) { 260 | 261 | self.fontSize = attributes.font.pointSize 262 | self.setFont(attributes.font.cgFont) 263 | self.fillColor = attributes.color.cgColor 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /Sources/Silica/UIKit/UIBezierPath.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIBezierPath.swift 3 | // Cacao 4 | // 5 | // Created by Alsey Coleman Miller on 5/12/16. 6 | // Copyright © 2016 PureSwift. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import Darwin.C.math 11 | #elseif os(Linux) 12 | import Glibc 13 | #endif 14 | 15 | import Foundation 16 | 17 | /// The `UIBezierPath` class lets you define a path consisting of straight and curved line segments 18 | /// and render that path in your custom views. You use this class initially to specify just the geometry for your path. 19 | /// Paths can define simple shapes such as rectangles, ovals, and arcs or they can define complex polygons that 20 | /// incorporate a mixture of straight and curved line segments. 21 | /// After defining the shape, you can use additional methods of this class to render the path in the current drawing context. 22 | /// 23 | /// A `UIBezierPath` object combines the geometry of a path with attributes that describe the path during rendering. 24 | /// You set the geometry and attributes separately and can change them independent of one another. 25 | /// Once you have the object configured the way you want it, you can tell it to draw itself in the current context. 26 | /// Because the creation, configuration, and rendering process are all distinct steps, 27 | /// Bezier path objects can be reused easily in your code. 28 | /// You can even use the same object to render the same shape multiple times, 29 | /// perhaps changing the rendering options between successive drawing calls. 30 | public final class UIBezierPath { 31 | 32 | // MARK: - Properties 33 | 34 | public var cgPath: Silica.CGPath 35 | 36 | public var lineWidth: CGFloat = 1.0 37 | 38 | public var lineCapStyle: Silica.CGLineCap = .butt 39 | 40 | public var lineJoinStyle: Silica.CGLineJoin = .miter 41 | 42 | public var miterLimit: CGFloat = 10 43 | 44 | public var flatness: CGFloat = 0.6 45 | 46 | public var usesEvenOddFillRule: Bool = false 47 | 48 | public var lineDash: (phase: CGFloat, lengths: [CGFloat]) = (0.0, []) 49 | 50 | // MARK: - Initialization 51 | 52 | public init(cgPath path: Silica.CGPath = CGPath()) { 53 | 54 | self.cgPath = path 55 | } 56 | 57 | public init(rect: CGRect) { 58 | 59 | var path = CGPath() 60 | 61 | path.addRect(rect) 62 | 63 | self.cgPath = path 64 | } 65 | 66 | public init(ovalIn rect: CGRect) { 67 | 68 | var path = CGPath() 69 | 70 | path.addEllipse(in: rect) 71 | 72 | self.cgPath = path 73 | } 74 | 75 | public convenience init(roundedRect rect: CGRect, cornerRadius: CGFloat) { 76 | 77 | self.init(roundedRect: rect, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: cornerRadius, height: cornerRadius)) 78 | } 79 | 80 | public init(roundedRect rect: CGRect, byRoundingCorners corners: UIRectCorner, cornerRadii: CGSize) { 81 | 82 | var path = CGPath() 83 | 84 | func addCurve(_ control1: CGPoint, _ control2: CGPoint, _ end: CGPoint) { 85 | 86 | path.addCurve(to: end, control1: control1, control2: control2) 87 | } 88 | 89 | let topLeft = rect.origin 90 | let topRight = CGPoint(x: rect.maxX, y: rect.minY) 91 | let bottomRight = CGPoint(x: rect.maxX, y: rect.maxY) 92 | let bottomLeft = CGPoint(x: rect.minX, y: rect.maxY) 93 | 94 | if corners.contains(.topLeft) { 95 | path.move(to: CGPoint(x: topLeft.x+cornerRadii.width, y:topLeft.y)) 96 | } else { 97 | path.move(to: CGPoint(x: topLeft.x, y:topLeft.y)) 98 | } 99 | if corners.contains(.topRight) { 100 | path.addLine(to: CGPoint(x: topRight.x-cornerRadii.width, y: topRight.y)) 101 | addCurve(CGPoint(x: topRight.x, y: topRight.y), 102 | CGPoint(x: topRight.x, y: topRight.y+cornerRadii.height), 103 | CGPoint(x: topRight.x, y: topRight.y+cornerRadii.height)) 104 | } else { 105 | path.addLine(to: CGPoint(x: topRight.x, y: topRight.y)) 106 | } 107 | if corners.contains(.bottomRight) { 108 | path.addLine(to: CGPoint(x: bottomRight.x, y: bottomRight.y-cornerRadii.height)) 109 | addCurve(CGPoint(x: bottomRight.x, y: bottomRight.y), 110 | CGPoint(x: bottomRight.x-cornerRadii.width, y: bottomRight.y), 111 | CGPoint(x: bottomRight.x-cornerRadii.width, y: bottomRight.y)) 112 | } else { 113 | path.addLine(to: CGPoint(x: bottomRight.x, y: bottomRight.y)) 114 | } 115 | if corners.contains(.bottomLeft) { 116 | path.addLine(to: CGPoint(x: bottomLeft.x+cornerRadii.width, y: bottomLeft.y)) 117 | addCurve(CGPoint(x: bottomLeft.x, y: bottomLeft.y), 118 | CGPoint(x: bottomLeft.x, y: bottomLeft.y-cornerRadii.height), 119 | CGPoint(x:bottomLeft.x, y: bottomLeft.y-cornerRadii.height)) 120 | } else { 121 | path.addLine(to: CGPoint(x: bottomLeft.x, y: bottomLeft.y)) 122 | } 123 | if corners.contains(.topLeft) { 124 | path.addLine(to: CGPoint(x: topLeft.x, y: topLeft.y+cornerRadii.height)) 125 | addCurve(CGPoint(x: topLeft.x, y: topLeft.y), 126 | CGPoint(x: topLeft.x+cornerRadii.width, y: topLeft.y), 127 | CGPoint(x: topLeft.x+cornerRadii.width, y: topLeft.y)) 128 | } else { 129 | path.addLine(to: CGPoint(x: topLeft.x, y: topLeft.y)) 130 | } 131 | 132 | path.closeSubpath() 133 | 134 | self.cgPath = path 135 | } 136 | 137 | // MARK: - Accessors 138 | 139 | public var currentPoint: CGPoint { 140 | 141 | fatalError("Not implemented") 142 | } 143 | 144 | public var isEmpty: Bool { 145 | 146 | return cgPath.elements.isEmpty 147 | } 148 | 149 | public var bounds: CGRect { 150 | 151 | fatalError("Not implemented") 152 | } 153 | 154 | // MARK: - Methods 155 | 156 | // MARK: Drawing 157 | 158 | public func fill() { 159 | 160 | guard let context = UIGraphicsGetCurrentContext() 161 | else { return } 162 | 163 | context.saveGState() 164 | setContextPath() 165 | let fillRule: Silica.CGPathFillRule = usesEvenOddFillRule ? .evenOdd : .winding 166 | context.fillPath(using: fillRule) 167 | context.beginPath() 168 | context.restoreGState() 169 | } 170 | 171 | public func stroke() { 172 | 173 | guard let context = UIGraphicsGetCurrentContext() 174 | else { return } 175 | 176 | context.saveGState() 177 | setContextPath() 178 | context.strokePath() 179 | context.beginPath() 180 | context.restoreGState() 181 | } 182 | 183 | // MARK: Clipping Paths 184 | 185 | public func addClip() { 186 | 187 | guard let context = UIGraphicsGetCurrentContext() 188 | else { return } 189 | 190 | setContextPath() 191 | 192 | context.clip(evenOdd: usesEvenOddFillRule) 193 | } 194 | 195 | // MARK: - Constructing a Path 196 | 197 | public func move(to point: CGPoint) { 198 | 199 | cgPath.elements.append(.moveToPoint(point)) 200 | } 201 | 202 | public func addLine(to point: CGPoint) { 203 | 204 | cgPath.elements.append(.addLineToPoint(point)) 205 | } 206 | 207 | public func addCurve(to endPoint: CGPoint, controlPoint1: CGPoint, controlPoint2: CGPoint) { 208 | 209 | cgPath.elements.append(.addCurveToPoint(controlPoint1, controlPoint2, endPoint)) 210 | } 211 | 212 | public func addQuadCurve(to endPoint: CGPoint, controlPoint: CGPoint) { 213 | 214 | cgPath.elements.append(.addQuadCurveToPoint(controlPoint, endPoint)) 215 | } 216 | 217 | public func close() { 218 | 219 | cgPath.elements.append(.closeSubpath) 220 | } 221 | 222 | public func addArc(with center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, clockwise: Bool) { 223 | 224 | fatalError("Not implemented") 225 | } 226 | 227 | // MARK: - Private Methods 228 | 229 | private func setContextPath() { 230 | 231 | guard let context = UIGraphicsGetCurrentContext() 232 | else { return } 233 | 234 | context.beginPath() 235 | context.addPath(cgPath) 236 | context.lineWidth = lineWidth 237 | context.lineCap = lineCapStyle 238 | context.lineJoin = lineJoinStyle 239 | context.miterLimit = miterLimit 240 | context.tolerance = flatness 241 | context.lineDash = lineDash 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /Sources/Silica/UIKit/UIColor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor.swift 3 | // Silica 4 | // 5 | // Created by Alsey Coleman Miller on 5/12/16. 6 | // Copyright © 2016 PureSwift. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public final class UIColor { 12 | 13 | // MARK: - Properties 14 | 15 | public let cgColor: Silica.CGColor 16 | 17 | // MARK: - Initialization 18 | 19 | public init(cgColor color: Silica.CGColor) { 20 | 21 | self.cgColor = color 22 | } 23 | 24 | /// An initialized color object. The color information represented by this object is in the device RGB colorspace. 25 | public init(red: CGFloat, 26 | green: CGFloat, 27 | blue: CGFloat, 28 | alpha: CGFloat = 1.0) { 29 | 30 | self.cgColor = Silica.CGColor(red: red, green: green, blue: blue, alpha: alpha) 31 | } 32 | 33 | // MARK: - Methods 34 | 35 | // MARK: Retrieving Color Information 36 | 37 | public func getRed(_ red: inout CGFloat, 38 | green: inout CGFloat, 39 | blue: inout CGFloat, 40 | alpha: inout CGFloat) -> Bool { 41 | 42 | red = cgColor.red 43 | green = cgColor.green 44 | blue = cgColor.blue 45 | alpha = cgColor.alpha 46 | 47 | return true 48 | } 49 | 50 | // MARK: Drawing 51 | 52 | /// Sets the color of subsequent stroke and fill operations to the color that the receiver represents. 53 | public func set() { 54 | setFill() 55 | setStroke() 56 | } 57 | 58 | /// Sets the color of subsequent fill operations to the color that the receiver represents. 59 | public func setFill() { 60 | UIGraphicsGetCurrentContext()?.fillColor = self.cgColor 61 | } 62 | 63 | /// Sets the color of subsequent stroke operations to the color that the receiver represents. 64 | public func setStroke() { 65 | UIGraphicsGetCurrentContext()?.strokeColor = self.cgColor 66 | } 67 | 68 | // MARK: - Singletons 69 | 70 | public static var red: UIColor { UIColor(cgColor: .red) } 71 | 72 | public static var green: UIColor { UIColor(cgColor: .green) } 73 | 74 | public static var blue: UIColor { UIColor(cgColor: .blue) } 75 | 76 | public static var white: UIColor { UIColor(cgColor: .white) } 77 | 78 | public static var black: UIColor { UIColor(cgColor: .black) } 79 | 80 | public static var clear: UIColor { UIColor(cgColor: .clear) } 81 | } 82 | -------------------------------------------------------------------------------- /Sources/Silica/UIKit/UIFont.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIFont.swift 3 | // Cacao 4 | // 5 | // Created by Alsey Coleman Miller on 5/31/16. 6 | // Copyright © 2016 PureSwift. All rights reserved. 7 | // 8 | 9 | import struct Foundation.CGFloat 10 | 11 | public final class UIFont { 12 | 13 | // MARK: - Properties 14 | 15 | public let cgFont: CGFont 16 | 17 | // MARK: Font Name Attributes 18 | 19 | public var fontName: String { return cgFont.name } 20 | 21 | public var familyName: String { return cgFont.family } 22 | 23 | // MARK: Font Metrics 24 | 25 | public let pointSize: CGFloat 26 | 27 | public lazy var descender: CGFloat = (CGFloat(self.cgFont.scaledFont.descent) * self.pointSize) / CGFloat(self.cgFont.scaledFont.unitsPerEm) 28 | 29 | public lazy var ascender: CGFloat = (CGFloat(self.cgFont.scaledFont.ascent) * self.pointSize) / CGFloat(self.cgFont.scaledFont.unitsPerEm) 30 | 31 | // MARK: - Initialization 32 | 33 | public init?(name: String, size: CGFloat) { 34 | 35 | guard let cgFont = CGFont(name: name) 36 | else { return nil } 37 | 38 | self.cgFont = cgFont 39 | self.pointSize = size 40 | } 41 | } 42 | 43 | // MARK: - Extensions 44 | 45 | public extension UIFont { 46 | 47 | static func systemFont(ofSize size: CGFloat) -> UIFont { 48 | return UIFont(name: "HelveticaNeu", size: size)! 49 | } 50 | 51 | static func boldSystemFont(ofSize size: CGFloat) -> UIFont { 52 | return UIFont(name: "HelveticaNeu-Bold", size: size)! 53 | } 54 | } 55 | 56 | // MARK: - Equatable 57 | 58 | extension UIFont: Equatable { 59 | 60 | public static func == (lhs: UIFont, rhs: UIFont) -> Bool { 61 | return lhs.fontName == rhs.fontName 62 | && lhs.pointSize == rhs.pointSize 63 | } 64 | } 65 | 66 | // MARK: - Hashable 67 | 68 | extension UIFont: Hashable { 69 | 70 | public func hash(into hasher: inout Hasher) { 71 | hasher.combine(fontName) 72 | hasher.combine(pointSize) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Sources/Silica/UIKit/UIGraphics.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIGraphics.swift 3 | // Cacao 4 | // 5 | // Created by Alsey Coleman Miller on 5/12/16. 6 | // Copyright © 2016 PureSwift. All rights reserved. 7 | // 8 | 9 | /// Returns the current graphics context. 10 | /// 11 | /// The current graphics context is `nil` by default. 12 | /// Prior to calling its drawRect: method, view objects push a valid context onto the stack, making it current. 13 | /// If you are not using a UIView object to do your drawing, however, 14 | /// you must push a valid context onto the stack manually using the `UIGraphicsPushContext()` function. 15 | /// 16 | /// This function may be called from any thread of your app. 17 | public func UIGraphicsGetCurrentContext() -> CGContext? { 18 | UIKitContextStack.last 19 | } 20 | 21 | /// Makes the specified graphics context the current context. 22 | public func UIGraphicsPushContext(_ context: CGContext) { 23 | UIKitContextStack.append(context) 24 | } 25 | 26 | /// Removes the current graphics context from the top of the stack, restoring the previous context. 27 | public func UIGraphicsPopContext() { 28 | if UIKitContextStack.isEmpty == false { 29 | UIKitContextStack.removeLast() 30 | } 31 | } 32 | 33 | // MARK: - Private 34 | 35 | nonisolated(unsafe) private var UIKitContextStack: [CGContext] = [] 36 | -------------------------------------------------------------------------------- /Sources/Silica/UIKit/UIImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage.swift 3 | // Silica 4 | // 5 | // Created by Alsey Coleman Miller on 6/3/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | /// UIKit compatibility layer for UIImage 12 | public final class UIImage { 13 | 14 | public let cgImage: Silica.CGImage 15 | 16 | public init(cgImage: Silica.CGImage) { 17 | self.cgImage = cgImage 18 | } 19 | 20 | public var size: CGSize { 21 | return CGSize(width: CGFloat(cgImage.width), height: CGFloat(cgImage.height)) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/Silica/UIKit/UIRectCorner.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIRectCorner.swift 3 | // Cacao 4 | // 5 | // Created by Alsey Coleman Miller on 6/15/17. 6 | // 7 | 8 | /// The corners of a rectangle. 9 | /// 10 | /// The specified constants reflect the corners of a rectangle that has not been modified by an affine transform and is drawn in 11 | /// the default coordinate system (where the origin is in the upper-left corner and positive values extend down and to the right). 12 | public struct UIRectCorner: OptionSet { 13 | 14 | public let rawValue: Int 15 | 16 | public init(rawValue: Int) { 17 | self.rawValue = rawValue 18 | } 19 | } 20 | 21 | public extension UIRectCorner { 22 | 23 | static var topLeft: UIRectCorner { UIRectCorner(rawValue: 1 << 0) } 24 | static var topRight: UIRectCorner { UIRectCorner(rawValue: 1 << 1) } 25 | static var bottomLeft: UIRectCorner { UIRectCorner(rawValue: 1 << 2) } 26 | static var bottomRight: UIRectCorner { UIRectCorner(rawValue: 1 << 3) } 27 | static var allCorners: UIRectCorner { UIRectCorner(rawValue: ~0) } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/Silica/UIKit/UIViewContentMode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewContentMode.swift 3 | // Cacao 4 | // 5 | // Created by Alsey Coleman Miller on 5/31/16. 6 | // Copyright © 2016 PureSwift. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import Darwin.C.math 11 | #elseif canImport(Glibc) 12 | import Glibc 13 | #endif 14 | 15 | import Foundation 16 | 17 | public enum UIViewContentMode: Int, Sendable, CaseIterable { 18 | 19 | public init() { self = .scaleToFill } 20 | 21 | case scaleToFill 22 | case scaleAspectFit 23 | case scaleAspectFill 24 | case redraw 25 | case center 26 | case top 27 | case bottom 28 | case left 29 | case right 30 | case topLeft 31 | case topRight 32 | case bottomLeft 33 | case bottomRight 34 | } 35 | 36 | // MARK: - Silica Extension 37 | 38 | public extension CGRect { 39 | 40 | init(contentMode: UIViewContentMode, bounds: CGRect, size: CGSize) { 41 | 42 | switch contentMode { 43 | 44 | case .redraw: fallthrough 45 | 46 | case .scaleToFill: 47 | 48 | self.init(origin: .zero, size: bounds.size) 49 | 50 | case .scaleAspectFit: 51 | 52 | let widthRatio = bounds.width / size.width 53 | let heightRatio = bounds.height / size.height 54 | 55 | var newSize = bounds.size 56 | 57 | if (widthRatio < heightRatio) { 58 | 59 | newSize.height = bounds.size.width / size.width * size.height 60 | 61 | } else if (heightRatio < widthRatio) { 62 | 63 | newSize.width = bounds.size.height / size.height * size.width 64 | } 65 | 66 | newSize = CGSize(width: ceil(newSize.width), height: ceil(newSize.height)) 67 | 68 | var origin = bounds.origin 69 | origin.x += (bounds.size.width - newSize.width) / 2.0 70 | origin.y += (bounds.size.height - newSize.height) / 2.0 71 | 72 | self.init(origin: origin, size: newSize) 73 | 74 | case .scaleAspectFill: 75 | 76 | let widthRatio = (bounds.size.width / size.width) 77 | let heightRatio = (bounds.size.height / size.height) 78 | 79 | var newSize = bounds.size 80 | 81 | if (widthRatio > heightRatio) { 82 | 83 | newSize.height = bounds.size.width / size.width * size.height 84 | 85 | } else if (heightRatio > widthRatio) { 86 | 87 | newSize.width = bounds.size.height / size.height * size.width 88 | } 89 | 90 | newSize = CGSize(width: ceil(newSize.width), height: ceil(newSize.height)) 91 | 92 | var origin = CGPoint() 93 | origin.x = (bounds.size.width - newSize.width) / 2.0 94 | origin.y = (bounds.size.height - newSize.height) / 2.0 95 | 96 | self.init(origin: origin, size: newSize) 97 | 98 | case .center: 99 | 100 | var rect = CGRect(origin: .zero, size: size) 101 | 102 | rect.origin.x = (bounds.size.width - rect.size.width) / 2.0 103 | rect.origin.y = (bounds.size.height - rect.size.height) / 2.0 104 | 105 | self = rect 106 | 107 | case .top: 108 | 109 | var rect = CGRect(origin: .zero, size: size) 110 | 111 | rect.origin.y = 0.0 112 | rect.origin.x = (bounds.size.width - rect.size.width) / 2.0 113 | 114 | self = rect 115 | 116 | case .bottom: 117 | 118 | var rect = CGRect(origin: .zero, size: size) 119 | 120 | rect.origin.x = (bounds.size.width - rect.size.width) / 2.0 121 | rect.origin.y = bounds.size.height - rect.size.height 122 | 123 | self = rect 124 | 125 | case .left: 126 | 127 | var rect = CGRect(origin: .zero, size: size) 128 | 129 | rect.origin.x = 0.0 130 | rect.origin.y = (bounds.size.height - rect.size.height) / 2.0 131 | 132 | self = rect 133 | 134 | case .right: 135 | 136 | var rect = CGRect(origin: .zero, size: size) 137 | 138 | rect.origin.x = bounds.size.width - rect.size.width 139 | rect.origin.y = (bounds.size.height - rect.size.height) / 2.0 140 | 141 | self = rect 142 | 143 | case .topLeft: 144 | 145 | self.init(origin: .zero, size: size) 146 | 147 | case .topRight: 148 | 149 | var rect = CGRect(origin: .zero, size: size) 150 | 151 | rect.origin.x = bounds.size.width - rect.size.width 152 | rect.origin.y = 0.0 153 | 154 | self = rect 155 | 156 | case .bottomLeft: 157 | 158 | var rect = CGRect(origin: .zero, size: size) 159 | 160 | rect.origin.x = 0.0 161 | rect.origin.y = bounds.size.height - rect.size.height 162 | 163 | self = rect 164 | 165 | case .bottomRight: 166 | 167 | var rect = CGRect(origin: .zero, size: size) 168 | 169 | rect.origin.x = bounds.size.width - rect.size.width 170 | rect.origin.y = bounds.size.height - rect.size.height 171 | 172 | self = rect 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /Tests/SilicaTests/FontTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FontTests.swift 3 | // Silica 4 | // 5 | // Created by Alsey Coleman Miller on 6/1/16. 6 | // Copyright © 2016 PureSwift. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Silica 11 | import FontConfig 12 | 13 | final class FontTests: XCTestCase { 14 | 15 | func testCreateFont() { 16 | 17 | #if os(Linux) 18 | var fontNames = [ 19 | ("LiberationSerif", "Liberation Serif", FontWeight.regular), 20 | ("LiberationSerif-Bold", "Liberation Serif", .bold) 21 | ] 22 | 23 | #else 24 | var fontNames = [ 25 | ("TimesNewRoman", "Times New Roman", FontWeight.regular), 26 | ("TimesNewRoman-Bold", "Times New Roman", .bold) 27 | ] 28 | #endif 29 | 30 | #if os(macOS) 31 | fontNames += [("MicrosoftSansSerif", "Microsoft Sans Serif", .regular), 32 | ("MicrosoftSansSerif-Bold", "Microsoft Sans Serif", .bold)] 33 | #endif 34 | 35 | for (fontName, expectedFullName, weight) in fontNames { 36 | 37 | guard let font = Silica.CGFont(name: fontName), 38 | let pattern = FontConfig.Pattern(cgFont: fontName) 39 | else { XCTFail("Could not create font \(fontName)"); return } 40 | 41 | XCTAssertEqual(font.name, font.name) 42 | XCTAssertEqual(expectedFullName, font.scaledFont.fullName) 43 | XCTAssertEqual(pattern.family, font.family) 44 | XCTAssertEqual(pattern.weight, weight) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Tests/SilicaTests/StyleKitTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StyleKitTests.swift 3 | // Silica 4 | // 5 | // Created by Alsey Coleman Miller on 5/11/16. 6 | // Copyright © 2016 PureSwift. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import Foundation 11 | import Cairo 12 | @testable import Silica 13 | 14 | final class StyleKitTests: XCTestCase { 15 | 16 | private func draw(_ drawingMethod: @autoclosure () -> (), _ name: String, _ size: CGSize) { 17 | 18 | let filename = TestPath.testData + name + ".pdf" 19 | 20 | let frame = CGRect(origin: .zero, size: size) 21 | 22 | let surface = try! Surface.PDF(filename: filename, width: Double(frame.width), height: Double(frame.height)) 23 | 24 | let context = try! Silica.CGContext(surface: surface, size: frame.size) 25 | 26 | UIGraphicsPushContext(context) 27 | 28 | drawingMethod() 29 | 30 | UIGraphicsPopContext() 31 | 32 | print("Wrote to \(filename)") 33 | } 34 | 35 | func testSimpleShapes() { 36 | 37 | draw(TestStyleKit.drawSimpleShapes(), "simpleShapes", CGSize(width: 240, height: 120)) 38 | } 39 | 40 | func testAdvancedShapes() { 41 | 42 | draw(TestStyleKit.drawAdvancedShapes(), "advancedShapes", CGSize(width: 240, height: 120)) 43 | } 44 | 45 | func testImagePNG() async throws { 46 | 47 | try await TestAssetManager.shared.fetchAssets() 48 | 49 | draw(TestStyleKit.drawImagePNG(), "imagePNG", CGSize(width: 240, height: 180)) 50 | } 51 | 52 | func testDrawSwiftLogo() { 53 | 54 | draw(TestStyleKit.drawSwiftLogo(frame: CGRect(origin: .zero, size: CGSize(width: 200, height: 200))), "SwiftLogo", CGSize(width: 200, height: 200)) 55 | } 56 | 57 | func testDrawSwiftLogoWithText() { 58 | 59 | draw(TestStyleKit.drawSwiftLogoWithText(frame: CGRect(origin: .zero, size: CGSize(width: 164 * 2, height: 48 * 2))), "SwiftLogoWithText", CGSize(width: 164 * 2, height: 48 * 2)) 60 | } 61 | 62 | func testDrawSingleLineText() { 63 | 64 | draw(TestStyleKit.drawSingleLineText(), "singleLineText", CGSize(width: 240, height: 120)) 65 | } 66 | 67 | func testDrawMultilineText() { 68 | 69 | draw(TestStyleKit.drawMultiLineText(), "multilineText", CGSize(width: 240, height: 180)) 70 | } 71 | 72 | func testContentMode() { 73 | 74 | let bounds = CGRect(origin: .zero, size: CGSize(width: 320, height: 240)) 75 | let testData: [((CGRect) -> (), CGSize)] = [ 76 | (TestStyleKit.drawSwiftLogo, CGSize(width: 32, height: 32)), 77 | (TestStyleKit.drawSwiftLogoWithText, CGSize(width: 32 * (41 / 12), height: 32)) 78 | ] 79 | for (index, (drawingFunction, intrinsicContentSize)) in testData.enumerated() { 80 | let imageNumber = index + 1 81 | for contentMode in UIViewContentMode.allCases { 82 | let frame = CGRect(contentMode: contentMode, bounds: bounds, size: intrinsicContentSize) 83 | print("Draw image \(imageNumber) with content mode \(contentMode). \(frame)") 84 | draw(drawingFunction(frame), "testContentMode_\(imageNumber)_\(contentMode)", bounds.size) 85 | } 86 | } 87 | 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Tests/SilicaTests/Utilities/Define.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Define.swift 3 | // Silica 4 | // 5 | // Created by Alsey Coleman Miller on 6/6/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | #if canImport(FoundationNetworking) 11 | import FoundationNetworking 12 | #endif 13 | 14 | struct TestPath { 15 | 16 | static let unitTests: String = try! createDirectory(at: NSTemporaryDirectory() + "SilicaTests" + "/") 17 | 18 | static let assets: String = try! createDirectory(at: unitTests + "TestAssets" + "/") 19 | 20 | static let testData: String = try! createDirectory(at: unitTests + "TestData" + "/", removeContents: true) 21 | 22 | private static func createDirectory(at filePath: String, removeContents: Bool = false) throws -> String { 23 | 24 | var isDirectory: ObjCBool = false 25 | 26 | if FileManager.default.fileExists(atPath: filePath, isDirectory: &isDirectory) == false { 27 | 28 | try! FileManager.default.createDirectory(atPath: filePath, withIntermediateDirectories: false) 29 | } 30 | 31 | if removeContents { 32 | 33 | // remove all files in directory (previous test cache) 34 | let contents = try! FileManager.default.contentsOfDirectory(atPath: filePath) 35 | 36 | contents.forEach { try! FileManager.default.removeItem(atPath: filePath + $0) } 37 | } 38 | 39 | return filePath 40 | } 41 | } 42 | 43 | extension TestAssetManager where HTTPClient == URLSession { 44 | 45 | nonisolated(unsafe) static let shared: TestAssetManager = TestAssetManager( 46 | assets: testAssets, 47 | cacheDirectory: URL(fileURLWithPath: TestPath.assets), 48 | httpClient: URLSession(configuration: .ephemeral) 49 | ) 50 | } 51 | 52 | private let testAssets: [TestAsset] = [ 53 | TestAsset(url: URL(string: "https://httpbin.org/image/png")!, filename: "png.png"), 54 | TestAsset(url: URL(string: "http://www.schaik.com/pngsuite/basn0g01.png")!, filename: "basn0g01.png"), 55 | TestAsset(url: URL(string: "http://www.schaik.com/pngsuite/basn0g02.png")!, filename: "basn0g02.png"), 56 | TestAsset(url: URL(string: "http://www.schaik.com/pngsuite/basn0g04.png")!, filename: "basn0g04.png"), 57 | TestAsset(url: URL(string: "http://www.schaik.com/pngsuite/basn0g08.png")!, filename: "basn0g08.png"), 58 | TestAsset(url: URL(string: "http://www.schaik.com/pngsuite/basn0g16.png")!, filename: "basn0g16.png") 59 | ] 60 | -------------------------------------------------------------------------------- /Tests/SilicaTests/Utilities/TestAssets.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestAssets.swift 3 | // Silica 4 | // 5 | // Created by Alsey Coleman Miller on 6/6/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | import Silica 11 | #if canImport(FoundationNetworking) 12 | import FoundationNetworking 13 | #endif 14 | 15 | struct TestAsset: Equatable, Hashable { 16 | 17 | let url: URL 18 | let filename: String 19 | } 20 | 21 | final class TestAssetManager { 22 | 23 | let assets: [TestAsset] 24 | 25 | let cacheDirectory: URL 26 | 27 | let httpClient: HTTPClient 28 | 29 | private(set) var downloadedAssets = [TestAsset]() 30 | 31 | init(assets: [TestAsset], cacheDirectory: URL, httpClient: HTTPClient) { 32 | 33 | self.assets = assets 34 | self.cacheDirectory = cacheDirectory 35 | self.httpClient = httpClient 36 | } 37 | 38 | public func fetchAssets(skipCached: Bool = true) async throws { 39 | 40 | let httpClient = self.httpClient 41 | 42 | for asset in assets { 43 | 44 | // get destination file path 45 | let fileURL = cacheURL(for: asset.filename) 46 | 47 | // skip if already downloaded 48 | if skipCached { 49 | 50 | guard FileManager.default.fileExists(atPath: fileURL.path) == false 51 | else { continue } 52 | } 53 | 54 | print("Fetching \(asset.filename)") 55 | 56 | // fetch data 57 | 58 | let request = URLRequest(url: asset.url) 59 | 60 | let (data, response) = try await httpClient.data(for: request) 61 | 62 | guard let httpResponse = response as? HTTPURLResponse else { 63 | fatalError() 64 | } 65 | 66 | guard httpResponse.statusCode == 200 67 | else { throw Error.invalidStatusCode(httpResponse.statusCode) } 68 | 69 | guard data.isEmpty == false 70 | else { throw Error.emptyData } 71 | 72 | // save to file system 73 | 74 | guard FileManager.default.createFile(atPath: fileURL.path, contents: data, attributes: nil) 75 | else { throw Error.fileWrite } 76 | 77 | downloadedAssets.append(asset) 78 | 79 | print("Downloaded \(asset.url.absoluteString)") 80 | } 81 | } 82 | 83 | func cacheURL(for assetFilename: String) -> URL { 84 | return cacheDirectory.appendingPathComponent(assetFilename) 85 | } 86 | 87 | func cachedImage(named name: String) -> CGImage? { 88 | let fileURL = cacheURL(for: name) 89 | guard let data = try? Data(contentsOf: fileURL), 90 | let imageSource = CGImageSourcePNG(data: data), 91 | let image = imageSource.createImage(at: 0) 92 | else { return nil } 93 | return image 94 | } 95 | } 96 | 97 | extension TestAssetManager { 98 | 99 | enum Error: Swift.Error { 100 | 101 | case invalidStatusCode(Int) 102 | case emptyData 103 | case fileWrite 104 | } 105 | } 106 | 107 | // MARK: - UIImage 108 | 109 | extension UIImage { 110 | 111 | convenience init?(named name: String) { 112 | guard let image = TestAssetManager.shared.cachedImage(named: name) else { 113 | return nil 114 | } 115 | self.init(cgImage: image) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Tests/SilicaTests/Utilities/URLClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLClient.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 8/21/23. 6 | // 7 | 8 | import Foundation 9 | #if canImport(FoundationNetworking) 10 | import FoundationNetworking 11 | #endif 12 | 13 | /// URL Client 14 | public protocol URLClient { 15 | 16 | func data(for request: URLRequest) async throws -> (Data, URLResponse) 17 | } 18 | 19 | extension URLSession: URLClient { 20 | 21 | public func data(for request: URLRequest) async throws -> (Data, URLResponse) { 22 | #if canImport(Darwin) 23 | if #available(macOS 12, iOS 15.0, tvOS 15, watchOS 8, *) { 24 | return try await self.data(for: request, delegate: nil) 25 | } else { 26 | return try await _data(for: request) 27 | } 28 | #else 29 | return try await _data(for: request) 30 | #endif 31 | } 32 | } 33 | 34 | internal extension URLSession { 35 | 36 | func _data(for request: URLRequest) async throws -> (Data, URLResponse) { 37 | try await withCheckedThrowingContinuation { continuation in 38 | let task = self.dataTask(with: request) { data, response, error in 39 | if let error = error { 40 | continuation.resume(throwing: error) 41 | } else { 42 | continuation.resume(returning: (data ?? .init(), response!)) 43 | } 44 | } 45 | task.resume() 46 | } 47 | } 48 | } 49 | --------------------------------------------------------------------------------