Skip to content

Commit d30fcee

Browse files
committed
Implement indexed encoding
1 parent 4612ea7 commit d30fcee

File tree

1 file changed

+81
-34
lines changed

1 file changed

+81
-34
lines changed

Sources/GateEngine/Resources/Import & Export/Coding/PNGCoder.swift

Lines changed: 81 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,11 @@ public final class PNGEncoder {
4545
*/
4646
public func encode(_ data: Data, width: Int, height: Int, sacrificePerformanceToShrinkData: Bool = false) throws(GateEngineError) -> Data {
4747
#if canImport(LibSPNG)
48-
// if sacrificePerformanceToShrinkData {
49-
// try LibSPNG.encodeSmallest(data: data, width: width, height: height)
50-
// }else{
48+
if sacrificePerformanceToShrinkData {
49+
try LibSPNG.encodeSmallest(data: data, width: width, height: height)
50+
}else{
5151
try LibSPNG.encodeRGBA(data: data, width: width, height: height, optimizeAlpha: false)
52-
// }
52+
}
5353
#else
5454
fatalError("PNGEncoder is not supported on this platform.")
5555
#endif
@@ -71,19 +71,44 @@ enum LibSPNG {
7171
// TODO: Give users more customizability to allow for more predictability for runtime use.
7272
@inlinable
7373
static func encodeSmallest(data: Data, width: Int, height: Int) throws(GateEngineError) -> Data {
74-
let indexed = try encodeIndexed(data: data, width: width, height: height)
7574
let rgba: Data = try encodeRGBA(data: data, width: width, height: height, optimizeAlpha: true)
76-
77-
if rgba.count <= indexed.count {
78-
return rgba
75+
if let indexed = try encodeIndexed(data: data, width: width, height: height) {
76+
if indexed.count < rgba.count {
77+
return indexed
78+
}
7979
}
80-
return indexed
80+
return rgba
8181
}
8282

8383
/// Makes a PNG with full color data. This creates efficient PNG data representing photos or images with many unique colors.
8484
@inlinable
8585
static func encodeRGBA(data: Data, width: Int, height: Int, optimizeAlpha: Bool) throws(GateEngineError) -> Data {
8686
do {
87+
var data: Data = data
88+
89+
let colorType: UInt8
90+
if optimizeAlpha {
91+
var hasAlpha: Bool = false
92+
for index in stride(from: 3, to: data.count, by: 4) {
93+
if data[index] < .max {
94+
// If any alpha value is less then 100% we need to store the alpha
95+
hasAlpha = true
96+
break
97+
}
98+
}
99+
if hasAlpha {
100+
colorType = UInt8(SPNG_COLOR_TYPE_TRUECOLOR_ALPHA.rawValue)
101+
}else{
102+
colorType = UInt8(SPNG_COLOR_TYPE_TRUECOLOR.rawValue)
103+
// Remove the alpha values since they are all 100%
104+
for index in stride(from: 3, to: data.count, by: 4).reversed() {
105+
data.remove(at: index)
106+
}
107+
}
108+
}else{
109+
colorType = UInt8(SPNG_COLOR_TYPE_TRUECOLOR_ALPHA.rawValue)
110+
}
111+
87112
return try data.withUnsafeBytes({ (bytes: UnsafeRawBufferPointer) throws -> Data in
88113
/* Create a context */
89114
let ctx: OpaquePointer? = spng_ctx_new(Int32(SPNG_CTX_ENCODER.rawValue))
@@ -94,25 +119,6 @@ enum LibSPNG {
94119

95120
spng_set_option(ctx, SPNG_ENCODE_TO_BUFFER, 1)
96121

97-
let colorType: UInt8
98-
if optimizeAlpha {
99-
var hasAlpha: Bool = false
100-
for index in stride(from: 3, to: data.count, by: 4) {
101-
if data[index] < .max {
102-
// If any alpha value is less then 100% we need to store the alpha
103-
hasAlpha = true
104-
break
105-
}
106-
}
107-
if hasAlpha {
108-
colorType = UInt8(SPNG_COLOR_TYPE_TRUECOLOR_ALPHA.rawValue)
109-
}else{
110-
colorType = UInt8(SPNG_COLOR_TYPE_TRUECOLOR.rawValue)
111-
}
112-
}else{
113-
colorType = UInt8(SPNG_COLOR_TYPE_TRUECOLOR_ALPHA.rawValue)
114-
}
115-
116122
var ihdr = spng_ihdr(
117123
width: UInt32(width),
118124
height: UInt32(height),
@@ -145,9 +151,42 @@ enum LibSPNG {
145151

146152
/// Makes a PNG with an color table backend. This creates efficient PNG data representing pixel art or other images with few unique colors.
147153
@inlinable
148-
static func encodeIndexed(data: Data, width: Int, height: Int) throws(GateEngineError) -> Data {
154+
static func encodeIndexed(data: Data, width: Int, height: Int) throws(GateEngineError) -> Data? {
149155
do {
150-
return try data.withUnsafeBytes({ (bytes: UnsafeRawBufferPointer) throws -> Data in
156+
struct Color: Equatable, Hashable {
157+
let red: UInt8
158+
let green: UInt8
159+
let blue: UInt8
160+
let alpha: UInt8
161+
}
162+
var colors: [(Color)] = []
163+
colors.reserveCapacity(width * height)
164+
for index in stride(from: 0, to: data.count, by: 4) {
165+
colors.append(
166+
Color(
167+
red: data[index + 0],
168+
green: data[index + 1],
169+
blue: data[index + 2],
170+
alpha: data[index + 3]
171+
)
172+
)
173+
}
174+
175+
let colorTable = Array(Set(colors))
176+
guard colorTable.count <= 256 else {return nil}
177+
178+
let indexedData = colors.map({UInt8(colorTable.firstIndex(of: $0)!)})
179+
180+
var plte: spng_plte = .init()
181+
plte.n_entries = UInt32(colorTable.count)
182+
withUnsafeMutableBytes(of: &plte) { plte in
183+
let entires = colorTable.map({spng_plte_entry(red: $0.red, green: $0.green, blue: $0.blue, alpha: $0.alpha)})
184+
entires.withUnsafeBytes { entriesBytes in
185+
plte.baseAddress!.advanced(by: MemoryLayout<UInt32>.size).copyMemory(from: entriesBytes.baseAddress!, byteCount: entriesBytes.count)
186+
}
187+
}
188+
189+
return try indexedData.withUnsafeBytes({ (bytes: UnsafeRawBufferPointer) throws -> Data in
151190
/* Create a context */
152191
let ctx: OpaquePointer? = spng_ctx_new(Int32(SPNG_CTX_ENCODER.rawValue))
153192
defer {
@@ -156,7 +195,7 @@ enum LibSPNG {
156195
}
157196

158197
spng_set_option(ctx, SPNG_ENCODE_TO_BUFFER, 1)
159-
198+
160199
var ihdr = spng_ihdr(
161200
width: UInt32(width),
162201
height: UInt32(height),
@@ -166,9 +205,17 @@ enum LibSPNG {
166205
filter_method: UInt8(SPNG_FILTER_NONE.rawValue),
167206
interlace_method: UInt8(SPNG_INTERLACE_NONE.rawValue)
168207
)
169-
spng_set_ihdr(ctx, &ihdr)
208+
if spng_set_ihdr(ctx, &ihdr) != SPNG_OK.rawValue {
209+
throw GateEngineError.failedToEncode("spng_set_ihdr")
210+
}
170211

171-
spng_encode_image(ctx, bytes.baseAddress, data.count, Int32(SPNG_FMT_PNG.rawValue), Int32(SPNG_ENCODE_FINALIZE.rawValue))
212+
if spng_set_plte(ctx, &plte) != SPNG_OK.rawValue {
213+
throw GateEngineError.failedToEncode("spng_set_plte")
214+
}
215+
216+
if spng_encode_image(ctx, bytes.baseAddress!, colors.count, Int32(SPNG_FMT_PNG.rawValue), Int32(SPNG_ENCODE_FINALIZE.rawValue)) != SPNG_OK.rawValue {
217+
throw GateEngineError.failedToEncode("spng_encode_image")
218+
}
172219

173220
var length: Int = 0
174221
var error: Int32 = 0
@@ -178,7 +225,7 @@ enum LibSPNG {
178225
return data
179226
}
180227

181-
throw GateEngineError.failedToEncode(String(cString: spng_strerror(error)))
228+
throw GateEngineError.failedToEncode("spng_get_png_buffer \(String(cString: spng_strerror(error)))")
182229
})
183230
}catch let error as GateEngineError {
184231
throw error // Typed throws not supported by closures as of Swift 6.2

0 commit comments

Comments
 (0)