|
8 | 8 | import ComposableArchitecture |
9 | 9 | import SwiftUI |
10 | 10 | import PhotosUI |
| 11 | +import ImageIO |
11 | 12 |
|
12 | 13 | @Reducer |
13 | 14 | public struct ImagePickerFeature { |
@@ -60,7 +61,7 @@ public struct ImagePickerFeature { |
60 | 61 | state.isLoading = true |
61 | 62 |
|
62 | 63 | return .run { send in |
63 | | - let entities = await imageUploadClient.convert(items) |
| 64 | + let entities = await Self.convert(items: items) |
64 | 65 | await send(.imageConverted(entities)) |
65 | 66 | } |
66 | 67 |
|
@@ -101,7 +102,80 @@ public struct ImagePickerFeature { |
101 | 102 |
|
102 | 103 | default: |
103 | 104 | return .none |
| 105 | + |
| 106 | + } |
| 107 | + } |
| 108 | + } |
| 109 | +} |
| 110 | + |
| 111 | + |
| 112 | +// MARK: - ImagePickerFeature Private Func |
| 113 | + |
| 114 | +private extension ImagePickerFeature { |
| 115 | + static func convert(items: [PhotosPickerItem]) async -> [ImageUploadEntity] { |
| 116 | + await withTaskGroup(of: ImageUploadEntity?.self) { group in |
| 117 | + for item in items { |
| 118 | + group.addTask { |
| 119 | + guard let data = try? await item.loadTransferable(type: Data.self) else { return nil } |
| 120 | + let dimensions = extractDimensions(from: data) |
| 121 | + let format = detectFormat(from: data) |
| 122 | + |
| 123 | + return ImageUploadEntity( |
| 124 | + data: data, |
| 125 | + format: format, |
| 126 | + width: dimensions?.width, |
| 127 | + height: dimensions?.height, |
| 128 | + size: data.count |
| 129 | + ) |
| 130 | + } |
104 | 131 | } |
| 132 | + |
| 133 | + var results: [ImageUploadEntity] = [] |
| 134 | + for await result in group { |
| 135 | + guard let result else { continue } |
| 136 | + results.append(result) |
| 137 | + } |
| 138 | + return results |
| 139 | + } |
| 140 | + } |
| 141 | + |
| 142 | + static func detectFormat(from data: Data) -> ImageFileFormat { |
| 143 | + // 데이터가 너무 짧으면 기본값(JPEG) 반환 |
| 144 | + guard data.count > 12 else { return .jpeg } |
| 145 | + |
| 146 | + let header = data.prefix(12) |
| 147 | + let firstByte = header[0] |
| 148 | + |
| 149 | + // PNG 확인 (0x89로 시작) |
| 150 | + if firstByte == 0x89 { |
| 151 | + return .png |
| 152 | + } |
| 153 | + |
| 154 | + // WebP 확인 |
| 155 | + // WebP 파일 구조: |
| 156 | + // Offset 0-3: "RIFF" (0x52, 0x49, 0x46, 0x46) |
| 157 | + // Offset 8-11: "WEBP" (0x57, 0x45, 0x42, 0x50) |
| 158 | + if header[0] == 0x52 && header[1] == 0x49 && header[2] == 0x46 && header[3] == 0x46 && // "RIFF" |
| 159 | + header[8] == 0x57 && header[9] == 0x45 && header[10] == 0x42 && header[11] == 0x50 { // "WEBP" |
| 160 | + return .webp |
| 161 | + } |
| 162 | + |
| 163 | + // 나머지는 JPEG로 |
| 164 | + return .jpeg |
| 165 | + } |
| 166 | + |
| 167 | + static func extractDimensions(from data: Data) -> (width: Int, height: Int)? { |
| 168 | + let options: [CFString: Any] = [kCGImageSourceShouldCache: false] |
| 169 | + |
| 170 | + guard let source = CGImageSourceCreateWithData(data as CFData, options as CFDictionary), |
| 171 | + let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, options as CFDictionary) as? [CFString: Any] else { |
| 172 | + return nil |
105 | 173 | } |
| 174 | + |
| 175 | + var width = properties[kCGImagePropertyPixelWidth] as? Int |
| 176 | + var height = properties[kCGImagePropertyPixelHeight] as? Int |
| 177 | + |
| 178 | + if let w = width, let h = height { return (w, h) } |
| 179 | + return nil |
106 | 180 | } |
107 | 181 | } |
0 commit comments