Skip to content

Commit

Permalink
Merge pull request #8 from guhungry/Feature/drawText-with-shadow
Browse files Browse the repository at this point in the history
Feature : Draw text with shadow
  • Loading branch information
guhungry authored Jul 14, 2024
2 parents 402467f + b591506 commit 3e10de8
Show file tree
Hide file tree
Showing 9 changed files with 303 additions and 22 deletions.
30 changes: 27 additions & 3 deletions WCPhotoManipulator.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
/* Begin PBXBuildFile section */
CA10C5262BB331B7008464A7 /* FlipMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA10C5252BB331B7008464A7 /* FlipMode.swift */; };
CA11C0FB2C0C1E9B005542AA /* RotationMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA11C0FA2C0C1E9B005542AA /* RotationMode.swift */; };
CAC665652C445049002FD9A7 /* TextStyleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAC665642C445049002FD9A7 /* TextStyleTests.swift */; };
CADEFF822C4147EC00DFE3E3 /* TextStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CADEFF812C4147EC00DFE3E3 /* TextStyle.swift */; };
CC92AF5124D250160061CC87 /* FoolSonarcloud.m in Sources */ = {isa = PBXBuildFile; fileRef = CC92AF5024D250160061CC87 /* FoolSonarcloud.m */; };
CC92AF6A24D545070061CC87 /* BitmapUtilsSwiftTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC92AF6924D545070061CC87 /* BitmapUtilsSwiftTests.swift */; };
CC92AF6C24D558500061CC87 /* FileUtilsSwiftTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC92AF6B24D558500061CC87 /* FileUtilsSwiftTests.swift */; };
Expand Down Expand Up @@ -55,6 +57,8 @@
/* Begin PBXFileReference section */
CA10C5252BB331B7008464A7 /* FlipMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlipMode.swift; sourceTree = "<group>"; };
CA11C0FA2C0C1E9B005542AA /* RotationMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RotationMode.swift; sourceTree = "<group>"; };
CAC665642C445049002FD9A7 /* TextStyleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextStyleTests.swift; sourceTree = "<group>"; };
CADEFF812C4147EC00DFE3E3 /* TextStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextStyle.swift; sourceTree = "<group>"; };
CC92AF4F24D250160061CC87 /* FoolSonarcloud.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FoolSonarcloud.h; sourceTree = "<group>"; };
CC92AF5024D250160061CC87 /* FoolSonarcloud.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FoolSonarcloud.m; sourceTree = "<group>"; };
CC92AF6924D545070061CC87 /* BitmapUtilsSwiftTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitmapUtilsSwiftTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -102,6 +106,25 @@
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
CAC665632C444FCA002FD9A7 /* Models */ = {
isa = PBXGroup;
children = (
CAC665642C445049002FD9A7 /* TextStyleTests.swift */,
);
path = Models;
sourceTree = "<group>";
};
CADEFF802C41479400DFE3E3 /* Models */ = {
isa = PBXGroup;
children = (
CA10C5252BB331B7008464A7 /* FlipMode.swift */,
CCC2506D24D05EE7000BAC48 /* ResizeMode.swift */,
CA11C0FA2C0C1E9B005542AA /* RotationMode.swift */,
CADEFF812C4147EC00DFE3E3 /* TextStyle.swift */,
);
path = Models;
sourceTree = "<group>";
};
CCC2502F24CFDE4B000BAC48 = {
isa = PBXGroup;
children = (
Expand All @@ -123,14 +146,12 @@
CCC2503A24CFDE4B000BAC48 /* WCPhotoManipulator */ = {
isa = PBXGroup;
children = (
CADEFF802C41479400DFE3E3 /* Models */,
CCC2506B24D05C97000BAC48 /* BitmapUtils.swift */,
CCC2505824CFF0C1000BAC48 /* FileUtils.swift */,
CA10C5252BB331B7008464A7 /* FlipMode.swift */,
CC92AF4F24D250160061CC87 /* FoolSonarcloud.h */,
CC92AF5024D250160061CC87 /* FoolSonarcloud.m */,
CCC2503B24CFDE4B000BAC48 /* MimeUtils.swift */,
CCC2506D24D05EE7000BAC48 /* ResizeMode.swift */,
CA11C0FA2C0C1E9B005542AA /* RotationMode.swift */,
CCC2506F24D060C9000BAC48 /* UIImage+PhotoManipulator.swift */,
CCC2505124CFE1D2000BAC48 /* WCPhotoManipulator-Bridging-Header.h */,
);
Expand All @@ -142,6 +163,7 @@
children = (
CCC2506024D000DA000BAC48 /* Helpers */,
CCC2506424D000E3000BAC48 /* Images */,
CAC665632C444FCA002FD9A7 /* Models */,
CCC2507124D066A2000BAC48 /* BitmapUtilsObjCTests.m */,
CC92AF6924D545070061CC87 /* BitmapUtilsSwiftTests.swift */,
CCC2505A24CFF272000BAC48 /* FileUtilsObjCTests.m */,
Expand Down Expand Up @@ -270,6 +292,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
CADEFF822C4147EC00DFE3E3 /* TextStyle.swift in Sources */,
CCC2506C24D05C97000BAC48 /* BitmapUtils.swift in Sources */,
CCC2507024D060C9000BAC48 /* UIImage+PhotoManipulator.swift in Sources */,
CA11C0FB2C0C1E9B005542AA /* RotationMode.swift in Sources */,
Expand All @@ -293,6 +316,7 @@
CC92AF6C24D558500061CC87 /* FileUtilsSwiftTests.swift in Sources */,
CC92AF6F24D55E950061CC87 /* NSData+Testing.m in Sources */,
CC92AF7124D5CD9F0061CC87 /* UIImage+PhotoManipulatorSwiftTests.swift in Sources */,
CAC665652C445049002FD9A7 /* TextStyleTests.swift in Sources */,
CCC2506324D000DA000BAC48 /* UIImage+Testing.m in Sources */,
CCC2505B24CFF272000BAC48 /* FileUtilsObjCTests.m in Sources */,
);
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
145 changes: 145 additions & 0 deletions WCPhotoManipulator/Models/TextStyle.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
//
// TextStyle.swift
// WCPhotoManipulator
//
// Created by Woraphot Chokratanasombat on 12/7/24.
// Copyright © 2024 Woraphot Chokratanasombat. All rights reserved.
//

import UIKit

/// A class representing the style of text, including color, font, thickness, rotation, and shadow properties.
@objc public class TextStyle : NSObject {
/// The color of the text.
let color: UIColor
/// The font of the text.
let font: UIFont
/// The thickness of the text. Default value is 0.
let thickness: CGFloat
/// The rotation angle of the text in degrees. Default value is 0.
let rotation: CGFloat
/// The blur radius of the text's shadow. Default value is 0.
let shadowRadius: CGFloat
/// The offset of the text's shadow. Default value is CGSize(width: 0, height: 0).
let shadowOffset: CGSize
/// The color of the text's shadow. Default value is nil (no shadow color).
let shadowColor: UIColor?

/// Computed property that creates and returns an NSShadow object based on the shadow properties.
var shadow: NSShadow {
let shadow = NSShadow()
shadow.shadowBlurRadius = shadowRadius
shadow.shadowOffset = shadowOffset
shadow.shadowColor = shadowColor
return shadow
}

/// Initializes a new TextStyle object with specified properties.
///
/// - Parameters:
/// - color: The color of the text.
/// - font: The font of the text.
/// - thickness: The thickness of the text. Default is 0.
/// - rotation: The rotation angle of the text in degrees. Default is 0.
/// - shadowRadius: The blur radius of the shadow. Default is 0.
/// - shadowOffsetX: The horizontal offset of the shadow. Default is 0.
/// - shadowOffsetY: The vertical offset of the shadow. Default is 0.
/// - shadowColor: The color of the shadow. Default is nil.
@objc public init(color: UIColor, font: UIFont, thickness: CGFloat, rotation: CGFloat, shadowRadius: CGFloat, shadowOffsetX: Int, shadowOffsetY: Int, shadowColor: UIColor?) {
self.color = color
self.font = font
self.thickness = thickness
self.rotation = rotation
self.shadowRadius = shadowRadius
self.shadowOffset = CGSize(width: shadowOffsetX, height: shadowOffsetY)
self.shadowColor = shadowColor
}

/// Convenience initializer with only color and font.
///
/// - Parameters:
/// - color: The color of the text.
/// - font: The font of the text.
@objc public convenience init(color: UIColor, font: UIFont) {
self.init(color: color, font: font, thickness: 0)
}

/// Convenience initializer with color, font, and thickness.
///
/// - Parameters:
/// - color: The color of the text.
/// - font: The font of the text.
/// - thickness: The thickness of the text.
@objc public convenience init(color: UIColor, font: UIFont, thickness: CGFloat) {
self.init(color: color, font: font, thickness: thickness, rotation: 0)
}

/// Convenience initializer with color, font, thickness, and rotation.
///
/// - Parameters:
/// - color: The color of the text.
/// - font: The font of the text.
/// - thickness: The thickness of the text.
/// - rotation: The rotation angle of the text in degrees.
@objc public convenience init(color: UIColor, font: UIFont, thickness: CGFloat, rotation: CGFloat) {
self.init(color: color, font: font, thickness: thickness, rotation: rotation, shadowRadius: 0, shadowOffsetX: 0, shadowOffsetY: 0, shadowColor: nil)
}

/// Convenience initializer with color, size, and other optional properties.
///
/// - Parameters:
/// - color: The color of the text.
/// - size: The size of the text's font.
/// - thickness: The thickness of the text. Default is 0.
/// - rotation: The rotation angle of the text in degrees. Default is 0.
/// - shadowRadius: The blur radius of the shadow. Default is 0.
/// - shadowOffsetX: The horizontal offset of the shadow. Default is 0.
/// - shadowOffsetY: The vertical offset of the shadow. Default is 0.
/// - shadowColor: The color of the shadow. Default is nil.
@objc public convenience init(color: UIColor, size: CGFloat, thickness: CGFloat, rotation: CGFloat, shadowRadius: CGFloat, shadowOffsetX: Int, shadowOffsetY: Int, shadowColor: UIColor?) {
self.init(color: color, font: UIFont.systemFont(ofSize: size), thickness: thickness, rotation: rotation, shadowRadius: shadowRadius, shadowOffsetX: shadowOffsetX, shadowOffsetY: shadowOffsetY, shadowColor: shadowColor)
}

/// Convenience initializer with only color and size.
///
/// - Parameters:
/// - color: The color of the text.
/// - size: The size of the text's font.
@objc public convenience init(color: UIColor, size: CGFloat) {
self.init(color: color, size: size, thickness: 0)
}

/// Convenience initializer with color, size, and thickness.
///
/// - Parameters:
/// - color: The color of the text.
/// - size: The size of the text's font.
/// - thickness: The thickness of the text.
@objc public convenience init(color: UIColor, size: CGFloat, thickness: CGFloat) {
self.init(color: color, size: size, thickness: thickness, rotation: 0)
}

/// Convenience initializer with color, size, thickness, and rotation.
///
/// - Parameters:
/// - color: The color of the text.
/// - size: The size of the text's font.
/// - thickness: The thickness of the text.
/// - rotation: The rotation angle of the text in degrees.
@objc public convenience init(color: UIColor, size: CGFloat, thickness: CGFloat, rotation: CGFloat) {
self.init(color: color, size: size, thickness: thickness, rotation: rotation, shadowRadius: 0, shadowOffsetX: 0, shadowOffsetY: 0, shadowColor: nil)
}

public override var description: String {
return """
TextStyle:
color: \(color)
font: \(font)
thickness: \(thickness)
rotation: \(rotation)
shadowRadius: \(shadowRadius)
shadowOffset: \(shadowOffset)
shadowColor: \(String(describing: shadowColor))
"""
}
}
37 changes: 27 additions & 10 deletions WCPhotoManipulator/UIImage+PhotoManipulator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,27 +45,29 @@ public extension UIImage {
}

// Text
@objc func drawText(_ text: String, position: CGPoint, color: UIColor, font: UIFont, thickness: CGFloat, rotation: CGFloat, scale: CGFloat) -> UIImage? {
@objc func drawText(_ text: String, position: CGPoint, style: TextStyle, scale: CGFloat) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(self.size, false, scale)
if let context = UIGraphicsGetCurrentContext() {
context.translateBy(x: position.x, y: position.y)
context.rotate (by: -rotation * CGFloat.pi / 180.0) //45˚
context.rotate (by: -style.rotation * CGFloat.pi / 180.0) //45˚
context.translateBy(x: -position.x, y: -position.y)
}

var textStyles: [NSAttributedString.Key: Any] = [
.font: font,
.foregroundColor: color,
.font: style.font,
.foregroundColor: style.color,
]
if (thickness > 0) {
textStyles[.strokeColor] = color;
textStyles[.strokeWidth] = thickness;
if (style.thickness > 0) {
textStyles[.strokeColor] = style.color;
textStyles[.strokeWidth] = style.thickness;
}
if (style.shadowRadius > 0 && style.shadowColor != nil) {
textStyles[.shadow] = style.shadow;
}

(text as NSString).draw(at: position, withAttributes: textStyles)

let rotatedImageWithText = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext()


let opaque = !hasAlpha()
UIGraphicsBeginImageContextWithOptions(self.size, opaque, scale)
Expand All @@ -80,14 +82,27 @@ public extension UIImage {
return result
}

@objc func drawText(_ text: String, position: CGPoint, style: TextStyle) -> UIImage? {
return drawText(text, position: position, style: style, scale: scale)
}

@available(*, deprecated, message: "Use drawText(, position:, style:, scale:)")
@objc func drawText(_ text: String, position: CGPoint, color: UIColor, font: UIFont, thickness: CGFloat, rotation: CGFloat, scale: CGFloat) -> UIImage? {
let style = TextStyle(color: color, font: font, thickness: thickness, rotation: rotation)
return drawText(text, position: position, style: style, scale: scale)
}

@available(*, deprecated, message: "Use drawText(, position:, style:, scale:)")
@objc func drawText(_ text: String, position: CGPoint, color: UIColor, font: UIFont, thickness: CGFloat, rotation: CGFloat) -> UIImage? {
return drawText(text, position: position, color: color, font:font, thickness: thickness, rotation: rotation, scale: scale)
}

@available(*, deprecated, message: "Use drawText(, position:, style:, scale:)")
@objc func drawText(_ text: String, position: CGPoint, color: UIColor, size: CGFloat, thickness: CGFloat, rotation: CGFloat, scale: CGFloat) -> UIImage? {
return drawText(text, position: position, color: color, font: UIFont.systemFont(ofSize: size), thickness: thickness, rotation: rotation, scale: scale)
}

@available(*, deprecated, message: "Use drawText(, position:, style:, scale:)")
@objc func drawText(_ text: String, position: CGPoint, color: UIColor, size: CGFloat, thickness: CGFloat, rotation: CGFloat) -> UIImage? {
return drawText(text, position: position, color: color, size: size, thickness: thickness, rotation: rotation, scale: scale)
}
Expand All @@ -111,7 +126,9 @@ public extension UIImage {

// Flip
@objc func flip(_ flipMode: FlipMode) -> UIImage {
if (flipMode == .None) { return self }
if (flipMode == .None) {
return self
}

let cgimage = ciImage().transformed(by: flipMode.transform())
return UIImage(ciImage: cgimage)
Expand Down
57 changes: 57 additions & 0 deletions WCPhotoManipulatorTests/Models/TextStyleTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//
// TextStyleTests.swift
// WCPhotoManipulatorTests
//
// Created by Woraphot Chokratanasombat on 15/7/2567 BE.
// Copyright © 2567 BE Woraphot Chokratanasombat. All rights reserved.
//

import XCTest

final class TextStyleTests: XCTestCase {
override func setUpWithError() throws {
}

override func tearDownWithError() throws {
}

func testDescription_WithAllValues() throws {
// Given
let color = UIColor.red
let font = UIFont.systemFont(ofSize: 12)
let thickness: CGFloat = 1.5
let rotation: CGFloat = 45
let shadowRadius: CGFloat = 2.0
let shadowOffsetX: Int = 3
let shadowOffsetY: Int = 4
let shadowColor: UIColor? = UIColor.black

let textStyle = TextStyle(
color: color,
font: font,
thickness: thickness,
rotation: rotation,
shadowRadius: shadowRadius,
shadowOffsetX: shadowOffsetX,
shadowOffsetY: shadowOffsetY,
shadowColor: shadowColor
)

// When
let description = textStyle.description

// Then
let expectedDescription = """
TextStyle:
color: \(color)
font: \(font)
thickness: \(thickness)
rotation: \(rotation)
shadowRadius: \(shadowRadius)
shadowOffset: \(CGSize(width: shadowOffsetX, height: shadowOffsetY))
shadowColor: \(String(describing: shadowColor))
"""

XCTAssertEqual(description, expectedDescription)
}
}
Loading

0 comments on commit 3e10de8

Please sign in to comment.