react-native-photo-video-editor 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/PhotoVideoEditor.podspec +1 -0
  3. package/README.md +10 -6
  4. package/THIRD_PARTY_NOTICES.md +20 -0
  5. package/android/build.gradle +1 -1
  6. package/android/src/main/AndroidManifest.xml +1 -1
  7. package/android/src/main/assets/Stickers/AUTUMN_1.png +0 -0
  8. package/android/src/main/assets/Stickers/AUTUMN_10.png +0 -0
  9. package/android/src/main/assets/Stickers/AUTUMN_11.png +0 -0
  10. package/android/src/main/assets/Stickers/AUTUMN_12.png +0 -0
  11. package/android/src/main/assets/Stickers/AUTUMN_13.png +0 -0
  12. package/android/src/main/assets/Stickers/AUTUMN_14.png +0 -0
  13. package/android/src/main/assets/Stickers/AUTUMN_15.png +0 -0
  14. package/android/src/main/assets/Stickers/AUTUMN_16.png +0 -0
  15. package/android/src/main/assets/Stickers/AUTUMN_17.png +0 -0
  16. package/android/src/main/assets/Stickers/AUTUMN_18.png +0 -0
  17. package/android/src/main/assets/Stickers/AUTUMN_2.png +0 -0
  18. package/android/src/main/assets/Stickers/AUTUMN_3.png +0 -0
  19. package/android/src/main/assets/Stickers/AUTUMN_4.png +0 -0
  20. package/android/src/main/assets/Stickers/AUTUMN_5.png +0 -0
  21. package/android/src/main/assets/Stickers/AUTUMN_6.png +0 -0
  22. package/android/src/main/assets/Stickers/AUTUMN_7.png +0 -0
  23. package/android/src/main/assets/Stickers/AUTUMN_8.png +0 -0
  24. package/android/src/main/assets/Stickers/AUTUMN_9.png +0 -0
  25. package/android/src/main/assets/Stickers/LOL_1.png +0 -0
  26. package/android/src/main/assets/Stickers/LOL_10.png +0 -0
  27. package/android/src/main/assets/Stickers/LOL_11.png +0 -0
  28. package/android/src/main/assets/Stickers/LOL_12.png +0 -0
  29. package/android/src/main/assets/Stickers/LOL_13.png +0 -0
  30. package/android/src/main/assets/Stickers/LOL_14.png +0 -0
  31. package/android/src/main/assets/Stickers/LOL_15.png +0 -0
  32. package/android/src/main/assets/Stickers/LOL_16.png +0 -0
  33. package/android/src/main/assets/Stickers/LOL_17.png +0 -0
  34. package/android/src/main/assets/Stickers/LOL_18.png +0 -0
  35. package/android/src/main/assets/Stickers/LOL_19.png +0 -0
  36. package/android/src/main/assets/Stickers/LOL_2.png +0 -0
  37. package/android/src/main/assets/Stickers/LOL_3.png +0 -0
  38. package/android/src/main/assets/Stickers/LOL_4.png +0 -0
  39. package/android/src/main/assets/Stickers/LOL_5.png +0 -0
  40. package/android/src/main/assets/Stickers/LOL_6.png +0 -0
  41. package/android/src/main/assets/Stickers/LOL_7.png +0 -0
  42. package/android/src/main/assets/Stickers/LOL_8.png +0 -0
  43. package/android/src/main/assets/Stickers/LOL_9.png +0 -0
  44. package/android/src/main/assets/Stickers/SPRING_1.png +0 -0
  45. package/android/src/main/assets/Stickers/SPRING_2.png +0 -0
  46. package/android/src/main/assets/Stickers/SPRING_3.png +0 -0
  47. package/android/src/main/assets/Stickers/SPRING_4.png +0 -0
  48. package/android/src/main/assets/Stickers/SPRING_5.png +0 -0
  49. package/android/src/main/assets/Stickers/SPRING_6.png +0 -0
  50. package/android/src/main/assets/Stickers/SUMMER_1.png +0 -0
  51. package/android/src/main/assets/Stickers/SUMMER_10.png +0 -0
  52. package/android/src/main/assets/Stickers/SUMMER_11.png +0 -0
  53. package/android/src/main/assets/Stickers/SUMMER_12.png +0 -0
  54. package/android/src/main/assets/Stickers/SUMMER_13.png +0 -0
  55. package/android/src/main/assets/Stickers/SUMMER_14.png +0 -0
  56. package/android/src/main/assets/Stickers/SUMMER_15.png +0 -0
  57. package/android/src/main/assets/Stickers/SUMMER_16.png +0 -0
  58. package/android/src/main/assets/Stickers/SUMMER_17.png +0 -0
  59. package/android/src/main/assets/Stickers/SUMMER_18.png +0 -0
  60. package/android/src/main/assets/Stickers/SUMMER_19.png +0 -0
  61. package/android/src/main/assets/Stickers/SUMMER_2.png +0 -0
  62. package/android/src/main/assets/Stickers/SUMMER_20.png +0 -0
  63. package/android/src/main/assets/Stickers/SUMMER_21.png +0 -0
  64. package/android/src/main/assets/Stickers/SUMMER_3.png +0 -0
  65. package/android/src/main/assets/Stickers/SUMMER_4.png +0 -0
  66. package/android/src/main/assets/Stickers/SUMMER_5.png +0 -0
  67. package/android/src/main/assets/Stickers/SUMMER_6.png +0 -0
  68. package/android/src/main/assets/Stickers/SUMMER_7.png +0 -0
  69. package/android/src/main/assets/Stickers/SUMMER_8.png +0 -0
  70. package/android/src/main/assets/Stickers/SUMMER_9.png +0 -0
  71. package/android/src/main/assets/Stickers/TEXT_1.png +0 -0
  72. package/android/src/main/assets/Stickers/TEXT_12.png +0 -0
  73. package/android/src/main/assets/Stickers/TEXT_13.png +0 -0
  74. package/android/src/main/assets/Stickers/TEXT_14.png +0 -0
  75. package/android/src/main/assets/Stickers/TEXT_15.png +0 -0
  76. package/android/src/main/assets/Stickers/TEXT_16.png +0 -0
  77. package/android/src/main/assets/Stickers/TEXT_2.png +0 -0
  78. package/android/src/main/assets/Stickers/TEXT_25.png +0 -0
  79. package/android/src/main/assets/Stickers/TEXT_26.png +0 -0
  80. package/android/src/main/assets/Stickers/TEXT_27.png +0 -0
  81. package/android/src/main/assets/Stickers/TEXT_28.png +0 -0
  82. package/android/src/main/assets/Stickers/TEXT_29.png +0 -0
  83. package/android/src/main/assets/Stickers/TEXT_3.png +0 -0
  84. package/android/src/main/assets/Stickers/TEXT_30.png +0 -0
  85. package/android/src/main/assets/Stickers/TEXT_31.png +0 -0
  86. package/android/src/main/assets/Stickers/TEXT_32.png +0 -0
  87. package/android/src/main/assets/Stickers/TEXT_7.png +0 -0
  88. package/android/src/main/assets/Stickers/TEXT_8.png +0 -0
  89. package/android/src/main/assets/Stickers/TEXT_9.png +0 -0
  90. package/android/src/main/java/com/photovideoeditor/editor/PhotoVideoEditorActivity.kt +901 -651
  91. package/android/src/main/java/com/photovideoeditor/photo/export/PhotoExporter.kt +8 -23
  92. package/android/src/main/java/com/photovideoeditor/photo/render/CropGeometry.kt +71 -0
  93. package/android/src/main/java/com/photovideoeditor/photo/render/OverlayGeometry.kt +36 -0
  94. package/android/src/main/java/com/photovideoeditor/photo/render/PhotoEditSession.kt +5 -7
  95. package/android/src/main/java/com/photovideoeditor/photo/render/PhotoFilterPresets.kt +71 -41
  96. package/android/src/main/java/com/photovideoeditor/photo/render/PhotoLayer.kt +5 -1
  97. package/android/src/main/java/com/photovideoeditor/photo/render/PhotoLayerRenderer.kt +24 -1
  98. package/android/src/main/java/com/photovideoeditor/photo/render/PhotoTransformRenderer.kt +33 -0
  99. package/android/src/main/java/com/photovideoeditor/photo/render/PhotoTransformState.kt +6 -1
  100. package/android/src/main/java/com/photovideoeditor/photo/ui/CropOverlayView.kt +112 -122
  101. package/android/src/main/java/com/photovideoeditor/photo/ui/DrawOverlayView.kt +66 -0
  102. package/android/src/main/java/com/photovideoeditor/photo/ui/LayerOverlayView.kt +63 -14
  103. package/android/src/main/java/com/photovideoeditor/photo/ui/ZoomableImageView.kt +10 -0
  104. package/android/src/main/java/com/photovideoeditor/stickers/OnlineStickerSheet.kt +135 -104
  105. package/android/src/main/java/com/photovideoeditor/ui/components/CropPanel.kt +122 -0
  106. package/android/src/main/java/com/photovideoeditor/ui/components/EditorPanel.kt +250 -0
  107. package/android/src/main/java/com/photovideoeditor/ui/components/EditorToolSlider.kt +103 -0
  108. package/android/src/main/java/com/photovideoeditor/video/export/VideoExporter.kt +38 -11
  109. package/android/src/main/java/com/photovideoeditor/video/render/VideoCropEffects.kt +35 -0
  110. package/android/src/main/java/com/photovideoeditor/video/render/VideoEditSession.kt +10 -0
  111. package/android/src/main/java/com/photovideoeditor/video/render/VideoTransformState.kt +1 -0
  112. package/android/src/main/res/drawable/ic_draw.xml +6 -0
  113. package/android/src/main/res/drawable/ic_pause.xml +7 -0
  114. package/android/src/main/res/drawable/ic_play.xml +7 -0
  115. package/docs/api.md +1 -1
  116. package/docs/editor-ui-validation.md +22 -0
  117. package/docs/photo-editor.md +5 -6
  118. package/ios/Editor/PhotoVideoEditorViewController.swift +918 -494
  119. package/ios/Editor/UI/CropPanel.swift +97 -0
  120. package/ios/Editor/UI/EditorPanel.swift +334 -0
  121. package/ios/Editor/UI/EditorToolSlider.swift +22 -0
  122. package/ios/Editor/UI/TextEditorSheet.swift +84 -0
  123. package/ios/Photo/Export/PhotoExporter.swift +3 -3
  124. package/ios/Photo/Rendering/CropGeometry.swift +48 -0
  125. package/ios/Photo/Rendering/PhotoEditSession.swift +21 -10
  126. package/ios/Photo/Rendering/PhotoFilterPresets.swift +53 -23
  127. package/ios/Photo/Rendering/PhotoLayer.swift +14 -2
  128. package/ios/Photo/Rendering/PhotoLayerRenderer.swift +16 -1
  129. package/ios/Photo/Rendering/PhotoTransformState.swift +5 -0
  130. package/ios/Photo/UI/CropOverlayView.swift +98 -134
  131. package/ios/Photo/UI/DrawOverlayView.swift +48 -0
  132. package/ios/Photo/UI/FilterThumbnailLoader.swift +62 -0
  133. package/ios/Photo/UI/LayerOverlayView.swift +36 -11
  134. package/ios/Photo/UI/ZoomableImageView.swift +19 -1
  135. package/ios/Resources/Stickers/AUTUMN_1.png +0 -0
  136. package/ios/Resources/Stickers/AUTUMN_10.png +0 -0
  137. package/ios/Resources/Stickers/AUTUMN_11.png +0 -0
  138. package/ios/Resources/Stickers/AUTUMN_12.png +0 -0
  139. package/ios/Resources/Stickers/AUTUMN_13.png +0 -0
  140. package/ios/Resources/Stickers/AUTUMN_14.png +0 -0
  141. package/ios/Resources/Stickers/AUTUMN_15.png +0 -0
  142. package/ios/Resources/Stickers/AUTUMN_16.png +0 -0
  143. package/ios/Resources/Stickers/AUTUMN_17.png +0 -0
  144. package/ios/Resources/Stickers/AUTUMN_18.png +0 -0
  145. package/ios/Resources/Stickers/AUTUMN_2.png +0 -0
  146. package/ios/Resources/Stickers/AUTUMN_3.png +0 -0
  147. package/ios/Resources/Stickers/AUTUMN_4.png +0 -0
  148. package/ios/Resources/Stickers/AUTUMN_5.png +0 -0
  149. package/ios/Resources/Stickers/AUTUMN_6.png +0 -0
  150. package/ios/Resources/Stickers/AUTUMN_7.png +0 -0
  151. package/ios/Resources/Stickers/AUTUMN_8.png +0 -0
  152. package/ios/Resources/Stickers/AUTUMN_9.png +0 -0
  153. package/ios/Resources/Stickers/LOL_1.png +0 -0
  154. package/ios/Resources/Stickers/LOL_10.png +0 -0
  155. package/ios/Resources/Stickers/LOL_11.png +0 -0
  156. package/ios/Resources/Stickers/LOL_12.png +0 -0
  157. package/ios/Resources/Stickers/LOL_13.png +0 -0
  158. package/ios/Resources/Stickers/LOL_14.png +0 -0
  159. package/ios/Resources/Stickers/LOL_15.png +0 -0
  160. package/ios/Resources/Stickers/LOL_16.png +0 -0
  161. package/ios/Resources/Stickers/LOL_17.png +0 -0
  162. package/ios/Resources/Stickers/LOL_18.png +0 -0
  163. package/ios/Resources/Stickers/LOL_19.png +0 -0
  164. package/ios/Resources/Stickers/LOL_2.png +0 -0
  165. package/ios/Resources/Stickers/LOL_3.png +0 -0
  166. package/ios/Resources/Stickers/LOL_4.png +0 -0
  167. package/ios/Resources/Stickers/LOL_5.png +0 -0
  168. package/ios/Resources/Stickers/LOL_6.png +0 -0
  169. package/ios/Resources/Stickers/LOL_7.png +0 -0
  170. package/ios/Resources/Stickers/LOL_8.png +0 -0
  171. package/ios/Resources/Stickers/LOL_9.png +0 -0
  172. package/ios/Resources/Stickers/SPRING_1.png +0 -0
  173. package/ios/Resources/Stickers/SPRING_2.png +0 -0
  174. package/ios/Resources/Stickers/SPRING_3.png +0 -0
  175. package/ios/Resources/Stickers/SPRING_4.png +0 -0
  176. package/ios/Resources/Stickers/SPRING_5.png +0 -0
  177. package/ios/Resources/Stickers/SPRING_6.png +0 -0
  178. package/ios/Resources/Stickers/SUMMER_1.png +0 -0
  179. package/ios/Resources/Stickers/SUMMER_10.png +0 -0
  180. package/ios/Resources/Stickers/SUMMER_11.png +0 -0
  181. package/ios/Resources/Stickers/SUMMER_12.png +0 -0
  182. package/ios/Resources/Stickers/SUMMER_13.png +0 -0
  183. package/ios/Resources/Stickers/SUMMER_14.png +0 -0
  184. package/ios/Resources/Stickers/SUMMER_15.png +0 -0
  185. package/ios/Resources/Stickers/SUMMER_16.png +0 -0
  186. package/ios/Resources/Stickers/SUMMER_17.png +0 -0
  187. package/ios/Resources/Stickers/SUMMER_18.png +0 -0
  188. package/ios/Resources/Stickers/SUMMER_19.png +0 -0
  189. package/ios/Resources/Stickers/SUMMER_2.png +0 -0
  190. package/ios/Resources/Stickers/SUMMER_20.png +0 -0
  191. package/ios/Resources/Stickers/SUMMER_21.png +0 -0
  192. package/ios/Resources/Stickers/SUMMER_3.png +0 -0
  193. package/ios/Resources/Stickers/SUMMER_4.png +0 -0
  194. package/ios/Resources/Stickers/SUMMER_5.png +0 -0
  195. package/ios/Resources/Stickers/SUMMER_6.png +0 -0
  196. package/ios/Resources/Stickers/SUMMER_7.png +0 -0
  197. package/ios/Resources/Stickers/SUMMER_8.png +0 -0
  198. package/ios/Resources/Stickers/SUMMER_9.png +0 -0
  199. package/ios/Resources/Stickers/TEXT_1.png +0 -0
  200. package/ios/Resources/Stickers/TEXT_12.png +0 -0
  201. package/ios/Resources/Stickers/TEXT_13.png +0 -0
  202. package/ios/Resources/Stickers/TEXT_14.png +0 -0
  203. package/ios/Resources/Stickers/TEXT_15.png +0 -0
  204. package/ios/Resources/Stickers/TEXT_16.png +0 -0
  205. package/ios/Resources/Stickers/TEXT_2.png +0 -0
  206. package/ios/Resources/Stickers/TEXT_25.png +0 -0
  207. package/ios/Resources/Stickers/TEXT_26.png +0 -0
  208. package/ios/Resources/Stickers/TEXT_27.png +0 -0
  209. package/ios/Resources/Stickers/TEXT_28.png +0 -0
  210. package/ios/Resources/Stickers/TEXT_29.png +0 -0
  211. package/ios/Resources/Stickers/TEXT_3.png +0 -0
  212. package/ios/Resources/Stickers/TEXT_30.png +0 -0
  213. package/ios/Resources/Stickers/TEXT_31.png +0 -0
  214. package/ios/Resources/Stickers/TEXT_32.png +0 -0
  215. package/ios/Resources/Stickers/TEXT_7.png +0 -0
  216. package/ios/Resources/Stickers/TEXT_8.png +0 -0
  217. package/ios/Resources/Stickers/TEXT_9.png +0 -0
  218. package/ios/Resources/Stickers/a_pety_icon.png +0 -0
  219. package/ios/Stickers/OnlineStickerSheetViewController.swift +102 -159
  220. package/ios/Video/Export/VideoExporter.swift +10 -1
  221. package/ios/Video/Render/VideoCropGeometry.swift +48 -0
  222. package/ios/Video/Render/VideoTransformState.swift +1 -0
  223. package/lib/module/index.js +2 -2
  224. package/lib/module/index.js.map +1 -1
  225. package/lib/typescript/src/types.d.ts +2 -2
  226. package/lib/typescript/src/types.d.ts.map +1 -1
  227. package/package.json +2 -1
  228. package/src/index.ts +1 -1
  229. package/src/types.ts +2 -2
  230. package/android/src/main/java/com/photovideoeditor/stickers/OpenMojiCatalog.kt +0 -172
  231. package/android/src/main/java/com/photovideoeditor/stickers/RemoteImageLoader.kt +0 -110
  232. package/ios/Stickers/OpenMojiCatalog.swift +0 -188
  233. package/ios/Stickers/RemoteImageLoader.swift +0 -127
@@ -12,6 +12,7 @@ import com.photovideoeditor.photo.render.PhotoAdjustmentRenderer
12
12
  import com.photovideoeditor.photo.render.PhotoAdjustments
13
13
  import com.photovideoeditor.photo.render.PhotoLayer
14
14
  import com.photovideoeditor.photo.render.PhotoLayerRenderer
15
+ import com.photovideoeditor.photo.render.PhotoTransformRenderer
15
16
  import com.photovideoeditor.photo.render.PhotoTransformState
16
17
  import org.json.JSONObject
17
18
  import java.io.File
@@ -68,11 +69,11 @@ object PhotoExporter {
68
69
  ?: throw PhotoExportException("E_OUT_OF_MEMORY", "The photo is too large to process on this device.")
69
70
 
70
71
  val upright = applyExifOrientation(decoded, exifOrientation)
71
- val transformed = applyTransform(upright, transform)
72
+ val transformed = PhotoTransformRenderer.transform(upright, transform)
72
73
  val adjusted = PhotoAdjustmentRenderer.apply(transformed, adjustments)
73
- val layered = PhotoLayerRenderer.render(adjusted, layers) { uri -> resolveImageUri(context, uri) }
74
- val cropped = cropBitmap(layered, transform)
75
- val resized = resizeToFit(cropped, maxWidth, maxHeight)
74
+ val cropped = PhotoTransformRenderer.crop(adjusted, transform)
75
+ val layered = PhotoLayerRenderer.render(cropped, layers) { uri -> resolveImageUri(context, uri) }
76
+ val resized = resizeToFit(layered, maxWidth, maxHeight)
76
77
 
77
78
  val format = exportOptions?.optString("imageFormat", "jpeg")?.lowercase() ?: "jpeg"
78
79
  val quality = qualityFor(exportOptions?.optString("quality", "high") ?: "high")
@@ -93,9 +94,9 @@ object PhotoExporter {
93
94
  if (decoded !== upright) decoded.recycle()
94
95
  if (upright !== transformed) upright.recycle()
95
96
  if (transformed !== adjusted) transformed.recycle()
96
- if (adjusted !== layered) adjusted.recycle()
97
- if (layered !== cropped) layered.recycle()
98
- if (cropped !== resized) cropped.recycle()
97
+ if (adjusted !== cropped) adjusted.recycle()
98
+ if (cropped !== layered) cropped.recycle()
99
+ if (layered !== resized) layered.recycle()
99
100
 
100
101
  return PhotoExportResult(
101
102
  uri = Uri.fromFile(outputFile).toString(),
@@ -130,22 +131,6 @@ object PhotoExporter {
130
131
  return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
131
132
  }
132
133
 
133
- private fun applyTransform(bitmap: Bitmap, transform: PhotoTransformState): Bitmap {
134
- if (transform.rotationDegrees == 0 && transform.straightenDegrees == 0f) return bitmap
135
- return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, transform.toMatrix(), true)
136
- }
137
-
138
- private fun cropBitmap(bitmap: Bitmap, transform: PhotoTransformState): Bitmap {
139
- if (transform.cropLeft <= 0f && transform.cropTop <= 0f && transform.cropRight >= 1f && transform.cropBottom >= 1f) {
140
- return bitmap
141
- }
142
- val left = (transform.cropLeft * bitmap.width).roundToInt().coerceIn(0, bitmap.width - 1)
143
- val top = (transform.cropTop * bitmap.height).roundToInt().coerceIn(0, bitmap.height - 1)
144
- val right = (transform.cropRight * bitmap.width).roundToInt().coerceIn(left + 1, bitmap.width)
145
- val bottom = (transform.cropBottom * bitmap.height).roundToInt().coerceIn(top + 1, bitmap.height)
146
- return Bitmap.createBitmap(bitmap, left, top, right - left, bottom - top)
147
- }
148
-
149
134
  private fun resizeToFit(bitmap: Bitmap, maxWidth: Int?, maxHeight: Int?): Bitmap {
150
135
  if (maxWidth == null && maxHeight == null) return bitmap
151
136
  val widthLimit = maxWidth ?: bitmap.width
@@ -0,0 +1,71 @@
1
+ package com.photovideoeditor.photo.render
2
+
3
+ import kotlin.math.abs
4
+ import kotlin.math.cos
5
+ import kotlin.math.sin
6
+
7
+ /** Resolution-independent crop constraints shared by gestures and rendering. */
8
+ object CropGeometry {
9
+ data class Rect(val left: Float, val top: Float, val right: Float, val bottom: Float) {
10
+ val width get() = right - left
11
+ val height get() = bottom - top
12
+ }
13
+ enum class Handle { MOVE, TOP, RIGHT, BOTTOM, LEFT, TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT }
14
+
15
+ fun fillScale(width: Float, height: Float, degrees: Float): Float {
16
+ val radians = Math.toRadians(degrees.toDouble())
17
+ val c = abs(cos(radians)).toFloat(); val s = abs(sin(radians)).toFloat()
18
+ return maxOf(c + height / width * s, c + width / height * s)
19
+ }
20
+
21
+ fun fit(bounds: Rect, ratio: Float?): Rect {
22
+ if (ratio == null || ratio <= 0) return bounds
23
+ val width = minOf(bounds.width, bounds.height * ratio)
24
+ val height = width / ratio
25
+ val x = (bounds.left + bounds.right - width) / 2
26
+ val y = (bounds.top + bounds.bottom - height) / 2
27
+ return Rect(x, y, x + width, y + height)
28
+ }
29
+
30
+ /** Keeps the opposite edge/corner anchored; clamps motion instead of shifting a resized box. */
31
+ fun resize(r: Rect, bounds: Rect, handle: Handle, dx: Float, dy: Float, ratio: Float?, minimum: Float): Rect {
32
+ if (handle == Handle.MOVE) {
33
+ val x = dx.coerceIn(bounds.left - r.left, bounds.right - r.right)
34
+ val y = dy.coerceIn(bounds.top - r.top, bounds.bottom - r.bottom)
35
+ return Rect(r.left + x, r.top + y, r.right + x, r.bottom + y)
36
+ }
37
+ val left = handle in listOf(Handle.LEFT, Handle.TOP_LEFT, Handle.BOTTOM_LEFT)
38
+ val right = handle in listOf(Handle.RIGHT, Handle.TOP_RIGHT, Handle.BOTTOM_RIGHT)
39
+ val top = handle in listOf(Handle.TOP, Handle.TOP_LEFT, Handle.TOP_RIGHT)
40
+ val bottom = handle in listOf(Handle.BOTTOM, Handle.BOTTOM_LEFT, Handle.BOTTOM_RIGHT)
41
+ var l = r.left + if (left) dx else 0f
42
+ var t = r.top + if (top) dy else 0f
43
+ var rr = r.right + if (right) dx else 0f
44
+ var b = r.bottom + if (bottom) dy else 0f
45
+ if (ratio != null && ratio > 0) {
46
+ if ((left || right) && (top || bottom)) {
47
+ val deltaW = if (left) -dx else dx
48
+ val deltaH = if (top) -dy else dy
49
+ val width = r.width + if (abs(deltaW) > abs(deltaH * ratio)) deltaW else deltaH * ratio
50
+ if (left) l = r.right - width else rr = r.left + width
51
+ if (top) t = r.bottom - width / ratio else b = r.top + width / ratio
52
+ } else if (left || right) {
53
+ val height = (rr - l) / ratio
54
+ t = (r.top + r.bottom - height) / 2; b = t + height
55
+ } else {
56
+ val width = (b - t) * ratio
57
+ l = (r.left + r.right - width) / 2; rr = l + width
58
+ }
59
+ }
60
+ // All constraints are linear along the drag; interpolation preserves the ratio and anchor.
61
+ var fraction = 1f
62
+ fun limit(before: Float, after: Float, minimumValue: Float) {
63
+ if (after < minimumValue && after < before) fraction = minOf(fraction, ((before - minimumValue) / (before - after)).coerceIn(0f, 1f))
64
+ }
65
+ limit(r.left, l, bounds.left); limit(r.top, t, bounds.top)
66
+ limit(-r.right, -rr, -bounds.right); limit(-r.bottom, -b, -bounds.bottom)
67
+ limit(r.width, rr - l, minOf(minimum, r.width)); limit(r.height, b - t, minOf(minimum, r.height))
68
+ return Rect(r.left + (l - r.left) * fraction, r.top + (t - r.top) * fraction,
69
+ r.right + (rr - r.right) * fraction, r.bottom + (b - r.bottom) * fraction)
70
+ }
71
+ }
@@ -2,6 +2,16 @@ package com.photovideoeditor.photo.render
2
2
 
3
3
  /** Pure coordinate mapping shared by preview and export renderers. */
4
4
  object OverlayGeometry {
5
+ /** New sticker width is 30% of media width; persisted layers keep their existing scale. */
6
+ fun initialStickerScale(width: Float, height: Float): Float =
7
+ if (width > 0 && height > 0) 0.3f * width / (0.18f * minOf(width, height)) else 0.3f / 0.18f
8
+
9
+ fun stickerSize(shortSide: Float, aspectRatio: Float): Pair<Float, Float> {
10
+ val width = shortSide * 0.18f
11
+ val aspect = aspectRatio.takeIf { it.isFinite() && it > 0f } ?: 1f
12
+ return width to (width / aspect)
13
+ }
14
+
5
15
  data class Transform(
6
16
  val centerX: Float,
7
17
  val centerY: Float,
@@ -11,6 +21,32 @@ object OverlayGeometry {
11
21
  val shortSide: Float,
12
22
  )
13
23
 
24
+ /**
25
+ * How to size the bitmap that [PhotoLayerRenderer] composites overlays onto for a
26
+ * [frameWidth]x[frameHeight] output frame, plus the scale a compositor must apply to that bitmap
27
+ * for it to cover the frame exactly.
28
+ *
29
+ * [PhotoLayerRenderer] works in coordinates normalized to the bitmap it is handed, so the invariant
30
+ * every caller must preserve is `width * scaleX == frameWidth` (and likewise for height): a bitmap
31
+ * that ends up covering less than the full frame shrinks every layer and pulls it toward the frame
32
+ * centre, which is precisely how exports came to disagree with the preview.
33
+ *
34
+ * [maxEdgePx] bounds the allocation for high-resolution outputs; the returned scale compensates for
35
+ * whatever downscale that forced, so geometry is identical at every resolution and only sharpness
36
+ * varies.
37
+ */
38
+ data class CanvasPlan(val width: Int, val height: Int, val scaleX: Float, val scaleY: Float)
39
+
40
+ fun canvasPlan(frameWidth: Int, frameHeight: Int, maxEdgePx: Float): CanvasPlan {
41
+ val safeWidth = frameWidth.coerceAtLeast(2)
42
+ val safeHeight = frameHeight.coerceAtLeast(2)
43
+ val fit = minOf(1f, maxEdgePx / maxOf(safeWidth, safeHeight))
44
+ val width = (safeWidth * fit).toInt().coerceAtLeast(2)
45
+ val height = (safeHeight * fit).toInt().coerceAtLeast(2)
46
+ // Divide by the rounded size actually allocated, not `fit`, so rounding cannot reintroduce drift.
47
+ return CanvasPlan(width, height, safeWidth.toFloat() / width, safeHeight.toFloat() / height)
48
+ }
49
+
14
50
  fun map(layer: PhotoLayer, frameWidth: Int, frameHeight: Int): Transform = Transform(
15
51
  centerX = layer.x * frameWidth,
16
52
  centerY = layer.y * frameHeight,
@@ -42,11 +42,7 @@ class PhotoEditSession(private val context: Context, sourceUri: String, maxPrevi
42
42
  val base = baseBitmap ?: return null
43
43
  val key = Pair(state, adjustments)
44
44
  cachedBaseBitmap?.let { if (cachedKey == key) return it }
45
- val transformed = if (state.rotationDegrees == 0 && state.straightenDegrees == 0f) {
46
- base
47
- } else {
48
- Bitmap.createBitmap(base, 0, 0, base.width, base.height, state.toMatrix(), true)
49
- }
45
+ val transformed = PhotoTransformRenderer.transform(base, state)
50
46
  val adjusted = PhotoAdjustmentRenderer.apply(transformed, adjustments)
51
47
  cachedBaseBitmap = adjusted
52
48
  cachedKey = key
@@ -54,8 +50,10 @@ class PhotoEditSession(private val context: Context, sourceUri: String, maxPrevi
54
50
  }
55
51
 
56
52
  /** Renders rotate/straighten + adjustments + layers — the crop overlay draws the crop live on top of this. */
57
- fun renderPreview(): Bitmap? {
58
- val base = computeBase() ?: return null
53
+ fun renderPreview(cropping: Boolean = false): Bitmap? {
54
+ val transformed = computeBase() ?: return null
55
+ if (cropping) return transformed
56
+ val base = PhotoTransformRenderer.crop(transformed, state)
59
57
  val layers = layerStack.layers
60
58
  cachedPreviewBitmap?.let { cached ->
61
59
  if (cachedPreviewBase === base && cachedPreviewLayerRevision == layerStack.revision) return cached
@@ -2,63 +2,93 @@ package com.photovideoeditor.photo.render
2
2
 
3
3
  import android.graphics.ColorMatrix
4
4
 
5
- /** A small set of original, neutrally named preset filters (no third-party looks or names copied). */
5
+ /**
6
+ * Original, neutrally named preset filters (no third-party looks or names copied).
7
+ *
8
+ * Every preset is expressed as the same four knobs the renderer already supports, so a preset can
9
+ * never be a button that does nothing: set [saturation], scale RGB by [scale], then add the
10
+ * per-channel offsets. `PhotoFilterPresets.swift` mirrors this table field-for-field — add presets to
11
+ * both or neither, or the two platforms will render different looks for the same id.
12
+ */
13
+ data class PhotoFilterRecipe(
14
+ val saturation: Float,
15
+ val scale: Float,
16
+ val redOffset: Float,
17
+ val greenOffset: Float,
18
+ val blueOffset: Float
19
+ )
20
+
6
21
  object PhotoFilterPresets {
7
- val PRESET_IDS = listOf("mono", "noir", "fade", "chrome", "warmth")
22
+ /** Display order in the carousel. "Original" (no preset) is prepended by the UI, not listed here. */
23
+ val PRESET_IDS = listOf(
24
+ "vivid", "vividWarm", "vividCool",
25
+ "warmth", "cool",
26
+ "natural", "soft", "fade",
27
+ "mono", "noir", "silvertone",
28
+ "vintage", "sepia", "retro",
29
+ "dramatic", "dramaticWarm", "dramaticCool"
30
+ )
31
+
32
+ private val recipes = mapOf(
33
+ "vivid" to PhotoFilterRecipe(1.45f, 1.05f, 0f, 0f, 0f),
34
+ "vividWarm" to PhotoFilterRecipe(1.45f, 1.05f, 12f, 4f, -10f),
35
+ "vividCool" to PhotoFilterRecipe(1.45f, 1.05f, -8f, 0f, 14f),
36
+ "warmth" to PhotoFilterRecipe(1.05f, 1f, 18f, 6f, -12f),
37
+ "cool" to PhotoFilterRecipe(1.05f, 1f, -12f, 0f, 18f),
38
+ "natural" to PhotoFilterRecipe(1.15f, 1.02f, 2f, 2f, 0f),
39
+ "soft" to PhotoFilterRecipe(0.9f, 0.96f, 14f, 12f, 12f),
40
+ "fade" to PhotoFilterRecipe(1f, 0.9f, 25f, 25f, 25f),
41
+ "mono" to PhotoFilterRecipe(0f, 1f, 0f, 0f, 0f),
42
+ "noir" to PhotoFilterRecipe(0f, 1.3f, -40f, -40f, -40f),
43
+ "silvertone" to PhotoFilterRecipe(0f, 1.1f, -6f, -2f, 10f),
44
+ "vintage" to PhotoFilterRecipe(0.6f, 0.95f, 24f, 12f, -6f),
45
+ "sepia" to PhotoFilterRecipe(0f, 1f, 38f, 20f, -14f),
46
+ "retro" to PhotoFilterRecipe(0.75f, 0.92f, 20f, 6f, 14f),
47
+ "dramatic" to PhotoFilterRecipe(1.2f, 1.25f, -28f, -28f, -28f),
48
+ "dramaticWarm" to PhotoFilterRecipe(1.2f, 1.25f, -14f, -26f, -38f),
49
+ "dramaticCool" to PhotoFilterRecipe(1.2f, 1.25f, -38f, -28f, -12f),
50
+ // Retained so photos saved with an earlier preset id still render; not offered in the carousel.
51
+ "chrome" to PhotoFilterRecipe(1.4f, 1.1f, 0f, 0f, 0f)
52
+ )
8
53
 
9
54
  fun labelFor(id: String): String = when (id) {
55
+ "vivid" -> "Vivid"
56
+ "vividWarm" -> "Vivid Warm"
57
+ "vividCool" -> "Vivid Cool"
58
+ "warmth" -> "Warm"
59
+ "cool" -> "Cool"
60
+ "natural" -> "Natural"
61
+ "soft" -> "Soft"
62
+ "fade" -> "Fade"
10
63
  "mono" -> "Mono"
11
64
  "noir" -> "Noir"
12
- "fade" -> "Fade"
65
+ "silvertone" -> "Silvertone"
66
+ "vintage" -> "Vintage"
67
+ "sepia" -> "Sepia"
68
+ "retro" -> "Retro"
69
+ "dramatic" -> "Dramatic"
70
+ "dramaticWarm" -> "Dramatic Warm"
71
+ "dramaticCool" -> "Dramatic Cool"
13
72
  "chrome" -> "Chrome"
14
- "warmth" -> "Warmth"
15
73
  else -> id
16
74
  }
17
75
 
18
- fun matrixFor(id: String): ColorMatrix? = when (id) {
19
- "mono" -> ColorMatrix().apply { setSaturation(0f) }
20
- "noir" -> ColorMatrix().apply {
21
- setSaturation(0f)
22
- postConcat(
23
- ColorMatrix(
24
- floatArrayOf(
25
- 1.3f, 0f, 0f, 0f, -40f,
26
- 0f, 1.3f, 0f, 0f, -40f,
27
- 0f, 0f, 1.3f, 0f, -40f,
28
- 0f, 0f, 0f, 1f, 0f
29
- )
30
- )
31
- )
32
- }
33
- "fade" -> ColorMatrix(
34
- floatArrayOf(
35
- 0.9f, 0f, 0f, 0f, 25f,
36
- 0f, 0.9f, 0f, 0f, 25f,
37
- 0f, 0f, 0.9f, 0f, 25f,
38
- 0f, 0f, 0f, 1f, 0f
39
- )
40
- )
41
- "chrome" -> ColorMatrix().apply {
42
- setSaturation(1.4f)
76
+ fun recipeFor(id: String): PhotoFilterRecipe? = recipes[id]
77
+
78
+ fun matrixFor(id: String): ColorMatrix? {
79
+ val recipe = recipes[id] ?: return null
80
+ return ColorMatrix().apply {
81
+ setSaturation(recipe.saturation)
43
82
  postConcat(
44
83
  ColorMatrix(
45
84
  floatArrayOf(
46
- 1.1f, 0f, 0f, 0f, 0f,
47
- 0f, 1.1f, 0f, 0f, 0f,
48
- 0f, 0f, 1.1f, 0f, 0f,
85
+ recipe.scale, 0f, 0f, 0f, recipe.redOffset,
86
+ 0f, recipe.scale, 0f, 0f, recipe.greenOffset,
87
+ 0f, 0f, recipe.scale, 0f, recipe.blueOffset,
49
88
  0f, 0f, 0f, 1f, 0f
50
89
  )
51
90
  )
52
91
  )
53
92
  }
54
- "warmth" -> ColorMatrix(
55
- floatArrayOf(
56
- 1f, 0f, 0f, 0f, 18f,
57
- 0f, 1f, 0f, 0f, 6f,
58
- 0f, 0f, 1f, 0f, -12f,
59
- 0f, 0f, 0f, 1f, 0f
60
- )
61
- )
62
- else -> null
63
93
  }
64
94
  }
@@ -3,7 +3,7 @@ package com.photovideoeditor.photo.render
3
3
  import android.graphics.Color
4
4
  import java.util.UUID
5
5
 
6
- enum class LayerType { TEXT, STICKER, OVERLAY }
6
+ enum class LayerType { TEXT, STICKER, OVERLAY, DRAWING }
7
7
 
8
8
  /**
9
9
  * A single non-destructive overlay layer (text, sticker, or uploaded image
@@ -33,6 +33,10 @@ data class PhotoLayer(
33
33
  // Overlay (uploaded image)
34
34
  var overlayUri: String? = null,
35
35
  var overlayAspectRatio: Float? = null,
36
+ // Drawing (points are normalized offsets from x/y)
37
+ var drawColor: Int = Color.RED,
38
+ var drawStrokeWidth: Float = 0.012f,
39
+ var drawPoints: List<Pair<Float, Float>> = emptyList(),
36
40
  // Timing (video overlays only; ignored for photo layers). endMs <= 0 means "to end of video".
37
41
  var startMs: Long = 0L,
38
42
  var endMs: Long = 0L
@@ -3,6 +3,7 @@ package com.photovideoeditor.photo.render
3
3
  import android.graphics.Bitmap
4
4
  import android.graphics.Canvas
5
5
  import android.graphics.Paint
6
+ import android.graphics.Path
6
7
  import android.graphics.RectF
7
8
  import android.text.TextPaint
8
9
 
@@ -41,6 +42,7 @@ object PhotoLayerRenderer {
41
42
  LayerType.TEXT -> drawText(canvas, layer, geometry.shortSide, alpha)
42
43
  LayerType.STICKER -> drawSticker(canvas, layer, geometry.shortSide, alpha, bitmapResolver)
43
44
  LayerType.OVERLAY -> drawOverlay(canvas, layer, geometry.shortSide, alpha, bitmapResolver)
45
+ LayerType.DRAWING -> drawStroke(canvas, layer, result.width.toFloat(), result.height.toFloat(), geometry.shortSide, alpha)
44
46
  }
45
47
  canvas.restore()
46
48
  }
@@ -75,7 +77,9 @@ object PhotoLayerRenderer {
75
77
  if (uri != null) {
76
78
  resolver(uri)?.let { bitmap ->
77
79
  val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { this.alpha = alpha }
78
- canvas.drawBitmap(bitmap, null, RectF(-size / 2, -size / 2, size / 2, size / 2), paint)
80
+ val aspect = layer.overlayAspectRatio?.takeIf { it > 0 } ?: (bitmap.width.toFloat() / bitmap.height)
81
+ val (width, height) = OverlayGeometry.stickerSize(shortSide, aspect)
82
+ canvas.drawBitmap(bitmap, null, RectF(-width / 2, -height / 2, width / 2, height / 2), paint)
79
83
  return
80
84
  }
81
85
  }
@@ -105,4 +109,23 @@ object PhotoLayerRenderer {
105
109
  val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { this.alpha = alpha }
106
110
  canvas.drawBitmap(bitmap, null, RectF(-width / 2, -height / 2, width / 2, height / 2), paint)
107
111
  }
112
+
113
+ private fun drawStroke(canvas: Canvas, layer: PhotoLayer, width: Float, height: Float, shortSide: Float, alpha: Int) {
114
+ if (layer.drawPoints.size < 2) return
115
+ val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
116
+ color = layer.drawColor
117
+ this.alpha = alpha
118
+ style = Paint.Style.STROKE
119
+ strokeWidth = layer.drawStrokeWidth * shortSide
120
+ strokeCap = Paint.Cap.ROUND
121
+ strokeJoin = Paint.Join.ROUND
122
+ }
123
+ val path = Path()
124
+ layer.drawPoints.forEachIndexed { index, (dx, dy) ->
125
+ val px = dx * width
126
+ val py = dy * height
127
+ if (index == 0) path.moveTo(px, py) else path.lineTo(px, py)
128
+ }
129
+ canvas.drawPath(path, paint)
130
+ }
108
131
  }
@@ -0,0 +1,33 @@
1
+ package com.photovideoeditor.photo.render
2
+
3
+ import android.graphics.Bitmap
4
+ import android.graphics.Canvas
5
+ import android.graphics.Matrix
6
+ import android.graphics.Paint
7
+ import kotlin.math.roundToInt
8
+
9
+ /** The same upright, auto-filled straighten and normalized crop pipeline for preview and export. */
10
+ object PhotoTransformRenderer {
11
+ fun transform(source: Bitmap, state: PhotoTransformState): Bitmap {
12
+ val oriented = if (state.rotationDegrees % 360 == 0) source else Bitmap.createBitmap(
13
+ source, 0, 0, source.width, source.height, Matrix().apply { postRotate(state.rotationDegrees.toFloat()) }, true)
14
+ if (state.straightenDegrees == 0f) return oriented
15
+ val output = Bitmap.createBitmap(oriented.width, oriented.height, Bitmap.Config.ARGB_8888)
16
+ val canvas = Canvas(output)
17
+ val fill = CropGeometry.fillScale(oriented.width.toFloat(), oriented.height.toFloat(), state.straightenDegrees)
18
+ canvas.translate(oriented.width / 2f, oriented.height / 2f)
19
+ canvas.rotate(state.straightenDegrees)
20
+ canvas.scale(fill, fill)
21
+ canvas.drawBitmap(oriented, -oriented.width / 2f, -oriented.height / 2f, Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG))
22
+ if (oriented !== source) oriented.recycle()
23
+ return output
24
+ }
25
+
26
+ fun crop(bitmap: Bitmap, state: PhotoTransformState): Bitmap {
27
+ val left = (state.cropLeft * bitmap.width).roundToInt().coerceIn(0, bitmap.width - 1)
28
+ val top = (state.cropTop * bitmap.height).roundToInt().coerceIn(0, bitmap.height - 1)
29
+ val right = (state.cropRight * bitmap.width).roundToInt().coerceIn(left + 1, bitmap.width)
30
+ val bottom = (state.cropBottom * bitmap.height).roundToInt().coerceIn(top + 1, bitmap.height)
31
+ return Bitmap.createBitmap(bitmap, left, top, right - left, bottom - top)
32
+ }
33
+ }
@@ -15,7 +15,12 @@ data class PhotoTransformState(
15
15
  val cropBottom: Float = 1f,
16
16
  val rotationDegrees: Int = 0,
17
17
  val straightenDegrees: Float = 0f,
18
- val aspectRatio: Float? = null
18
+ val aspectRatio: Float? = null,
19
+ val aspectPreset: String = "Free",
20
+ // Viewport metadata only: zoom/pan are already encoded in normalized crop coordinates for export.
21
+ val zoom: Float = 1f,
22
+ val panX: Float = 0f,
23
+ val panY: Float = 0f
19
24
  ) {
20
25
  val isIdentity: Boolean
21
26
  get() = cropLeft == 0f && cropTop == 0f && cropRight == 1f && cropBottom == 1f &&