1- import { SaveFormat } from 'expo-image-manipulator'
2-
31import { createLimit } from '#/lib/async/limit'
4- import { moveIfNecessary } from '#/lib/media/image/cache'
5- import { renderImage } from '#/lib/media/image-manipulator'
2+ import { compressToFit } from '#/lib/media/image/compressToFit'
63import { type PickerImage } from '#/lib/media/picker.shared'
7- import { getUriSize } from '#/lib/media/uriSize'
84import { type ComposerImage } from '#/state/gallery'
95
106/*
11- * Each image being compressed holds full-size decoded bitmaps while it renders
12- * (about 48 MB each for a 12 MP photo on iOS), and iOS starts decoding as
13- * soon as a manipulation is created. Compressing a 10-image gallery all at
14- * once is enough to get the app killed for memory (APP-3190). iOS encodes on a
15- * single serial queue anyway, so running more than two at once buys little.
7+ * Each image being compressed holds at least one full-size decoded bitmap
8+ * (about 48 MB for a 12 MP photo), so compressing a 10-image gallery all at
9+ * once is enough to get the app killed for memory (APP-3190).
1610 */
1711const MAX_CONCURRENT_COMPRESSIONS = 2
1812const limit = createLimit ( MAX_CONCURRENT_COMPRESSIONS )
@@ -26,105 +20,5 @@ export function compressImage(
2620 img : ComposerImage ,
2721 opts : { maxDimension : number ; maxSize : number } ,
2822) : Promise < PickerImage > {
29- return limit ( ( ) => compressImageNow ( img , opts ) )
30- }
31-
32- async function compressImageNow (
33- img : ComposerImage ,
34- { maxDimension, maxSize} : { maxDimension : number ; maxSize : number } ,
35- ) : Promise < PickerImage > {
36- const source = img . transformed || img . source
37-
38- let attempts = 0
39- /*
40- * Seeded from `maxDimension` but shrunk per attempt below, so keep the
41- * passed-in value pristine.
42- */
43- let currentDimension = maxDimension
44- const maxBytes = maxSize
45-
46- let minQualityPercentage = 0
47- let maxQualityPercentage = 101 // exclusive
48- let newDataUri
49-
50- while ( maxQualityPercentage - minQualityPercentage > 1 ) {
51- if ( attempts >= 4 ) break
52-
53- const [ w , h ] = containImageRes (
54- source . width ,
55- source . height ,
56- currentDimension ,
57- )
58- const qualityPercentage = Math . round (
59- ( maxQualityPercentage + minQualityPercentage ) / 2 ,
60- )
61-
62- /*
63- * In the event the image doesn't compress well, we want to avoid
64- * unecessary iterations. In this case, binary search will check 51, 26,
65- * 13(rounded). We don't want to go below 25, so if we've halved to 13,
66- * reset the loop and reduce the image dimensions instead.
67- */
68- if ( qualityPercentage <= 13 ) {
69- minQualityPercentage = 0
70- maxQualityPercentage = 101
71- attempts ++
72- /*
73- * max.width -> 0.8x -> 0.64x -> 0.512x -> ~0.41x
74- * e.g. 4000px -> 3200px -> 2560px -> 2048px -> ~1638px
75- */
76- currentDimension = Math . floor ( currentDimension * 0.8 )
77- continue
78- }
79-
80- const res = await renderImage (
81- source . path ,
82- context => context . resize ( { width : w , height : h } ) ,
83- {
84- compress : qualityPercentage / 100 ,
85- format : SaveFormat . JPEG ,
86- } ,
87- )
88-
89- /*
90- * Measure the written file rather than requesting base64, which builds a
91- * base64 copy of every pass and sends it to JS (Android even encodes the
92- * JPEG a second time to do it).
93- */
94- const size = await getUriSize ( res . uri )
95- if ( size <= maxBytes ) {
96- minQualityPercentage = qualityPercentage
97- newDataUri = {
98- path : await moveIfNecessary ( res . uri ) ,
99- width : res . width ,
100- height : res . height ,
101- mime : 'image/jpeg' ,
102- size,
103- }
104- } else {
105- maxQualityPercentage = qualityPercentage
106- }
107- }
108-
109- if ( newDataUri ) {
110- return newDataUri
111- }
112-
113- throw new Error ( `Unable to compress image` )
114- }
115-
116- function containImageRes (
117- w : number ,
118- h : number ,
119- max : number ,
120- ) : [ width : number , height : number ] {
121- let scale = 1
122-
123- if ( w > max || h > max ) {
124- scale = w > h ? max / w : max / h
125- w = Math . floor ( w * scale )
126- h = Math . floor ( h * scale )
127- }
128-
129- return [ w , h ]
23+ return limit ( ( ) => compressToFit ( img . transformed || img . source , opts ) )
13024}
0 commit comments