Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
vincentisambart committed Nov 29, 2023
0 parents commit 691f03f
Show file tree
Hide file tree
Showing 16 changed files with 778 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
I do not intend to merge pull requests with something else than small bug fixes.

Do not hesitate to fork this project and adapt it to your own needs.
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
22 changes: 22 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
The MIT License (MIT)

Copyright (c) 2019 Daniel Griesser <daniel.griesser.86@gmail.com>
Copyright (c) 2023 Vincent Isambart

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
14 changes: 14 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"pins" : [
{
"identity" : "swift-argument-parser",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser.git",
"state" : {
"revision" : "8f4d2753f0e4778c76d5f05ad16c74f707390531",
"version" : "1.2.3"
}
}
],
"version" : 2
}
31 changes: 31 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "Swadgics",
// macOS 13 to be able to use `Regex`.
// (It should not be hard to support older versions of macOS using `NSRegularExpression` if needed)
platforms: [.macOS(.v13)],
products: [
.executable(name: "swadgics", targets: ["Swadgics"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.2.3"),
],
targets: [
.executableTarget(
name: "Swadgics",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
],
resources: [
.embedInCode("Resources/alpha_badge_dark.png"),
.embedInCode("Resources/alpha_badge_light.png"),
.embedInCode("Resources/beta_badge_dark.png"),
.embedInCode("Resources/beta_badge_light.png"),
]
),
]
)
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Swadgics

Partial reimplementation of [badge](https://github.com/HazAT/badge) in Swift using Core Graphics (Swadgics = Sw(ift) + (b)adg(e) + (Core Graph)ics).

Its main advantage is that it does not require ImageMagick, GraphicsMagick, or RSVG, and that at runtime it does not use the network. As it uses Core Graphics it needs to be run on macOS, but for iOS applications that should not be much of a limitation.

Instead of sending pull requests to add new features, do not hesitate to fork it and adapt it to your own needs.

This project under the MIT license. The files under `Sources/Resources/` have been copied as-is from [badge](https://github.com/HazAT/badge) but were already under MIT license.

## Differences

It handles many of the flags of [badge](https://github.com/HazAT/badge) but the generated image will not be exactly the same.

Also, Swadgics will not automatically search for icons ([badge](https://github.com/HazAT/badge) looks at `./**/*.appiconset/*.{png,PNG}`), and does not take a `--glob` option. You have to give it the path to the images to modify. You can use your shell's glob features of course.

```console
$ swadgics badge --shield "Version-0.0.3-blue" --dark --shield_geometry "+0+25%" --shield_scale 0.75 path/to/my/icon.png
```

WARNING: As in [badge](https://github.com/HazAT/badge), the files are modified in place so make sure to make a copy before applying Swadgics on them. However, for testing purpose, when only once input file is specified, you can use `--output-file` to specify the file path to output to.
82 changes: 82 additions & 0 deletions Sources/Extensions/CoreGraphicsExtensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import CoreGraphics

extension CGColorSpace {
// Unfortunately, `sRGB` is already taken for the name of the sRGB color space.
static let sRGBColorSpace = CGColorSpace(name: CGColorSpace.sRGB)!
// `ColorSpace` suffix for uniformity.
static let grayscaleColorSpace = CGColorSpaceCreateDeviceGray()
}

extension CGColor {
/// sRGB color from 3 0-255 8-bit values (plus an optional alpha).
static func sRGB8(_ red: UInt8, _ green: UInt8, _ blue: UInt8, alpha: CGFloat = 1.0) -> CGColor {
CGColor(srgbRed: CGFloat(red) / 0xff, green: CGFloat(green) / 0xff, blue: CGFloat(blue) / 0xff, alpha: alpha)
}
}

extension CGImage {
var hasAlpha: Bool {
switch alphaInfo {
case .none, .noneSkipLast, .noneSkipFirst:
return false
default:
return true
}
}
}

extension CGContext {
static func makeGrayscaleContext(size: CGSize) -> CGContext {
CGContext(
data: nil,
width: Int(size.width.rounded(.up)),
height: Int(size.height.rounded(.up)),
bitsPerComponent: 8,
bytesPerRow: 0,
space: CGColorSpace.grayscaleColorSpace,
// `CGImageAlphaInfo` is part of `CGBitmapInfo`.
// Unfortunately, the grayscale color space only seems to support no alpha or alpha only.
// https://developer.apple.com/library/archive/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_context/dq_context.html#//apple_ref/doc/uid/TP30001066-CH203-BCIBHHBB
bitmapInfo: CGImageAlphaInfo.none.rawValue
)!
}

static func makeSRBGContext(size: CGSize) -> CGContext {
CGContext(
data: nil,
width: Int(size.width.rounded(.up)),
height: Int(size.height.rounded(.up)),
bitsPerComponent: 8,
bytesPerRow: 0,
space: CGColorSpace.sRGBColorSpace,
// `CGImageAlphaInfo` is part of `CGBitmapInfo`.
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
)!
}

/// Add a rounded rect to the current path of the context.
func addRoundedRect(_ rect: CGRect, radius: CGFloat) {
move(to: CGPoint(x: rect.minX, y: rect.midY))
addArc(
tangent1End: CGPoint(x: rect.minX, y: rect.minY),
tangent2End: CGPoint(x: rect.midX, y: rect.minY),
radius: radius
)
addArc(
tangent1End: CGPoint(x: rect.maxX, y: rect.minY),
tangent2End: CGPoint(x: rect.maxX, y: rect.midY),
radius: radius
)
addArc(
tangent1End: CGPoint(x: rect.maxX, y: rect.maxY),
tangent2End: CGPoint(x: rect.midX, y: rect.maxY),
radius: radius
)
addArc(
tangent1End: CGPoint(x: rect.minX, y: rect.maxY),
tangent2End: CGPoint(x: rect.minX, y: rect.midY),
radius: radius
)
closePath()
}
}
6 changes: 6 additions & 0 deletions Sources/Extensions/CoreTextExtensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import CoreText

extension CTFont {
var ascent: CGFloat { CTFontGetAscent(self) }
var descent: CGFloat { CTFontGetDescent(self) }
}
119 changes: 119 additions & 0 deletions Sources/Gravity.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Partial implementation of ImageMagick gravity and geometry concepts.

import ArgumentParser
import CoreGraphics

enum Gravity: String, ExpressibleByArgument {
case northWest = "NorthWest"
case north = "North"
case northEast = "NorthEast"
case west = "West"
case center = "Center"
case east = "East"
case southWest = "SouthWest"
case south = "South"
case southEast = "SouthEast"

func objectCenter(objectSize: CGSize, canvasSize: CGSize, geometry: Geometry?) -> CGPoint {
var centerX, centerY: CGFloat
switch self {
case .northWest, .west, .southWest:
centerX = 0
case .north, .center, .south:
centerX = canvasSize.width / 2
case .northEast, .east, .southEast:
centerX = canvasSize.width
}
switch self {
case .southWest, .south, .southEast:
centerY = 0
case .west, .center, .east:
centerY = canvasSize.height / 2
case .northWest, .north, .northEast:
centerY = canvasSize.height
}

if let geometry {
switch geometry.x {
case .pixels(let x):
centerX += x
case .percent(let x):
centerX += canvasSize.width * x / 100
}
// Geometry's `y` is inverted compared to Core Graphics
switch geometry.y {
case .pixels(let y):
centerY += -y
case .percent(let y):
centerY += canvasSize.width * -y / 100
}
}

let halfObjectSize = CGSize(
width: objectSize.width / 2,
height: objectSize.height / 2
)

if objectSize.width <= canvasSize.width {
if centerX - halfObjectSize.width < 0 {
centerX = halfObjectSize.width
} else if centerX + halfObjectSize.width > canvasSize.width {
centerX = canvasSize.width - halfObjectSize.width
}
} else {
switch self {
case .northWest, .west, .southWest:
centerX = halfObjectSize.width
case .north, .center, .south:
centerX = canvasSize.width / 2
case .northEast, .east, .southEast:
centerX = canvasSize.width - halfObjectSize.width
}
}

if objectSize.height <= canvasSize.height {
if centerY - halfObjectSize.height < 0 {
centerY = halfObjectSize.height
} else if centerY + halfObjectSize.height > canvasSize.height {
centerY = canvasSize.height - halfObjectSize.height
}
} else {
switch self {
case .southWest, .south, .southEast:
centerY = halfObjectSize.height
case .west, .center, .east:
centerY = canvasSize.height / 2
case .northWest, .north, .northEast:
centerY = canvasSize.height - halfObjectSize.height
}
}
return CGPoint(x: centerX, y: centerY)
}
}

struct Geometry: ExpressibleByArgument {
var x: Offset
var y: Offset

enum Offset {
case pixels(CGFloat)
case percent(CGFloat)
}

private static let geometryRegex = try! Regex(#"([+\-]\d+)(%?)([+\-]\d+)(%?)"#, as: (Substring, Substring, Substring, Substring, Substring).self)

init?(argument: String) {
guard let match = try! Self.geometryRegex.wholeMatch(in: argument) else { return nil }
let (_, x, percentX, y, percentY) = match.output
if percentX.isEmpty {
self.x = .pixels(Double(x)!)
} else {
self.x = .percent(Double(x)!)
}
if percentY.isEmpty {
self.y = .pixels(Double(y)!)
} else {
self.y = .percent(Double(y)!)
}
}
}
Binary file added Sources/Resources/alpha_badge_dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Sources/Resources/alpha_badge_light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Sources/Resources/beta_badge_dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Sources/Resources/beta_badge_light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 691f03f

Please sign in to comment.