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

Add Optimize Images setting and enable it by default #21981

Merged
merged 31 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
624c1f4
Add optimize image popup
fluiddot Nov 3, 2023
574f2d1
Add image quality setting to App settings screen
fluiddot Nov 3, 2023
454ed20
Add image optimization setting to App Settings creen
fluiddot Nov 3, 2023
92aa560
Disable max image setting when image optimization is off
fluiddot Nov 3, 2023
9df1f57
Set optimized value as default for max image size
fluiddot Nov 3, 2023
39b5c63
Use image quality setting when compressing images before upload
fluiddot Nov 3, 2023
b9f0892
Add new event for tracking max image size setting changes
fluiddot Nov 3, 2023
351b740
Display optimize images popup when uploading from device media
fluiddot Nov 3, 2023
c0bd61d
Display optimize images popup when uploading after taking a photo
fluiddot Nov 3, 2023
a3c5fe8
Fix typos in `MediaCoordinator`
fluiddot Nov 3, 2023
1040b88
Add inline comment to image size for upload calculation
fluiddot Nov 6, 2023
bf760b9
Move media settings default values to constants
fluiddot Nov 6, 2023
eaba7b1
Update media setting unit test
fluiddot Nov 6, 2023
637a196
Remove unneeded self reference in `advertiseImageOptimization` handlers
fluiddot Nov 6, 2023
5b2c7a6
Add `MediaSettings` unit tests
fluiddot Nov 7, 2023
2a4dd6b
Update release notes
fluiddot Nov 7, 2023
b6f0127
Update image quality values
fluiddot Nov 7, 2023
3bc4737
Merge branch 'trunk' into add/image-optimization-setting
fluiddot Nov 7, 2023
2ce126f
Use multiplication sign to display dimenstions in upload settings
kean Nov 9, 2023
a51fb8f
Merge pull request #22019 from wordpress-mobile/task/use-multiplicati…
fluiddot Nov 10, 2023
d64232c
Use Leave On option as preferred in image optimization alert
fluiddot Nov 10, 2023
a85f4b1
Remove unused function from `ImageQuality` enum
fluiddot Nov 10, 2023
f29f4d2
Remove casting from event properties passed in `AppSettingsViewContro…
fluiddot Nov 10, 2023
b968b02
Use unique reverse-DNS naming style in updated strings
fluiddot Nov 13, 2023
477777c
Rename constant `defaultMaxImageDimension`
fluiddot Nov 13, 2023
92d4d13
Rename `imageOptimizationSetting`
fluiddot Nov 13, 2023
294a62f
Merge branch 'trunk' into add/image-optimization-setting
fluiddot Nov 13, 2023
63d6250
Update release notes
fluiddot Nov 13, 2023
b71b7f8
Apply code improvements in `AppSettingsViewController`
fluiddot Nov 22, 2023
132bfbc
Improve show/hide animation of image optimization settings
fluiddot Nov 22, 2023
119b325
Merge branch 'trunk' into add/image-optimization-setting
fluiddot Nov 22, 2023
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
1 change: 1 addition & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
-----
* [*] Bug fix: Reader now scrolls to the top when tapping the status bar. [#21914]
* [*] Fix an issue in Menu screen where it fails to create default menu items. [#21949]
* [**] Add Optimize Images setting for image uploads and enable it by default [#21981]

23.6
-----
Expand Down
4 changes: 2 additions & 2 deletions WordPress/Classes/Services/MediaCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ class MediaCoordinator: NSObject {
addMedia(from: asset, post: post, coordinator: coordinator(for: post), analyticsInfo: analyticsInfo)
}

/// Create a `Media` instance from the main context and upload the asset to the Meida Library.
/// Create a `Media` instance from the main context and upload the asset to the Media Library.
///
/// - Warning: This function must be called from the main thread.
///
Expand Down Expand Up @@ -186,7 +186,7 @@ class MediaCoordinator: NSObject {
return media
}

/// Create a `Media` instance and upload the asset to the Meida Library.
/// Create a `Media` instance and upload the asset to the Media Library.
///
/// - SeeAlso: `MediaImportService.createMedia(with:blog:post:receiveUpdate:thumbnailCallback:completion:)`
private func addMedia(from asset: ExportableAsset, blog: Blog, post: AbstractPost?, coordinator: MediaProgressCoordinator, analyticsInfo: MediaAnalyticsInfo? = nil) {
Expand Down
31 changes: 31 additions & 0 deletions WordPress/Classes/Services/MediaHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,37 @@ class MediaHelper: NSObject {
}

}

static func advertiseImageOptimization(completion: @escaping (() -> Void)) {
guard MediaSettings().advertiseImageOptimization else {
completion()
return
}

let title = NSLocalizedString("Keep optimizing images?",
fluiddot marked this conversation as resolved.
Show resolved Hide resolved
comment: "Title of an alert informing users to enable image optimization in uploads.")
let message = NSLocalizedString("Image optimization shrinks images for quicker uploading.\n\nBy default it's enabled but you can change this any time in app settings.",
comment: "Message of an alert informing users to enable image optimization in uploads.")
let turnOffTitle = NSLocalizedString("No, turn off", comment: "Title of button for turning off image optimization, displayed in the alert informing users to enable image optimization in uploads.")
let leaveOnTitle = NSLocalizedString("Yes, leave on", comment: "Title of button for leaving on image optimization, displayed in the alert informing users to enable image optimization in uploads.")

let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
twstokes marked this conversation as resolved.
Show resolved Hide resolved
alert.addAction(UIAlertAction(title: turnOffTitle, style: .default) { _ in
MediaSettings().imageOptimizationSetting = false
WPAnalytics.track(.appSettingsOptimizeImagesPopupTapped, properties: ["option": "off" as
AnyObject])
completion()
})
alert.addAction(UIAlertAction(title: leaveOnTitle, style: .default) { _ in
MediaSettings().imageOptimizationSetting = true
WPAnalytics.track(.appSettingsOptimizeImagesPopupTapped, properties: ["option": "on" as
AnyObject])
completion()
})
alert.presentFromRootViewController()

MediaSettings().advertiseImageOptimization = false
}
}

extension Media {
Expand Down
2 changes: 1 addition & 1 deletion WordPress/Classes/Services/MediaImportService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ class MediaImportService: NSObject {
var options = MediaImageExporter.Options()
options.maximumImageSize = self.exporterMaximumImageSize()
options.stripsGeoLocationIfNeeded = MediaSettings().removeLocationSetting
options.imageCompressionQuality = MediaImportService.preferredImageCompressionQuality
options.imageCompressionQuality = MediaSettings().imageQualityForUpload.doubleValue
return options
}

Expand Down
114 changes: 110 additions & 4 deletions WordPress/Classes/Services/MediaSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,70 @@ import AVFoundation

class MediaSettings: NSObject {
// MARK: - Constants
fileprivate let imageOptimizationKey = "SavedImageOptimizationSetting"
fileprivate let maxImageSizeKey = "SavedMaxImageSizeSetting"
fileprivate let imageQualityKey = "SavedImageQualitySetting"
fileprivate let removeLocationKey = "SavedRemoveLocationSetting"
fileprivate let maxVideoSizeKey = "SavedMaxVideoSizeSetting"
fileprivate let advertiseImageOptimizationKey = "SavedAdvertiseImageOptimization"

fileprivate let defaultImageOptimization = true
fileprivate let defaultMaxImageSize = 2000
fluiddot marked this conversation as resolved.
Show resolved Hide resolved
fileprivate let defaultImageQuality: ImageQuality = .medium
fileprivate let defaultMaxVideoSize: VideoResolution = .sizeOriginal
fileprivate let defaultRemoveLocation = true

fileprivate let minImageDimension = 150
fileprivate let maxImageDimension = 3000

enum ImageQuality: String {
case maximum = "MaximumQuality100"
case high = "HighQuality90"
case medium = "MediumQuality80"
case low = "LowQuality70"

var doubleValue: Double {
switch self {
case .maximum:
return 1.0
case .high:
return 0.9
case .medium:
return 0.8
case .low:
return 0.7
}
}

var description: String {
switch self {
case .maximum:
return NSLocalizedString("Maximum", comment: "Indicates an image will use maximum quality when uploaded.")
case .high:
return NSLocalizedString("High", comment: "Indicates an image will use high quality when uploaded.")
case .medium:
return NSLocalizedString("Medium", comment: "Indicates an image will use medium quality when uploaded.")
case(.low):
return NSLocalizedString("Low", comment: "Indicates an image will use low quality when uploaded.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

80%, which is the current default, is considered optimal for web uploads. I would suggest going with 10% increments where 80% would be "Default", "Standard" or something like that.

Copy link
Contributor Author

@fluiddot fluiddot Nov 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @kean for sharing this useful info 🙇 ! To be honest, I merely chose these quality values based on the ones we have in the Android version (reference). However, as you suggested, it might be interesting to use different values and more close to standards:

  • Maximum/Very High: 100%.
  • High: 90% (following this article, this JPEG quality gives a very high-quality image while gaining a significant reduction on the original 100% file size).
  • Medium (default): 80% (following this article, this JPEG quality gives a greater file size reduction with almost no loss in quality).
  • Low: 70% (following this article, 75% JPEG quality and lower begins to show obvious differences in the image, which can reduce your website user experience).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion applied in b6f0127.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that I removed Very High option as the above four options should be enough to set different quality settings.

}
}

static func imageQuality(from value: Float) -> MediaSettings.ImageQuality {
switch value {
case 1.0:
return .maximum
case 0.9:
return .high
case 0.8:
return .medium
case 0.7:
return .low
default:
return .medium
}
}
fluiddot marked this conversation as resolved.
Show resolved Hide resolved
}

enum VideoResolution: String {
case size640x480 = "AVAssetExportPreset640x480"
case size1280x720 = "AVAssetExportPreset1280x720"
Expand Down Expand Up @@ -110,13 +166,19 @@ class MediaSettings: NSObject {
/// - Note: if the image doesn't need to be resized, it returns `Int.max`
///
@objc var imageSizeForUpload: Int {
if maxImageSizeSetting >= maxImageDimension {
// When image optimization is enabled, setting the max image size setting to
// the maximum value will be considered as to using the original size.
if !imageOptimizationSetting || maxImageSizeSetting >= maxImageDimension {
return Int.max
} else {
return maxImageSizeSetting
}
}

var imageQualityForUpload: ImageQuality {
return imageOptimizationSetting ? imageQualitySetting : .high
}

/// The stored value for the maximum size images can have before uploading.
/// If you set this to `maxImageDimension` or higher, it means images won't
/// be resized on upload.
Expand All @@ -134,7 +196,7 @@ class MediaSettings: NSObject {
database.set(newSize, forKey: maxImageSizeKey)
return Int(newSize)
} else {
return maxImageDimension
return defaultMaxImageSize
}
}
set {
Expand All @@ -148,7 +210,7 @@ class MediaSettings: NSObject {
if let savedRemoveLocation = database.object(forKey: removeLocationKey) as? Bool {
return savedRemoveLocation
} else {
return true
return defaultRemoveLocation
}
}
set {
Expand All @@ -160,12 +222,56 @@ class MediaSettings: NSObject {
get {
guard let savedSize = database.object(forKey: maxVideoSizeKey) as? String,
let videoSize = VideoResolution(rawValue: savedSize) else {
return .sizeOriginal
return defaultMaxVideoSize
}
return videoSize
}
set {
database.set(newValue.rawValue, forKey: maxVideoSizeKey)
}
}

var imageOptimizationSetting: Bool {
fluiddot marked this conversation as resolved.
Show resolved Hide resolved
get {
if let savedImageOptimization = database.object(forKey: imageOptimizationKey) as? Bool {
return savedImageOptimization
} else {
return defaultImageOptimization
}
}
set {
database.set(newValue, forKey: imageOptimizationKey)

// If the user changes this setting manually, we disable the image optimization popup.
if advertiseImageOptimization {
advertiseImageOptimization = false
}
}
}

var imageQualitySetting: ImageQuality {
get {
guard let savedQuality = database.object(forKey: imageQualityKey) as? String,
let imageQuality = ImageQuality(rawValue: savedQuality) else {
return defaultImageQuality
}
return imageQuality
}
set {
database.set(newValue.rawValue, forKey: imageQualityKey)
}
}

var advertiseImageOptimization: Bool {
get {
if let savedAdvertiseImageOptimization = database.object(forKey: advertiseImageOptimizationKey) as? Bool {
return savedAdvertiseImageOptimization
} else {
return true
}
}
set {
database.set(newValue, forKey: advertiseImageOptimizationKey)
}
}
}
12 changes: 12 additions & 0 deletions WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -283,10 +283,14 @@ import Foundation
case accountCloseCompleted

// App Settings
case appSettingsOptimizeImagesChanged
case appSettingsMaxImageSizeChanged
case appSettingsImageQualityChanged
case appSettingsClearMediaCacheTapped
case appSettingsClearSpotlightIndexTapped
case appSettingsClearSiriSuggestionsTapped
case appSettingsOpenDeviceSettingsTapped
case appSettingsOptimizeImagesPopupTapped

// Notifications
case notificationsPreviousTapped
Expand Down Expand Up @@ -1007,6 +1011,14 @@ import Foundation
return "app_settings_clear_siri_suggestions_tapped"
case .appSettingsOpenDeviceSettingsTapped:
return "app_settings_open_device_settings_tapped"
case .appSettingsOptimizeImagesChanged:
return "app_settings_optimize_images_changed"
case .appSettingsMaxImageSizeChanged:
return "app_settings_max_image_size_changed"
case .appSettingsImageQualityChanged:
return "app_settings_image_quality_changed"
case .appSettingsOptimizeImagesPopupTapped:
return "app_settings_optimize_images_popup_tapped"

// Account Close
case .accountCloseTapped:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,10 @@ extension GutenbergMediaPickerHelper: ImagePickerControllerDelegate {
switch mediaType {
case UTType.image.identifier:
if let image = info[.originalImage] as? UIImage {
self.didPickMediaCallback?([image])
self.didPickMediaCallback = nil
MediaHelper.advertiseImageOptimization() { [self] in
self.didPickMediaCallback?([image])
self.didPickMediaCallback = nil
}
}

case UTType.movie.identifier:
Expand Down Expand Up @@ -191,8 +193,21 @@ extension GutenbergMediaPickerHelper: PHPickerViewControllerDelegate {
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
context.dismiss(animated: true)

didPickMediaCallback?(results.map(\.itemProvider))
didPickMediaCallback = nil
guard results.count > 0 else {
return
}

let mediaFilter = picker.configuration.filter
if mediaFilter == PHPickerFilter(.all) || mediaFilter == PHPickerFilter(.image) {
MediaHelper.advertiseImageOptimization() { [self] in
didPickMediaCallback?(results.map(\.itemProvider))
didPickMediaCallback = nil
}
}
else {
didPickMediaCallback?(results.map(\.itemProvider))
didPickMediaCallback = nil
}
}
}

Expand Down
Loading