@shopify/react-native-skia 2.4.21 → 2.5.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.
@@ -4,73 +4,36 @@ cmake_minimum_required(VERSION 3.4.1)
4
4
  set (CMAKE_VERBOSE_MAKEFILE ON)
5
5
  set (CMAKE_CXX_STANDARD 20)
6
6
 
7
- # Import prebuilt SKIA libraries path
8
- set (SKIA_LIBS_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../libs/android/${ANDROID_ABI}")
7
+ # SKIA_LIBS_PATH is passed from Gradle (resolved via Node.js package resolution)
8
+ # Append the ABI to get the full path
9
+ set (SKIA_LIBS_PATH "${SKIA_LIBS_PATH}/${ANDROID_ABI}")
9
10
 
10
11
  # Check if Skia prebuilt binaries are installed
11
- # The postinstall script downloads these - if missing, the user needs to run it
12
12
  if(NOT EXISTS "${SKIA_LIBS_PATH}/libskia.a")
13
13
  message("")
14
14
  message("┌─────────────────────────────────────────────────────────────────────────────┐")
15
15
  message("│ │")
16
16
  message("│ ERROR: Skia prebuilt binaries not found! │")
17
17
  message("│ │")
18
- message("│ The postinstall script has not run. This is required to download the │")
19
- message("│ Skia binaries. Some package managers (pnpm, bun, yarn berry) require │")
20
- message("│ explicit trust for packages with postinstall scripts. │")
18
+ message("│ Could not find libskia.a at: ${SKIA_LIBS_PATH} │")
21
19
  message("│ │")
22
- message("│ To fix this: │")
23
- message("│ │")
24
- message("│ • npm/yarn classic: Run 'npm rebuild @shopify/react-native-skia' or │")
25
- message("│ reinstall the package │")
26
- message("│ │")
27
- message("│ • bun: Run 'bun add --trust @shopify/react-native-skia' │")
28
- message("│ │")
29
- message("│ • pnpm: Add to package.json: │")
30
- message("│ \"pnpm\": { \"onlyBuiltDependencies\": [\"@shopify/react-native-skia\"] }│")
31
- message("│ Then reinstall the package │")
20
+ message("│ Make sure react-native-skia-android is installed: │")
21
+ message("│ yarn add react-native-skia-android │")
32
22
  message("│ │")
33
23
  message("│ See: https://shopify.github.io/react-native-skia/docs/getting-started/installation │")
34
24
  message("│ │")
35
25
  message("└─────────────────────────────────────────────────────────────────────────────┘")
36
26
  message("")
37
- message(FATAL_ERROR "Skia prebuilt binaries not found at ${SKIA_LIBS_PATH}. Please run the postinstall script.")
27
+ message(FATAL_ERROR "Skia prebuilt binaries not found at ${SKIA_LIBS_PATH}")
38
28
  endif()
39
29
 
40
- # Import libskia first so we can check for symbols
30
+ # Import libskia
41
31
  add_library(skia STATIC IMPORTED)
42
32
  set_property(TARGET skia PROPERTY IMPORTED_LOCATION "${SKIA_LIBS_PATH}/libskia.a")
43
33
 
44
- # Check if Graphite is available
45
- # Detection method priority:
46
- # 1. SK_GRAPHITE environment variable (explicit override, fastest)
47
- # 2. Marker file in libs directory (set during Skia build)
48
- # 3. Fall back to nm symbol detection (slow on some CI systems)
49
- set(SK_GRAPHITE_AVAILABLE OFF)
50
-
51
- if(DEFINED ENV{SK_GRAPHITE})
52
- # Explicit override via environment variable
53
- if($ENV{SK_GRAPHITE})
54
- set(SK_GRAPHITE_AVAILABLE ON)
55
- message("-- SK_GRAPHITE detection: using environment variable (ON)")
56
- else()
57
- message("-- SK_GRAPHITE detection: using environment variable (OFF)")
58
- endif()
59
- elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/../libs/android/graphite.enabled")
60
- # Marker file indicates Graphite-enabled build
61
- set(SK_GRAPHITE_AVAILABLE ON)
62
- message("-- SK_GRAPHITE detection: marker file found")
63
- else()
64
- message("-- SK_GRAPHITE detection: no marker file, assuming OFF")
65
- endif()
66
-
67
- if(SK_GRAPHITE_AVAILABLE)
68
- set(SK_GRAPHITE ON)
69
- message("-- SK_GRAPHITE: ON")
70
- else()
71
- set(SK_GRAPHITE OFF)
72
- message("-- SK_GRAPHITE: OFF")
73
- endif()
34
+ # SK_GRAPHITE is passed from Gradle
35
+ message("-- SKIA_LIBS_PATH: ${SKIA_LIBS_PATH}")
36
+ message("-- SK_GRAPHITE: ${SK_GRAPHITE}")
74
37
 
75
38
  string(APPEND CMAKE_CXX_FLAGS " -DSK_BUILD_FOR_ANDROID -DSK_DISABLE_LEGACY_SHAPER_FACTORY -DSK_IMAGE_READ_PIXELS_DISABLE_LEGACY_API -DFOLLY_NO_CONFIG=1 -DFOLLY_HAVE_CLOCK_GETTIME=1 -DFOLLY_HAVE_MEMRCHR=1 -DFOLLY_USE_LIBCPP=1 -DFOLLY_MOBILE=1 -DON_ANDROID -DONANDROID")
76
39
 
@@ -101,7 +64,7 @@ message("-- PREBUILT: " ${PREBUILT_DIR})
101
64
  message("-- BUILD : " ${build_DIR})
102
65
  message("-- LIBRN : " ${LIBRN_DIR})
103
66
 
104
- link_directories(../libs/android/${ANDROID_ABI}/)
67
+ link_directories(${SKIA_LIBS_PATH}/)
105
68
 
106
69
  if(SK_GRAPHITE)
107
70
  add_definitions(-DSK_GRAPHITE)
@@ -56,7 +56,41 @@ static def findNodeModules(baseDir) {
56
56
  throw new GradleException("React-Native-Skia: Failed to find node_modules/ path!")
57
57
  }
58
58
 
59
+ // Resolve npm package path using Node.js resolution (handles monorepos, pnpm, etc.)
60
+ def resolveSkiaPackage(packageName) {
61
+ def cmdResult = providers.exec {
62
+ commandLine "node", "-e", "console.log(require.resolve('${packageName}/package.json'))"
63
+ ignoreExitValue = true
64
+ }
65
+
66
+ if (cmdResult.result.get().exitValue == 0) {
67
+ def packageJsonPath = cmdResult.standardOutput.asText.get().trim()
68
+ return new File(packageJsonPath).parent
69
+ }
70
+
71
+ // Fallback: walk up directories looking for node_modules
72
+ def basePath = projectDir.toPath().normalize()
73
+ while (basePath) {
74
+ def candidate = Paths.get(basePath.toString(), "node_modules", packageName)
75
+ if (candidate.toFile().exists() && new File(candidate.toString(), "package.json").exists()) {
76
+ return candidate.toString()
77
+ }
78
+ basePath = basePath.getParent()
79
+ }
80
+
81
+ throw new GradleException("React-Native-Skia: Could not find ${packageName}. Make sure you have run 'yarn install' or 'npm install'.")
82
+ }
83
+
59
84
  def nodeModules = findNodeModules(projectDir)
85
+
86
+ // Resolve Skia Android package
87
+ def useGraphite = System.getenv("SK_GRAPHITE") == "1" || System.getenv("SK_GRAPHITE") == "true"
88
+ def skiaPackageName = useGraphite ? "react-native-skia-graphite-android" : "react-native-skia-android"
89
+ def skiaAndroidPackage = resolveSkiaPackage(skiaPackageName)
90
+ def skiaLibsPath = "${skiaAndroidPackage}/libs"
91
+
92
+ logger.warn("react-native-skia: SK_GRAPHITE: ${useGraphite}")
93
+ logger.warn("react-native-skia: Skia Android package: ${skiaAndroidPackage}")
60
94
  logger.warn("react-native-skia: node_modules/ found at: ${nodeModules}")
61
95
 
62
96
  def sourceBuild = false
@@ -158,6 +192,8 @@ android {
158
192
  "-DREACT_NATIVE_DIR=${defaultDir}",
159
193
  "-DNODE_MODULES_DIR=${nodeModules}",
160
194
  "-DPREBUILT_DIR=${prebuiltDir}",
195
+ "-DSKIA_LIBS_PATH=${skiaLibsPath}",
196
+ "-DSK_GRAPHITE=${useGraphite ? 'ON' : 'OFF'}",
161
197
  "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
162
198
 
163
199
  }
@@ -175,7 +211,7 @@ android {
175
211
 
176
212
  sourceSets.main {
177
213
  jniLibs {
178
- srcDirs += ['../libs/android']
214
+ srcDirs += [skiaLibsPath]
179
215
  }
180
216
  java {
181
217
  if (!isNewArchitectureEnabled()) {
@@ -9,7 +9,7 @@
9
9
 
10
10
  #import <vector>
11
11
 
12
- #import <CoreMedia/CMSampleBuffer.h>
12
+ #import <CoreVideo/CVPixelBuffer.h>
13
13
  #import <CoreVideo/CVMetalTextureCache.h>
14
14
  #import <MetalKit/MetalKit.h>
15
15
 
@@ -104,7 +104,11 @@ public:
104
104
  private:
105
105
  static SkYUVAInfo::PlaneConfig getPlaneConfig(OSType pixelFormat);
106
106
  static SkYUVAInfo::Subsampling getSubsampling(OSType pixelFormat);
107
- static SkYUVColorSpace getColorspace(OSType pixelFormat);
107
+ static SkYUVColorSpace getColorspace(CVPixelBufferRef pixelBuffer);
108
+ static bool isFullRangeYUVFormat(OSType pixelFormat);
109
+ static bool isTenBitYUVFormat(OSType pixelFormat);
110
+ static SkYUVColorSpace getSkYUVColorSpaceFromMatrix(CFStringRef matrix,
111
+ OSType pixelFormat);
108
112
  static SkYUVAInfo getYUVAInfoForCVPixelBuffer(CVPixelBufferRef pixelBuffer);
109
113
  };
110
114
 
@@ -13,7 +13,7 @@
13
13
  #import "include/core/SkCanvas.h"
14
14
  #import "include/core/SkColorSpace.h"
15
15
 
16
- #import <CoreMedia/CMSampleBuffer.h>
16
+ #import <CoreVideo/CVImageBuffer.h>
17
17
  #import <CoreVideo/CVMetalTextureCache.h>
18
18
 
19
19
  #import <include/gpu/ganesh/GrBackendSurface.h>
@@ -28,6 +28,7 @@
28
28
  #pragma clang diagnostic pop
29
29
 
30
30
  #include <TargetConditionals.h>
31
+ #include <cmath>
31
32
  #if TARGET_RT_BIG_ENDIAN
32
33
  #define FourCC2Str(fourcc) \
33
34
  (const char[]) { \
@@ -90,6 +91,10 @@ SkiaCVPixelBufferUtils::getCVPixelBufferBaseFormat(
90
91
  case kCVPixelFormatType_420YpCbCr8PlanarFullRange:
91
92
  case kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange:
92
93
  case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange:
94
+ case kCVPixelFormatType_422YpCbCr8BiPlanarVideoRange:
95
+ case kCVPixelFormatType_422YpCbCr8BiPlanarFullRange:
96
+ case kCVPixelFormatType_444YpCbCr8BiPlanarVideoRange:
97
+ case kCVPixelFormatType_444YpCbCr8BiPlanarFullRange:
93
98
  // 10-bit YUV formats
94
99
  case kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange:
95
100
  case kCVPixelFormatType_420YpCbCr10BiPlanarFullRange:
@@ -198,7 +203,7 @@ SkYUVAInfo SkiaCVPixelBufferUtils::YUV::getYUVAInfoForCVPixelBuffer(
198
203
  OSType format = CVPixelBufferGetPixelFormatType(pixelBuffer);
199
204
  SkYUVAInfo::PlaneConfig planeConfig = getPlaneConfig(format);
200
205
  SkYUVAInfo::Subsampling subsampling = getSubsampling(format);
201
- SkYUVColorSpace colorspace = getColorspace(format);
206
+ SkYUVColorSpace colorspace = getColorspace(pixelBuffer);
202
207
 
203
208
  return SkYUVAInfo(size, planeConfig, subsampling, colorspace);
204
209
  }
@@ -210,9 +215,13 @@ SkiaCVPixelBufferUtils::YUV::getPlaneConfig(OSType pixelFormat) {
210
215
  switch (pixelFormat) {
211
216
  case kCVPixelFormatType_420YpCbCr8Planar:
212
217
  case kCVPixelFormatType_420YpCbCr8PlanarFullRange:
213
- return SkYUVAInfo::PlaneConfig::kYUV;
218
+ return SkYUVAInfo::PlaneConfig::kY_U_V;
214
219
  case kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange:
215
220
  case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange:
221
+ case kCVPixelFormatType_422YpCbCr8BiPlanarVideoRange:
222
+ case kCVPixelFormatType_422YpCbCr8BiPlanarFullRange:
223
+ case kCVPixelFormatType_444YpCbCr8BiPlanarVideoRange:
224
+ case kCVPixelFormatType_444YpCbCr8BiPlanarFullRange:
216
225
  case kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange:
217
226
  case kCVPixelFormatType_420YpCbCr10BiPlanarFullRange:
218
227
  case kCVPixelFormatType_422YpCbCr10BiPlanarVideoRange:
@@ -241,9 +250,13 @@ SkiaCVPixelBufferUtils::YUV::getSubsampling(OSType pixelFormat) {
241
250
  case kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange:
242
251
  case kCVPixelFormatType_420YpCbCr10BiPlanarFullRange:
243
252
  [[likely]] return SkYUVAInfo::Subsampling::k420;
253
+ case kCVPixelFormatType_422YpCbCr8BiPlanarVideoRange:
254
+ case kCVPixelFormatType_422YpCbCr8BiPlanarFullRange:
244
255
  case kCVPixelFormatType_422YpCbCr10BiPlanarVideoRange:
245
256
  case kCVPixelFormatType_422YpCbCr10BiPlanarFullRange:
246
257
  return SkYUVAInfo::Subsampling::k422;
258
+ case kCVPixelFormatType_444YpCbCr8BiPlanarVideoRange:
259
+ case kCVPixelFormatType_444YpCbCr8BiPlanarFullRange:
247
260
  case kCVPixelFormatType_444YpCbCr10BiPlanarVideoRange:
248
261
  case kCVPixelFormatType_444YpCbCr10BiPlanarFullRange:
249
262
  return SkYUVAInfo::Subsampling::k444;
@@ -257,34 +270,97 @@ SkiaCVPixelBufferUtils::YUV::getSubsampling(OSType pixelFormat) {
257
270
 
258
271
  // pragma MARK: YUV getColorspace()
259
272
 
260
- SkYUVColorSpace SkiaCVPixelBufferUtils::YUV::getColorspace(OSType pixelFormat) {
273
+ SkYUVColorSpace SkiaCVPixelBufferUtils::YUV::getColorspace(
274
+ CVPixelBufferRef pixelBuffer) {
275
+ const OSType pixelFormat = CVPixelBufferGetPixelFormatType(pixelBuffer);
276
+
277
+ CFTypeRef matrixAttachment =
278
+ CVBufferCopyAttachment(pixelBuffer, kCVImageBufferYCbCrMatrixKey, nullptr);
279
+ CFStringRef matrix = nullptr;
280
+ if (matrixAttachment != nullptr &&
281
+ CFGetTypeID(matrixAttachment) == CFStringGetTypeID()) {
282
+ matrix = reinterpret_cast<CFStringRef>(matrixAttachment);
283
+ }
284
+
285
+ SkYUVColorSpace colorspace = getSkYUVColorSpaceFromMatrix(matrix, pixelFormat);
286
+ if (matrixAttachment != nullptr) {
287
+ CFRelease(matrixAttachment);
288
+ }
289
+ return colorspace;
290
+ }
291
+
292
+ // pragma MARK: YUV helpers
293
+
294
+ bool SkiaCVPixelBufferUtils::YUV::isFullRangeYUVFormat(OSType pixelFormat) {
261
295
  switch (pixelFormat) {
262
- case kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange:
263
- [[likely]]
264
- // 8-bit limited
265
- return SkYUVColorSpace::kRec709_Limited_SkYUVColorSpace;
266
- case kCVPixelFormatType_420YpCbCr8Planar:
267
296
  case kCVPixelFormatType_420YpCbCr8PlanarFullRange:
268
297
  case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange:
269
- [[likely]]
270
- // 8-bit full
271
- return SkYUVColorSpace::kRec709_Full_SkYUVColorSpace;
298
+ case kCVPixelFormatType_422YpCbCr8BiPlanarFullRange:
299
+ case kCVPixelFormatType_444YpCbCr8BiPlanarFullRange:
300
+ case kCVPixelFormatType_420YpCbCr10BiPlanarFullRange:
301
+ case kCVPixelFormatType_422YpCbCr10BiPlanarFullRange:
302
+ case kCVPixelFormatType_444YpCbCr10BiPlanarFullRange:
303
+ return true;
304
+ default:
305
+ return false;
306
+ }
307
+ }
308
+
309
+ bool SkiaCVPixelBufferUtils::YUV::isTenBitYUVFormat(OSType pixelFormat) {
310
+ switch (pixelFormat) {
272
311
  case kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange:
273
- case kCVPixelFormatType_422YpCbCr10BiPlanarVideoRange:
274
- case kCVPixelFormatType_444YpCbCr10BiPlanarVideoRange:
275
- // 10-bit limited
276
- return SkYUVColorSpace::kBT2020_10bit_Limited_SkYUVColorSpace;
277
312
  case kCVPixelFormatType_420YpCbCr10BiPlanarFullRange:
313
+ case kCVPixelFormatType_422YpCbCr10BiPlanarVideoRange:
278
314
  case kCVPixelFormatType_422YpCbCr10BiPlanarFullRange:
315
+ case kCVPixelFormatType_444YpCbCr10BiPlanarVideoRange:
279
316
  case kCVPixelFormatType_444YpCbCr10BiPlanarFullRange:
280
- // 10-bit full
281
- return SkYUVColorSpace::kBT2020_10bit_Full_SkYUVColorSpace;
282
- // This can be extended with branches for specific YUV formats if Apple
283
- // uses new formats.
317
+ return true;
284
318
  default:
285
- [[unlikely]] throw std::runtime_error("Invalid pixel format! " +
286
- std::string(FourCC2Str(pixelFormat)));
319
+ return false;
320
+ }
321
+ }
322
+
323
+ SkYUVColorSpace SkiaCVPixelBufferUtils::YUV::getSkYUVColorSpaceFromMatrix(
324
+ CFStringRef matrix, OSType pixelFormat) {
325
+ const bool isFullRange = isFullRangeYUVFormat(pixelFormat);
326
+ const bool isTenBit = isTenBitYUVFormat(pixelFormat);
327
+
328
+ if (matrix != nullptr) {
329
+ if (CFEqual(matrix, kCVImageBufferYCbCrMatrix_ITU_R_2020)) {
330
+ if (isTenBit) {
331
+ return isFullRange ? kBT2020_10bit_Full_SkYUVColorSpace
332
+ : kBT2020_10bit_Limited_SkYUVColorSpace;
333
+ }
334
+ return isFullRange ? kBT2020_8bit_Full_SkYUVColorSpace
335
+ : kBT2020_8bit_Limited_SkYUVColorSpace;
336
+ }
337
+ if (CFEqual(matrix, kCVImageBufferYCbCrMatrix_ITU_R_601_4)) {
338
+ return isFullRange ? kJPEG_Full_SkYUVColorSpace
339
+ : kRec601_Limited_SkYUVColorSpace;
340
+ }
341
+ if (CFEqual(matrix, kCVImageBufferYCbCrMatrix_SMPTE_240M_1995)) {
342
+ return isFullRange ? kSMPTE240_Full_SkYUVColorSpace
343
+ : kSMPTE240_Limited_SkYUVColorSpace;
344
+ }
345
+ if (CFEqual(matrix, kCVImageBufferYCbCrMatrix_ITU_R_709_2)) {
346
+ return isFullRange ? kRec709_Full_SkYUVColorSpace
347
+ : kRec709_Limited_SkYUVColorSpace;
348
+ }
349
+ }
350
+
351
+ if (pixelFormat == kCVPixelFormatType_422YpCbCr8BiPlanarVideoRange ||
352
+ pixelFormat == kCVPixelFormatType_422YpCbCr8BiPlanarFullRange) {
353
+ return isFullRange ? kJPEG_Full_SkYUVColorSpace
354
+ : kRec601_Limited_SkYUVColorSpace;
355
+ }
356
+
357
+ // Fallback for buffers that don't provide matrix metadata.
358
+ if (isTenBit) {
359
+ return isFullRange ? kBT2020_10bit_Full_SkYUVColorSpace
360
+ : kBT2020_10bit_Limited_SkYUVColorSpace;
287
361
  }
362
+ return isFullRange ? kRec709_Full_SkYUVColorSpace
363
+ : kRec709_Limited_SkYUVColorSpace;
288
364
  }
289
365
 
290
366
  // pragma MARK: CVPixelBuffer -> Skia Texture
@@ -294,9 +370,22 @@ TextureHolder *SkiaCVPixelBufferUtils::getSkiaTextureForCVPixelBufferPlane(
294
370
  // 1. Get cache
295
371
  CVMetalTextureCacheRef textureCache = getTextureCache(device);
296
372
 
297
- // 2. Get MetalTexture from CMSampleBuffer
298
- size_t width = CVPixelBufferGetWidthOfPlane(pixelBuffer, planeIndex);
299
- size_t height = CVPixelBufferGetHeightOfPlane(pixelBuffer, planeIndex);
373
+ // 2. Get MetalTexture from CVPixelBuffer
374
+ const size_t planesCount = CVPixelBufferGetPlaneCount(pixelBuffer);
375
+ if (planesCount == 0 && planeIndex != 0) [[unlikely]] {
376
+ throw std::runtime_error("Pixel buffer is not planar, but plane index " +
377
+ std::to_string(planeIndex) + " was requested.");
378
+ }
379
+ if (planesCount > 0 && planeIndex >= planesCount) [[unlikely]] {
380
+ throw std::runtime_error(
381
+ "Requested out-of-bounds plane index " + std::to_string(planeIndex) +
382
+ " for pixel buffer with " + std::to_string(planesCount) + " planes.");
383
+ }
384
+ size_t width = planesCount > 0 ? CVPixelBufferGetWidthOfPlane(pixelBuffer, planeIndex)
385
+ : CVPixelBufferGetWidth(pixelBuffer);
386
+ size_t height =
387
+ planesCount > 0 ? CVPixelBufferGetHeightOfPlane(pixelBuffer, planeIndex)
388
+ : CVPixelBufferGetHeight(pixelBuffer);
300
389
  MTLPixelFormat pixelFormat =
301
390
  getMTLPixelFormatForCVPixelBufferPlane(pixelBuffer, planeIndex);
302
391
 
@@ -307,7 +396,7 @@ TextureHolder *SkiaCVPixelBufferUtils::getSkiaTextureForCVPixelBufferPlane(
307
396
 
308
397
  if (result != kCVReturnSuccess) [[unlikely]] {
309
398
  throw std::runtime_error(
310
- "Failed to create Metal Texture from CMSampleBuffer! Result: " +
399
+ "Failed to create Metal Texture from CVPixelBuffer! Result: " +
311
400
  std::to_string(result));
312
401
  }
313
402
 
@@ -334,19 +423,102 @@ SkiaCVPixelBufferUtils::getTextureCache(id<MTLDevice> device) {
334
423
 
335
424
  MTLPixelFormat SkiaCVPixelBufferUtils::getMTLPixelFormatForCVPixelBufferPlane(
336
425
  CVPixelBufferRef pixelBuffer, size_t planeIndex) {
337
- size_t width = CVPixelBufferGetWidthOfPlane(pixelBuffer, planeIndex);
338
- size_t bytesPerRow =
339
- CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, planeIndex);
340
- double bytesPerPixel = round(static_cast<double>(bytesPerRow) / width);
426
+ const OSType format = CVPixelBufferGetPixelFormatType(pixelBuffer);
427
+ auto throwInvalidPlaneIndexForFormat = [&](size_t expectedPlanes)
428
+ -> MTLPixelFormat {
429
+ throw std::runtime_error(
430
+ "Invalid plane index " + std::to_string(planeIndex) +
431
+ " for pixel format " + std::string(FourCC2Str(format)) + " (expected 0.." +
432
+ std::to_string(expectedPlanes - 1) + ").");
433
+ };
434
+
435
+ switch (format) {
436
+ case kCVPixelFormatType_32BGRA:
437
+ // 1 plane, 8-bit interleaved BGRA.
438
+ if (planeIndex != 0) {
439
+ return throwInvalidPlaneIndexForFormat(1);
440
+ }
441
+ return MTLPixelFormatBGRA8Unorm;
442
+ case kCVPixelFormatType_32RGBA:
443
+ // 1 plane, 8-bit interleaved RGBA.
444
+ if (planeIndex != 0) {
445
+ return throwInvalidPlaneIndexForFormat(1);
446
+ }
447
+ return MTLPixelFormatRGBA8Unorm;
448
+ case kCVPixelFormatType_420YpCbCr8Planar:
449
+ case kCVPixelFormatType_420YpCbCr8PlanarFullRange:
450
+ // 3 planes, 8-bit 4:2:0 planar (Y, U, V), each plane is single channel.
451
+ switch (planeIndex) {
452
+ case 0:
453
+ case 1:
454
+ case 2:
455
+ return MTLPixelFormatR8Unorm;
456
+ default:
457
+ return throwInvalidPlaneIndexForFormat(3);
458
+ }
459
+ case kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange:
460
+ case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange:
461
+ case kCVPixelFormatType_422YpCbCr8BiPlanarVideoRange:
462
+ case kCVPixelFormatType_422YpCbCr8BiPlanarFullRange:
463
+ case kCVPixelFormatType_444YpCbCr8BiPlanarVideoRange:
464
+ case kCVPixelFormatType_444YpCbCr8BiPlanarFullRange:
465
+ // 2 planes, 8-bit bi-planar (plane 0 = Y, plane 1 = interleaved CbCr).
466
+ switch (planeIndex) {
467
+ case 0:
468
+ return MTLPixelFormatR8Unorm;
469
+ case 1:
470
+ return MTLPixelFormatRG8Unorm;
471
+ default:
472
+ return throwInvalidPlaneIndexForFormat(2);
473
+ }
474
+ case kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange:
475
+ case kCVPixelFormatType_420YpCbCr10BiPlanarFullRange:
476
+ case kCVPixelFormatType_422YpCbCr10BiPlanarVideoRange:
477
+ case kCVPixelFormatType_422YpCbCr10BiPlanarFullRange:
478
+ case kCVPixelFormatType_444YpCbCr10BiPlanarVideoRange:
479
+ case kCVPixelFormatType_444YpCbCr10BiPlanarFullRange:
480
+ // 2 planes, 10-bit bi-planar stored in 16-bit lanes (Y + interleaved CbCr).
481
+ switch (planeIndex) {
482
+ case 0:
483
+ return MTLPixelFormatR16Unorm;
484
+ case 1:
485
+ return MTLPixelFormatRG16Unorm;
486
+ default:
487
+ return throwInvalidPlaneIndexForFormat(2);
488
+ }
489
+ default:
490
+ break;
491
+ }
492
+
493
+ const size_t planesCount = CVPixelBufferGetPlaneCount(pixelBuffer);
494
+ const size_t width =
495
+ planesCount > 0 ? CVPixelBufferGetWidthOfPlane(pixelBuffer, planeIndex)
496
+ : CVPixelBufferGetWidth(pixelBuffer);
497
+ const size_t bytesPerRow =
498
+ planesCount > 0 ? CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, planeIndex)
499
+ : CVPixelBufferGetBytesPerRow(pixelBuffer);
500
+ if (width == 0) [[unlikely]] {
501
+ throw std::runtime_error("Invalid plane width for pixel format " +
502
+ std::string(FourCC2Str(format)) + "!");
503
+ }
504
+ if (bytesPerRow % width != 0) [[unlikely]] {
505
+ throw std::runtime_error(
506
+ "Invalid bytes per row! Bytes per row must be evenly divisible by width "
507
+ "for pixel format " +
508
+ std::string(FourCC2Str(format)) + "!");
509
+ }
510
+ const size_t bytesPerPixel = bytesPerRow / width;
341
511
  if (bytesPerPixel == 1) {
342
512
  return MTLPixelFormatR8Unorm;
343
- } else if (bytesPerPixel == 2) {
513
+ }
514
+ if (bytesPerPixel == 2) {
344
515
  return MTLPixelFormatRG8Unorm;
345
- } else if (bytesPerPixel == 4) {
516
+ }
517
+ if (bytesPerPixel == 4) {
346
518
  return MTLPixelFormatBGRA8Unorm;
347
- } else [[unlikely]] {
348
- throw std::runtime_error("Invalid bytes per row! Expected 1 (R), 2 (RG) or "
349
- "4 (RGBA), but received " +
350
- std::to_string(bytesPerPixel));
351
519
  }
520
+
521
+ [[unlikely]] throw std::runtime_error(
522
+ "Invalid bytes per row! Expected 1 (R), 2 (RG) or 4 (RGBA), but received " +
523
+ std::to_string(bytesPerPixel));
352
524
  }
@@ -1,4 +1,5 @@
1
1
  #import <React/RCTBridge.h>
2
+ #import <QuartzCore/CATransaction.h>
2
3
 
3
4
  #import "RNSkiaModule.h"
4
5
  #import "SkiaUIView.h"
@@ -140,7 +141,10 @@
140
141
  - (void)layoutSubviews {
141
142
  [super layoutSubviews];
142
143
  if (_impl != nullptr) {
144
+ [CATransaction begin];
145
+ [CATransaction setDisableActions:YES];
143
146
  _impl->setSize(self.bounds.size.width, self.bounds.size.height);
147
+ [CATransaction commit];
144
148
  }
145
149
  }
146
150
 
@@ -44,6 +44,23 @@ public:
44
44
 
45
45
  static SkColor fromValue(jsi::Runtime &runtime, const jsi::Value &obj) {
46
46
  const auto &object = obj.asObject(runtime);
47
+
48
+ // Handle regular JavaScript arrays
49
+ if (object.isArray(runtime)) {
50
+ auto array = object.asArray(runtime);
51
+ if (array.size(runtime) != 4) {
52
+ throw jsi::JSError(runtime,
53
+ "Expected array of length 4 for color, got " +
54
+ std::to_string(array.size(runtime)));
55
+ }
56
+ auto r = array.getValueAtIndex(runtime, 0).asNumber();
57
+ auto g = array.getValueAtIndex(runtime, 1).asNumber();
58
+ auto b = array.getValueAtIndex(runtime, 2).asNumber();
59
+ auto a = array.getValueAtIndex(runtime, 3).asNumber();
60
+ return SkColorSetARGB(a * 255, r * 255, g * 255, b * 255);
61
+ }
62
+
63
+ // Handle Float32Array (has buffer property)
47
64
  jsi::ArrayBuffer buffer =
48
65
  object
49
66
  .getProperty(runtime, jsi::PropNameID::forAscii(runtime, "buffer"))
@@ -76,7 +93,42 @@ public:
76
93
  return JsiSkColor::toValue(
77
94
  runtime, SkColorSetARGB(color.a * 255, color.r, color.g, color.b));
78
95
  } else if (arguments[0].isObject()) {
79
- return arguments[0].getObject(runtime);
96
+ auto obj = arguments[0].getObject(runtime);
97
+
98
+ // Check if it's a regular array - convert to Float32Array
99
+ if (obj.isArray(runtime)) {
100
+ auto arr = obj.getArray(runtime);
101
+ if (arr.size(runtime) != 4) {
102
+ throw jsi::JSError(runtime,
103
+ "Expected array of length 4 for color, got " +
104
+ std::to_string(arr.size(runtime)));
105
+ }
106
+ auto r = static_cast<float>(arr.getValueAtIndex(runtime, 0).asNumber());
107
+ auto g = static_cast<float>(arr.getValueAtIndex(runtime, 1).asNumber());
108
+ auto b = static_cast<float>(arr.getValueAtIndex(runtime, 2).asNumber());
109
+ auto a = static_cast<float>(arr.getValueAtIndex(runtime, 3).asNumber());
110
+
111
+ // Create Float32Array and populate
112
+ auto result = runtime.global()
113
+ .getPropertyAsFunction(runtime, "Float32Array")
114
+ .callAsConstructor(runtime, 4)
115
+ .getObject(runtime);
116
+ jsi::ArrayBuffer buffer =
117
+ result
118
+ .getProperty(runtime,
119
+ jsi::PropNameID::forAscii(runtime, "buffer"))
120
+ .asObject(runtime)
121
+ .getArrayBuffer(runtime);
122
+ auto bfrPtr = reinterpret_cast<float *>(buffer.data(runtime));
123
+ bfrPtr[0] = r;
124
+ bfrPtr[1] = g;
125
+ bfrPtr[2] = b;
126
+ bfrPtr[3] = a;
127
+ return result;
128
+ }
129
+
130
+ // Already a Float32Array or similar - return as-is
131
+ return obj;
80
132
  }
81
133
  return jsi::Value::undefined();
82
134
  };
@@ -49,8 +49,13 @@ template <> struct JSIConverter<std::shared_ptr<ArrayBuffer>> {
49
49
  auto buff = bufferProp.getObject(runtime);
50
50
  auto bytesPerElements =
51
51
  obj.getProperty(runtime, "BYTES_PER_ELEMENT").asNumber();
52
- return createArrayBufferFromJSI(
53
- runtime, buff.getArrayBuffer(runtime),
52
+ auto arrayBuffer = buff.getArrayBuffer(runtime);
53
+ auto byteOffset = static_cast<size_t>(
54
+ obj.getProperty(runtime, "byteOffset").asNumber());
55
+ auto byteLength = static_cast<size_t>(
56
+ obj.getProperty(runtime, "byteLength").asNumber());
57
+ return std::make_shared<ArrayBuffer>(
58
+ arrayBuffer.data(runtime) + byteOffset, byteLength,
54
59
  static_cast<size_t>(bytesPerElements));
55
60
  }
56
61
  }
@@ -19,6 +19,8 @@ export declare const useCanvasSize: (userRef?: RefObject<CanvasRef | null>) => {
19
19
  export declare const isFabric: boolean;
20
20
  export interface CanvasProps extends Omit<ViewProps, "onLayout"> {
21
21
  debug?: boolean;
22
+ /** @deprecated Not supported on Fabric. Use `onSize` or `useCanvasSize()` instead. */
23
+ onLayout?: ViewProps["onLayout"];
22
24
  opaque?: boolean;
23
25
  onSize?: SharedValue<SkSize>;
24
26
  colorSpace?: "p3" | "srgb";
@@ -56,9 +56,6 @@ const Canvas = ({
56
56
  colorSpace = "p3",
57
57
  androidWarmup = false,
58
58
  ref,
59
- // Here know this is a type error but this is done on purpose to check it at runtime
60
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
61
- // @ts-expect-error
62
59
  onLayout,
63
60
  ...viewProps
64
61
  }) => {