Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: change screenshot image format from Jpeg to WebP #273

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## Next

- feat: change screenshot encoding format from JPEG to WebP ([#273](https://github.com/PostHog/posthog-ios/pull/273))

## 3.17.0 - 2024-12-10

- feat: ability to add a custom label to autocapture elements ([#271](https://github.com/PostHog/posthog-ios/pull/271))
Expand Down
16 changes: 8 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@ buildSdk:

buildExamples:
set -o pipefail && xcrun xcodebuild -downloadAllPlatforms
set -o pipefail && xcrun xcodebuild build -scheme PostHogExample -destination generic/platform=ios | xcpretty #ios
set -o pipefail && xcrun xcodebuild build -scheme PostHogExample -destination 'platform=visionOS Simulator,name=Apple Vision Pro' | xcpretty #visionOS
set -o pipefail && xcrun xcodebuild build -scheme PostHogObjCExample -destination generic/platform=ios | xcpretty #ObjC
set -o pipefail && xcrun xcodebuild build -scheme PostHogExampleMacOS -destination generic/platform=macos | xcpretty #macOS
set -o pipefail && xcrun xcodebuild build -scheme 'PostHogExampleWatchOS Watch App' -destination generic/platform=watchos | xcpretty #watchOS
set -o pipefail && xcrun xcodebuild build -scheme PostHogExampleTvOS -destination generic/platform=tvos | xcpretty #watchOS
set -o pipefail && xcrun xcodebuild clean build -scheme PostHogExample -destination generic/platform=ios | xcpretty #ios
set -o pipefail && xcrun xcodebuild clean build -scheme PostHogExample -destination 'platform=visionOS Simulator,name=Apple Vision Pro' | xcpretty #visionOS
set -o pipefail && xcrun xcodebuild clean build -scheme PostHogObjCExample -destination generic/platform=ios | xcpretty #ObjC
set -o pipefail && xcrun xcodebuild clean build -scheme PostHogExampleMacOS -destination generic/platform=macos | xcpretty #macOS
set -o pipefail && xcrun xcodebuild clean build -scheme 'PostHogExampleWatchOS Watch App' -destination generic/platform=watchos | xcpretty #watchOS
set -o pipefail && xcrun xcodebuild clean build -scheme PostHogExampleTvOS -destination generic/platform=tvos | xcpretty #watchOS
cd PostHogExampleWithPods && pod install
cd ..
set -o pipefail && xcrun xcodebuild build -workspace PostHogExampleWithPods/PostHogExampleWithPods.xcworkspace -scheme PostHogExampleWithPods -destination generic/platform=ios | xcpretty #CocoaPods
set -o pipefail && xcrun xcodebuild build -scheme PostHogExampleWithSPM -destination generic/platform=ios | xcpretty #SPM
set -o pipefail && xcrun xcodebuild clean build -workspace PostHogExampleWithPods/PostHogExampleWithPods.xcworkspace -scheme PostHogExampleWithPods -destination generic/platform=ios | xcpretty #CocoaPods
set -o pipefail && xcrun xcodebuild clean build -scheme PostHogExampleWithSPM -destination generic/platform=ios | xcpretty #SPM

format: swiftLint swiftFormat

Expand Down
14 changes: 13 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,20 @@ let package = Package(
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "PostHog",
dependencies: ["libwebp"],
path: "PostHog",
resources: [
.copy("Resources/PrivacyInfo.xcprivacy"),
]
),
.target(
name: "libwebp",
path: "vendor/libwebp",
sources: ["src", "include"],
cSettings: [
.headerSearchPath("include"),
]
),
.testTarget(
name: "PostHogTests",
dependencies: [
Expand All @@ -39,7 +48,10 @@ let package = Package(
"OHHTTPStubs",
.product(name: "OHHTTPStubsSwift", package: "OHHTTPStubs"),
],
path: "PostHogTests"
path: "PostHogTests",
resources: [
.process("Resources"),
]
),
]
)
3 changes: 2 additions & 1 deletion PostHog.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ Pod::Spec.new do |s|
s.frameworks = 'Foundation'

s.source_files = [
'PostHog/**/*.{swift,h,hpp,m,mm,c,cpp}'
'PostHog/**/*.{swift,h,hpp,m,mm,c,cpp}',
'vendor/libwebp/**/*.{h,c}'
]
s.resource_bundles = { "PostHog" => "PostHog/Resources/PrivacyInfo.xcprivacy" }
end
844 changes: 748 additions & 96 deletions PostHog.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

45 changes: 44 additions & 1 deletion PostHog/PostHog.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
// Created by Ben White on 10.01.23.
//


#import <Foundation/Foundation.h>

//! Project version number for PostHog.
Expand All @@ -15,3 +14,47 @@ FOUNDATION_EXPORT double PostHogVersionNumber;
FOUNDATION_EXPORT const unsigned char PostHogVersionString[];

// In this header, you should import all the public headers of your framework using statements like #import <PostHog/PublicHeader.h>
#import <PostHog/backward_references_enc.h>
#import <PostHog/bit_reader_utils.h>
#import <PostHog/bit_writer_utils.h>
#import <PostHog/color_cache_utils.h>
#import <PostHog/common_dec.h>
#import <PostHog/common_sse2.h>
#import <PostHog/common_sse41.h>
#import <PostHog/cost_enc.h>
#import <PostHog/cpu.h>
#import <PostHog/decode.h>
#import <PostHog/dsp.h>
#import <PostHog/encode.h>
#import <PostHog/endian_inl_utils.h>
#import <PostHog/filters_utils.h>
#import <PostHog/format_constants.h>
#import <PostHog/histogram_enc.h>
#import <PostHog/huffman_encode_utils.h>
#import <PostHog/lossless.h>
#import <PostHog/lossless_common.h>
#import <PostHog/mux.h>
#import <PostHog/muxi.h>
#import <PostHog/mux_types.h>
#import <PostHog/neon.h>
#import <PostHog/palette.h>
#import <PostHog/quant.h>
#import <PostHog/quant_levels_utils.h>
#import <PostHog/random_utils.h>
#import <PostHog/rescaler_utils.h>
#import <PostHog/sharpyuv.h>
#import <PostHog/sharpyuv_cpu.h>
#import <PostHog/sharpyuv_csp.h>
#import <PostHog/sharpyuv_dsp.h>
#import <PostHog/sharpyuv_gamma.h>
#import <PostHog/thread_utils.h>
#import <PostHog/types.h>
#import <PostHog/utils.h>
#import <PostHog/vp8i_enc.h>
#import <PostHog/vp8li_enc.h>
#import <PostHog/vp8_dec.h>
#import <PostHog/vp8i_dec.h>
#import <PostHog/vp8li_dec.h>
#import <PostHog/webpi_dec.h>
#import <PostHog/huffman_utils.h>
#import <PostHog/yuv.h>
6 changes: 6 additions & 0 deletions PostHog/PostHog.modulemap
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
framework module PostHog {
umbrella header "PostHog.h"

export *
module * { export * }
}
15 changes: 10 additions & 5 deletions PostHog/Replay/UIImage+Util.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,19 @@

extension UIImage {
func toBase64(_ compressionQuality: CGFloat = 0.3) -> String? {
let jpegData = jpegData(compressionQuality: compressionQuality)
let base64 = jpegData?.base64EncodedString()
toWebPBase64(compressionQuality) ?? toJpegBase64(compressionQuality)
ioannisj marked this conversation as resolved.
Show resolved Hide resolved
marandaneto marked this conversation as resolved.
Show resolved Hide resolved
}

if let base64 = base64 {
return "data:image/jpeg;base64,\(base64)"
private func toWebPBase64(_ compressionQuality: CGFloat) -> String? {
marandaneto marked this conversation as resolved.
Show resolved Hide resolved
webpData(compressionQuality: compressionQuality).map { data in
"data:image/webp;base64,\(data.base64EncodedString())"
}
}

return nil
private func toJpegBase64(_ compressionQuality: CGFloat) -> String? {
jpegData(compressionQuality: compressionQuality).map { data in
"data:image/jpeg;base64,\(data.base64EncodedString())"
}
}
}

Expand Down
133 changes: 133 additions & 0 deletions PostHog/Utils/UIImage+WebP.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
//
ioannisj marked this conversation as resolved.
Show resolved Hide resolved
// UIImage+WebP.swift
// PostHog
//
// Created by Yiannis Josephides on 09/12/2024.
//
// Adapted from: https://github.com/SDWebImage/SDWebImageWebPCoder/blob/master/SDWebImageWebPCoder/Classes/SDImageWebPCoder.m

#if os(iOS)
import Accelerate
import CoreGraphics
import Foundation
#if canImport(libwebp)
// SPM package is linked via a lib since mix-code is not yet supported
import libwebp
#endif
import UIKit

extension UIImage {
/**
Returns a data object that contains the image in WebP format.

- Parameters:
- compressionQuality: desired compression quality [0...1] (0=max/lowest quality, 1=low/high quality)
- Returns: A data object containing the WebP data, or nil if there’s a problem generating the data.
*/
func webpData(compressionQuality: CGFloat) -> Data? {
// Early exit if image is missing
guard let cgImage = cgImage else {
return nil
}

// validate dimensions
let width = Int(cgImage.width)
let height = Int(cgImage.height)

guard width > 0, width <= WEBP_MAX_DIMENSION, height > 0, height <= WEBP_MAX_DIMENSION else {
return nil
}
marandaneto marked this conversation as resolved.
Show resolved Hide resolved

let bitmapInfo = cgImage.bitmapInfo
let alphaInfo = CGImageAlphaInfo(rawValue: bitmapInfo.rawValue & CGBitmapInfo.alphaInfoMask.rawValue)

// Prepare destination format

let hasAlpha = !(
alphaInfo == CGImageAlphaInfo.none ||
alphaInfo == CGImageAlphaInfo.noneSkipFirst ||
alphaInfo == CGImageAlphaInfo.noneSkipLast
)

// try to use image color space if ~rgb
let colorSpace: CGColorSpace = cgImage.colorSpace?.model == .rgb
? cgImage.colorSpace! // safe from previous check
: CGColorSpace(name: CGColorSpace.linearSRGB)!
let renderingIntent = cgImage.renderingIntent

guard let destFormat = vImage_CGImageFormat(
bitsPerComponent: 8,
bitsPerPixel: hasAlpha ? 32 : 24, // RGB888/RGBA8888
colorSpace: colorSpace,
bitmapInfo: hasAlpha
? CGBitmapInfo(rawValue: CGImageAlphaInfo.last.rawValue | CGBitmapInfo.byteOrderDefault.rawValue)
: CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue | CGBitmapInfo.byteOrderDefault.rawValue),
renderingIntent: renderingIntent
) else {
return nil
}

guard let dest = try? vImage_Buffer(cgImage: cgImage, format: destFormat, flags: .noFlags) else {
hedgeLog("Error initializing WebP image buffer")
return nil
}
defer { dest.data?.deallocate() }

guard let rgba = dest.data else { // byte array
hedgeLog("Could not get rgba byte array from destination format")
return nil
}
let bytesPerRow = dest.rowBytes

let quality = Float(compressionQuality * 100) // WebP quality is 0-100

var config = WebPConfig()
var picture = WebPPicture()
var writer = WebPMemoryWriter()

// get present...
guard WebPConfigPreset(&config, WEBP_PRESET_DEFAULT, quality) != 0, WebPPictureInit(&picture) != 0 else {
hedgeLog("Error initializing WebPPicture")
return nil
}

withUnsafeMutablePointer(to: &writer) { writerPointer in
picture.use_argb = 1 // Lossy encoding uses YUV for internal bitstream
picture.width = Int32(width)
picture.height = Int32(height)
picture.writer = WebPMemoryWrite
picture.custom_ptr = UnsafeMutableRawPointer(writerPointer)
}

WebPMemoryWriterInit(&writer)

defer {
WebPMemoryWriterClear(&writer)
WebPPictureFree(&picture)
}

let result: Int32
if hasAlpha {
// RGBA8888 - 4 channels
result = WebPPictureImportRGBA(&picture, rgba.bindMemory(to: UInt8.self, capacity: 4), Int32(bytesPerRow))
} else {
// RGB888 - 3 channels
result = WebPPictureImportRGB(&picture, rgba.bindMemory(to: UInt8.self, capacity: 3), Int32(bytesPerRow))
}

if result == 0 {
hedgeLog("Could not read WebPPicture")
return nil
}

if WebPEncode(&config, &picture) == 0 {
hedgeLog("Could not encode WebP image")
return nil
}

let webpData = Data(bytes: writer.mem, count: writer.size)

return webpData
}
}
#endif
Loading
Loading