English | 简体中文
Compress images for upload with a small, predictable Swift API.
WICompress is an ImageIO-backed Swift image compression library that operates
directly on original image Data or file URL input. ImageIO handles format
inspection, orientation, alpha, metadata, color profiles, resizing, and encoding;
the public API stays simple and returns compressed Data.
It preserves JPEG/PNG/HEIC by default, can convert to an explicit output format
when your upload endpoint requires it, strips metadata for privacy, and resizes
large images without depending on UIImage or NSImage.
let compressedData = try WICompress.compress(originalData)let uploadData = try WICompress.compress(
originalData,
options: WICompressOptions(
resize: .maxPixel(1600),
format: .jpeg(background: .white),
metadata: .strip,
quality: .compression(0.7)
)
)- Data in, Data out: keep picker/file/network bytes and pass them directly to the compressor.
- Upload-ready defaults: Luban resize, metadata stripping, and JPEG/HEIC lossy quality are configured for common app uploads.
- Format control: preserve the source container or explicitly output JPEG, PNG, or HEIC.
- Alpha-safe JPEG conversion: transparent sources require an explicit white or black background instead of silently flattening.
- Orientation-safe resizing: display dimensions are resolved from ImageIO metadata, then redraw paths bake orientation into pixels.
- UIKit/AppKit-free core: the compression pipeline works in iOS apps, macOS tools, and SwiftPM tests without UI image types.
- Typed failures: errors are surfaced as
WICompressError, not optionalnilresults.
The comparison image below is generated from repository fixtures with
scripts/generate-doc-assets.swift, so it can be regenerated when compression
behavior changes.
swift run WICompressDocAssetGeneratorThe preview uses the default API for every row. It shows three HEIC photos first because HEIC is the most important real-world case, then JPEG and PNG examples. PNG is not skipped: the panoramic screenshot shrinks when Luban resize is triggered, while the alpha PNG is a no-op case where the original data is already the better result.
The repository includes a SwiftUI example app:
- Open
Example/WICompressExample/WICompressExample.xcodeproj. - Build and run on an iOS device or simulator.
- Pick an image and compare the original data with the compressed data.
The example demonstrates:
PhotosPickerandPHPickerViewControllerdata loading- raw
Datacompression - format detection
- original/compressed preview
- file-size and compression-ratio display
import WICompress
let compressedData = try WICompress.compress(originalData)Compress a file URL:
let compressedData = try WICompress.compress(contentsOf: imageURL)Use explicit options:
let compressedData = try WICompress.compress(
originalData,
options: WICompressOptions(
resize: .luban,
format: .preserve,
metadata: .strip,
quality: .compression(0.7)
)
)WICompress does not take UIImage or NSImage. Keep the original image data
from your picker, file, network response, or database, pass that data to
WICompress, and decode the result at the UI boundary if you need a preview.
guard let originalData = try await photosPickerItem.loadTransferable(type: Data.self) else {
throw MyError.missingImageData
}
let compressedData = try WICompress.compress(originalData)
let previewImage = UIImage(data: compressedData)This shape avoids asking callers to pass both a rendered image and separate format data. ImageIO can inspect dimensions, orientation, format, and metadata directly from the original bytes.
WICompressOptions.default is tuned for upload-style compression:
WICompressOptions(
resize: .luban,
format: .preserve,
metadata: .strip,
quality: .compression(0.6)
)public enum WIResizePolicy {
case none
case luban
case maxPixel(Int)
}.luban: default. Downsamples large images using the Luban ratio..maxPixel(value): caps the longest display side tovaluepixels and never upscales smaller images..none: keeps the source display dimensions.
public enum WIJPEGBackground {
case disallow
case white
case black
}
public enum WIFormatPolicy {
case preserve
case jpeg(background: WIJPEGBackground = .disallow)
case png
case heic
}.preserve: default. Keeps the source image container..jpeg(background:): writes JPEG. Transparent sources require.whiteor.black;.disallowthrows instead of silently flattening alpha..png: writes PNG. The quality policy is ignored because PNG is lossless..heic: writes HEIC when the current platform can encode it.
Explicit format conversion always rewrites the image. The size guard will not return original bytes when the caller requested a concrete destination format.
public enum WIMetadataPolicy {
case strip
case preserve
}.strip: default. Removes strippable metadata such as Exif/GPS/TIFF/maker dictionaries when rewriting is required..preserve: keeps normal metadata and orientation tags by using the source-copy write path when possible.
When format conversion forces the redraw path, .preserve re-attaches ordinary
metadata dictionaries where ImageIO supports them. Orientation is still baked
into pixels and reset to 1, because preserving the original rotation tag after
redraw would double-rotate readers.
Color profiles are display semantics, not privacy metadata. Display P3 profiles are expected to survive both source-copy and redraw paths.
HDR gain maps are not preserved by the initial public release. They require a separate policy and test contract because gain maps are auxiliary image data, not ordinary Exif/GPS metadata.
public enum WIQualityPolicy {
case none
case compression(Double)
}.compression(value): clampsvalueinto0.0...1.0and applies it to lossy destination formats such as JPEG and HEIC..none: does not setkCGImageDestinationLossyCompressionQuality.
.none does not mean lossless and does not promise byte-for-byte output unless
the write plan can safely return the original data.
PNG is lossless; the quality policy is intentionally a no-op for PNG.
All public APIs throw WICompressError.
do {
let compressedData = try WICompress.compress(data)
} catch let error as WICompressError {
// Decide whether to show an error, retry, or keep the original data.
print(error)
}Common cases:
invalidImageDataimageInfoUnavailableunsupportedSourceFormatunsupportedDestinationFormattransparentSourceRequiresBackgroundanimatedSourceUnsupportedthumbnailCreationFaileddestinationCreationFailedencodeFailed
The initial public release intentionally does not include:
UIImage/NSImageconvenience adapters- Live Photo compression
- async API
- GPS-only metadata stripping
- target-byte-size compression
- HDR gain map preservation
- animated image output
- WebP / JPEG XL writing
For Live Photos, compressing the still image resource alone is not enough: the paired video resource and pairing metadata also need to be handled. That belongs in a Photos-level workflow, not the v1 ImageIO core.
WICompress 1.0.0 replaces the old UIImage-oriented API with the Data/URL
core API shown above. See CHANGELOG.md for the breaking change
summary.
WICompress is available under the Apache-2.0 license. See LICENSE.txt for details.
