diff --git a/Benchmarks/Compression/Swift/Main.swift b/Benchmarks/Compression/Swift/Main.swift index cf141477..171e68be 100644 --- a/Benchmarks/Compression/Swift/Main.swift +++ b/Benchmarks/Compression/Swift/Main.swift @@ -55,7 +55,7 @@ extension Benchmark.Encode static func rgba8(level:Int, path:String, trials:Int) -> ([(time:Int, hash:Int)], Int) { - guard let image:PNG.Data.Rectangular = try? .decompress(path: path) + guard let image:PNG.Image = try? .decompress(path: path) else { fatalError("failed to decode test image '\(path)'") diff --git a/Benchmarks/Decompression/Swift/Main.swift b/Benchmarks/Decompression/Swift/Main.swift index b08701b7..ab1a8ff2 100644 --- a/Benchmarks/Decompression/Swift/Main.swift +++ b/Benchmarks/Decompression/Swift/Main.swift @@ -102,7 +102,7 @@ extension Benchmark.Decode { let start:Int = clock() - let image:PNG.Data.Rectangular = try .decompress(stream: &blob) + let image:PNG.Image = try .decompress(stream: &blob) let pixels:[PNG.RGBA] = image.unpack(as: PNG.RGBA.self) let stop:Int = clock() diff --git a/README.md b/README.md index ea3f13f0..f552d430 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ import PNG func decode(png path:String) throws { guard - let image:PNG.Data.Rectangular = try .decompress(path: path) + let image:PNG.Image = try .decompress(path: path) else { // failed to access file from file system @@ -66,7 +66,7 @@ Encode an image: ```swift func encode(png path:String, size:(x:Int, y:Int), pixels:[PNG.RGBA]) throws { - let image:PNG.Data.Rectangular = .init(packing: pixels, size: size, + let image:PNG.Image = .init(packing: pixels, size: size, layout: .init(format: .rgba8(palette: [], fill: nil))) try image.compress(path: path, level: 9) } diff --git a/Snippets/PNG/BasicDecoding.swift b/Snippets/PNG/BasicDecoding.swift index d04bb174..6c00c8fe 100644 --- a/Snippets/PNG/BasicDecoding.swift +++ b/Snippets/PNG/BasicDecoding.swift @@ -7,7 +7,7 @@ import PNG let path:String = "Sources/PNG/docs.docc/BasicDecoding/BasicDecoding" guard -let image:PNG.Data.Rectangular = try .decompress(path: "\(path).png") +let image:PNG.Image = try .decompress(path: "\(path).png") else { fatalError("failed to open file '\(path).png'") diff --git a/Snippets/PNG/BasicEncoding.swift b/Snippets/PNG/BasicEncoding.swift index e0768e21..0defc820 100644 --- a/Snippets/PNG/BasicEncoding.swift +++ b/Snippets/PNG/BasicEncoding.swift @@ -33,7 +33,7 @@ let layout:(rgb:PNG.Layout, v:PNG.Layout) = do { - let image:PNG.Data.Rectangular = .init(packing: rgba, size: size, layout: layout.rgb) + let image:PNG.Image = .init(packing: rgba, size: size, layout: layout.rgb) try image.compress(path: "\(path)-color-rgb.png", level: 9) for level:Int in [0, 4, 8, 13] @@ -43,7 +43,7 @@ do } do { - let image:PNG.Data.Rectangular = .init(packing: rgba, size: size, layout: layout.v) + let image:PNG.Image = .init(packing: rgba, size: size, layout: layout.v) try image.compress(path: "\(path)-color-v.png", level: 9) } @@ -57,11 +57,11 @@ let luminance:[UInt8] = rgba.map } do { - let image:PNG.Data.Rectangular = .init(packing: luminance, size: size, layout: layout.v) + let image:PNG.Image = .init(packing: luminance, size: size, layout: layout.v) try image.compress(path: "\(path)-luminance-v.png", level: 9) } do { - let image:PNG.Data.Rectangular = .init(packing: luminance, size: size, layout: layout.rgb) + let image:PNG.Image = .init(packing: luminance, size: size, layout: layout.rgb) try image.compress(path: "\(path)-luminance-rgb.png", level: 9) } diff --git a/Snippets/PNG/CustomColor.swift b/Snippets/PNG/CustomColor.swift index 4671a919..84f8e956 100644 --- a/Snippets/PNG/CustomColor.swift +++ b/Snippets/PNG/CustomColor.swift @@ -296,7 +296,7 @@ extension HSVA:PNG.Color } guard -let image:PNG.Data.Rectangular = try .decompress(path: "\(path).png") +let image:PNG.Image = try .decompress(path: "\(path).png") else { fatalError("failed to open file '\(path).png'") @@ -304,28 +304,28 @@ else let hsva:[HSVA] = image.unpack(as: HSVA.self) -let hue:PNG.Data.Rectangular = .init( +let hue:PNG.Image = .init( packing: hsva.map{ HSVA.init(h: $0.h, s: .max / 2, v: .max, a: $0.a) }, size: image.size, layout: image.layout, metadata: image.metadata) try hue.compress(path: "\(path)-hue.png") -let saturation:PNG.Data.Rectangular = .init( +let saturation:PNG.Image = .init( packing: hsva.map{ HSVA.init(h: 370000, s: $0.s, v: .max, a: $0.a) }, size: image.size, layout: image.layout, metadata: image.metadata) try saturation.compress(path: "\(path)-saturation.png") -let value:PNG.Data.Rectangular = .init( +let value:PNG.Image = .init( packing: hsva.map{ HSVA.init(h: 0, s: 0, v: $0.v, a: $0.a) }, size: image.size, layout: image.layout, metadata: image.metadata) try value.compress(path: "\(path)-value.png") -let new:PNG.Data.Rectangular = .init(packing: hsva, +let new:PNG.Image = .init(packing: hsva, size: image.size, layout: image.layout, metadata: image.metadata) diff --git a/Snippets/PNG/ImageMetadata.swift b/Snippets/PNG/ImageMetadata.swift index 74209819..de8e92ae 100644 --- a/Snippets/PNG/ImageMetadata.swift +++ b/Snippets/PNG/ImageMetadata.swift @@ -4,7 +4,7 @@ import PNG let path:String = "Sources/PNG/docs.docc/ImageMetadata/ImageMetadata" guard -var image:PNG.Data.Rectangular = try .decompress(path: "\(path).png") +var image:PNG.Image = try .decompress(path: "\(path).png") else { fatalError("failed to open file '\(path).png'") @@ -30,7 +30,7 @@ image.metadata.time = .init(year: 1992, month: 8, day: 3, hour: 0, minute: 0, se try image.compress(path: "\(path)-newtime.png") -if let image:PNG.Data.Rectangular = try .decompress(path: "\(path)-newtime.png"), +if let image:PNG.Image = try .decompress(path: "\(path)-newtime.png"), let time:PNG.TimeModified = image.metadata.time ?? nil { print(time) diff --git a/Snippets/PNG/ImagesInMemory.swift b/Snippets/PNG/ImagesInMemory.swift index d892c907..c292b755 100644 --- a/Snippets/PNG/ImagesInMemory.swift +++ b/Snippets/PNG/ImagesInMemory.swift @@ -64,7 +64,7 @@ else var blob:System.Blob = .init(data) // read from blob -let image:PNG.Data.Rectangular = try .decompress(stream: &blob) +let image:PNG.Image = try .decompress(stream: &blob) let rgba:[PNG.RGBA] = image.unpack(as: PNG.RGBA.self) guard let _:Void = (System.File.Destination.open(path: "\(path).png.rgba") diff --git a/Snippets/PNG/Indexing.swift b/Snippets/PNG/Indexing.swift index 72e9ecc7..2ddf703a 100644 --- a/Snippets/PNG/Indexing.swift +++ b/Snippets/PNG/Indexing.swift @@ -3,7 +3,7 @@ import PNG let path:String = "Sources/PNG/docs.docc/Indexing/Indexing" guard -let image:PNG.Data.Rectangular = try .decompress(path: "\(path).png") +let image:PNG.Image = try .decompress(path: "\(path).png") else { fatalError("failed to open file '\(path).png'") @@ -77,12 +77,12 @@ let swatch:[PNG.RGBA] = (0 ..< 16).flatMap return .init(r, g, b, a) } } -let visualization:PNG.Data.Rectangular = .init(packing: swatch, size: (256, 16), +let visualization:PNG.Image = .init(packing: swatch, size: (256, 16), layout: .init(format: .rgb8(palette: [], fill: nil, key: nil))) try visualization.compress(path: "\(path)-gradient.png") // encode colorized image -let indexed:PNG.Data.Rectangular = .init(packing: v, size: image.size, +let indexed:PNG.Image = .init(packing: v, size: image.size, layout: .init(format: .indexed8(palette: gradient, fill: nil)), metadata: image.metadata) { diff --git a/Snippets/PNG/OnlineDecoding.swift b/Snippets/PNG/OnlineDecoding.swift index 6809b5f7..406040d5 100644 --- a/Snippets/PNG/OnlineDecoding.swift +++ b/Snippets/PNG/OnlineDecoding.swift @@ -108,7 +108,7 @@ func waitChunk(stream:inout Stream) throws -> (type:PNG.Chunk, data:[UInt8]) func decodeOnline(stream:inout Stream, overdraw:Bool, - capture:(PNG.Data.Rectangular) throws -> ()) throws -> PNG.Data.Rectangular + capture:(PNG.Image) throws -> ()) throws -> PNG.Image { // lex PNG signature bytes try waitSignature(stream: &stream) @@ -212,9 +212,9 @@ func decodeOnline(stream:inout Stream, var stream:Stream = .init(path: "\(path).png") var counter:Int = 0 -let image:PNG.Data.Rectangular = try decodeOnline(stream: &stream, overdraw: false) +let image:PNG.Image = try decodeOnline(stream: &stream, overdraw: false) { - (snapshot:PNG.Data.Rectangular) in + (snapshot:PNG.Image) in let _:[PNG.RGBA] = snapshot.unpack(as: PNG.RGBA.self) @@ -223,15 +223,15 @@ let image:PNG.Data.Rectangular = try decodeOnline(stream: &stream, overdraw: fal } let layout:PNG.Layout = .init(format: image.layout.format, interlaced: true) -let progressive:PNG.Data.Rectangular = image.bindStorage(to: layout) +let progressive:PNG.Image = image.bindStorage(to: layout) try progressive.compress(path: "\(path)-progressive.png", hint: 1 << 12) stream = .init(path: "\(path)-progressive.png") counter = 0 -let _:PNG.Data.Rectangular = try decodeOnline(stream: &stream, overdraw: false) +let _:PNG.Image = try decodeOnline(stream: &stream, overdraw: false) { - (snapshot:PNG.Data.Rectangular) in + (snapshot:PNG.Image) in try snapshot.compress(path: "\(path)-progressive-\(counter).png") counter += 1 @@ -240,9 +240,9 @@ let _:PNG.Data.Rectangular = try decodeOnline(stream: &stream, overdraw: false) stream.reset(position: 0) counter = 0 -let _:PNG.Data.Rectangular = try decodeOnline(stream: &stream, overdraw: true) +let _:PNG.Image = try decodeOnline(stream: &stream, overdraw: true) { - (snapshot:PNG.Data.Rectangular) in + (snapshot:PNG.Image) in try snapshot.compress(path: "\(path)-progressive-overdrawn-\(counter).png") counter += 1 diff --git a/Snippets/PNG/iPhoneOptimized.swift b/Snippets/PNG/iPhoneOptimized.swift index a6c9c269..db19ecbe 100644 --- a/Snippets/PNG/iPhoneOptimized.swift +++ b/Snippets/PNG/iPhoneOptimized.swift @@ -3,7 +3,7 @@ import PNG let path:String = "Sources/PNG/docs.docc/iPhoneOptimized/iPhoneOptimized" guard -var image:PNG.Data.Rectangular = try .decompress(path: "\(path).png") +var image:PNG.Image = try .decompress(path: "\(path).png") else { fatalError("failed to open file '\(path).png'") @@ -15,14 +15,14 @@ let rgba:[PNG.RGBA] = image.unpack(as: PNG.RGBA.self).map(\.straig print(image.storage[..<16]) -let standard:PNG.Data.Rectangular = .init( +let standard:PNG.Image = .init( packing: rgba, size: image.size, layout: .init(format: .rgb8(palette: [], fill: nil, key: nil))) try standard.compress(path: "\(path)-rgb8.png") -let apple:PNG.Data.Rectangular = .init( +let apple:PNG.Image = .init( packing: standard.unpack(as: PNG.RGBA.self).map(\.premultiplied), size: standard.size, layout: .init(format: .bgr8(palette: [], fill: nil, key: nil))) diff --git a/Snippets/README.md b/Snippets/README.md index 70fc9b99..6e204ef6 100644 --- a/Snippets/README.md +++ b/Snippets/README.md @@ -117,7 +117,7 @@ The non-grayscale color formats include a `palette` field. Setting it to the emp To create a rectangular image data instance, use the [`init(packing:size:layout:metadata:)`](https://tayloraswift.github.io/swift-png/PNG/Data/Rectangular/init(packing:size:layout:metadata:)/) initializer. This initializer is the inverse of the [`unpack(as:)`](https://tayloraswift.github.io/swift-png/PNG/Data/Rectangular/unpack(as:)/) method we used in the [basic decoding](#basic-decoding) tutorial. Needless to say, the length of the pixel array must equal `size.x * size.y`. The `metadata` argument has a default value, which is an empty metadata record. ```swift -let image:PNG.Data.Rectangular = .init(packing: rgba, size: size, layout: layout.rgb) +let image:PNG.Image = .init(packing: rgba, size: size, layout: layout.rgb) ``` On platforms with built-in file system support, we can compress it to a file using the [`compress(path:level:hint:)`](https://tayloraswift.github.io/swift-png/PNG/Data/Rectangular/compress(path:level:hint:)/) method. The `hint` argument provides a size hint for the emitted image data chunks. Its default value is `32768`, which is fine for almost all use cases. We will explore the `hint` parameter in more detail in the [online decoding](#online-decoding) tutorial. @@ -158,7 +158,7 @@ If we inspect the emitted PNG files, we can verify that the higher compression s We can also encode the same pixel data using the grayscale layout we defined earlier. ```swift -let image:PNG.Data.Rectangular = .init(packing: rgba, size: size, layout: layout.v) +let image:PNG.Image = .init(packing: rgba, size: size, layout: layout.v) try image.compress(path: "\(path)-color-v.png", level: 9) ``` @@ -184,7 +184,7 @@ let luminance:[UInt8] = rgba.map We can encode it to a file just as we did with the array of [`PNG.RGBA`](https://tayloraswift.github.io/swift-png/PNG/RGBA/) colors: ```swift -let image:PNG.Data.Rectangular = .init(packing: luminance, size: size, layout: layout.v) +let image:PNG.Image = .init(packing: luminance, size: size, layout: layout.v) try image.compress(path: "\(path)-luminance-v.png", level: 9) ``` @@ -197,7 +197,7 @@ Observe that it looks different from the previous output, since we used informat We could also have encoded it using an RGB color format, which produces a visually identical image. ```swift -let image:PNG.Data.Rectangular = .init(packing: luminance, size: size, layout: layout.rgb) +let image:PNG.Image = .init(packing: luminance, size: size, layout: layout.rgb) try image.compress(path: "\(path)-luminance-rgb.png", level: 9) ``` @@ -238,7 +238,7 @@ import PNG let path:String = "examples/indexing/example" -guard let image:PNG.Data.Rectangular = try .decompress(path: "\(path).png") +guard let image:PNG.Image = try .decompress(path: "\(path).png") else { fatalError("failed to open file '\(path).png'") @@ -592,7 +592,7 @@ let swatch:[PNG.RGBA] = (0 ..< 16).flatMap return .init(r, g, b, a) } } -let visualization:PNG.Data.Rectangular = .init(packing: swatch, size: (256, 16), +let visualization:PNG.Image = .init(packing: swatch, size: (256, 16), layout: .init(format: .rgb8(palette: [], fill: nil, key: nil))) try visualization.compress(path: "examples/indexing/gradient-visualization.png") ``` @@ -604,7 +604,7 @@ try visualization.compress(path: "examples/indexing/gradient-visualization.png") We can create an indexed image by defining an indexed layout, and passing the grayscale samples we obtained earlier to one of the pixel-packing APIs. The [`init(packing:size:layout:metadata:)`](https://tayloraswift.github.io/swift-png/PNG/Data/Rectangular/1-init(packing:size:layout:metadata:)/) initializer will treat the grayscale samples as pixel colors, not indices, and will try to match the pixel colors to entries in the given palette. This is not what we want, so we need to use a variant of that function, [`init(packing:size:layout:metadata:indexer:)`](https://tayloraswift.github.io/swift-png/PNG/Data/Rectangular/1-init(packing:size:layout:metadata:indexer:)/), and pass it a custom **indexing function**. ```swift -let indexed:PNG.Data.Rectangular = .init(packing: v, size: image.size, +let indexed:PNG.Image = .init(packing: v, size: image.size, layout: .init(format: .indexed8(palette: gradient, fill: nil)), metadata: image.metadata) { @@ -726,7 +726,7 @@ import PNG let path:String = "examples/iphone-optimized/example" -guard var image:PNG.Data.Rectangular = try .decompress(path: "\(path).png") +guard var image:PNG.Image = try .decompress(path: "\(path).png") else { fatalError("failed to open file '\(path).png'") @@ -755,7 +755,7 @@ It is often convenient to work in the premultiplied color space, so the library > **note:** unpacking bgra images to a scalar target discards the alpha channel, making it impossible to straighten the grayscale pixels. if you trying to unpack grayscale values from an iphone-optimized image with transparency, unpack it to the [`PNG.VA`](https://tayloraswift.github.io/swift-png/PNG/VA/) color target, and take the gray channel *after* straightening the grayscale-alpha pixels. -Depending on your use case, you may not be getting the most out of iphone-optimized images by unpacking them to a color target. As mentioned previously, the iphone-optimized format is designed such that the raw, packed image data can be uploaded directly to the graphics hardware. We can access the packed data buffer through the [`storage`](https://tayloraswift.github.io/swift-png/PNG/Data/Rectangular/storage) property on [`PNG.Data.Rectangular`](https://tayloraswift.github.io/swift-png/PNG/Data/Rectangular/). +Depending on your use case, you may not be getting the most out of iphone-optimized images by unpacking them to a color target. As mentioned previously, the iphone-optimized format is designed such that the raw, packed image data can be uploaded directly to the graphics hardware. We can access the packed data buffer through the [`storage`](https://tayloraswift.github.io/swift-png/PNG/Data/Rectangular/storage) property on [`PNG.Image`](https://tayloraswift.github.io/swift-png/PNG/Data/Rectangular/). ```swift print(image.storage[..<16]) @@ -768,7 +768,7 @@ print(image.storage[..<16]) We can convert the iphone-optimized example image to a standard PNG file by re-encoding it as any of the standard color formats. ```swift -let standard:PNG.Data.Rectangular = .init( +let standard:PNG.Image = .init( packing: rgba, size: image.size, layout: .init(format: .rgb8(palette: [], fill: nil, key: nil))) @@ -783,7 +783,7 @@ try standard.compress(path: "\(path)-rgb8.png") We can convert it back into an iphone-optimized image by specifying one of the iphone-optimized color formats. The [`premultiplied`](https://tayloraswift.github.io/swift-png/PNG/RGBA/premultiplied/) property on the [`PNG.RGBA`](https://tayloraswift.github.io/swift-png/PNG/RGBA/) color target converts the pixels to the premultiplied color space. Again, this step is unnecessary if you know the image contains no transparency. ```swift -let apple:PNG.Data.Rectangular = .init( +let apple:PNG.Image = .init( packing: standard.unpack(as: PNG.RGBA.self).map(\.premultiplied), size: standard.size, layout: .init(format: .bgr8(palette: [], fill: nil, key: nil))) @@ -822,7 +822,7 @@ import PNG let path:String = "examples/metadata/example" -guard var image:PNG.Data.Rectangular = try .decompress(path: "\(path).png") +guard var image:PNG.Image = try .decompress(path: "\(path).png") else { fatalError("failed to open file '\(path).png'") @@ -905,7 +905,7 @@ We can save it and read it back to show that the new image now has a different v try image.compress(path: "\(path)-newtime.png") if let time:PNG.TimeModified = - (try PNG.Data.Rectangular.decompress(path: "\(path)-newtime.png")).map(\.metadata.time) ?? nil + (try PNG.Image.decompress(path: "\(path)-newtime.png")).map(\.metadata.time) ?? nil { print(time) } @@ -1026,7 +1026,7 @@ var blob:System.Blob = .init(data) To decode from our `System.Blob` type, we use the [`decompress(stream:)`](https://tayloraswift.github.io/swift-png/PNG/Data/Rectangular/decompress(stream:)/) function, which is part of the core library, and does essentially the same thing as the file system-aware [`decompress(path:)`](https://tayloraswift.github.io/swift-png/PNG/Data/Rectangular/decompress(path:)/) function. We can then unpack pixels from the returned image data structure as we would in any other situation. ```swift -let image:PNG.Data.Rectangular = try .decompress(stream: &blob) +let image:PNG.Image = try .decompress(stream: &blob) let rgba:[PNG.RGBA] = image.unpack(as: PNG.RGBA.self) ``` @@ -1287,8 +1287,8 @@ To do online decoding, we are going to write a function `decodeOnline(stream:ove ```swift func decodeOnline(stream:inout Stream, overdraw:Bool, - capture:(PNG.Data.Rectangular) throws -> ()) throws - -> PNG.Data.Rectangular + capture:(PNG.Image) throws -> ()) throws + -> PNG.Image ``` The `capture` parameter is a delegate that will receive a partially-decoded image each time an image data chunk has been decompressed. The `overdraw` parameter is a switch which we will use later in this tutorial. @@ -1434,13 +1434,13 @@ In the last phase of the loop, we process all the trailing metadata chunks by pa > **note:** you can pass an [`IEND`](https://tayloraswift.github.io/swift-png/PNG/Chunk/IEND) chunk to the [`push(ancillary:)`](https://tayloraswift.github.io/swift-png/PNG/Context/push(ancillary:)/) method, as we have done above, even though [`IEND`](https://tayloraswift.github.io/swift-png/PNG/Chunk/IEND) is a critical chunk type. this makes the decoder context check that the compressed image data stream has been properly terminated. -We can invoke `decodeOnline(stream:overdraw:capture:)` on the stream structure we created earlier, saving each partially-decoded image snapshot to a separate PNG file. The pixel-unpacking call in the middle of the delegate function doesn’t do anything; it’s just there to demonstrate that the snapshots are normal [`PNG.Data.Rectangular`](https://tayloraswift.github.io/swift-png/PNG/Data/Rectangular) instances that we can treat like any other image data instance. +We can invoke `decodeOnline(stream:overdraw:capture:)` on the stream structure we created earlier, saving each partially-decoded image snapshot to a separate PNG file. The pixel-unpacking call in the middle of the delegate function doesn’t do anything; it’s just there to demonstrate that the snapshots are normal [`PNG.Image`](https://tayloraswift.github.io/swift-png/PNG/Data/Rectangular) instances that we can treat like any other image data instance. ```swift var counter:Int = 0 -let image:PNG.Data.Rectangular = try decodeOnline(stream: &stream, overdraw: false) +let image:PNG.Image = try decodeOnline(stream: &stream, overdraw: false) { - (snapshot:PNG.Data.Rectangular) in + (snapshot:PNG.Image) in let _:[PNG.RGBA] = snapshot.unpack(as: PNG.RGBA.self) @@ -1471,7 +1471,7 @@ When emitting the preprocessed file, we have manually set the **chunk granularit ```swift let layout:PNG.Layout = .init(format: image.layout.format, interlaced: true) -let progressive:PNG.Data.Rectangular = image.bindStorage(to: layout) +let progressive:PNG.Image = image.bindStorage(to: layout) try progressive.compress(path: "\(path)-progressive.png", hint: 1 << 12) ``` @@ -1481,9 +1481,9 @@ We can invoke `decodeOnline(stream:overdraw:capture:)` on the interlaced image a ```swift stream = .init(path: "\(path)-progressive.png") counter = 0 -let _:PNG.Data.Rectangular = try decodeOnline(stream: &stream, overdraw: false) +let _:PNG.Image = try decodeOnline(stream: &stream, overdraw: false) { - (snapshot:PNG.Data.Rectangular) in + (snapshot:PNG.Image) in try snapshot.compress(path: "\(path)-progressive-\(counter).png") counter += 1 @@ -1511,9 +1511,9 @@ If we enable **overdrawing**, *Swift PNG* will pre-fill missing pixels with valu stream.reset(position: 0) counter = 0 -let _:PNG.Data.Rectangular = try decodeOnline(stream: &stream, overdraw: true) +let _:PNG.Image = try decodeOnline(stream: &stream, overdraw: true) { - (snapshot:PNG.Data.Rectangular) in + (snapshot:PNG.Image) in try snapshot.compress(path: "\(path)-progressive-overdrawn-\(counter).png") counter += 1 @@ -2085,7 +2085,7 @@ Now, we can put our custom `HSVA` color target to work. ```swift let path:String = "examples/custom-color/example" -guard let image:PNG.Data.Rectangular = try .decompress(path: "\(path).png") +guard let image:PNG.Image = try .decompress(path: "\(path).png") else { fatalError("failed to open file '\(path).png'") @@ -2097,7 +2097,7 @@ let hsva:[HSVA] = image.unpack(as: HSVA.self) We can visualize the hue, saturation, and value channels as follows: ```swift -let hue:PNG.Data.Rectangular = .init( +let hue:PNG.Image = .init( packing: hsva.map{ HSVA.init(h: $0.h, s: .max / 2, v: .max, a: $0.a) }, size: image.size, layout: image.layout, metadata: image.metadata) try hue.compress(path: "\(path)-hue.png") @@ -2108,7 +2108,7 @@ try hue.compress(path: "\(path)-hue.png") > a visualization of the example image hue. ```swift -let saturation:PNG.Data.Rectangular = .init( +let saturation:PNG.Image = .init( packing: hsva.map{ HSVA.init(h: 370000, s: $0.s, v: .max, a: $0.a) }, size: image.size, layout: image.layout, metadata: image.metadata) try saturation.compress(path: "\(path)-saturation.png") @@ -2119,7 +2119,7 @@ try saturation.compress(path: "\(path)-saturation.png") > a visualization of the example image saturation. ```swift -let value:PNG.Data.Rectangular = .init( +let value:PNG.Image = .init( packing: hsva.map{ HSVA.init(h: 0, s: 0, v: $0.v, a: $0.a) }, size: image.size, layout: image.layout, metadata: image.metadata) try value.compress(path: "\(path)-value.png") @@ -2132,7 +2132,7 @@ try value.compress(path: "\(path)-value.png") We can test our pixel packing implementation by re-encoding the HSVA image. ```swift -let new:PNG.Data.Rectangular = .init(packing: hsva, +let new:PNG.Image = .init(packing: hsva, size: image.size, layout: image.layout, metadata: image.metadata) try new.compress(path: "\(path).png.png") ``` diff --git a/Sources/PNG/ColorTargets/PNG (ext).swift b/Sources/PNG/ColorTargets/PNG (ext).swift deleted file mode 100644 index 0249a100..00000000 --- a/Sources/PNG/ColorTargets/PNG (ext).swift +++ /dev/null @@ -1,93 +0,0 @@ -extension PNG -{ - /// Premultiplies a color component with an alpha value. - /// - /// - Parameters: - /// - color: - /// The color component to premultiply. - /// - alpha: - /// The alpha component to premultiply `color` with. - /// - /// - Returns: - /// The premultiplied color component, rounded to the nearest integer. - /// - /// The `color` and `alpha` parameters are interpreted as rational numbers in the range - /// [0, 1], where `T.min` maps to 0, and `T.max` maps to 1. - /// - /// This function uses no floating point operations, and satisfies the property that - /// ``premultiply(_:alpha:)`` is equivalent to - /// - /// ``premultiply(_:alpha:)`` ∘ ``straighten(_:alpha:)`` ∘ ``premultiply(_:alpha:)`` - /// - /// The computed properties ``RGBA.premultiplied`` and ``VA.premultiplied`` can be used to - /// premultiply an entire instance of one of the built-in color targets. - /// - /// Premultiplication is a destructive operation. In the most extreme case, if `alpha` is - /// `T.min`, this function will return `T.min` for any value of `color`. - @inlinable - public static - func premultiply(_ color:T, alpha:T) -> T - where T:FixedWidthInteger & UnsignedInteger - { - // this generates pretty good assembly, though Swift/LLVM doesn’t - // seem to know it can perform the full width arithmetic in one register - // for T.bitWidth <= 32 - let product:(high:T, low:T.Magnitude) = color.multipliedFullWidth(by: alpha) - let biased:(high:T, low:T.Magnitude), - carried:Bool - (biased.low, carried) = product.low.addingReportingOverflow(.max >> 1) - biased.high = product.high &+ (carried ? 1 : 0) - return T.max.dividingFullWidth(biased).quotient - } - /// Straightens a premultiplied color component given an alpha value. - /// - /// - Parameters: - /// - premultiplied: - /// The premultiplied color component to straighten. - /// - alpha: - /// The alpha component that `premultiplied` was premultiplied by. - /// - /// - Returns: - /// The straightened color component, rounded to the nearest integer. - /// If `alpha` is `T.min`, this function returns the original - /// `premultiplied` argument. - /// - /// The `color` and `alpha` parameters are interpreted as rational numbers - /// in the range [0, 1], where `T.min` maps to 0, - /// and `T.max` maps to 1. - /// - /// This function uses no floating point operations, and satisfies the - /// property that ``premultiply(_:alpha:)`` - /// - /// is equivalent to - /// - /// ``premultiply(_:alpha:)`` ∘ ``straighten(_:alpha:)`` ∘ ``premultiply(_:alpha:)`` - /// - /// The computed properties ``RGBA.straightened`` and ``VA.straightened`` - /// can be used to straighten an entire instance of one of the built-in - /// color targets. - /// - /// Premultiplication is a destructive operation. This function cannot - /// recover the original color unless `alpha` is `T.max`, in which case - /// this function performs a division by 1, and returns the original - /// `premultiplied` argument. - @inlinable - public static - func straighten(_ premultiplied:T, alpha:T) -> T - where T:FixedWidthInteger & UnsignedInteger - { - guard alpha > 0 - else - { - return premultiplied - } - - let biased:(high:T, low:T.Magnitude) = - T.max.multipliedFullWidth(by: premultiplied) - let product:(high:T, low:T.Magnitude), - carried:Bool - (product.low, carried) = biased.low.addingReportingOverflow(alpha.magnitude >> 1) - product.high = biased.high &+ (carried ? 1 : 0) - return alpha.dividingFullWidth(product).quotient - } -} diff --git a/Sources/PNG/ColorTargets/PNG.Data.Rectangular (ext).swift b/Sources/PNG/ColorTargets/PNG.Data.Rectangular (ext).swift deleted file mode 100644 index 2051fa2e..00000000 --- a/Sources/PNG/ColorTargets/PNG.Data.Rectangular (ext).swift +++ /dev/null @@ -1,476 +0,0 @@ -extension PNG.Data.Rectangular -{ - // factoring out the specialized entry point reduces module overhead, because - // the emitted entry point can make use of the specialized function bodies - @usableFromInline - @_specialize(where T == UInt8) - @_specialize(where T == UInt16) - @_specialize(where T == UInt32) - @_specialize(where T == UInt64) - @_specialize(where T == UInt) - static - func unpack(_ interleaved:[UInt8], of format:PNG.Format, as _:T.Type, - deindexer:([(r:UInt8, g:UInt8, b:UInt8, a:UInt8)]) -> (Int) -> UInt8) - -> [T] - where T:FixedWidthInteger & UnsignedInteger - { - let depth:Int = format.pixel.depth - switch format - { - case .indexed1(palette: let palette, fill: _), - .indexed2(palette: let palette, fill: _), - .indexed4(palette: let palette, fill: _), - .indexed8(palette: let palette, fill: _): - return PNG.convolve(interleaved, dereference: deindexer(palette)) - { - (c) in c - } - - case .v1(fill: _, key: _), - .v2(fill: _, key: _), - .v4(fill: _, key: _), - .v8(fill: _, key: _): - return PNG.convolve(interleaved, of: UInt8.self, depth: depth) - { - (c:T, _) in c - } - case .v16(fill: _, key: _): - return PNG.convolve(interleaved, of: UInt16.self, depth: depth) - { - (c:T, _) in c - } - - case .va8(fill: _): - return PNG.convolve(interleaved, of: UInt8.self, depth: depth) - { - (c:(T, T)) in c.0 - } - case .va16(fill: _): - return PNG.convolve(interleaved, of: UInt16.self, depth: depth) - { - (c:(T, T)) in c.0 - } - - case .bgr8(palette: _, fill: _, key: _): - return PNG.convolve(interleaved, of: UInt8.self, depth: depth) - { - (c:(T, T, T), _) in c.2 - } - - case .rgb8(palette: _, fill: _, key: _): - return PNG.convolve(interleaved, of: UInt8.self, depth: depth) - { - (c:(T, T, T), _) in c.0 - } - case .rgb16(palette: _, fill: _, key: _): - return PNG.convolve(interleaved, of: UInt16.self, depth: depth) - { - (c:(T, T, T), _) in c.0 - } - - case .bgra8(palette: _, fill: _): - return PNG.convolve(interleaved, of: UInt8.self, depth: depth) - { - (c:(T, T, T, T)) in c.2 - } - - case .rgba8(palette: _, fill: _): - return PNG.convolve(interleaved, of: UInt8.self, depth: depth) - { - (c:(T, T, T, T)) in c.0 - } - case .rgba16(palette: _, fill: _): - return PNG.convolve(interleaved, of: UInt16.self, depth: depth) - { - (c:(T, T, T, T)) in c.0 - } - } - } - - @usableFromInline - @_specialize(where T == UInt8) - @_specialize(where T == UInt16) - @_specialize(where T == UInt32) - @_specialize(where T == UInt64) - @_specialize(where T == UInt) - static - func pack(_ pixels:[T], as format:PNG.Format, - indexer:([(r:UInt8, g:UInt8, b:UInt8, a:UInt8)]) -> (UInt8) -> Int) - -> [UInt8] - where T:FixedWidthInteger & UnsignedInteger - { - let depth:Int = format.pixel.depth - switch format - { - case .indexed1(palette: let palette, fill: _), - .indexed2(palette: let palette, fill: _), - .indexed4(palette: let palette, fill: _), - .indexed8(palette: let palette, fill: _): - return PNG.deconvolve(pixels, reference: indexer(palette)) - { - (v) in v - } - - case .v1(fill: _, key: _), - .v2(fill: _, key: _), - .v4(fill: _, key: _), - .v8(fill: _, key: _): - return PNG.deconvolve(pixels, as: UInt8.self, depth: depth) - { - (v) in v - } - case .v16(fill: _, key: _): - return PNG.deconvolve(pixels, as: UInt16.self, depth: depth) - { - (v) in v - } - - case .va8(fill: _): - return PNG.deconvolve(pixels, as: UInt8.self, depth: depth) - { - (v) in (v, .max) - } - case .va16(fill: _): - return PNG.deconvolve(pixels, as: UInt16.self, depth: depth) - { - (v) in (v, .max) - } - - case .bgr8(palette: _, fill: _, key: _), - .rgb8(palette: _, fill: _, key: _): - return PNG.deconvolve(pixels, as: UInt8.self, depth: depth) - { - (v) in (v, v, v) - } - case .rgb16(palette: _, fill: _, key: _): - return PNG.deconvolve(pixels, as: UInt16.self, depth: depth) - { - (v) in (v, v, v) - } - - case .bgra8(palette: _, fill: _), - .rgba8(palette: _, fill: _): - return PNG.deconvolve(pixels, as: UInt8.self, depth: depth) - { - (v) in (v, v, v, .max) - } - case .rgba16(palette: _, fill: _): - return PNG.deconvolve(pixels, as: UInt16.self, depth: depth) - { - (v) in (v, v, v, .max) - } - } - } -} -// custom-indexer APIs -extension PNG.Data.Rectangular -{ - /// Unpacks this image to a pixel array, using a custom deindexing function. - /// - /// - Parameter _: - /// A color target type. This type provides the ``Color/unpack(_:of:deindexer:)`` - /// implementation used to unpack the image data. - /// - Parameter deindexer: - /// A function which uses the palette entries in the color ``Layout/format`` to - /// generate a dereferencing function. This function is only expected to - /// be invoked if `layout.format` is an indexed format. - /// - /// See the [indexed color tutorial](https://github.com/tayloraswift/swift-png/tree/master/examples#using-indexed-images) - /// for more about the semantics of this function. - /// - /// - Returns: - /// A pixel array. Its elements are arranged in row-major order. The - /// first pixel in this array corresponds to the top-left corner of - /// the image. Its length is equal to `size.x` multiplied by `size.y`. - @inlinable - public - func unpack(as _:Color.Type, - deindexer:([(r:UInt8, g:UInt8, b:UInt8, a:UInt8)]) -> (Int) -> Color.Aggregate) - -> [Color] - where Color:PNG.Color - { - Color.unpack(self.storage, of: self.layout.format, deindexer: deindexer) - } - /// Unpacks this image to a scalar pixel array, using a custom deindexing function. - /// - /// For an image with a grayscale-alpha color ``Layout/format``, - /// this function selects the *v* component from pixels of the form (*v*, *a*) - /// - /// For an image with an RGB color ``Layout/format``, - /// this function selects the *r* component from pixels of the form (*r*, *g*, *b*). - /// - /// For an image with an RGBA color ``Layout/format``, this function selects the *r* component from - /// pixels of the form (*r*, *g*, *b*, *a*). - /// - /// For an image with a BGR color ``Layout/format``, - /// this function selects the *r* component from pixels of the form (*b*, *g*, *r*). - /// - /// For an image with a BGRA color ``Layout/format``, - /// this function selects the *r* component from pixels of the form (*b*, *g*, *r*, *a*). - /// - /// This function ignores chroma keys, as its scalar color target is not - /// capable of representing transparency. The unpacked components - /// are scaled to fill the range of `T`, according to the color depth - /// computed from the color ``Layout/format``. - /// - /// - Parameter _: - /// A scalar color target type. - /// - Parameter deindexer: - /// A function which uses the palette entries in the color ``Layout/format`` to - /// generate a dereferencing function. This function will only - /// be invoked if `layout.format` is an indexed format. - /// - /// See the [indexed color tutorial](https://github.com/tayloraswift/swift-png/tree/master/examples#using-indexed-images) - /// for more about the semantics of this function. - /// - Returns: - /// A scalar pixel array. Its elements are arranged in row-major order. The - /// first pixel in this array corresponds to the top-left corner of - /// the image. Its length is equal to `size.x` multiplied by `size.y`. - @inlinable - public - func unpack(as _:T.Type, - deindexer:([(r:UInt8, g:UInt8, b:UInt8, a:UInt8)]) -> (Int) -> UInt8) - -> [T] - where T:FixedWidthInteger & UnsignedInteger - { - Self.unpack(self.storage, of: self.layout.format, as: T.self, deindexer: deindexer) - } - /// Creates an image from a pixel array, using a custom indexing function. - /// - /// - Parameter pixels: - /// A pixel array. Its elements are arranged in row-major order. The - /// first pixel in this array corresponds to the top-left corner of - /// the image. The `Color` type provides the ``Color/pack(_:as:indexer:)`` - /// implementation used to pack the image data. - /// - /// The length of this array must match `size.x * size.y`. Passing an - /// array of the wrong length will result in a precondition failure. - /// - Parameter size: - /// The size of the image. Both dimensions must be greater than zero. - /// Passing an invalid image size will result in a precondition failure. - /// - Parameter layout: - /// An image layout. - /// - Parameter metadata: - /// A metadata structure. The default value is an empty metadata structure. - /// - Parameter indexer: - /// A function which uses the palette entries in the color ``Layout/format`` to - /// generate a referencing function. This function is only expected to - /// be invoked if the image color ``Layout/format`` is an indexed format. - /// - /// See the [indexed color tutorial](https://github.com/tayloraswift/swift-png/tree/master/examples#using-indexed-images) - /// for more about the semantics of this function. - @inlinable - public - init(packing pixels:[Color], - size:(x:Int, y:Int), layout:PNG.Layout, metadata:PNG.Metadata = .init(), - indexer:([(r:UInt8, g:UInt8, b:UInt8, a:UInt8)]) -> (Color.Aggregate) -> Int) - where Color:PNG.Color - { - precondition(size.x > 0 && size.y > 0, - "image dimensions must be greater than zero") - precondition(pixels.count == size.x * size.y, - "pixel array `count` must be equal to `size.x * size.y`") - self.init(size: size, layout: layout, metadata: metadata, - storage: Color.pack(pixels, as: layout.format, indexer: indexer)) - } - /// Creates an image from a scalar pixel array, using a custom indexing function. - /// - /// For an image with a grayscale-alpha color ``Layout/format``, - /// this function assigns the gray channel to the given scalars, and - /// sets the alpha channel to `T.max`. - /// - /// For an image with an RGB or BGR color ``Layout/format``, - /// this function assigns all channels to the given scalars, replicating - /// each scalar three times. - /// - /// For an image with an RGBA or BGRA color ``Layout/format``, this - /// function assigns all opaque channels to the given scalars, replicating - /// each scalar three times, and sets the alpha channel to `T.max`. - /// - /// The scalar values are assumed to fill the entire range of `T`. - /// - Parameter pixels: - /// A scalar pixel array. Its elements are arranged in row-major order. The - /// first pixel in this array corresponds to the top-left corner of - /// the image. - /// - /// The length of this array must match `size.x * size.y`. Passing an - /// array of the wrong length will result in a precondition failure. - /// - Parameter size: - /// The size of the image. Both dimensions must be greater than zero. - /// Passing an invalid image size will result in a precondition failure. - /// - Parameter layout: - /// An image layout. - /// - Parameter metadata: - /// A metadata structure. The default value is an empty metadata structure. - /// - Parameter indexer: - /// A function which uses the palette entries in the color ``Layout/format`` to - /// generate a referencing function. This function will only - /// be invoked if the image color ``Layout/format`` is an indexed format. - /// - /// See the [indexed color tutorial](https://github.com/tayloraswift/swift-png/tree/master/examples#using-indexed-images) - /// for more about the semantics of this function. - @inlinable - public - init(packing pixels:[T], - size:(x:Int, y:Int), layout:PNG.Layout, metadata:PNG.Metadata = .init(), - indexer:([(r:UInt8, g:UInt8, b:UInt8, a:UInt8)]) -> (UInt8) -> Int) - where T:FixedWidthInteger & UnsignedInteger - { - precondition(size.x > 0 && size.y > 0, - "image dimensions must be greater than zero") - precondition(pixels.count == size.x * size.y, - "pixel array `count` must be equal to `size.x * size.y`") - self.init(size: size, layout: layout, metadata: metadata, - storage: Self.pack(pixels, as: layout.format, indexer: indexer)) - } -} -extension PNG.Data.Rectangular -{ - /// Unpacks this image to a pixel array. - /// - /// - Parameter _: - /// A color target type. This type provides the ``Color/unpack(_:of:)`` - /// implementation used to unpack the image data. - /// - Returns: - /// A pixel array. Its elements are arranged in row-major order. The - /// first pixel in this array corresponds to the top-left corner of - /// the image. Its length is equal to `size.x` multiplied by `size.y`. - @inlinable - public - func unpack(as _:Color.Type) -> [Color] where Color:PNG.Color - { - Color.unpack(self.storage, of: self.layout.format) - } - /// Creates an image from a pixel array. - /// - /// - Parameter pixels: - /// A pixel array. Its elements are arranged in row-major order. The - /// first pixel in this array corresponds to the top-left corner of - /// the image. The `Color` type provides the ``Color/pack(_:as:)`` - /// implementation used to pack the image data. - /// - /// The length of this array must match `size.x * size.y`. Passing an - /// array of the wrong length will result in a precondition failure. - /// - Parameter size: - /// The size of the image. Both dimensions must be greater than zero. - /// Passing an invalid image size will result in a precondition failure. - /// - Parameter layout: - /// An image layout. - /// - Parameter metadata: - /// A metadata structure. The default value is an empty metadata structure. - @inlinable - public - init(packing pixels:[Color], - size:(x:Int, y:Int), layout:PNG.Layout, metadata:PNG.Metadata = .init()) - where Color:PNG.Color - { - precondition(size.x > 0 && size.y > 0, - "image dimensions must be greater than zero") - precondition(pixels.count == size.x * size.y, - "pixel array `count` must be equal to `size.x * size.y`") - self.init(size: size, layout: layout, metadata: metadata, - storage: Color.pack(pixels, as: layout.format)) - } - /// Unpacks this image to a scalar pixel array. - /// - /// For an image with a grayscale-alpha color ``Layout/format``, - /// this function selects the *v* component from pixels of the form (*v*, *a*) - /// - /// For an image with an RGB color ``Layout/format``, - /// this function selects the *r* component from pixels of the form (*r*, *g*, *b*). - /// - /// For an image with an indexed color ``Layout/format``, - /// this function selects the *r* component from palette entries of the - /// form (*r*, *g*, *b*, *a*). The palette entry is chosen by taking the - /// i*th element in the palette, from pixels of the form (*i*). - /// - /// For an image with an RGBA color ``Layout/format``, this function - /// selects the *r* component from pixels of the form (*r*, *g*, *b*, *a*). - /// - /// For an image with a BGR color ``Layout/format``, - /// this function selects the *r* component from pixels of the form (*b*, *g*, *r*). - /// - /// For an image with a BGRA color ``Layout/format``, - /// this function selects the *r* component from pixels of the form (*b*, *g*, *r*, *a*). - /// - /// This function ignores chroma keys, as its scalar color target is not - /// capable of representing transparency. The unpacked components - /// are scaled to fill the range of `T`, according to the color depth - /// computed from the color ``Layout/format``. - /// - /// - Parameter _: - /// A scalar color target type. - /// - Returns: - /// A scalar pixel array. Its elements are arranged in row-major order. The - /// first pixel in this array corresponds to the top-left corner of - /// the image. Its length is equal to `size.x` multiplied by `size.y`. - @inlinable - public - func unpack(as _:T.Type) -> [T] where T:FixedWidthInteger & UnsignedInteger - { - self.unpack(as: T.self) - { - (palette:[(r:UInt8, g:UInt8, b:UInt8, a:UInt8)]) in - { - (i:Int) in palette[i].r - } - } - } - /// Creates an image from a scalar pixel array. - /// - /// For an image with a grayscale-alpha color ``Layout/format``, - /// this function assigns the gray channel to the given scalars, and - /// sets the alpha channel to `T.max`. - /// - /// For an image with an indexed color ``Layout/format``, - /// this function expands the given scalars, each of the form (*v*), to - /// RGBA quadruplets (*v*, *v*, *v*, `T.max`), and assigns the index - /// channel to the index of a matching palette entry. If more than one - /// palette entry matches, the matching entry is chosen arbitrarily. - /// If no palette entries match, the first palette entry is chosen. - /// - /// For an image with an RGB or BGR color ``Layout/format``, - /// this function assigns all channels to the given scalars, replicating - /// each scalar three times. - /// - /// For an image with an RGBA or BGRA color ``Layout/format``, this - /// function assigns all opaque channels to the given scalars, replicating - /// each scalar three times, and sets the alpha channel to `T.max`. - /// - /// The scalar values are assumed to fill the entire range of `T`. - /// - Parameter pixels: - /// A scalar pixel array. Its elements are arranged in row-major order. The - /// first pixel in this array corresponds to the top-left corner of - /// the image. - /// - /// The length of this array must match `size.x * size.y`. Passing an - /// array of the wrong length will result in a precondition failure. - /// - Parameter size: - /// The size of the image. Both dimensions must be greater than zero. - /// Passing an invalid image size will result in a precondition failure. - /// - Parameter layout: - /// An image layout. - /// - Parameter metadata: - /// A metadata structure. The default value is an empty metadata structure. - @inlinable - public - init(packing pixels:[T], - size:(x:Int, y:Int), layout:PNG.Layout, metadata:PNG.Metadata = .init()) - where T:FixedWidthInteger & UnsignedInteger - { - self.init(packing: pixels, size: size, layout: layout, metadata: metadata) - { - (palette:[(r:UInt8, g:UInt8, b:UInt8, a:UInt8)]) in - // currently blocked by the issue discussed at - // https://github.com/apple/swift/pull/28833 - // as a workaround, we box the UInt8s into an RGBA struct - let lookup:[PNG.RGBA: Int] = .init(uniqueKeysWithValues: - zip(palette.map{ .init($0.r, $0.g, $0.b, $0.a) }, palette.indices)) - return - { - (v:UInt8) in - lookup[.init(v, v, v, .max), default: 0] - } - } - } -} diff --git a/Sources/PNG/ColorTargets/PNG.RGBA.swift b/Sources/PNG/ColorTargets/PNG.RGBA.swift index e3ffc784..4f964586 100644 --- a/Sources/PNG/ColorTargets/PNG.RGBA.swift +++ b/Sources/PNG/ColorTargets/PNG.RGBA.swift @@ -233,7 +233,7 @@ extension PNG.RGBA:PNG.Color /// computed from the color `format`. /// - Parameter interleaved: /// An image data buffer. It is expected to be obtained from the - /// ``Data.Rectangular/storage`` property of a ``Data/Rectangular`` + /// ``Image/storage`` property of a ``Data/Rectangular`` /// image. /// - Parameter format: /// The color format associated with the given data buffer. @@ -399,7 +399,7 @@ extension PNG.RGBA:PNG.Color /// /// When the library uses an implementation of this function to construct /// a ``Data/Rectangular`` image, this data buffer will be stored in - /// its ``Data.Rectangular/storage`` property. + /// its ``Image/storage`` property. @_specialize(where T == UInt8) @_specialize(where T == UInt16) @_specialize(where T == UInt32) diff --git a/Sources/PNG/ColorTargets/PNG.VA.swift b/Sources/PNG/ColorTargets/PNG.VA.swift index 8314dfdc..8976a3ae 100644 --- a/Sources/PNG/ColorTargets/PNG.VA.swift +++ b/Sources/PNG/ColorTargets/PNG.VA.swift @@ -158,7 +158,7 @@ extension PNG.VA:PNG.Color /// computed from the color `format`. /// - Parameter interleaved: /// An image data buffer. It is expected to be obtained from the - /// ``Data.Rectangular/storage`` property of a ``Data/Rectangular`` + /// ``Image/storage`` property of a ``Data/Rectangular`` /// image. /// - Parameter format: /// The color format associated with the given data buffer. @@ -324,7 +324,7 @@ extension PNG.VA:PNG.Color /// /// When the library uses an implementation of this function to construct /// a ``Data/Rectangular`` image, this data buffer will be stored in - /// its ``Data.Rectangular/storage`` property. + /// its ``Image/storage`` property. @_specialize(where T == UInt8) @_specialize(where T == UInt16) @_specialize(where T == UInt32) diff --git a/Sources/PNG/Decoding/PNG.Context.swift b/Sources/PNG/Decoding/PNG.Context.swift index c06a98c6..6ffdf155 100644 --- a/Sources/PNG/Decoding/PNG.Context.swift +++ b/Sources/PNG/Decoding/PNG.Context.swift @@ -10,7 +10,7 @@ extension PNG { /// The current image state. public private(set) - var image:PNG.Data.Rectangular + var image:PNG.Image private var decoder:PNG.Decoder @@ -46,7 +46,7 @@ extension PNG.Context /// previously-encountered ancillary chunks, with the exception of /// ``Chunk/bKGD`` and ``Chunk/tRNS``. /// - Parameter uninitialized: - /// Specifies if the ``image`` ``Data.Rectangular/storage`` should + /// Specifies if the ``image`` ``Image/storage`` should /// be initialized. If `false`, the storage buffer will be initialized /// to all zeros. This can be safely set to `true` if there is no need /// to access the image while it is in a partially-decoded state. @@ -58,7 +58,7 @@ extension PNG.Context metadata:PNG.Metadata, uninitialized:Bool = true) { - guard let image:PNG.Data.Rectangular = PNG.Data.Rectangular.init( + guard let image:PNG.Image = PNG.Image.init( standard: standard, header: header, palette: palette, @@ -101,7 +101,7 @@ extension PNG.Context }) } /// Parses an ancillary chunk appearing after the last ``Chunk/IDAT`` - /// chunk, and adds it to the ``image`` ``Data.Rectangular/metadata``. + /// chunk, and adds it to the ``image`` ``Image/metadata``. /// /// This function validates the multiplicity of the given `chunk`, and /// its chunk ordering with respect to the ``Chunk/IDAT`` chunks. The diff --git a/Sources/PNG/Encoding/PNG.Encoder.Pass.swift b/Sources/PNG/Encoding/PNG.Encoder.Pass.swift new file mode 100644 index 00000000..c2aae8c9 --- /dev/null +++ b/Sources/PNG/Encoding/PNG.Encoder.Pass.swift @@ -0,0 +1,8 @@ +extension PNG.Encoder +{ + enum Pass + { + case subimage(Int) + case image + } +} diff --git a/Sources/PNG/Encoding/PNG.Encoder.swift b/Sources/PNG/Encoding/PNG.Encoder.swift index f17d3cd1..a6806e33 100644 --- a/Sources/PNG/Encoding/PNG.Encoder.swift +++ b/Sources/PNG/Encoding/PNG.Encoder.swift @@ -4,12 +4,6 @@ extension PNG { struct Encoder { - enum Pass - { - case subimage(Int) - case image - } - private var row:(index:Int, reference:[UInt8])?, pass:Pass? diff --git a/Sources/PNG/Layout/PNG.Data.Rectangular.swift b/Sources/PNG/PNG.Image.swift similarity index 54% rename from Sources/PNG/Layout/PNG.Data.Rectangular.swift rename to Sources/PNG/PNG.Image.swift index 6cdaccc4..a1bf5af6 100644 --- a/Sources/PNG/Layout/PNG.Data.Rectangular.swift +++ b/Sources/PNG/PNG.Image.swift @@ -1,6 +1,6 @@ extension PNG { - /// A namespace containing the ``Data.Rectangular`` type. + /// A namespace containing the ``Image`` type. @available(*, deprecated) public enum Data @@ -46,7 +46,7 @@ extension PNG } } } -extension PNG.Data.Rectangular +extension PNG.Image { internal init?(standard:PNG.Standard, header:PNG.Header, @@ -283,7 +283,7 @@ extension PNG.Data.Rectangular } } } -extension PNG.Data.Rectangular +extension PNG.Image { /// Decompresses and decodes a PNG from the given bytestream. /// @@ -401,7 +401,7 @@ extension PNG.Data.Rectangular } // encoding -extension PNG.Data.Rectangular +extension PNG.Image { func encode() -> ( @@ -543,7 +543,7 @@ extension PNG.Data.Rectangular } } // compression -extension PNG.Data.Rectangular +extension PNG.Image { /// Encodes and compresses a PNG to the given bytestream. /// @@ -670,3 +670,481 @@ extension PNG.Data.Rectangular try stream.format(type: .IEND) } } + +extension PNG.Image +{ + // factoring out the specialized entry point reduces module overhead, because + // the emitted entry point can make use of the specialized function bodies + @usableFromInline + @_specialize(where T == UInt8) + @_specialize(where T == UInt16) + @_specialize(where T == UInt32) + @_specialize(where T == UInt64) + @_specialize(where T == UInt) + static + func unpack(_ interleaved:[UInt8], of format:PNG.Format, as _:T.Type, + deindexer:([(r:UInt8, g:UInt8, b:UInt8, a:UInt8)]) -> (Int) -> UInt8) + -> [T] + where T:FixedWidthInteger & UnsignedInteger + { + let depth:Int = format.pixel.depth + switch format + { + case .indexed1(palette: let palette, fill: _), + .indexed2(palette: let palette, fill: _), + .indexed4(palette: let palette, fill: _), + .indexed8(palette: let palette, fill: _): + return PNG.convolve(interleaved, dereference: deindexer(palette)) + { + (c) in c + } + + case .v1(fill: _, key: _), + .v2(fill: _, key: _), + .v4(fill: _, key: _), + .v8(fill: _, key: _): + return PNG.convolve(interleaved, of: UInt8.self, depth: depth) + { + (c:T, _) in c + } + case .v16(fill: _, key: _): + return PNG.convolve(interleaved, of: UInt16.self, depth: depth) + { + (c:T, _) in c + } + + case .va8(fill: _): + return PNG.convolve(interleaved, of: UInt8.self, depth: depth) + { + (c:(T, T)) in c.0 + } + case .va16(fill: _): + return PNG.convolve(interleaved, of: UInt16.self, depth: depth) + { + (c:(T, T)) in c.0 + } + + case .bgr8(palette: _, fill: _, key: _): + return PNG.convolve(interleaved, of: UInt8.self, depth: depth) + { + (c:(T, T, T), _) in c.2 + } + + case .rgb8(palette: _, fill: _, key: _): + return PNG.convolve(interleaved, of: UInt8.self, depth: depth) + { + (c:(T, T, T), _) in c.0 + } + case .rgb16(palette: _, fill: _, key: _): + return PNG.convolve(interleaved, of: UInt16.self, depth: depth) + { + (c:(T, T, T), _) in c.0 + } + + case .bgra8(palette: _, fill: _): + return PNG.convolve(interleaved, of: UInt8.self, depth: depth) + { + (c:(T, T, T, T)) in c.2 + } + + case .rgba8(palette: _, fill: _): + return PNG.convolve(interleaved, of: UInt8.self, depth: depth) + { + (c:(T, T, T, T)) in c.0 + } + case .rgba16(palette: _, fill: _): + return PNG.convolve(interleaved, of: UInt16.self, depth: depth) + { + (c:(T, T, T, T)) in c.0 + } + } + } + + @usableFromInline + @_specialize(where T == UInt8) + @_specialize(where T == UInt16) + @_specialize(where T == UInt32) + @_specialize(where T == UInt64) + @_specialize(where T == UInt) + static + func pack(_ pixels:[T], as format:PNG.Format, + indexer:([(r:UInt8, g:UInt8, b:UInt8, a:UInt8)]) -> (UInt8) -> Int) + -> [UInt8] + where T:FixedWidthInteger & UnsignedInteger + { + let depth:Int = format.pixel.depth + switch format + { + case .indexed1(palette: let palette, fill: _), + .indexed2(palette: let palette, fill: _), + .indexed4(palette: let palette, fill: _), + .indexed8(palette: let palette, fill: _): + return PNG.deconvolve(pixels, reference: indexer(palette)) + { + (v) in v + } + + case .v1(fill: _, key: _), + .v2(fill: _, key: _), + .v4(fill: _, key: _), + .v8(fill: _, key: _): + return PNG.deconvolve(pixels, as: UInt8.self, depth: depth) + { + (v) in v + } + case .v16(fill: _, key: _): + return PNG.deconvolve(pixels, as: UInt16.self, depth: depth) + { + (v) in v + } + + case .va8(fill: _): + return PNG.deconvolve(pixels, as: UInt8.self, depth: depth) + { + (v) in (v, .max) + } + case .va16(fill: _): + return PNG.deconvolve(pixels, as: UInt16.self, depth: depth) + { + (v) in (v, .max) + } + + case .bgr8(palette: _, fill: _, key: _), + .rgb8(palette: _, fill: _, key: _): + return PNG.deconvolve(pixels, as: UInt8.self, depth: depth) + { + (v) in (v, v, v) + } + case .rgb16(palette: _, fill: _, key: _): + return PNG.deconvolve(pixels, as: UInt16.self, depth: depth) + { + (v) in (v, v, v) + } + + case .bgra8(palette: _, fill: _), + .rgba8(palette: _, fill: _): + return PNG.deconvolve(pixels, as: UInt8.self, depth: depth) + { + (v) in (v, v, v, .max) + } + case .rgba16(palette: _, fill: _): + return PNG.deconvolve(pixels, as: UInt16.self, depth: depth) + { + (v) in (v, v, v, .max) + } + } + } +} + +// custom-indexer APIs +extension PNG.Image +{ + /// Unpacks this image to a pixel array, using a custom deindexing function. + /// + /// - Parameter _: + /// A color target type. This type provides the ``Color/unpack(_:of:deindexer:)`` + /// implementation used to unpack the image data. + /// - Parameter deindexer: + /// A function which uses the palette entries in the color ``Layout/format`` to + /// generate a dereferencing function. This function is only expected to + /// be invoked if `layout.format` is an indexed format. + /// + /// See the [indexed color tutorial](https://github.com/tayloraswift/swift-png/tree/master/examples#using-indexed-images) + /// for more about the semantics of this function. + /// + /// - Returns: + /// A pixel array. Its elements are arranged in row-major order. The + /// first pixel in this array corresponds to the top-left corner of + /// the image. Its length is equal to `size.x` multiplied by `size.y`. + @inlinable + public + func unpack(as _:Color.Type, + deindexer:([(r:UInt8, g:UInt8, b:UInt8, a:UInt8)]) -> (Int) -> Color.Aggregate) + -> [Color] + where Color:PNG.Color + { + Color.unpack(self.storage, of: self.layout.format, deindexer: deindexer) + } + /// Unpacks this image to a scalar pixel array, using a custom deindexing function. + /// + /// For an image with a grayscale-alpha color ``Layout/format``, + /// this function selects the *v* component from pixels of the form (*v*, *a*) + /// + /// For an image with an RGB color ``Layout/format``, + /// this function selects the *r* component from pixels of the form (*r*, *g*, *b*). + /// + /// For an image with an RGBA color ``Layout/format``, this function selects the *r* component from + /// pixels of the form (*r*, *g*, *b*, *a*). + /// + /// For an image with a BGR color ``Layout/format``, + /// this function selects the *r* component from pixels of the form (*b*, *g*, *r*). + /// + /// For an image with a BGRA color ``Layout/format``, + /// this function selects the *r* component from pixels of the form (*b*, *g*, *r*, *a*). + /// + /// This function ignores chroma keys, as its scalar color target is not + /// capable of representing transparency. The unpacked components + /// are scaled to fill the range of `T`, according to the color depth + /// computed from the color ``Layout/format``. + /// + /// - Parameter _: + /// A scalar color target type. + /// - Parameter deindexer: + /// A function which uses the palette entries in the color ``Layout/format`` to + /// generate a dereferencing function. This function will only + /// be invoked if `layout.format` is an indexed format. + /// + /// See the [indexed color tutorial](https://github.com/tayloraswift/swift-png/tree/master/examples#using-indexed-images) + /// for more about the semantics of this function. + /// - Returns: + /// A scalar pixel array. Its elements are arranged in row-major order. The + /// first pixel in this array corresponds to the top-left corner of + /// the image. Its length is equal to `size.x` multiplied by `size.y`. + @inlinable + public + func unpack(as _:T.Type, + deindexer:([(r:UInt8, g:UInt8, b:UInt8, a:UInt8)]) -> (Int) -> UInt8) + -> [T] + where T:FixedWidthInteger & UnsignedInteger + { + Self.unpack(self.storage, of: self.layout.format, as: T.self, deindexer: deindexer) + } + /// Creates an image from a pixel array, using a custom indexing function. + /// + /// - Parameter pixels: + /// A pixel array. Its elements are arranged in row-major order. The + /// first pixel in this array corresponds to the top-left corner of + /// the image. The `Color` type provides the ``Color/pack(_:as:indexer:)`` + /// implementation used to pack the image data. + /// + /// The length of this array must match `size.x * size.y`. Passing an + /// array of the wrong length will result in a precondition failure. + /// - Parameter size: + /// The size of the image. Both dimensions must be greater than zero. + /// Passing an invalid image size will result in a precondition failure. + /// - Parameter layout: + /// An image layout. + /// - Parameter metadata: + /// A metadata structure. The default value is an empty metadata structure. + /// - Parameter indexer: + /// A function which uses the palette entries in the color ``Layout/format`` to + /// generate a referencing function. This function is only expected to + /// be invoked if the image color ``Layout/format`` is an indexed format. + /// + /// See the [indexed color tutorial](https://github.com/tayloraswift/swift-png/tree/master/examples#using-indexed-images) + /// for more about the semantics of this function. + @inlinable + public + init(packing pixels:[Color], + size:(x:Int, y:Int), layout:PNG.Layout, metadata:PNG.Metadata = .init(), + indexer:([(r:UInt8, g:UInt8, b:UInt8, a:UInt8)]) -> (Color.Aggregate) -> Int) + where Color:PNG.Color + { + precondition(size.x > 0 && size.y > 0, + "image dimensions must be greater than zero") + precondition(pixels.count == size.x * size.y, + "pixel array `count` must be equal to `size.x * size.y`") + self.init(size: size, layout: layout, metadata: metadata, + storage: Color.pack(pixels, as: layout.format, indexer: indexer)) + } + /// Creates an image from a scalar pixel array, using a custom indexing function. + /// + /// For an image with a grayscale-alpha color ``Layout/format``, + /// this function assigns the gray channel to the given scalars, and + /// sets the alpha channel to `T.max`. + /// + /// For an image with an RGB or BGR color ``Layout/format``, + /// this function assigns all channels to the given scalars, replicating + /// each scalar three times. + /// + /// For an image with an RGBA or BGRA color ``Layout/format``, this + /// function assigns all opaque channels to the given scalars, replicating + /// each scalar three times, and sets the alpha channel to `T.max`. + /// + /// The scalar values are assumed to fill the entire range of `T`. + /// - Parameter pixels: + /// A scalar pixel array. Its elements are arranged in row-major order. The + /// first pixel in this array corresponds to the top-left corner of + /// the image. + /// + /// The length of this array must match `size.x * size.y`. Passing an + /// array of the wrong length will result in a precondition failure. + /// - Parameter size: + /// The size of the image. Both dimensions must be greater than zero. + /// Passing an invalid image size will result in a precondition failure. + /// - Parameter layout: + /// An image layout. + /// - Parameter metadata: + /// A metadata structure. The default value is an empty metadata structure. + /// - Parameter indexer: + /// A function which uses the palette entries in the color ``Layout/format`` to + /// generate a referencing function. This function will only + /// be invoked if the image color ``Layout/format`` is an indexed format. + /// + /// See the [indexed color tutorial](https://github.com/tayloraswift/swift-png/tree/master/examples#using-indexed-images) + /// for more about the semantics of this function. + @inlinable + public + init(packing pixels:[T], + size:(x:Int, y:Int), layout:PNG.Layout, metadata:PNG.Metadata = .init(), + indexer:([(r:UInt8, g:UInt8, b:UInt8, a:UInt8)]) -> (UInt8) -> Int) + where T:FixedWidthInteger & UnsignedInteger + { + precondition(size.x > 0 && size.y > 0, + "image dimensions must be greater than zero") + precondition(pixels.count == size.x * size.y, + "pixel array `count` must be equal to `size.x * size.y`") + self.init(size: size, layout: layout, metadata: metadata, + storage: Self.pack(pixels, as: layout.format, indexer: indexer)) + } +} +extension PNG.Image +{ + /// Unpacks this image to a pixel array. + /// + /// - Parameter _: + /// A color target type. This type provides the ``Color/unpack(_:of:)`` + /// implementation used to unpack the image data. + /// - Returns: + /// A pixel array. Its elements are arranged in row-major order. The + /// first pixel in this array corresponds to the top-left corner of + /// the image. Its length is equal to `size.x` multiplied by `size.y`. + @inlinable + public + func unpack(as _:Color.Type) -> [Color] where Color:PNG.Color + { + Color.unpack(self.storage, of: self.layout.format) + } + /// Creates an image from a pixel array. + /// + /// - Parameter pixels: + /// A pixel array. Its elements are arranged in row-major order. The + /// first pixel in this array corresponds to the top-left corner of + /// the image. The `Color` type provides the ``Color/pack(_:as:)`` + /// implementation used to pack the image data. + /// + /// The length of this array must match `size.x * size.y`. Passing an + /// array of the wrong length will result in a precondition failure. + /// - Parameter size: + /// The size of the image. Both dimensions must be greater than zero. + /// Passing an invalid image size will result in a precondition failure. + /// - Parameter layout: + /// An image layout. + /// - Parameter metadata: + /// A metadata structure. The default value is an empty metadata structure. + @inlinable + public + init(packing pixels:[Color], + size:(x:Int, y:Int), layout:PNG.Layout, metadata:PNG.Metadata = .init()) + where Color:PNG.Color + { + precondition(size.x > 0 && size.y > 0, + "image dimensions must be greater than zero") + precondition(pixels.count == size.x * size.y, + "pixel array `count` must be equal to `size.x * size.y`") + self.init(size: size, layout: layout, metadata: metadata, + storage: Color.pack(pixels, as: layout.format)) + } + /// Unpacks this image to a scalar pixel array. + /// + /// For an image with a grayscale-alpha color ``Layout/format``, + /// this function selects the *v* component from pixels of the form (*v*, *a*) + /// + /// For an image with an RGB color ``Layout/format``, + /// this function selects the *r* component from pixels of the form (*r*, *g*, *b*). + /// + /// For an image with an indexed color ``Layout/format``, + /// this function selects the *r* component from palette entries of the + /// form (*r*, *g*, *b*, *a*). The palette entry is chosen by taking the + /// i*th element in the palette, from pixels of the form (*i*). + /// + /// For an image with an RGBA color ``Layout/format``, this function + /// selects the *r* component from pixels of the form (*r*, *g*, *b*, *a*). + /// + /// For an image with a BGR color ``Layout/format``, + /// this function selects the *r* component from pixels of the form (*b*, *g*, *r*). + /// + /// For an image with a BGRA color ``Layout/format``, + /// this function selects the *r* component from pixels of the form (*b*, *g*, *r*, *a*). + /// + /// This function ignores chroma keys, as its scalar color target is not + /// capable of representing transparency. The unpacked components + /// are scaled to fill the range of `T`, according to the color depth + /// computed from the color ``Layout/format``. + /// + /// - Parameter _: + /// A scalar color target type. + /// - Returns: + /// A scalar pixel array. Its elements are arranged in row-major order. The + /// first pixel in this array corresponds to the top-left corner of + /// the image. Its length is equal to `size.x` multiplied by `size.y`. + @inlinable + public + func unpack(as _:T.Type) -> [T] where T:FixedWidthInteger & UnsignedInteger + { + self.unpack(as: T.self) + { + (palette:[(r:UInt8, g:UInt8, b:UInt8, a:UInt8)]) in + { + (i:Int) in palette[i].r + } + } + } + /// Creates an image from a scalar pixel array. + /// + /// For an image with a grayscale-alpha color ``Layout/format``, + /// this function assigns the gray channel to the given scalars, and + /// sets the alpha channel to `T.max`. + /// + /// For an image with an indexed color ``Layout/format``, + /// this function expands the given scalars, each of the form (*v*), to + /// RGBA quadruplets (*v*, *v*, *v*, `T.max`), and assigns the index + /// channel to the index of a matching palette entry. If more than one + /// palette entry matches, the matching entry is chosen arbitrarily. + /// If no palette entries match, the first palette entry is chosen. + /// + /// For an image with an RGB or BGR color ``Layout/format``, + /// this function assigns all channels to the given scalars, replicating + /// each scalar three times. + /// + /// For an image with an RGBA or BGRA color ``Layout/format``, this + /// function assigns all opaque channels to the given scalars, replicating + /// each scalar three times, and sets the alpha channel to `T.max`. + /// + /// The scalar values are assumed to fill the entire range of `T`. + /// - Parameter pixels: + /// A scalar pixel array. Its elements are arranged in row-major order. The + /// first pixel in this array corresponds to the top-left corner of + /// the image. + /// + /// The length of this array must match `size.x * size.y`. Passing an + /// array of the wrong length will result in a precondition failure. + /// - Parameter size: + /// The size of the image. Both dimensions must be greater than zero. + /// Passing an invalid image size will result in a precondition failure. + /// - Parameter layout: + /// An image layout. + /// - Parameter metadata: + /// A metadata structure. The default value is an empty metadata structure. + @inlinable + public + init(packing pixels:[T], + size:(x:Int, y:Int), layout:PNG.Layout, metadata:PNG.Metadata = .init()) + where T:FixedWidthInteger & UnsignedInteger + { + self.init(packing: pixels, size: size, layout: layout, metadata: metadata) + { + (palette:[(r:UInt8, g:UInt8, b:UInt8, a:UInt8)]) in + // currently blocked by the issue discussed at + // https://github.com/apple/swift/pull/28833 + // as a workaround, we box the UInt8s into an RGBA struct + let lookup:[PNG.RGBA: Int] = .init(uniqueKeysWithValues: + zip(palette.map{ .init($0.r, $0.g, $0.b, $0.a) }, palette.indices)) + return + { + (v:UInt8) in + lookup[.init(v, v, v, .max), default: 0] + } + } + } +} diff --git a/Sources/PNG/Layout/PNG.Layout.swift b/Sources/PNG/PNG.Layout.swift similarity index 100% rename from Sources/PNG/Layout/PNG.Layout.swift rename to Sources/PNG/PNG.Layout.swift diff --git a/Sources/PNG/Layout/PNG.swift b/Sources/PNG/PNG.swift similarity index 93% rename from Sources/PNG/Layout/PNG.swift rename to Sources/PNG/PNG.swift index accd798d..dbd5a03b 100644 --- a/Sources/PNG/Layout/PNG.swift +++ b/Sources/PNG/PNG.swift @@ -42,6 +42,98 @@ enum PNG typealias Destination = _PNGBytestreamDestination } } +extension PNG +{ + /// Premultiplies a color component with an alpha value. + /// + /// - Parameters: + /// - color: + /// The color component to premultiply. + /// - alpha: + /// The alpha component to premultiply `color` with. + /// + /// - Returns: + /// The premultiplied color component, rounded to the nearest integer. + /// + /// The `color` and `alpha` parameters are interpreted as rational numbers in the range + /// [0, 1], where `T.min` maps to 0, and `T.max` maps to 1. + /// + /// This function uses no floating point operations, and satisfies the property that + /// ``premultiply(_:alpha:)`` is equivalent to + /// + /// ``premultiply(_:alpha:)`` ∘ ``straighten(_:alpha:)`` ∘ ``premultiply(_:alpha:)`` + /// + /// The computed properties ``RGBA.premultiplied`` and ``VA.premultiplied`` can be used to + /// premultiply an entire instance of one of the built-in color targets. + /// + /// Premultiplication is a destructive operation. In the most extreme case, if `alpha` is + /// `T.min`, this function will return `T.min` for any value of `color`. + @inlinable public static + func premultiply(_ color:T, alpha:T) -> T + where T:FixedWidthInteger & UnsignedInteger + { + // this generates pretty good assembly, though Swift/LLVM doesn’t + // seem to know it can perform the full width arithmetic in one register + // for T.bitWidth <= 32 + let product:(high:T, low:T.Magnitude) = color.multipliedFullWidth(by: alpha) + let biased:(high:T, low:T.Magnitude), + carried:Bool + (biased.low, carried) = product.low.addingReportingOverflow(.max >> 1) + biased.high = product.high &+ (carried ? 1 : 0) + return T.max.dividingFullWidth(biased).quotient + } + /// Straightens a premultiplied color component given an alpha value. + /// + /// - Parameters: + /// - premultiplied: + /// The premultiplied color component to straighten. + /// - alpha: + /// The alpha component that `premultiplied` was premultiplied by. + /// + /// - Returns: + /// The straightened color component, rounded to the nearest integer. + /// If `alpha` is `T.min`, this function returns the original + /// `premultiplied` argument. + /// + /// The `color` and `alpha` parameters are interpreted as rational numbers + /// in the range [0, 1], where `T.min` maps to 0, + /// and `T.max` maps to 1. + /// + /// This function uses no floating point operations, and satisfies the + /// property that ``premultiply(_:alpha:)`` + /// + /// is equivalent to + /// + /// ``premultiply(_:alpha:)`` ∘ ``straighten(_:alpha:)`` ∘ ``premultiply(_:alpha:)`` + /// + /// The computed properties ``RGBA.straightened`` and ``VA.straightened`` + /// can be used to straighten an entire instance of one of the built-in + /// color targets. + /// + /// Premultiplication is a destructive operation. This function cannot + /// recover the original color unless `alpha` is `T.max`, in which case + /// this function performs a division by 1, and returns the original + /// `premultiplied` argument. + @inlinable public static + func straighten(_ premultiplied:T, alpha:T) -> T + where T:FixedWidthInteger & UnsignedInteger + { + guard alpha > 0 + else + { + return premultiplied + } + + let biased:(high:T, low:T.Magnitude) = + T.max.multipliedFullWidth(by: premultiplied) + let product:(high:T, low:T.Magnitude), + carried:Bool + (product.low, carried) = biased.low.addingReportingOverflow(alpha.magnitude >> 1) + product.high = biased.high &+ (carried ? 1 : 0) + return alpha.dividingFullWidth(product).quotient + } +} + extension PNG { /// Returns the value of the paeth filter function with the given parameters. diff --git a/Sources/PNG/Parsing/PNG.Format.swift b/Sources/PNG/Parsing/PNG.Format.swift index 7ae5cb36..810ef949 100644 --- a/Sources/PNG/Parsing/PNG.Format.swift +++ b/Sources/PNG/Parsing/PNG.Format.swift @@ -15,7 +15,7 @@ extension PNG /// can be set to the empty array `[]`. /// /// Color format validation takes place when initializing a ``Layout`` instance, - /// which stores the color format in a ``Data.Rectangular`` image. + /// which stores the color format in a ``Image`` image. /// # [Grayscale formats](grayscale-color-formats) /// # [Grayscale-alpha formats](grayscale-alpha-color-formats) /// # [Indexed formats](indexed-color-formats) diff --git a/Sources/PNG/System.swift b/Sources/PNG/System.swift index 4eb4d325..2f303915 100644 --- a/Sources/PNG/System.swift +++ b/Sources/PNG/System.swift @@ -256,9 +256,9 @@ extension System.File.Destination:PNG.Bytestream.Destination { } -extension PNG.Data.Rectangular +extension PNG.Image { - /// static func PNG.Data.Rectangular.decompress(path:) + /// static func PNG.Image.decompress(path:) /// throws /// Decompresses and decodes a PNG from a file at the given file path. /// @@ -281,7 +281,7 @@ extension PNG.Data.Rectangular try .decompress(stream: &$0) } } - /// func PNG.Data.Rectangular.compress(path:level:hint:) + /// func PNG.Image.compress(path:level:hint:) /// throws /// Encodes and compresses a PNG to a file at the given file path. /// diff --git a/Sources/PNG/__Entrypoint.swift b/Sources/PNG/__Entrypoint.swift index 252fb019..e9ef277c 100644 --- a/Sources/PNG/__Entrypoint.swift +++ b/Sources/PNG/__Entrypoint.swift @@ -134,7 +134,7 @@ extension __Entrypoint.Benchmark.Decode { let start:Int = clock() - let image:PNG.Data.Rectangular = try .decompress(stream: &blob) + let image:PNG.Image = try .decompress(stream: &blob) let pixels:[PNG.RGBA] = image.unpack(as: PNG.RGBA.self) let stop:Int = clock() @@ -152,7 +152,7 @@ extension __Entrypoint.Benchmark.Encode public static func rgba8(level:Int, path:String, trials:Int) -> ([(time:Int, hash:Int)], Int) { - guard let image:PNG.Data.Rectangular = try? .decompress(path: path) + guard let image:PNG.Image = try? .decompress(path: path) else { fatalError("failed to decode test image '\(path)'") diff --git a/Sources/PNGCompressionTests/Main.swift b/Sources/PNGCompressionTests/Main.swift index 672de70c..b31c2f19 100644 --- a/Sources/PNGCompressionTests/Main.swift +++ b/Sources/PNGCompressionTests/Main.swift @@ -125,7 +125,7 @@ enum Main:TestMain, TestBattery do { - guard let baseline:(image:PNG.Data.Rectangular, size:Int) = + guard let baseline:(image:PNG.Image, size:Int) = (try System.File.Source.open(path: path.png) { (try .decompress(stream: &$0), $0.count!) @@ -137,7 +137,7 @@ enum Main:TestMain, TestBattery try baseline.image.compress(path: path.out, level: 9) - guard let output:(image:PNG.Data.Rectangular, size:Int) = + guard let output:(image:PNG.Image, size:Int) = (try System.File.Source.open(path: path.out) { (try .decompress(stream: &$0), $0.count!) diff --git a/Sources/PNGIntegrationTests/Main.swift b/Sources/PNGIntegrationTests/Main.swift index 11e4e555..8b81f54b 100644 --- a/Sources/PNGIntegrationTests/Main.swift +++ b/Sources/PNGIntegrationTests/Main.swift @@ -384,7 +384,7 @@ enum Main:TestMain, TestBattery { do { - guard let rectangular:PNG.Data.Rectangular = try .decompress(path: path.in) + guard let rectangular:PNG.Image = try .decompress(path: path.in) else { return .failure(.init(message: "failed to open file '\(path.in)'")) @@ -498,7 +498,7 @@ enum Main:TestMain, TestBattery func decode(_ name:String) throws -> Result? { let path:String = "Sources/PNGIntegrationTests/Inputs/Invalid/\(name).png" - if let _:PNG.Data.Rectangular = try .decompress(path: path) + if let _:PNG.Image = try .decompress(path: path) { return .failure(.init(message: "file '\(path)' is invalid, but decoded without errors")) } @@ -638,7 +638,7 @@ enum Main:TestMain, TestBattery { do { - guard let rectangular:PNG.Data.Rectangular = try .decompress(path: path.in) + guard let rectangular:PNG.Image = try .decompress(path: path.in) else { return .failure(.init(message: "failed to open file '\(path.in)'"))