├── .gitignore ├── Demo ├── Package.swift ├── SVG2PathDemo.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── SVG2PathDemo.xcscheme └── SVG2PathDemo │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── ContentView.swift │ ├── SVG2PathDemo.entitlements │ └── SVG2PathDemoApp.swift ├── LICENSE ├── Package.swift ├── README.md ├── SVG ├── figures.ai └── figures.svg ├── Sources └── SVG2Path │ ├── Command.swift │ ├── Extensions │ ├── Array+Extension.swift │ ├── CGPoint+Extension.swift │ ├── Path+Extension.swift │ └── String+Extensions.swift │ ├── SVG2Path.swift │ ├── State.swift │ └── TagAndPath.swift ├── Tests └── SVG2PathTests │ └── SVG2PathTests.swift └── demo.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac 2 | .DS_Store 3 | 4 | # Xcode 5 | xcuserdata/ 6 | *.xcuserstate 7 | 8 | # Swift Package Manager 9 | Packages.resolved 10 | .swiftpm/ 11 | .build/ 12 | -------------------------------------------------------------------------------- /Demo/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.6 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "", 7 | products: [], 8 | targets: [] 9 | ) 10 | -------------------------------------------------------------------------------- /Demo/SVG2PathDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 77; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1C24D83E28C0DB1600853461 /* SVG2PathDemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C24D83D28C0DB1600853461 /* SVG2PathDemoApp.swift */; }; 11 | 1C24D84028C0DB1600853461 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C24D83F28C0DB1600853461 /* ContentView.swift */; }; 12 | 1C24D84228C0DB1700853461 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1C24D84128C0DB1700853461 /* Assets.xcassets */; }; 13 | 1C49187A28C0E51C001DC1B5 /* SVG2Path in Frameworks */ = {isa = PBXBuildFile; productRef = 1C49187928C0E51C001DC1B5 /* SVG2Path */; }; 14 | /* End PBXBuildFile section */ 15 | 16 | /* Begin PBXFileReference section */ 17 | 1C24D83A28C0DB1600853461 /* SVG2PathDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SVG2PathDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 18 | 1C24D83D28C0DB1600853461 /* SVG2PathDemoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SVG2PathDemoApp.swift; sourceTree = ""; }; 19 | 1C24D83F28C0DB1600853461 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 20 | 1C24D84128C0DB1700853461 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 21 | 1C24D84628C0DB1700853461 /* SVG2PathDemo.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SVG2PathDemo.entitlements; sourceTree = ""; }; 22 | 1C24D85028C0DF4200853461 /* SVG2Path */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SVG2Path; path = ..; sourceTree = ""; }; 23 | /* End PBXFileReference section */ 24 | 25 | /* Begin PBXFrameworksBuildPhase section */ 26 | 1C24D83728C0DB1600853461 /* Frameworks */ = { 27 | isa = PBXFrameworksBuildPhase; 28 | buildActionMask = 2147483647; 29 | files = ( 30 | 1C49187A28C0E51C001DC1B5 /* SVG2Path in Frameworks */, 31 | ); 32 | runOnlyForDeploymentPostprocessing = 0; 33 | }; 34 | /* End PBXFrameworksBuildPhase section */ 35 | 36 | /* Begin PBXGroup section */ 37 | 1C24D83128C0DB1600853461 = { 38 | isa = PBXGroup; 39 | children = ( 40 | 1C24D84F28C0DF4200853461 /* Packages */, 41 | 1C24D83C28C0DB1600853461 /* SVG2PathDemo */, 42 | 1C24D83B28C0DB1600853461 /* Products */, 43 | 1C49187828C0E51C001DC1B5 /* Frameworks */, 44 | ); 45 | sourceTree = ""; 46 | }; 47 | 1C24D83B28C0DB1600853461 /* Products */ = { 48 | isa = PBXGroup; 49 | children = ( 50 | 1C24D83A28C0DB1600853461 /* SVG2PathDemo.app */, 51 | ); 52 | name = Products; 53 | sourceTree = ""; 54 | }; 55 | 1C24D83C28C0DB1600853461 /* SVG2PathDemo */ = { 56 | isa = PBXGroup; 57 | children = ( 58 | 1C24D84628C0DB1700853461 /* SVG2PathDemo.entitlements */, 59 | 1C24D84128C0DB1700853461 /* Assets.xcassets */, 60 | 1C24D83D28C0DB1600853461 /* SVG2PathDemoApp.swift */, 61 | 1C24D83F28C0DB1600853461 /* ContentView.swift */, 62 | ); 63 | path = SVG2PathDemo; 64 | sourceTree = ""; 65 | }; 66 | 1C24D84F28C0DF4200853461 /* Packages */ = { 67 | isa = PBXGroup; 68 | children = ( 69 | 1C24D85028C0DF4200853461 /* SVG2Path */, 70 | ); 71 | name = Packages; 72 | sourceTree = ""; 73 | }; 74 | 1C49187828C0E51C001DC1B5 /* Frameworks */ = { 75 | isa = PBXGroup; 76 | children = ( 77 | ); 78 | name = Frameworks; 79 | sourceTree = ""; 80 | }; 81 | /* End PBXGroup section */ 82 | 83 | /* Begin PBXNativeTarget section */ 84 | 1C24D83928C0DB1600853461 /* SVG2PathDemo */ = { 85 | isa = PBXNativeTarget; 86 | buildConfigurationList = 1C24D84928C0DB1700853461 /* Build configuration list for PBXNativeTarget "SVG2PathDemo" */; 87 | buildPhases = ( 88 | 1C24D83628C0DB1600853461 /* Sources */, 89 | 1C24D83728C0DB1600853461 /* Frameworks */, 90 | 1C24D83828C0DB1600853461 /* Resources */, 91 | ); 92 | buildRules = ( 93 | ); 94 | dependencies = ( 95 | ); 96 | name = SVG2PathDemo; 97 | packageProductDependencies = ( 98 | 1C49187928C0E51C001DC1B5 /* SVG2Path */, 99 | ); 100 | productName = SVG2PathDemo; 101 | productReference = 1C24D83A28C0DB1600853461 /* SVG2PathDemo.app */; 102 | productType = "com.apple.product-type.application"; 103 | }; 104 | /* End PBXNativeTarget section */ 105 | 106 | /* Begin PBXProject section */ 107 | 1C24D83228C0DB1600853461 /* Project object */ = { 108 | isa = PBXProject; 109 | attributes = { 110 | BuildIndependentTargetsInParallel = 1; 111 | LastSwiftUpdateCheck = 1340; 112 | LastUpgradeCheck = 1620; 113 | TargetAttributes = { 114 | 1C24D83928C0DB1600853461 = { 115 | CreatedOnToolsVersion = 13.4.1; 116 | }; 117 | }; 118 | }; 119 | buildConfigurationList = 1C24D83528C0DB1600853461 /* Build configuration list for PBXProject "SVG2PathDemo" */; 120 | developmentRegion = en; 121 | hasScannedForEncodings = 0; 122 | knownRegions = ( 123 | en, 124 | Base, 125 | ); 126 | mainGroup = 1C24D83128C0DB1600853461; 127 | preferredProjectObjectVersion = 77; 128 | productRefGroup = 1C24D83B28C0DB1600853461 /* Products */; 129 | projectDirPath = ""; 130 | projectRoot = ""; 131 | targets = ( 132 | 1C24D83928C0DB1600853461 /* SVG2PathDemo */, 133 | ); 134 | }; 135 | /* End PBXProject section */ 136 | 137 | /* Begin PBXResourcesBuildPhase section */ 138 | 1C24D83828C0DB1600853461 /* Resources */ = { 139 | isa = PBXResourcesBuildPhase; 140 | buildActionMask = 2147483647; 141 | files = ( 142 | 1C24D84228C0DB1700853461 /* Assets.xcassets in Resources */, 143 | ); 144 | runOnlyForDeploymentPostprocessing = 0; 145 | }; 146 | /* End PBXResourcesBuildPhase section */ 147 | 148 | /* Begin PBXSourcesBuildPhase section */ 149 | 1C24D83628C0DB1600853461 /* Sources */ = { 150 | isa = PBXSourcesBuildPhase; 151 | buildActionMask = 2147483647; 152 | files = ( 153 | 1C24D84028C0DB1600853461 /* ContentView.swift in Sources */, 154 | 1C24D83E28C0DB1600853461 /* SVG2PathDemoApp.swift in Sources */, 155 | ); 156 | runOnlyForDeploymentPostprocessing = 0; 157 | }; 158 | /* End PBXSourcesBuildPhase section */ 159 | 160 | /* Begin XCBuildConfiguration section */ 161 | 1C24D84728C0DB1700853461 /* Debug */ = { 162 | isa = XCBuildConfiguration; 163 | buildSettings = { 164 | ALWAYS_SEARCH_USER_PATHS = NO; 165 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 166 | CLANG_ANALYZER_NONNULL = YES; 167 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 168 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 169 | CLANG_ENABLE_MODULES = YES; 170 | CLANG_ENABLE_OBJC_ARC = YES; 171 | CLANG_ENABLE_OBJC_WEAK = YES; 172 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 173 | CLANG_WARN_BOOL_CONVERSION = YES; 174 | CLANG_WARN_COMMA = YES; 175 | CLANG_WARN_CONSTANT_CONVERSION = YES; 176 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 177 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 178 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 179 | CLANG_WARN_EMPTY_BODY = YES; 180 | CLANG_WARN_ENUM_CONVERSION = YES; 181 | CLANG_WARN_INFINITE_RECURSION = YES; 182 | CLANG_WARN_INT_CONVERSION = YES; 183 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 184 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 185 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 186 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 187 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 188 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 189 | CLANG_WARN_STRICT_PROTOTYPES = YES; 190 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 191 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 192 | CLANG_WARN_UNREACHABLE_CODE = YES; 193 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 194 | COPY_PHASE_STRIP = NO; 195 | DEAD_CODE_STRIPPING = YES; 196 | DEBUG_INFORMATION_FORMAT = dwarf; 197 | ENABLE_STRICT_OBJC_MSGSEND = YES; 198 | ENABLE_TESTABILITY = YES; 199 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 200 | GCC_C_LANGUAGE_STANDARD = gnu11; 201 | GCC_DYNAMIC_NO_PIC = NO; 202 | GCC_NO_COMMON_BLOCKS = YES; 203 | GCC_OPTIMIZATION_LEVEL = 0; 204 | GCC_PREPROCESSOR_DEFINITIONS = ( 205 | "DEBUG=1", 206 | "$(inherited)", 207 | ); 208 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 209 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 210 | GCC_WARN_UNDECLARED_SELECTOR = YES; 211 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 212 | GCC_WARN_UNUSED_FUNCTION = YES; 213 | GCC_WARN_UNUSED_VARIABLE = YES; 214 | MACOSX_DEPLOYMENT_TARGET = 12.0; 215 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 216 | MTL_FAST_MATH = YES; 217 | ONLY_ACTIVE_ARCH = YES; 218 | SDKROOT = macosx; 219 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 220 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 221 | }; 222 | name = Debug; 223 | }; 224 | 1C24D84828C0DB1700853461 /* Release */ = { 225 | isa = XCBuildConfiguration; 226 | buildSettings = { 227 | ALWAYS_SEARCH_USER_PATHS = NO; 228 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 229 | CLANG_ANALYZER_NONNULL = YES; 230 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 231 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 232 | CLANG_ENABLE_MODULES = YES; 233 | CLANG_ENABLE_OBJC_ARC = YES; 234 | CLANG_ENABLE_OBJC_WEAK = YES; 235 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 236 | CLANG_WARN_BOOL_CONVERSION = YES; 237 | CLANG_WARN_COMMA = YES; 238 | CLANG_WARN_CONSTANT_CONVERSION = YES; 239 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 240 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 241 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 242 | CLANG_WARN_EMPTY_BODY = YES; 243 | CLANG_WARN_ENUM_CONVERSION = YES; 244 | CLANG_WARN_INFINITE_RECURSION = YES; 245 | CLANG_WARN_INT_CONVERSION = YES; 246 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 247 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 248 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 249 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 250 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 251 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 252 | CLANG_WARN_STRICT_PROTOTYPES = YES; 253 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 254 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 255 | CLANG_WARN_UNREACHABLE_CODE = YES; 256 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 257 | COPY_PHASE_STRIP = NO; 258 | DEAD_CODE_STRIPPING = YES; 259 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 260 | ENABLE_NS_ASSERTIONS = NO; 261 | ENABLE_STRICT_OBJC_MSGSEND = YES; 262 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 263 | GCC_C_LANGUAGE_STANDARD = gnu11; 264 | GCC_NO_COMMON_BLOCKS = YES; 265 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 266 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 267 | GCC_WARN_UNDECLARED_SELECTOR = YES; 268 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 269 | GCC_WARN_UNUSED_FUNCTION = YES; 270 | GCC_WARN_UNUSED_VARIABLE = YES; 271 | MACOSX_DEPLOYMENT_TARGET = 12.0; 272 | MTL_ENABLE_DEBUG_INFO = NO; 273 | MTL_FAST_MATH = YES; 274 | SDKROOT = macosx; 275 | SWIFT_COMPILATION_MODE = wholemodule; 276 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 277 | }; 278 | name = Release; 279 | }; 280 | 1C24D84A28C0DB1700853461 /* Debug */ = { 281 | isa = XCBuildConfiguration; 282 | buildSettings = { 283 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 284 | CODE_SIGN_ENTITLEMENTS = SVG2PathDemo/SVG2PathDemo.entitlements; 285 | CODE_SIGN_STYLE = Automatic; 286 | COMBINE_HIDPI_IMAGES = YES; 287 | CURRENT_PROJECT_VERSION = 1; 288 | DEAD_CODE_STRIPPING = YES; 289 | DEVELOPMENT_TEAM = VJ5N2X84K8; 290 | ENABLE_HARDENED_RUNTIME = YES; 291 | ENABLE_PREVIEWS = YES; 292 | GENERATE_INFOPLIST_FILE = YES; 293 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 294 | LD_RUNPATH_SEARCH_PATHS = ( 295 | "$(inherited)", 296 | "@executable_path/../Frameworks", 297 | ); 298 | MARKETING_VERSION = 1.0; 299 | PRODUCT_BUNDLE_IDENTIFIER = com.kyome.SVG2PathDemo; 300 | PRODUCT_NAME = "$(TARGET_NAME)"; 301 | SWIFT_EMIT_LOC_STRINGS = YES; 302 | SWIFT_VERSION = 6.0; 303 | }; 304 | name = Debug; 305 | }; 306 | 1C24D84B28C0DB1700853461 /* Release */ = { 307 | isa = XCBuildConfiguration; 308 | buildSettings = { 309 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 310 | CODE_SIGN_ENTITLEMENTS = SVG2PathDemo/SVG2PathDemo.entitlements; 311 | CODE_SIGN_STYLE = Automatic; 312 | COMBINE_HIDPI_IMAGES = YES; 313 | CURRENT_PROJECT_VERSION = 1; 314 | DEAD_CODE_STRIPPING = YES; 315 | DEVELOPMENT_TEAM = VJ5N2X84K8; 316 | ENABLE_HARDENED_RUNTIME = YES; 317 | ENABLE_PREVIEWS = YES; 318 | GENERATE_INFOPLIST_FILE = YES; 319 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 320 | LD_RUNPATH_SEARCH_PATHS = ( 321 | "$(inherited)", 322 | "@executable_path/../Frameworks", 323 | ); 324 | MARKETING_VERSION = 1.0; 325 | PRODUCT_BUNDLE_IDENTIFIER = com.kyome.SVG2PathDemo; 326 | PRODUCT_NAME = "$(TARGET_NAME)"; 327 | SWIFT_EMIT_LOC_STRINGS = YES; 328 | SWIFT_VERSION = 6.0; 329 | }; 330 | name = Release; 331 | }; 332 | /* End XCBuildConfiguration section */ 333 | 334 | /* Begin XCConfigurationList section */ 335 | 1C24D83528C0DB1600853461 /* Build configuration list for PBXProject "SVG2PathDemo" */ = { 336 | isa = XCConfigurationList; 337 | buildConfigurations = ( 338 | 1C24D84728C0DB1700853461 /* Debug */, 339 | 1C24D84828C0DB1700853461 /* Release */, 340 | ); 341 | defaultConfigurationIsVisible = 0; 342 | defaultConfigurationName = Release; 343 | }; 344 | 1C24D84928C0DB1700853461 /* Build configuration list for PBXNativeTarget "SVG2PathDemo" */ = { 345 | isa = XCConfigurationList; 346 | buildConfigurations = ( 347 | 1C24D84A28C0DB1700853461 /* Debug */, 348 | 1C24D84B28C0DB1700853461 /* Release */, 349 | ); 350 | defaultConfigurationIsVisible = 0; 351 | defaultConfigurationName = Release; 352 | }; 353 | /* End XCConfigurationList section */ 354 | 355 | /* Begin XCSwiftPackageProductDependency section */ 356 | 1C49187928C0E51C001DC1B5 /* SVG2Path */ = { 357 | isa = XCSwiftPackageProductDependency; 358 | productName = SVG2Path; 359 | }; 360 | /* End XCSwiftPackageProductDependency section */ 361 | }; 362 | rootObject = 1C24D83228C0DB1600853461 /* Project object */; 363 | } 364 | -------------------------------------------------------------------------------- /Demo/SVG2PathDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/SVG2PathDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Demo/SVG2PathDemo.xcodeproj/xcshareddata/xcschemes/SVG2PathDemo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Demo/SVG2PathDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Demo/SVG2PathDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/SVG2PathDemo/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // SVG2PathDemo 4 | // 5 | // Created by Takuto Nakamura on 2022/09/01. 6 | // 7 | 8 | import SwiftUI 9 | import SVG2Path 10 | 11 | struct ContentView: View { 12 | @State var size = CGSize.zero 13 | @State var paths = [Path]() 14 | @State var importerPresented = false 15 | private let svg2Path = SVG2Path() 16 | 17 | var body: some View { 18 | VStack(spacing: 0) { 19 | ZStack { 20 | if paths.isEmpty { 21 | Image(systemName: "cube") 22 | .resizable() 23 | .scaledToFit() 24 | .frame(width: 50, height: 50, alignment: .center) 25 | .foregroundColor(Color.primary) 26 | } else { 27 | ForEach(paths, id: \.description) { path in 28 | path.stroke(Color.primary) 29 | .frame(width: size.width, height: size.height) 30 | } 31 | } 32 | } 33 | .padding() 34 | Button { 35 | importerPresented = true 36 | } label: { 37 | Text("Load SVG File") 38 | } 39 | .padding() 40 | ScrollView { 41 | if !paths.isEmpty { 42 | VStack(spacing: 16) { 43 | ForEach(paths, id: \.description) { path in 44 | Text(path.codeString()) 45 | .multilineTextAlignment(.leading) 46 | .textSelection(.enabled) 47 | .frame(maxWidth: .infinity, alignment: .leading) 48 | } 49 | } 50 | .padding() 51 | } 52 | } 53 | } 54 | .frame(minWidth: 400, maxWidth: .infinity, maxHeight: .infinity) 55 | .fileImporter( 56 | isPresented: $importerPresented, 57 | allowedContentTypes: [.svg], 58 | onCompletion: { result in 59 | switch result { 60 | case let .success(url): 61 | guard url.startAccessingSecurityScopedResource() else { return } 62 | defer { url.stopAccessingSecurityScopedResource() } 63 | guard let text = try? String(contentsOf: url, encoding: .utf8), 64 | let result = svg2Path.extractPath(text: text) else { 65 | return 66 | } 67 | size = result.size 68 | paths = result.paths 69 | case let .failure(error): 70 | Swift.print(error.localizedDescription) 71 | } 72 | } 73 | ) 74 | } 75 | } 76 | 77 | #Preview { 78 | ContentView() 79 | } 80 | -------------------------------------------------------------------------------- /Demo/SVG2PathDemo/SVG2PathDemo.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Demo/SVG2PathDemo/SVG2PathDemoApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SVG2PathDemoApp.swift 3 | // SVG2PathDemo 4 | // 5 | // Created by Takuto Nakamura on 2022/09/01. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct SVG2PathDemoApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Takuto NAKAMURA (Kyome) 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 | 3 | import PackageDescription 4 | 5 | let swiftSettings: [SwiftSetting] = [ 6 | .enableUpcomingFeature("ExistentialAny"), 7 | ] 8 | 9 | let package = Package( 10 | name: "SVG2Path", 11 | platforms: [ 12 | .macOS(.v11), 13 | .iOS(.v15) 14 | ], 15 | products: [ 16 | .library( 17 | name: "SVG2Path", 18 | targets: ["SVG2Path"] 19 | ) 20 | ], 21 | targets: [ 22 | .target( 23 | name: "SVG2Path", 24 | swiftSettings: swiftSettings 25 | ), 26 | .testTarget( 27 | name: "SVG2PathTests", 28 | dependencies: ["SVG2Path"], 29 | swiftSettings: swiftSettings 30 | ) 31 | ] 32 | ) 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SVG2Path 2 | 3 | - Convert SVG to Path of SwiftUI. 4 | - Output codes of the Path. 5 | 6 | ## Requirements 7 | 8 | - Development with Xcode 16.0+ 9 | - Written in Swift 6.0 10 | - Compatible with iOS 15.0+, macOS 11.0+ 11 | 12 | ## Usage 13 | 14 | ```swift 15 | let text = // String of SVG 16 | 17 | let svg2Path = SVG2Path() 18 | let data = svg2Path.extractPath(text: text) 19 | 20 | // data.size := CGSize, width & height of viewBox 21 | // data.paths := [Path], Path of SwiftUI 22 | 23 | data.paths.forEach { path in 24 | Swift.print(path.codeString()) // Output code of SwiftUI Path 25 | } 26 | ``` 27 | 28 | ## Example 29 | 30 | ```svg 31 | 32 | 33 | 39 | 40 | ``` 41 | 42 | ↓↓ `Path.codeString()` ↓↓ 43 | 44 | ```swift 45 | path.move(to: CGPoint(x: 50.9042, y: 67.4653)) 46 | path.addCurve(to: CGPoint(x: 45.6919, y: 55.1048), 47 | control1: CGPoint(x: 50.9042, y: 67.4653), 48 | control2: CGPoint(x: 41.5221, y: 63.2955)) 49 | path.addCurve(to: CGPoint(x: 62.8179, y: 53.3177), 50 | control1: CGPoint(x: 49.8617, y: 46.9141), 51 | control2: CGPoint(x: 59.0949, y: 48.8501)) 52 | path.addCurve(to: CGPoint(x: 57.1589, y: 72.5286), 53 | control1: CGPoint(x: 66.5409, y: 57.7853), 54 | control2: CGPoint(x: 66.9532, y: 68.6808)) 55 | path.addCurve(to: CGPoint(x: 34.8206, y: 55.4026), 56 | control1: CGPoint(x: 46.7344, y: 76.6239), 57 | control2: CGPoint(x: 33.6293, y: 65.2314)) 58 | path.addCurve(to: CGPoint(x: 50.6063, y: 32.1708), 59 | control1: CGPoint(x: 36.0120, y: 45.5738), 60 | control2: CGPoint(x: 46.2876, y: 37.2341)) 61 | path.addCurve(to: CGPoint(x: 58.8445, y: 9.6402), 62 | control1: CGPoint(x: 54.9250, y: 27.1075), 63 | control2: CGPoint(x: 61.7754, y: 17.6509)) 64 | path.addCurve(to: CGPoint(x: 48.6703, y: 11.9919), 65 | control1: CGPoint(x: 56.5059, y: 3.2482), 66 | control2: CGPoint(x: 51.4787, y: 2.9424)) 67 | path.addCurve(to: CGPoint(x: 51.3509, y: 43.1165), 68 | control1: CGPoint(x: 45.9897, y: 20.6294), 69 | control2: CGPoint(x: 48.9817, y: 31.1189)) 70 | path.addCurve(to: CGPoint(x: 58.2013, y: 90.7715), 71 | control1: CGPoint(x: 57.1588, y: 72.5285), 72 | control2: CGPoint(x: 62.3711, y: 84.0700)) 73 | path.addCurve(to: CGPoint(x: 41.8480, y: 88.8513), 74 | control1: CGPoint(x: 54.0315, y: 97.4730), 75 | control2: CGPoint(x: 43.3091, y: 95.7437)) 76 | path.addCurve(to: CGPoint(x: 51.0531, y: 86.4528), 77 | control1: CGPoint(x: 39.9280, y: 79.7942), 78 | control2: CGPoint(x: 50.9041, y: 80.4959)) 79 | path.addCurve(to: CGPoint(x: 45.0962, y: 91.3672), 80 | control1: CGPoint(x: 51.2021, y: 92.4097), 81 | control2: CGPoint(x: 45.0962, y: 91.3672)) 82 | ``` 83 | 84 | ## Demo 85 | 86 | This repository includes a demo app. 87 | 88 | 89 | -------------------------------------------------------------------------------- /SVG/figures.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyome22/SVG2Path/15a888cc63c4f3983b745e8bf16d53efc4f43462/SVG/figures.ai -------------------------------------------------------------------------------- /SVG/figures.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Sources/SVG2Path/Command.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct Command { 4 | let type: Character 5 | let points: [CGFloat] 6 | let relative: Bool 7 | 8 | init(type: Character, points: [CGFloat]) { 9 | self.type = type 10 | self.points = points 11 | self.relative = type.isLowercase 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/SVG2Path/Extensions/Array+Extension.swift: -------------------------------------------------------------------------------- 1 | extension Array { 2 | func chunked(by chunkSize: Int) -> [[Element]] { 3 | guard chunkSize > 0 else { return [[]] } 4 | return stride(from: 0, to: count, by: chunkSize).map { i in 5 | Array(self[i ..< Swift.min(i + chunkSize, count)]) 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Sources/SVG2Path/Extensions/CGPoint+Extension.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | 3 | extension CGPoint { 4 | func output(_ n: Int = 4) -> String { 5 | assert(n > 0, "n must be greater than 0.") 6 | return String(format: "CGPoint(x: %.\(n)f, y: %.\(n)f)", x, y) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Sources/SVG2Path/Extensions/Path+Extension.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public extension Path { 4 | func codeString(n: Int = 4) -> String { 5 | var texts = [String]() 6 | cgPath.applyWithBlock { element in 7 | switch element.pointee.type { 8 | case CGPathElementType.moveToPoint: 9 | texts.append("path.move(to: \(element.pointee.points[0].output(n)))") 10 | case CGPathElementType.addLineToPoint: 11 | texts.append("path.addLine(to: \(element.pointee.points[0].output(n)))") 12 | case CGPathElementType.addCurveToPoint: 13 | texts.append("path.addCurve(to: \(element.pointee.points[2].output(n)),") 14 | texts.append(" control1: \(element.pointee.points[0].output(n)),") 15 | texts.append(" control2: \(element.pointee.points[1].output(n)))") 16 | case CGPathElementType.addQuadCurveToPoint: 17 | texts.append("path.addCurve(to: \(element.pointee.points[1].output(n)),") 18 | texts.append(" control1: \(element.pointee.points[0].output(n)),") 19 | texts.append(" control2: \(element.pointee.points[0].output(n)))") 20 | case CGPathElementType.closeSubpath: 21 | texts.append("path.closeSubpath()") 22 | @unknown default: 23 | fatalError() 24 | } 25 | } 26 | return texts.joined(separator: "\n") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/SVG2Path/Extensions/String+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | typealias MatchResult = (leading: String, trailing: String, items: [String]) 4 | 5 | extension String { 6 | func replace(pattern: String, expect: String) -> String { 7 | replacingOccurrences(of: pattern, with: expect, options: .regularExpression, range: range(of: self)) 8 | } 9 | 10 | func match(pattern: String) -> MatchResult { 11 | let range = NSRange(location: 0, length: utf16.count) 12 | guard let regex = try? NSRegularExpression(pattern: pattern), 13 | let matched = regex.firstMatch(in: self, range: range) else { 14 | return ("", self, []) 15 | } 16 | let lowerIndex = index(startIndex, offsetBy: matched.range.lowerBound) 17 | let leading = String(self[startIndex ..< lowerIndex]) 18 | let upperIndex = index(startIndex, offsetBy: matched.range.upperBound) 19 | let trailing = String(self[upperIndex ..< endIndex]) 20 | let items = (0 ..< matched.numberOfRanges).compactMap { i -> String? in 21 | let r = matched.range(at: i) 22 | guard r.location != NSNotFound else { return nil } 23 | return NSString(string: self).substring(with: r) 24 | } 25 | return (leading, trailing, items) 26 | } 27 | 28 | func toFloat() -> Double { 29 | NumberFormatter().number(from: self)?.doubleValue ?? 0 30 | } 31 | 32 | var trimmingWhitespaces: String { 33 | trimmingCharacters(in: .whitespaces) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/SVG2Path/SVG2Path.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public typealias SVGPathData = (size: CGSize, paths: [Path]) 4 | 5 | public struct SVG2Path { 6 | private let whiteList: [String] = [ 7 | " CGPoint { 30 | if relative { 31 | CGPoint(x: state.preP.x + pX, y: state.preP.y + pY) 32 | } else { 33 | CGPoint(x: pX, y: pY) 34 | } 35 | } 36 | 37 | private func callMove(_ state: inout State, pX: CGFloat, pY: CGFloat, relative: Bool) { 38 | let p = callPoint(state, pX: pX, pY: pY, relative: relative) 39 | state.path.move(to: p) 40 | state.preP = p 41 | state.preCP = nil 42 | } 43 | 44 | private func callLine(_ state: inout State, pX: CGFloat, pY: CGFloat, relative: Bool) { 45 | let p = callPoint(state, pX: pX, pY: pY, relative: relative) 46 | state.path.addLine(to: p) 47 | state.preP = p 48 | state.preCP = nil 49 | } 50 | 51 | private func callH(_ state: inout State, pX: CGFloat, relative: Bool) { 52 | let pY = relative ? .zero : state.preP.y 53 | callLine(&state, pX: pX, pY: pY, relative: relative) 54 | } 55 | 56 | private func callV(_ state: inout State, pY: CGFloat, relative: Bool) { 57 | let pX = relative ? .zero : state.preP.x 58 | callLine(&state, pX: pX, pY: pY, relative: relative) 59 | } 60 | 61 | private func callCurve( 62 | _ state: inout State, 63 | pX: CGFloat, 64 | pY: CGFloat, 65 | c1X: CGFloat, 66 | c1Y: CGFloat, 67 | c2X: CGFloat, 68 | c2Y: CGFloat, 69 | relative: Bool 70 | ) { 71 | let p = callPoint(state, pX: pX, pY: pY, relative: relative) 72 | let c1 = callPoint(state, pX: c1X, pY: c1Y, relative: relative) 73 | let c2 = callPoint(state, pX: c2X, pY: c2Y, relative: relative) 74 | state.path.addCurve(to: p, control1: c1, control2: c2) 75 | state.preP = p 76 | state.preCP = c2 77 | } 78 | 79 | private func callS( 80 | _ state: inout State, 81 | pX: CGFloat, 82 | pY: CGFloat, 83 | c2X: CGFloat, 84 | c2Y: CGFloat, 85 | relative: Bool 86 | ) { 87 | var c1 = CGPoint(x: state.preP.x, y: state.preP.y) 88 | if let preCP = state.preCP { 89 | c1 = CGPoint(x: 2 * state.preP.x - preCP.x, y: 2 * state.preP.y - preCP.y) 90 | } 91 | let p = callPoint(state, pX: pX, pY: pY, relative: relative) 92 | let c2 = callPoint(state, pX: c2X, pY: c2Y, relative: relative) 93 | state.path.addCurve(to: p, control1: c1, control2: c2) 94 | state.preP = p 95 | state.preCP = c2 96 | } 97 | 98 | private func callQ( 99 | _ state: inout State, 100 | pX: CGFloat, 101 | pY: CGFloat, 102 | c1X: CGFloat, 103 | c1Y: CGFloat, 104 | relative: Bool 105 | ) { 106 | callCurve(&state, pX: pX, pY: pY, c1X: c1X, c1Y: c1Y, c2X: c1X, c2Y: c1Y, relative: relative) 107 | } 108 | 109 | private func callT(_ state: inout State, pX: CGFloat, pY: CGFloat, relative: Bool) { 110 | var c1 = CGPoint(x: state.preP.x, y: state.preP.y) 111 | if let preCP = state.preCP { 112 | c1 = CGPoint(x: 2 * state.preP.x - preCP.x, y: 2 * state.preP.y - preCP.y) 113 | } 114 | let p = callPoint(state, pX: pX, pY: pY, relative: relative) 115 | state.path.addCurve(to: p, control1: c1, control2: c1) 116 | state.preP = p 117 | state.preCP = c1 118 | } 119 | 120 | private func callCommand(_ state: inout State, with command: Command) { 121 | switch command.type.uppercased() { 122 | case "M": 123 | callMove( 124 | &state, 125 | pX: command.points[0], 126 | pY: command.points[1], 127 | relative: command.relative 128 | ) 129 | case "Z": 130 | callClose(&state) 131 | case "L": 132 | callLine( 133 | &state, 134 | pX: command.points[0], 135 | pY: command.points[1], 136 | relative: command.relative 137 | ) 138 | case "H": 139 | callH( 140 | &state, 141 | pX: command.points[0], 142 | relative: command.relative 143 | ) 144 | case "V": 145 | callV( 146 | &state, 147 | pY: command.points[0], 148 | relative: command.relative 149 | ) 150 | case "C": 151 | callCurve( 152 | &state, 153 | pX: command.points[4], 154 | pY: command.points[5], 155 | c1X: command.points[0], 156 | c1Y: command.points[1], 157 | c2X: command.points[2], 158 | c2Y: command.points[3], 159 | relative: command.relative 160 | ) 161 | case "S": 162 | callS( 163 | &state, 164 | pX: command.points[2], 165 | pY: command.points[3], 166 | c2X: command.points[0], 167 | c2Y: command.points[1], 168 | relative: command.relative 169 | ) 170 | case "Q": 171 | callQ( 172 | &state, 173 | pX: command.points[2], 174 | pY: command.points[3], 175 | c1X: command.points[0], 176 | c1Y: command.points[1], 177 | relative: command.relative 178 | ) 179 | case "T": 180 | callT( 181 | &state, 182 | pX: command.points[0], 183 | pY: command.points[1], 184 | relative: command.relative 185 | ) 186 | default: break 187 | } 188 | } 189 | 190 | // MARK: - Convenient Getter 191 | private func getFloats(text: String) -> [CGFloat] { 192 | var code = text 193 | var floats = [CGFloat]() 194 | repeat { 195 | let d = code.match(pattern: #",?\s?(-?\d*\.?\d*)"#) 196 | if d.items.isEmpty { 197 | code = "" 198 | } else { 199 | code = d.trailing.trimmingWhitespaces 200 | floats.append(d.items[1].toFloat()) 201 | } 202 | } while !code.isEmpty 203 | return floats 204 | } 205 | 206 | private func getPoints(text: String) -> [CGPoint] { 207 | getFloats(text: text) 208 | .chunked(by: 2) 209 | .compactMap { v -> CGPoint? in 210 | guard v.count == 2 else { return nil } 211 | return CGPoint(x: v[0], y: v[1]) 212 | } 213 | } 214 | 215 | // MARK: - Graphics Element Getter 216 | private func getPath(text: String) -> Path { 217 | var state = State() 218 | var code = text 219 | repeat { 220 | let d = code.match(pattern: #"[a-zA-Z](,?\s?-?\d*\.?\d*)+"#) 221 | if var item = d.items.first { 222 | code = d.trailing 223 | let type = item.removeFirst() 224 | let chunk: Int 225 | switch type.uppercased() { 226 | case "Z": chunk = 0 227 | case "H", "V": chunk = 1 228 | case "M", "L", "T": chunk = 2 229 | case "S", "Q": chunk = 4 230 | case "C": chunk = 6 231 | default: chunk = -1 232 | } 233 | if chunk < .zero { continue } 234 | getFloats(text: item).chunked(by: chunk).forEach { values in 235 | callCommand(&state, with: Command(type: type, points: values)) 236 | } 237 | } else { 238 | code = "" 239 | } 240 | } while !code.isEmpty 241 | return state.path 242 | } 243 | 244 | private func getRect( 245 | x: CGFloat, 246 | y: CGFloat, 247 | width: CGFloat, 248 | height: CGFloat, 249 | rx: CGFloat, 250 | ry: CGFloat 251 | ) -> Path { 252 | let rect = CGRect(x: x, y: y, width: width, height: height) 253 | if rx == .zero, ry == .zero { 254 | return Path(rect) 255 | } else { 256 | let size = CGSize(width: rx, height: ry) 257 | return Path(roundedRect: rect, cornerSize: size) 258 | } 259 | } 260 | 261 | private func getCircle(cx: CGFloat, cy: CGFloat, r: CGFloat) -> Path { 262 | Path(ellipseIn: CGRect(x: cx - r, y: cy - r, width: 2 * r, height: 2 * r)) 263 | } 264 | 265 | private func getEllipse(cx: CGFloat, cy: CGFloat, rx: CGFloat, ry: CGFloat) -> Path { 266 | Path(ellipseIn: CGRect(x: cx - rx, y: cy - ry, width: 2 * rx, height: 2 * ry)) 267 | } 268 | 269 | private func getLine(x1: CGFloat, y1: CGFloat, x2: CGFloat, y2: CGFloat) -> Path { 270 | var path = Path() 271 | path.move(to: CGPoint(x: x1, y: y1)) 272 | path.addLine(to: CGPoint(x: x2, y: y2)) 273 | return path 274 | } 275 | 276 | private func getPolyline(points: [CGPoint]) -> Path { 277 | var path = Path() 278 | path.addLines(points) 279 | return path 280 | } 281 | 282 | private func getPolygon(points: [CGPoint]) -> Path { 283 | var path = Path() 284 | path.addLines(points) 285 | path.closeSubpath() 286 | return path 287 | } 288 | 289 | private func getTransform(text: String) -> CGAffineTransform? { 290 | let items = text.match(pattern: #"transform="([^"]+)""#).items 291 | guard !items.isEmpty else { return nil } 292 | var code = items[1] 293 | var transforms = [CGAffineTransform]() 294 | repeat { 295 | // matrix 296 | var d = code.match(pattern: #"^\s?matrix\(([^)]+)\)"#) 297 | if !d.items.isEmpty { 298 | code = d.trailing 299 | let v = getFloats(text: d.items[1]) 300 | if v.count == 6 { 301 | transforms.insert(CGAffineTransform(a: v[0], b: v[1], c: v[2], d: v[3], tx: v[4], ty: v[5]), at: 0) 302 | } 303 | continue 304 | } 305 | // translate 306 | d = code.match(pattern: #"^\s?translate\(([^)]+)\)"#) 307 | if !d.items.isEmpty { 308 | code = d.trailing 309 | let v = getFloats(text: d.items[1]) 310 | if v.count == 2 { 311 | transforms.insert(CGAffineTransform(translationX: v[0], y: v[1]), at: 0) 312 | } 313 | continue 314 | } 315 | // scale 316 | d = code.match(pattern: #"^\s?scale\(([^)]+)\)"#) 317 | if !d.items.isEmpty { 318 | code = d.trailing 319 | let v = getFloats(text: d.items[1]) 320 | if v.count >= 1 { 321 | let sy = (v.count == 2) ? v[1] : v[0] 322 | transforms.insert(CGAffineTransform(scaleX: v[0], y: sy), at: 0) 323 | } 324 | continue 325 | } 326 | // rotate 327 | d = code.match(pattern: #"^\s?rotate\(([^)]+)\)"#) 328 | if !d.items.isEmpty { 329 | code = d.trailing 330 | let v = getFloats(text: d.items[1]) 331 | if v.count == 1 { 332 | let angle = v[0] * Double.pi / 180.0 333 | transforms.insert(CGAffineTransform(rotationAngle: angle), at: 0) 334 | } else if v.count == 3 { 335 | let angle = v[0] * Double.pi / 180.0 336 | transforms.insert(CGAffineTransform(translationX: v[1], y: v[2]), at: 0) 337 | transforms.insert(CGAffineTransform(rotationAngle: angle), at: 0) 338 | transforms.insert(CGAffineTransform(translationX: -v[1], y: -v[2]), at: 0) 339 | } 340 | continue 341 | } 342 | } while !code.isEmpty 343 | guard !transforms.isEmpty else { return nil } 344 | let transform = transforms.removeFirst() 345 | return transforms.reduce(into: transform) { $0 = $0.concatenating($1) } 346 | } 347 | 348 | // MARK: - Extract Graphic Element from SVG 349 | private func extractPath(tag: String) -> Path? { 350 | let d = tag.match(pattern: #"\sd="([^"]+)""#) 351 | guard !d.items.isEmpty else { return nil } 352 | let dText = d.items[1] 353 | .replacingOccurrences(of: #" +([a-zA-Z])"#, with: "$1", options: .regularExpression) 354 | .replacingOccurrences(of: " ", with: ",") 355 | var path = getPath(text: dText) 356 | if let transform = getTransform(text: tag) { 357 | path = path.applying(transform) 358 | } 359 | return path 360 | } 361 | 362 | private func extractRect(tag: String) -> Path? { 363 | let patterns: [String] = [ 364 | #"x="(.+?)""#, 365 | #"y="(.+?)""#, 366 | #"width="(.+?)""#, 367 | #"height="(.+?)""#, 368 | #"rx="(.+?)""#, 369 | #"ry="(.+?)""# 370 | ] 371 | let v = patterns.map { pattern -> CGFloat in 372 | let d = tag.match(pattern: pattern) 373 | return d.items.isEmpty ? .zero : d.items[1].toFloat() 374 | } 375 | guard v[2] != .zero && v[3] != .zero else { return nil } 376 | var path = getRect(x: v[0], y: v[1], width: v[2], height: v[3], rx: v[4], ry: v[5]) 377 | if let transform = getTransform(text: tag) { 378 | path = path.applying(transform) 379 | } 380 | return path 381 | } 382 | 383 | private func extractCircle(tag: String) -> Path? { 384 | let patterns: [String] = [ 385 | #"cx="(.+?)""#, 386 | #"cy="(.+?)""#, 387 | #"r="(.+?)""# 388 | ] 389 | let v = patterns.map { pattern -> CGFloat in 390 | let d = tag.match(pattern: pattern) 391 | return d.items.isEmpty ? .zero : d.items[1].toFloat() 392 | } 393 | guard v[2] != .zero else { return nil } 394 | var path = getCircle(cx: v[0], cy: v[1], r: v[2]) 395 | if let transform = getTransform(text: tag) { 396 | path = path.applying(transform) 397 | } 398 | return path 399 | } 400 | 401 | private func extractEllipse(tag: String) -> Path? { 402 | let patterns: [String] = [ 403 | #"cx="(.+?)""#, 404 | #"cy="(.+?)""#, 405 | #"rx="(.+?)""#, 406 | #"ry="(.+?)""# 407 | ] 408 | let v = patterns.map { pattern -> CGFloat in 409 | let d = tag.match(pattern: pattern) 410 | return d.items.isEmpty ? .zero : d.items[1].toFloat() 411 | } 412 | guard v[2] != .zero && v[3] != .zero else { return nil } 413 | var path = getEllipse(cx: v[0], cy: v[1], rx: v[2], ry: v[3]) 414 | if let transform = getTransform(text: tag) { 415 | path = path.applying(transform) 416 | } 417 | return path 418 | } 419 | 420 | private func extractLine(tag: String) -> Path? { 421 | let patterns: [String] = [ 422 | #"x1="(.+?)""#, 423 | #"y1="(.+?)""#, 424 | #"x2="(.+?)""#, 425 | #"y2="(.+?)""# 426 | ] 427 | let v = patterns.map { pattern -> CGFloat in 428 | let d = tag.match(pattern: pattern) 429 | return d.items.isEmpty ? .zero : d.items[1].toFloat() 430 | } 431 | guard v[0] != v[2] || v[1] != v[3] else { return nil } 432 | var path = getLine(x1: v[0], y1: v[1], x2: v[2], y2: v[3]) 433 | if let transform = getTransform(text: tag) { 434 | path = path.applying(transform) 435 | } 436 | return path 437 | } 438 | 439 | private func extractPolyline(tag: String) -> Path? { 440 | let d = tag.match(pattern: #"\spoints="([^"]+)""#) 441 | guard !d.items.isEmpty else { return nil } 442 | let v = getPoints(text: d.items[1]) 443 | guard v.count >= 2 else { return nil } 444 | var path = getPolyline(points: v) 445 | if let transform = getTransform(text: tag) { 446 | path = path.applying(transform) 447 | } 448 | return path 449 | } 450 | 451 | private func extractPolygon(tag: String) -> Path? { 452 | let d = tag.match(pattern: #"\spoints="([^"]+)""#) 453 | if d.items.isEmpty { return nil } 454 | let v = getPoints(text: d.items[1]) 455 | guard v.count >= 2 else { return nil } 456 | var path = getPolygon(points: v) 457 | if let transform = getTransform(text: tag) { 458 | path = path.applying(transform) 459 | } 460 | return path 461 | } 462 | 463 | // MARK: - Parsing SVG 464 | private func extractFigure(text: String) -> (trailing: String, items: [String]) { 465 | var code = text 466 | var d: MatchResult 467 | var array = [String]() 468 | repeat { 469 | d = code.match(pattern: #"^<[^>]*?/>"#) 470 | if !d.items.isEmpty { 471 | array.append(d.items[0]) 472 | code = d.trailing 473 | } 474 | } while !d.items.isEmpty 475 | return (code, array) 476 | } 477 | 478 | private func extractGroup(text: String) -> [String] { 479 | var array = [String]() 480 | let d = text.match(pattern: #"^]*?>"#) 481 | if d.items.isEmpty { // head is not 482 | let m = extractFigure(text: d.trailing) 483 | array.append(contentsOf: m.items) 484 | let e = m.trailing.match(pattern: #"^"#) 485 | if e.items.isEmpty { // head is not 486 | let code = e.trailing 487 | if !code.isEmpty, code.hasPrefix("<"), code.contains("/>") { 488 | array.append(contentsOf: extractGroup(text: code)) 489 | } 490 | } else { // head is 491 | array.append(e.items[0]) 492 | array.append(contentsOf: extractGroup(text: e.trailing)) 493 | } 494 | } else { // head is 495 | array.append(d.items[0]) 496 | array.append(contentsOf: extractGroup(text: d.trailing)) 497 | } 498 | return array 499 | } 500 | 501 | private func extractSVG(text: String) -> (width: CGFloat, height: CGFloat, tags: [String])? { 502 | let code = text.replacingOccurrences(of: "\n", with: "") 503 | .replace(pattern: #">\s*?<"#, expect: "><") 504 | .trimmingWhitespaces 505 | .replace(pattern: #">"#, expect: ">\n") 506 | .components(separatedBy: .newlines) 507 | .filter { str in whiteList.contains { str.hasPrefix($0) } } 508 | .joined() 509 | let d = code.match(pattern: #"]*?)>(.*)"#) 510 | guard !d.items.isEmpty else { return nil } 511 | let e = d.items[1].match(pattern: #"viewBox="([^"]+)""#) 512 | guard !e.items.isEmpty else { return nil } 513 | let v = getFloats(text: e.items[1]) 514 | guard v.count == 4 else { return nil } 515 | let tags = extractGroup(text: d.items[2]) 516 | return (v[2], v[3], tags) 517 | } 518 | 519 | public func extractPath(text: String) -> SVGPathData? { 520 | guard let svg = extractSVG(text: text) else { return nil } 521 | let isParity = svg.tags.reduce(into: Int.zero) { partialResult, tag in 522 | partialResult += tag.hasPrefix(" TagAndPath in 526 | if tag.hasPrefix(" .zero else { continue } 554 | let groupTag = array.remove(at: s) 555 | var path = (s ..< e) 556 | .map { _ in array.remove(at: s) } 557 | .compactMap(\.path) 558 | .reduce(into: Path()) { partialResult, path in 559 | partialResult.addPath(path) 560 | } 561 | if let transform = getTransform(text: groupTag.tag) { 562 | path = path.applying(transform) 563 | } 564 | array.insert(TagAndPath(tag: "grouped-path", path: path), at: s) 565 | break 566 | } 567 | } while array.contains { $0.tag.hasPrefix(" 10 | 14 | 15 | """ 16 | let actual = try XCTUnwrap(svg2Path.extractPath(text: text)) 17 | XCTAssertEqual(actual.size, CGSize(width: 280, height: 40)) 18 | XCTAssertEqual(actual.paths.count, 1) 19 | let expect = """ 20 | path.move(to: CGPoint(x: 10.7223, y: 33.3258)) 21 | path.addCurve(to: CGPoint(x: 8.7621, y: 22.1983), 22 | control1: CGPoint(x: 10.7223, y: 33.3258), 23 | control2: CGPoint(x: 2.3914, y: 27.0989)) 24 | path.addCurve(to: CGPoint(x: 15.3778, y: 11.6622), 25 | control1: CGPoint(x: 15.1328, y: 17.2978), 26 | control2: CGPoint(x: 16.2318, y: 14.8837)) 27 | path.addCurve(to: CGPoint(x: 23.3153, y: 6.3035), 28 | control1: CGPoint(x: 13.7233, y: 5.4211), 29 | control2: CGPoint(x: 20.0103, y: 2.8812)) 30 | path.addCurve(to: CGPoint(x: 24.1987, y: 16.3177), 31 | control1: CGPoint(x: 26.6320, y: 9.7379), 32 | control2: CGPoint(x: 22.3881, y: 13.0093)) 33 | path.addCurve(to: CGPoint(x: 32.2845, y: 24.4844), 34 | control1: CGPoint(x: 27.3874, y: 22.1440), 35 | control2: CGPoint(x: 29.3408, y: 20.5775)) 36 | path.addCurve(to: CGPoint(x: 23.8020, y: 35.0936), 37 | control1: CGPoint(x: 35.2967, y: 28.4820), 38 | control2: CGPoint(x: 33.6168, y: 36.3510)) 39 | path.addCurve(to: CGPoint(x: 20.0000, y: 24.0613), 40 | control1: CGPoint(x: 16.7875, y: 34.1950), 41 | control2: CGPoint(x: 20.0000, y: 24.0613)) 42 | """ 43 | XCTAssertEqual(actual.paths[0].codeString(), expect) 44 | } 45 | 46 | func testRect() throws { 47 | let text = """ 48 | 49 | 50 | 51 | """ 52 | let actual = try XCTUnwrap(svg2Path.extractPath(text: text)) 53 | XCTAssertEqual(actual.size, CGSize(width: 280, height: 40)) 54 | XCTAssertEqual(actual.paths.count, 1) 55 | let expect = """ 56 | path.move(to: CGPoint(x: 46.0000, y: 9.0000)) 57 | path.addLine(to: CGPoint(x: 74.0000, y: 9.0000)) 58 | path.addLine(to: CGPoint(x: 74.0000, y: 31.0000)) 59 | path.addLine(to: CGPoint(x: 46.0000, y: 31.0000)) 60 | path.closeSubpath() 61 | """ 62 | XCTAssertEqual(actual.paths[0].codeString(), expect) 63 | } 64 | 65 | func testCircle() throws { 66 | let text = """ 67 | 68 | 69 | 70 | """ 71 | let actual = try XCTUnwrap(svg2Path.extractPath(text: text)) 72 | XCTAssertEqual(actual.size, CGSize(width: 280, height: 40)) 73 | XCTAssertEqual(actual.paths.count, 1) 74 | let expect = """ 75 | path.move(to: CGPoint(x: 115.0000, y: 20.0000)) 76 | path.addCurve(to: CGPoint(x: 100.0000, y: 35.0000), 77 | control1: CGPoint(x: 115.0000, y: 28.2843), 78 | control2: CGPoint(x: 108.2843, y: 35.0000)) 79 | path.addCurve(to: CGPoint(x: 85.0000, y: 20.0000), 80 | control1: CGPoint(x: 91.7157, y: 35.0000), 81 | control2: CGPoint(x: 85.0000, y: 28.2843)) 82 | path.addCurve(to: CGPoint(x: 100.0000, y: 5.0000), 83 | control1: CGPoint(x: 85.0000, y: 11.7157), 84 | control2: CGPoint(x: 91.7157, y: 5.0000)) 85 | path.addCurve(to: CGPoint(x: 115.0000, y: 20.0000), 86 | control1: CGPoint(x: 108.2843, y: 5.0000), 87 | control2: CGPoint(x: 115.0000, y: 11.7157)) 88 | path.closeSubpath() 89 | """ 90 | XCTAssertEqual(actual.paths[0].codeString(), expect) 91 | } 92 | 93 | func testEllipse() throws { 94 | let text = """ 95 | 96 | 97 | 98 | """ 99 | let actual = try XCTUnwrap(svg2Path.extractPath(text: text)) 100 | XCTAssertEqual(actual.size, CGSize(width: 280, height: 40)) 101 | XCTAssertEqual(actual.paths.count, 1) 102 | let expect = """ 103 | path.move(to: CGPoint(x: 150.0568, y: 20.0000)) 104 | path.addCurve(to: CGPoint(x: 140.0000, y: 36.5000), 105 | control1: CGPoint(x: 150.0568, y: 29.1127), 106 | control2: CGPoint(x: 145.5542, y: 36.5000)) 107 | path.addCurve(to: CGPoint(x: 129.9432, y: 20.0000), 108 | control1: CGPoint(x: 134.4458, y: 36.5000), 109 | control2: CGPoint(x: 129.9432, y: 29.1127)) 110 | path.addCurve(to: CGPoint(x: 140.0000, y: 3.5000), 111 | control1: CGPoint(x: 129.9432, y: 10.8873), 112 | control2: CGPoint(x: 134.4458, y: 3.5000)) 113 | path.addCurve(to: CGPoint(x: 150.0568, y: 20.0000), 114 | control1: CGPoint(x: 145.5542, y: 3.5000), 115 | control2: CGPoint(x: 150.0568, y: 10.8873)) 116 | path.closeSubpath() 117 | """ 118 | XCTAssertEqual(actual.paths[0].codeString(), expect) 119 | } 120 | 121 | func testLine() throws { 122 | let text = """ 123 | 124 | 125 | 126 | """ 127 | let actual = try XCTUnwrap(svg2Path.extractPath(text: text)) 128 | XCTAssertEqual(actual.size, CGSize(width: 280, height: 40)) 129 | XCTAssertEqual(actual.paths.count, 1) 130 | let expect = """ 131 | path.move(to: CGPoint(x: 167.0253, y: 10.9283)) 132 | path.addLine(to: CGPoint(x: 192.9747, y: 29.0717)) 133 | """ 134 | XCTAssertEqual(actual.paths[0].codeString(), expect) 135 | } 136 | 137 | func testPolyline() throws { 138 | let text = """ 139 | 140 | 141 | 142 | """ 143 | let actual = try XCTUnwrap(svg2Path.extractPath(text: text)) 144 | XCTAssertEqual(actual.size, CGSize(width: 280, height: 40)) 145 | XCTAssertEqual(actual.paths.count, 1) 146 | let expect = """ 147 | path.move(to: CGPoint(x: 207.0253, y: 4.9156)) 148 | path.addLine(to: CGPoint(x: 225.1688, y: 4.9156)) 149 | path.addLine(to: CGPoint(x: 234.2405, y: 20.3291)) 150 | path.addLine(to: CGPoint(x: 220.6329, y: 35.0844)) 151 | path.addLine(to: CGPoint(x: 205.7595, y: 21.5823)) 152 | """ 153 | XCTAssertEqual(actual.paths[0].codeString(), expect) 154 | } 155 | 156 | func testPolygon() throws { 157 | let text = """ 158 | 159 | 160 | 161 | """ 162 | let actual = try XCTUnwrap(svg2Path.extractPath(text: text)) 163 | XCTAssertEqual(actual.size, CGSize(width: 280, height: 40)) 164 | XCTAssertEqual(actual.paths.count, 1) 165 | let expect = """ 166 | path.move(to: CGPoint(x: 257.4531, y: 6.4742)) 167 | path.addLine(to: CGPoint(x: 246.9172, y: 19.4850)) 168 | path.addLine(to: CGPoint(x: 256.0354, y: 33.5258)) 169 | path.addLine(to: CGPoint(x: 272.2066, y: 29.1927)) 170 | path.addLine(to: CGPoint(x: 273.0828, y: 12.4739)) 171 | path.addLine(to: CGPoint(x: 257.4531, y: 6.4742)) 172 | path.closeSubpath() 173 | """ 174 | XCTAssertEqual(actual.paths[0].codeString(), expect) 175 | } 176 | 177 | func testTransformMatrix() throws { 178 | let text = """ 179 | 180 | 181 | 182 | """ 183 | let actual = try XCTUnwrap(svg2Path.extractPath(text: text)) 184 | XCTAssertEqual(actual.size, CGSize(width: 260, height: 80)) 185 | XCTAssertEqual(actual.paths.count, 1) 186 | let expect = """ 187 | path.move(to: CGPoint(x: 13.6291, y: 32.9978)) 188 | path.addLine(to: CGPoint(x: 40.6743, y: 25.7514)) 189 | path.addLine(to: CGPoint(x: 46.3679, y: 47.0012)) 190 | path.addLine(to: CGPoint(x: 19.3227, y: 54.2476)) 191 | path.closeSubpath() 192 | """ 193 | XCTAssertEqual(actual.paths[0].codeString(), expect) 194 | } 195 | 196 | @MainActor 197 | func testTransformTranslate() throws { 198 | try XCTContext.runActivity(named: "only tx", block: { _ in 199 | let text = """ 200 | 201 | 202 | 203 | """ 204 | let actual = try XCTUnwrap(svg2Path.extractPath(text: text)) 205 | XCTAssertEqual(actual.size, CGSize(width: 280, height: 40)) 206 | XCTAssertEqual(actual.paths.count, 1) 207 | let expect = """ 208 | path.move(to: CGPoint(x: 46.0000, y: 9.0000)) 209 | path.addLine(to: CGPoint(x: 74.0000, y: 9.0000)) 210 | path.addLine(to: CGPoint(x: 74.0000, y: 31.0000)) 211 | path.addLine(to: CGPoint(x: 46.0000, y: 31.0000)) 212 | path.closeSubpath() 213 | """ 214 | XCTAssertEqual(actual.paths[0].codeString(), expect) 215 | }) 216 | 217 | try XCTContext.runActivity(named: "tx & ty", block: { _ in 218 | let text = """ 219 | 220 | 221 | 222 | """ 223 | let actual = try XCTUnwrap(svg2Path.extractPath(text: text)) 224 | XCTAssertEqual(actual.size, CGSize(width: 280, height: 40)) 225 | XCTAssertEqual(actual.paths.count, 1) 226 | let expect = """ 227 | path.move(to: CGPoint(x: 56.0000, y: 4.0000)) 228 | path.addLine(to: CGPoint(x: 84.0000, y: 4.0000)) 229 | path.addLine(to: CGPoint(x: 84.0000, y: 26.0000)) 230 | path.addLine(to: CGPoint(x: 56.0000, y: 26.0000)) 231 | path.closeSubpath() 232 | """ 233 | XCTAssertEqual(actual.paths[0].codeString(), expect) 234 | }) 235 | } 236 | 237 | @MainActor 238 | func testTransformScale() throws { 239 | try XCTContext.runActivity(named: "only sx", block: { _ in 240 | let text = """ 241 | 242 | 243 | 244 | """ 245 | let actual = try XCTUnwrap(svg2Path.extractPath(text: text)) 246 | XCTAssertEqual(actual.size, CGSize(width: 280, height: 40)) 247 | XCTAssertEqual(actual.paths.count, 1) 248 | let expect = """ 249 | path.move(to: CGPoint(x: 230.0000, y: 40.0000)) 250 | path.addCurve(to: CGPoint(x: 200.0000, y: 70.0000), 251 | control1: CGPoint(x: 230.0000, y: 56.5685), 252 | control2: CGPoint(x: 216.5685, y: 70.0000)) 253 | path.addCurve(to: CGPoint(x: 170.0000, y: 40.0000), 254 | control1: CGPoint(x: 183.4315, y: 70.0000), 255 | control2: CGPoint(x: 170.0000, y: 56.5685)) 256 | path.addCurve(to: CGPoint(x: 200.0000, y: 10.0000), 257 | control1: CGPoint(x: 170.0000, y: 23.4315), 258 | control2: CGPoint(x: 183.4315, y: 10.0000)) 259 | path.addCurve(to: CGPoint(x: 230.0000, y: 40.0000), 260 | control1: CGPoint(x: 216.5685, y: 10.0000), 261 | control2: CGPoint(x: 230.0000, y: 23.4315)) 262 | path.closeSubpath() 263 | """ 264 | XCTAssertEqual(actual.paths[0].codeString(), expect) 265 | }) 266 | 267 | try XCTContext.runActivity(named: "sx & sy", block: { _ in 268 | let text = """ 269 | 270 | 271 | 272 | """ 273 | let actual = try XCTUnwrap(svg2Path.extractPath(text: text)) 274 | XCTAssertEqual(actual.size, CGSize(width: 280, height: 40)) 275 | XCTAssertEqual(actual.paths.count, 1) 276 | let expect = """ 277 | path.move(to: CGPoint(x: 230.0000, y: 30.0000)) 278 | path.addCurve(to: CGPoint(x: 200.0000, y: 52.5000), 279 | control1: CGPoint(x: 230.0000, y: 42.4264), 280 | control2: CGPoint(x: 216.5685, y: 52.5000)) 281 | path.addCurve(to: CGPoint(x: 170.0000, y: 30.0000), 282 | control1: CGPoint(x: 183.4315, y: 52.5000), 283 | control2: CGPoint(x: 170.0000, y: 42.4264)) 284 | path.addCurve(to: CGPoint(x: 200.0000, y: 7.5000), 285 | control1: CGPoint(x: 170.0000, y: 17.5736), 286 | control2: CGPoint(x: 183.4315, y: 7.5000)) 287 | path.addCurve(to: CGPoint(x: 230.0000, y: 30.0000), 288 | control1: CGPoint(x: 216.5685, y: 7.5000), 289 | control2: CGPoint(x: 230.0000, y: 17.5736)) 290 | path.closeSubpath() 291 | """ 292 | XCTAssertEqual(actual.paths[0].codeString(), expect) 293 | }) 294 | } 295 | 296 | @MainActor 297 | func testTransformRotate() throws { 298 | try XCTContext.runActivity(named: "only angle", block: { _ in 299 | let text = """ 300 | 301 | 302 | 303 | """ 304 | let actual = try XCTUnwrap(svg2Path.extractPath(text: text)) 305 | XCTAssertEqual(actual.size, CGSize(width: 280, height: 40)) 306 | XCTAssertEqual(actual.paths.count, 1) 307 | let expect = """ 308 | path.move(to: CGPoint(x: 35.3372, y: 30.7942)) 309 | path.addLine(to: CGPoint(x: 59.5859, y: 44.7942)) 310 | path.addLine(to: CGPoint(x: 48.5859, y: 63.8468)) 311 | path.addLine(to: CGPoint(x: 24.3372, y: 49.8468)) 312 | path.closeSubpath() 313 | """ 314 | XCTAssertEqual(actual.paths[0].codeString(), expect) 315 | }) 316 | 317 | try XCTContext.runActivity(named: "angle & cx & cy", block: { _ in 318 | let text = """ 319 | 320 | 321 | 322 | """ 323 | let actual = try XCTUnwrap(svg2Path.extractPath(text: text)) 324 | XCTAssertEqual(actual.size, CGSize(width: 280, height: 40)) 325 | XCTAssertEqual(actual.paths.count, 1) 326 | let expect = """ 327 | path.move(to: CGPoint(x: 28.0167, y: 18.1147)) 328 | path.addLine(to: CGPoint(x: 52.2654, y: 32.1147)) 329 | path.addLine(to: CGPoint(x: 41.2654, y: 51.1673)) 330 | path.addLine(to: CGPoint(x: 17.0167, y: 37.1673)) 331 | path.closeSubpath() 332 | """ 333 | XCTAssertEqual(actual.paths[0].codeString(), expect) 334 | }) 335 | } 336 | 337 | func testGroupedPath() throws { 338 | let text = """ 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | """ 354 | let actual = try XCTUnwrap(svg2Path.extractPath(text: text)) 355 | XCTAssertEqual(actual.size, CGSize(width: 260, height: 80)) 356 | XCTAssertEqual(actual.paths.count, 3) 357 | let expect1 = """ 358 | path.move(to: CGPoint(x: 56.3172, y: 4.3141)) 359 | path.addLine(to: CGPoint(x: 63.5636, y: 31.3593)) 360 | path.addLine(to: CGPoint(x: 42.3138, y: 37.0529)) 361 | path.addLine(to: CGPoint(x: 35.0674, y: 10.0077)) 362 | path.closeSubpath() 363 | path.move(to: CGPoint(x: 49.3150, y: 75.6850)) 364 | path.addCurve(to: CGPoint(x: 34.3150, y: 60.6850), 365 | control1: CGPoint(x: 41.0307, y: 75.6850), 366 | control2: CGPoint(x: 34.3150, y: 68.9693)) 367 | path.addCurve(to: CGPoint(x: 49.3150, y: 45.6850), 368 | control1: CGPoint(x: 34.3150, y: 52.4007), 369 | control2: CGPoint(x: 41.0307, y: 45.6850)) 370 | path.addCurve(to: CGPoint(x: 64.3150, y: 60.6850), 371 | control1: CGPoint(x: 57.5993, y: 45.6850), 372 | control2: CGPoint(x: 64.3150, y: 52.4007)) 373 | path.addCurve(to: CGPoint(x: 49.3150, y: 75.6850), 374 | control1: CGPoint(x: 64.3150, y: 68.9693), 375 | control2: CGPoint(x: 57.5993, y: 75.6850)) 376 | path.closeSubpath() 377 | path.move(to: CGPoint(x: 123.5190, y: 36.3776)) 378 | path.addCurve(to: CGPoint(x: 112.9726, y: 51.1087), 379 | control1: CGPoint(x: 125.1627, y: 42.5123), 380 | control2: CGPoint(x: 120.4409, y: 49.1076)) 381 | path.addCurve(to: CGPoint(x: 96.4738, y: 43.6240), 382 | control1: CGPoint(x: 105.5043, y: 53.1097), 383 | control2: CGPoint(x: 98.1175, y: 49.7587)) 384 | path.addCurve(to: CGPoint(x: 107.0202, y: 28.8930), 385 | control1: CGPoint(x: 94.8301, y: 37.4893), 386 | control2: CGPoint(x: 99.5519, y: 30.8940)) 387 | path.addCurve(to: CGPoint(x: 123.5190, y: 36.3776), 388 | control1: CGPoint(x: 114.4885, y: 26.8919), 389 | control2: CGPoint(x: 121.8753, y: 30.2429)) 390 | path.closeSubpath() 391 | """ 392 | let expect2 = """ 393 | path.move(to: CGPoint(x: 137.0000, y: 30.9000)) 394 | path.addLine(to: CGPoint(x: 163.0000, y: 49.1000)) 395 | """ 396 | let expect3 = """ 397 | path.move(to: CGPoint(x: 177.0000, y: 9.8000)) 398 | path.addLine(to: CGPoint(x: 195.2000, y: 9.8000)) 399 | path.addLine(to: CGPoint(x: 204.2000, y: 40.6000)) 400 | path.addLine(to: CGPoint(x: 190.6000, y: 70.2000)) 401 | path.addLine(to: CGPoint(x: 175.8000, y: 43.2000)) 402 | path.move(to: CGPoint(x: 227.5000, y: 13.0000)) 403 | path.addLine(to: CGPoint(x: 216.9000, y: 39.0000)) 404 | path.addLine(to: CGPoint(x: 226.0000, y: 67.0000)) 405 | path.addLine(to: CGPoint(x: 242.2000, y: 58.4000)) 406 | path.addLine(to: CGPoint(x: 243.1000, y: 25.0000)) 407 | path.closeSubpath() 408 | """ 409 | let results = actual.paths.map { $0.codeString() } 410 | XCTAssertEqual(results, [expect1, expect2, expect3]) 411 | } 412 | 413 | func testIssue1() throws { 414 | let text = """ 415 | 416 | 417 | 418 | 419 | 420 | 421 | """ 422 | let actual = try XCTUnwrap(svg2Path.extractPath(text: text)) 423 | XCTAssertEqual(actual.size, CGSize(width: 280, height: 280)) 424 | XCTAssertEqual(actual.paths.count, 4) 425 | let results = actual.paths.map { $0.codeString() } 426 | let expect1 = """ 427 | path.move(to: CGPoint(x: 153.0000, y: 20.0000)) 428 | path.addCurve(to: CGPoint(x: 133.0000, y: 40.0000), 429 | control1: CGPoint(x: 153.0000, y: 31.0457), 430 | control2: CGPoint(x: 144.0457, y: 40.0000)) 431 | path.addCurve(to: CGPoint(x: 113.0000, y: 20.0000), 432 | control1: CGPoint(x: 121.9543, y: 40.0000), 433 | control2: CGPoint(x: 113.0000, y: 31.0457)) 434 | path.addCurve(to: CGPoint(x: 133.0000, y: 0.0000), 435 | control1: CGPoint(x: 113.0000, y: 8.9543), 436 | control2: CGPoint(x: 121.9543, y: 0.0000)) 437 | path.addCurve(to: CGPoint(x: 153.0000, y: 20.0000), 438 | control1: CGPoint(x: 144.0457, y: 0.0000), 439 | control2: CGPoint(x: 153.0000, y: 8.9543)) 440 | path.closeSubpath() 441 | """ 442 | let expect2 = """ 443 | path.move(to: CGPoint(x: 67.0000, y: 78.0000)) 444 | path.addCurve(to: CGPoint(x: 133.0000, y: 12.0000), 445 | control1: CGPoint(x: 67.0000, y: 41.5500), 446 | control2: CGPoint(x: 96.5500, y: 12.0000)) 447 | path.addCurve(to: CGPoint(x: 199.0000, y: 78.0000), 448 | control1: CGPoint(x: 169.4510, y: 12.0000), 449 | control2: CGPoint(x: 199.0000, y: 41.5500)) 450 | path.addLine(to: CGPoint(x: 199.0000, y: 83.0000)) 451 | path.addLine(to: CGPoint(x: 67.0000, y: 83.0000)) 452 | path.addLine(to: CGPoint(x: 67.0000, y: 78.0000)) 453 | path.closeSubpath() 454 | """ 455 | let expect3 = """ 456 | path.move(to: CGPoint(x: 64.0000, y: 69.7720)) 457 | path.addCurve(to: CGPoint(x: 67.0460, y: 63.8090), 458 | control1: CGPoint(x: 64.0000, y: 67.3830), 459 | control2: CGPoint(x: 65.0580, y: 65.1320)) 460 | path.addCurve(to: CGPoint(x: 133.0730, y: 46.0000), 461 | control1: CGPoint(x: 74.8460, y: 58.6200), 462 | control2: CGPoint(x: 97.4700, y: 46.0000)) 463 | path.addCurve(to: CGPoint(x: 198.9710, y: 63.8100), 464 | control1: CGPoint(x: 168.6790, y: 46.0000), 465 | control2: CGPoint(x: 191.2100, y: 58.6200)) 466 | path.addCurve(to: CGPoint(x: 202.0000, y: 69.7570), 467 | control1: CGPoint(x: 200.9500, y: 65.1330), 468 | control2: CGPoint(x: 202.0000, y: 67.3770)) 469 | path.addLine(to: CGPoint(x: 202.0000, y: 99.9500)) 470 | path.addCurve(to: CGPoint(x: 195.2170, y: 103.7060), 471 | control1: CGPoint(x: 202.0000, y: 103.2560), 472 | control2: CGPoint(x: 198.0930, y: 105.3350)) 473 | path.addCurve(to: CGPoint(x: 133.8040, y: 88.0000), 474 | control1: CGPoint(x: 184.8420, y: 97.8290), 475 | control2: CGPoint(x: 163.1090, y: 88.0000)) 476 | path.addCurve(to: CGPoint(x: 70.6320, y: 103.9770), 477 | control1: CGPoint(x: 104.0450, y: 88.0000), 478 | control2: CGPoint(x: 81.2790, y: 98.1370)) 479 | path.addCurve(to: CGPoint(x: 64.0000, y: 100.2280), 480 | control1: CGPoint(x: 67.7790, y: 105.5420), 481 | control2: CGPoint(x: 64.0000, y: 103.4810)) 482 | path.addLine(to: CGPoint(x: 64.0000, y: 69.7720)) 483 | path.closeSubpath() 484 | """ 485 | let expect4 = """ 486 | path.move(to: CGPoint(x: 64.0000, y: 67.7720)) 487 | path.addCurve(to: CGPoint(x: 67.0460, y: 61.8090), 488 | control1: CGPoint(x: 64.0000, y: 65.3830), 489 | control2: CGPoint(x: 65.0580, y: 63.1320)) 490 | path.addCurve(to: CGPoint(x: 133.0730, y: 44.0000), 491 | control1: CGPoint(x: 74.8460, y: 56.6200), 492 | control2: CGPoint(x: 97.4700, y: 44.0000)) 493 | path.addCurve(to: CGPoint(x: 198.9710, y: 61.8100), 494 | control1: CGPoint(x: 168.6790, y: 44.0000), 495 | control2: CGPoint(x: 191.2100, y: 56.6200)) 496 | path.addCurve(to: CGPoint(x: 202.0000, y: 67.7570), 497 | control1: CGPoint(x: 200.9500, y: 63.1330), 498 | control2: CGPoint(x: 202.0000, y: 65.3770)) 499 | path.addLine(to: CGPoint(x: 202.0000, y: 97.9500)) 500 | path.addCurve(to: CGPoint(x: 195.2170, y: 101.7060), 501 | control1: CGPoint(x: 202.0000, y: 101.2560), 502 | control2: CGPoint(x: 198.0930, y: 103.3350)) 503 | path.addCurve(to: CGPoint(x: 133.8040, y: 86.0000), 504 | control1: CGPoint(x: 184.8420, y: 95.8290), 505 | control2: CGPoint(x: 163.1090, y: 86.0000)) 506 | path.addCurve(to: CGPoint(x: 70.6320, y: 101.9770), 507 | control1: CGPoint(x: 104.0450, y: 86.0000), 508 | control2: CGPoint(x: 81.2790, y: 96.1370)) 509 | path.addCurve(to: CGPoint(x: 64.0000, y: 98.2270), 510 | control1: CGPoint(x: 67.7790, y: 103.5420), 511 | control2: CGPoint(x: 64.0000, y: 101.4810)) 512 | path.addLine(to: CGPoint(x: 64.0000, y: 67.7730)) 513 | path.closeSubpath() 514 | """ 515 | XCTAssertEqual(results[0], expect1) 516 | XCTAssertEqual(results[1], expect2) 517 | XCTAssertEqual(results[2], expect3) 518 | XCTAssertEqual(results[3], expect4) 519 | } 520 | } 521 | -------------------------------------------------------------------------------- /demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyome22/SVG2Path/15a888cc63c4f3983b745e8bf16d53efc4f43462/demo.png --------------------------------------------------------------------------------