@shopify/react-native-skia 2.4.21 → 2.5.1

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 (35) hide show
  1. package/android/CMakeLists.txt +12 -49
  2. package/android/build.gradle +37 -1
  3. package/apple/SkiaCVPixelBufferUtils.h +6 -2
  4. package/apple/SkiaCVPixelBufferUtils.mm +208 -36
  5. package/apple/SkiaUIView.mm +4 -0
  6. package/cpp/api/JsiSkColor.h +53 -1
  7. package/cpp/api/recorder/Drawings.h +1 -1
  8. package/cpp/rnwgpu/ArrayBuffer.h +7 -2
  9. package/lib/commonjs/dom/types/Drawings.d.ts +1 -0
  10. package/lib/commonjs/dom/types/Drawings.js.map +1 -1
  11. package/lib/commonjs/renderer/Canvas.d.ts +2 -0
  12. package/lib/commonjs/renderer/Canvas.js +0 -3
  13. package/lib/commonjs/renderer/Canvas.js.map +1 -1
  14. package/lib/commonjs/sksg/Container.native.js +3 -5
  15. package/lib/commonjs/sksg/Container.native.js.map +1 -1
  16. package/lib/commonjs/sksg/Recorder/commands/Drawing.js +2 -2
  17. package/lib/commonjs/sksg/Recorder/commands/Drawing.js.map +1 -1
  18. package/lib/module/dom/types/Drawings.d.ts +1 -0
  19. package/lib/module/dom/types/Drawings.js.map +1 -1
  20. package/lib/module/renderer/Canvas.d.ts +2 -0
  21. package/lib/module/renderer/Canvas.js +0 -3
  22. package/lib/module/renderer/Canvas.js.map +1 -1
  23. package/lib/module/sksg/Container.native.js +3 -5
  24. package/lib/module/sksg/Container.native.js.map +1 -1
  25. package/lib/module/sksg/Recorder/commands/Drawing.js +2 -2
  26. package/lib/module/sksg/Recorder/commands/Drawing.js.map +1 -1
  27. package/lib/typescript/src/dom/types/Drawings.d.ts +1 -0
  28. package/lib/typescript/src/renderer/Canvas.d.ts +2 -0
  29. package/package.json +10 -30
  30. package/react-native-skia.podspec +98 -63
  31. package/src/dom/types/Drawings.ts +1 -0
  32. package/src/renderer/Canvas.tsx +2 -3
  33. package/src/sksg/Container.native.ts +3 -4
  34. package/src/sksg/Recorder/commands/Drawing.ts +3 -2
  35. package/scripts/install-skia.mjs +0 -728
package/package.json CHANGED
@@ -8,37 +8,13 @@
8
8
  "setup-skia-web": "scripts/setup-canvaskit.js"
9
9
  },
10
10
  "title": "React Native Skia",
11
- "version": "2.4.21",
12
- "skia": {
13
- "version": "m144c",
14
- "checksums": {
15
- "android-armeabi-v7a": "e406c3e8103a2efb6a514ac91346d7f9f81adbae14dc2d0e302e84fb8c5f80f7",
16
- "android-arm64-v8a": "75069b0f7c66ad3382553e947d583265de033cc856c394110243da098306955f",
17
- "android-x86": "714b93a7bdf005a23699f47e6255e4e690086c52a1998c20196a46f95b709b09",
18
- "android-x86_64": "5bd2972d13293b09b35e2c0149b7d103dc4fb0f2837c3dd169ce06795b812714",
19
- "apple-ios-xcframeworks": "43f62ea742c55ecc57864505ff752a517fd2c31412a19914032d044ac4f987ee",
20
- "apple-tvos-xcframeworks": "0f6b5c75b4e686e72f5cc8508e60074463f757ca7a0dcbd07e095c055a537c58",
21
- "apple-macos-xcframeworks": "31f57bcf6caff1c268984609b0e4a2abd966bfa8ddcf074331d94e0f988f93d3"
22
- }
23
- },
24
- "skia-graphite": {
25
- "version": "m142b",
26
- "checksums": {
27
- "android-armeabi-v7a": "3e40f44c804194fa0983c46903f834e8f834c6dd96534c7fa1b4ebb4f409527d",
28
- "android-arm64-v8a": "d6c3035449fcf7ef13369b0d6c4ef41f3177d15074eeb195201e0ba4cfb2f527",
29
- "android-x86": "6dc229847420b8a43c15098cdcb4fe45b2df20a38ad69f37e0aa91c832499c2a",
30
- "android-x86_64": "ecae351d98af3b175d40d894520c2e3263034276bb0b0e9d2f9c19ba7dc046fa",
31
- "apple-ios-xcframeworks": "6f60f03faaeebfb798615d09715040790ff95e75bf95028893ced6f514ab5196",
32
- "apple-macos-xcframeworks": "07467cd1778053537528ed91c7a35d116e1a4644819f0aad8e06e7d884591dea"
33
- }
34
- },
11
+ "version": "2.5.1",
35
12
  "description": "High-performance React Native Graphics using Skia",
36
13
  "main": "lib/module/index.js",
37
14
  "react-native": "src/index.ts",
38
15
  "module": "lib/module/index.js",
39
16
  "types": "lib/typescript/index.d.ts",
40
17
  "files": [
41
- "scripts/install-skia.mjs",
42
18
  "scripts/setup-canvaskit.js",
43
19
  "src/**",
44
20
  "lib/**",
@@ -68,14 +44,13 @@
68
44
  "clean-skia": "yarn rimraf ./libs && yarn rimraf ../../externals/skia/out",
69
45
  "build-skia": "tsx ./scripts/build-skia.ts",
70
46
  "copy-skia-headers": "tsx ./scripts/copy-skia-headers.ts",
71
- "install-skia": "node ./scripts/install-skia.mjs --force && yarn copy-skia-headers",
47
+ "install-skia-graphite": "tsx ./scripts/install-skia-graphite.ts",
72
48
  "clang-format": "yarn clang-format-ios && yarn clang-format-android && yarn clang-format-common",
73
49
  "clang-format-ios": "find apple/ -iname '*.h' -o -iname '*.mm' -o -iname '*.cpp' | xargs clang-format -i",
74
50
  "clang-format-android": "find android/cpp/ -iname '*.h' -o -iname '*.m' -o -iname '*.cpp' | xargs clang-format -i",
75
51
  "clang-format-common": "find cpp/ \\( -path 'cpp//skia' -prune \\) -o \\( -iname '*.h' -o -iname '*.m' -o -iname '*.cpp' \\) -print | xargs clang-format -i",
76
52
  "workflow-copy-libs": "tsx ./scripts/workflow-copy-libs.ts",
77
- "cpplint": "cpplint --linelength=230 --filter=-legal/copyright,-whitespace/indent,-whitespace/comments,-whitespace/ending_newline,-build/include_order,-runtime/references,-readability/todo,-whitespace/blank_line,-whitespace/todo,-runtime/int,-build/c++11,-whitespace/parens --exclude=package/cpp/skia --exclude=package/ios --exclude=package/android/build --exclude=package/node_modules --recursive package",
78
- "postinstall": "node ./scripts/install-skia.mjs"
53
+ "cpplint": "cpplint --linelength=230 --filter=-legal/copyright,-whitespace/indent,-whitespace/comments,-whitespace/ending_newline,-build/include_order,-runtime/references,-readability/todo,-whitespace/blank_line,-whitespace/todo,-runtime/int,-build/c++11,-whitespace/parens --exclude=package/cpp/skia --exclude=package/ios --exclude=package/android/build --exclude=package/node_modules --recursive package"
79
54
  },
80
55
  "repository": {
81
56
  "type": "git",
@@ -137,11 +112,12 @@
137
112
  "react": "19.0.0",
138
113
  "react-native": "0.83.1",
139
114
  "react-native-builder-bob": "0.18.2",
140
- "react-native-reanimated": "4.2.1",
141
- "react-native-worklets": "0.7.1",
115
+ "react-native-reanimated": "^4.2.1",
116
+ "react-native-worklets": "^0.7.0",
142
117
  "rimraf": "3.0.2",
143
118
  "semantic-release": "^24.1.0",
144
119
  "semantic-release-yarn": "^3.0.2",
120
+ "tar": "^7.5.9",
145
121
  "ts-jest": "29.4.3",
146
122
  "tsx": "^4.21.0",
147
123
  "typescript": "^5.2.2",
@@ -149,6 +125,10 @@
149
125
  },
150
126
  "dependencies": {
151
127
  "canvaskit-wasm": "0.40.0",
128
+ "react-native-skia-android": "144.3.0",
129
+ "react-native-skia-apple-ios": "144.3.0",
130
+ "react-native-skia-apple-macos": "144.3.0",
131
+ "react-native-skia-apple-tvos": "144.3.0",
152
132
  "react-reconciler": "0.31.0"
153
133
  },
154
134
  "eslintIgnore": [
@@ -4,76 +4,104 @@ require "json"
4
4
 
5
5
  package = JSON.parse(File.read(File.join(__dir__, "package.json")))
6
6
 
7
- # Check if Skia prebuilt binaries are installed
8
- # The postinstall script downloads these - if missing, the user needs to run it
9
- skia_libs_path = File.join(__dir__, "libs/apple/ios")
10
- unless File.exist?(skia_libs_path) && Dir.glob(File.join(skia_libs_path, "*.xcframework")).any?
11
- Pod::UI.puts "\n"
12
- Pod::UI.puts "┌─────────────────────────────────────────────────────────────────────────────┐".red
13
- Pod::UI.puts "│ │".red
14
- Pod::UI.puts "│ ERROR: Skia prebuilt binaries not found! │".red
15
- Pod::UI.puts "│ │".red
16
- Pod::UI.puts "│ The postinstall script has not run. This is required to download the │".red
17
- Pod::UI.puts "│ Skia binaries. Some package managers (pnpm, bun, yarn berry) require │".red
18
- Pod::UI.puts "│ explicit trust for packages with postinstall scripts. │".red
19
- Pod::UI.puts "│ │".red
20
- Pod::UI.puts "│ To fix this: │".red
21
- Pod::UI.puts "│ │".red
22
- Pod::UI.puts "│ • npm/yarn classic: Run 'npm rebuild @shopify/react-native-skia' or │".red
23
- Pod::UI.puts "│ reinstall the package │".red
24
- Pod::UI.puts "│ │".red
25
- Pod::UI.puts "│ • bun: Run 'bun add --trust @shopify/react-native-skia' │".red
26
- Pod::UI.puts "│ │".red
27
- Pod::UI.puts "│ • pnpm: Add to package.json: │".red
28
- Pod::UI.puts "│ \"pnpm\": { \"onlyBuiltDependencies\": [\"@shopify/react-native-skia\"] }│".red
29
- Pod::UI.puts "│ Then reinstall the package │".red
30
- Pod::UI.puts "│ │".red
31
- Pod::UI.puts "│ See: https://shopify.github.io/react-native-skia/docs/getting-started/installation │".red
32
- Pod::UI.puts "│ │".red
33
- Pod::UI.puts "└─────────────────────────────────────────────────────────────────────────────┘".red
34
- Pod::UI.puts "\n"
35
- raise "Skia prebuilt binaries not found. Please run the postinstall script."
36
- end
37
-
38
- # Check if Graphite is available
39
- # Detection method priority:
40
- # 1. SK_GRAPHITE environment variable (explicit override, fastest)
41
- # 2. Marker file in libs directory (set during Skia build)
42
- # 3. Default to OFF (no slow nm symbol detection)
7
+ # Check if Graphite is enabled
43
8
  use_graphite = false
44
-
45
9
  if ENV['SK_GRAPHITE']
46
- # Explicit override via environment variable
47
10
  use_graphite = ENV['SK_GRAPHITE'] == '1' || ENV['SK_GRAPHITE'].downcase == 'true'
48
- puts "-- SK_GRAPHITE detection: using environment variable (#{use_graphite ? 'ON' : 'OFF'})"
49
- elsif File.exist?(File.join(__dir__, "libs/apple/graphite.enabled"))
50
- # Marker file indicates Graphite-enabled build
51
- use_graphite = true
52
- puts "-- SK_GRAPHITE detection: marker file found"
11
+ puts "-- SK_GRAPHITE: using environment variable (#{use_graphite ? 'ON' : 'OFF'})"
53
12
  else
54
- puts "-- SK_GRAPHITE detection: no marker file, assuming OFF"
55
- end
56
-
57
- if use_graphite
58
- puts "-- SK_GRAPHITE: ON"
59
- else
60
- puts "-- SK_GRAPHITE: OFF"
13
+ puts "-- SK_GRAPHITE: OFF (set SK_GRAPHITE=1 to enable)"
61
14
  end
62
15
 
63
16
  # Set preprocessor definitions based on GRAPHITE flag
64
- preprocessor_defs = use_graphite ?
65
- '$(inherited) SK_GRAPHITE=1 SK_IMAGE_READ_PIXELS_DISABLE_LEGACY_API=1 SK_DISABLE_LEGACY_SHAPER_FACTORY=1' :
17
+ preprocessor_defs = use_graphite ?
18
+ '$(inherited) SK_GRAPHITE=1 SK_IMAGE_READ_PIXELS_DISABLE_LEGACY_API=1 SK_DISABLE_LEGACY_SHAPER_FACTORY=1' :
66
19
  '$(inherited) SK_METAL=1 SK_GANESH=1 SK_IMAGE_READ_PIXELS_DISABLE_LEGACY_API=1 SK_DISABLE_LEGACY_SHAPER_FACTORY=1'
67
20
 
68
- # Define framework names (without paths)
21
+ # Define framework names
69
22
  framework_names = ['libskia', 'libsvg', 'libskshaper', 'libskparagraph',
70
23
  'libskunicode_core', 'libskunicode_libgrapheme',
71
24
  'libskottie', 'libsksg']
72
25
 
73
- # Build platform-specific framework paths
74
- ios_frameworks = framework_names.map { |f| "libs/apple/ios/#{f}.xcframework" }
75
- tvos_frameworks = framework_names.map { |f| "libs/apple/tvos/#{f}.xcframework" }
76
- osx_frameworks = framework_names.map { |f| "libs/apple/macos/#{f}.xcframework" }
26
+ # Build platform-specific framework paths (relative to pod's libs directory)
27
+ ios_frameworks = framework_names.map { |f| "libs/ios/#{f}.xcframework" }
28
+ osx_frameworks = framework_names.map { |f| "libs/macos/#{f}.xcframework" }
29
+ # tvOS frameworks - only declare if libs/tvos/ already exists (otherwise leave empty)
30
+ tvos_frameworks = if use_graphite || !Dir.exist?(File.join(__dir__, 'libs', 'tvos'))
31
+ []
32
+ else
33
+ framework_names.map { |f| "libs/tvos/#{f}.xcframework" }
34
+ end
35
+
36
+ # Prepare command resolves paths at RUNTIME (not evaluation time) to ensure deterministic checksums
37
+ # This script:
38
+ # 1. Checks if xcframeworks are already installed (e.g., from Graphite script)
39
+ # 2. If not, resolves npm package paths and copies xcframeworks
40
+ # The script content is deterministic - no machine-specific paths embedded in the podspec
41
+ prepare_command_script = <<-'SCRIPT'
42
+ node -e "
43
+ const path = require('path');
44
+ const fs = require('fs');
45
+ const { execSync } = require('child_process');
46
+
47
+ const iosLibs = 'libs/ios';
48
+ const macosLibs = 'libs/macos';
49
+
50
+ // Check if xcframeworks are already installed
51
+ const hasIos = fs.existsSync(iosLibs) && fs.readdirSync(iosLibs).some(f => f.endsWith('.xcframework'));
52
+ const hasMacos = fs.existsSync(macosLibs) && fs.readdirSync(macosLibs).some(f => f.endsWith('.xcframework'));
53
+
54
+ if (hasIos && hasMacos) {
55
+ console.log('-- Using pre-installed xcframeworks from libs/');
56
+ process.exit(0);
57
+ }
58
+
59
+ // Determine package prefix based on SK_GRAPHITE env var
60
+ const useGraphite = process.env.SK_GRAPHITE === '1' || (process.env.SK_GRAPHITE || '').toLowerCase() === 'true';
61
+ const prefix = useGraphite ? 'react-native-skia-graphite' : 'react-native-skia';
62
+
63
+ // Resolve package paths
64
+ let iosPackage, macosPackage, tvosPackage;
65
+ try {
66
+ iosPackage = path.dirname(require.resolve(prefix + '-apple-ios/package.json'));
67
+ macosPackage = path.dirname(require.resolve(prefix + '-apple-macos/package.json'));
68
+ } catch (e) {
69
+ console.error('ERROR: Could not find ' + prefix + '-apple-ios or ' + prefix + '-apple-macos');
70
+ console.error('Make sure you have run yarn install or npm install');
71
+ process.exit(1);
72
+ }
73
+
74
+ // Verify xcframeworks exist in the packages
75
+ const iosXcf = path.join(iosPackage, 'libs');
76
+ if (!fs.existsSync(iosXcf) || !fs.readdirSync(iosXcf).some(f => f.endsWith('.xcframework'))) {
77
+ console.error('ERROR: Skia prebuilt binaries not found in ' + prefix + '-apple-ios!');
78
+ process.exit(1);
79
+ }
80
+
81
+ console.log('-- Skia iOS package: ' + iosPackage);
82
+ console.log('-- Skia macOS package: ' + macosPackage);
83
+
84
+ // Clean and copy
85
+ execSync('rm -rf libs/ios libs/macos libs/tvos', { stdio: 'inherit' });
86
+ execSync('mkdir -p libs/ios libs/macos', { stdio: 'inherit' });
87
+ execSync('cp -R \"' + iosPackage + '/libs/\"*.xcframework libs/ios/', { stdio: 'inherit' });
88
+ execSync('cp -R \"' + macosPackage + '/libs/\"*.xcframework libs/macos/', { stdio: 'inherit' });
89
+
90
+ // Handle tvOS (non-Graphite only)
91
+ if (!useGraphite) {
92
+ try {
93
+ tvosPackage = path.dirname(require.resolve(prefix + '-apple-tvos/package.json'));
94
+ console.log('-- Skia tvOS package: ' + tvosPackage);
95
+ execSync('mkdir -p libs/tvos', { stdio: 'inherit' });
96
+ execSync('cp -R \"' + tvosPackage + '/libs/\"*.xcframework libs/tvos/', { stdio: 'inherit' });
97
+ } catch (e) {
98
+ console.log('-- tvOS package not found, skipping');
99
+ }
100
+ }
101
+
102
+ console.log('-- Copied xcframeworks from npm packages');
103
+ "
104
+ SCRIPT
77
105
 
78
106
  Pod::Spec.new do |s|
79
107
  s.name = "react-native-skia"
@@ -85,13 +113,17 @@ Pod::Spec.new do |s|
85
113
  s.homepage = "https://github.com/shopify/react-native-skia"
86
114
  s.license = "MIT"
87
115
  s.license = { :type => "MIT", :file => "LICENSE.md" }
88
- s.authors = {
116
+ s.authors = {
89
117
  "Christian Falch" => "christian.falch@gmail.com",
90
118
  "William Candillon" => "wcandillon@gmail.com"
91
119
  }
92
120
  s.platforms = { :ios => "14.0", :tvos => "13.0", :osx => "11" }
93
121
  s.source = { :git => "https://github.com/shopify/react-native-skia/react-native-skia.git", :tag => "#{s.version}" }
94
122
 
123
+ # Copy xcframeworks from npm packages into pod directory structure
124
+ # The script is deterministic - path resolution happens at runtime, not evaluation time
125
+ s.prepare_command = prepare_command_script
126
+
95
127
  s.requires_arc = true
96
128
  s.pod_target_xcconfig = {
97
129
  'GCC_PREPROCESSOR_DEFINITIONS' => preprocessor_defs,
@@ -102,7 +134,7 @@ Pod::Spec.new do |s|
102
134
 
103
135
  s.frameworks = ['MetalKit', 'AVFoundation', 'AVKit', 'CoreMedia']
104
136
 
105
- # Platform-specific vendored frameworks
137
+ # Platform-specific vendored frameworks (copied into libs/)
106
138
  s.ios.vendored_frameworks = ios_frameworks
107
139
  s.osx.vendored_frameworks = osx_frameworks
108
140
 
@@ -111,9 +143,12 @@ Pod::Spec.new do |s|
111
143
  s.tvos.vendored_frameworks = tvos_frameworks
112
144
  end
113
145
 
146
+ # Preserve the copied libs directory
147
+ s.preserve_paths = ["libs/**/*"]
148
+
114
149
  # All iOS cpp/h files
115
150
  s.source_files = [
116
- "apple/**/*.{h,c,cc,cpp,m,mm,swift}",
151
+ "apple/**/*.{h,c,cc,cpp,m,mm,swift}",
117
152
  "cpp/**/*.{h,cpp}"
118
153
  ]
119
154
 
@@ -123,9 +158,10 @@ Pod::Spec.new do |s|
123
158
  'cpp/rnskia/RNDawnWindowContext.h',
124
159
  'cpp/rnskia/RNDawnWindowContext.cpp',
125
160
  'cpp/rnskia/RNImageProvider.h',
126
- 'cpp/rnwgpu/**/*.{h,cpp}'
161
+ 'cpp/rnwgpu/**/*.{h,cpp}',
162
+ 'cpp/jsi2/**/*.{h,cpp}'
127
163
  ]
128
- s.exclude_files = graphite_exclusions unless use_graphite
164
+ s.exclude_files = graphite_exclusions unless use_graphite
129
165
 
130
166
  if defined?(install_modules_dependencies()) != nil
131
167
  install_modules_dependencies(s)
@@ -138,4 +174,3 @@ Pod::Spec.new do |s|
138
174
  s.dependency "React-Core"
139
175
  end
140
176
  end
141
-
@@ -69,6 +69,7 @@ export interface AtlasProps extends DrawingNodeProps {
69
69
  sprites: SkRect[];
70
70
  transforms: SkRSXform[];
71
71
  colors?: SkColor[];
72
+ colorBlendMode?: SkEnum<typeof BlendMode>;
72
73
  sampling?: SamplingOptions;
73
74
  }
74
75
 
@@ -64,6 +64,8 @@ export const isFabric = Boolean((global as any)?.nativeFabricUIManager);
64
64
 
65
65
  export interface CanvasProps extends Omit<ViewProps, "onLayout"> {
66
66
  debug?: boolean;
67
+ /** @deprecated Not supported on Fabric. Use `onSize` or `useCanvasSize()` instead. */
68
+ onLayout?: ViewProps["onLayout"];
67
69
  opaque?: boolean;
68
70
  onSize?: SharedValue<SkSize>;
69
71
  colorSpace?: "p3" | "srgb";
@@ -80,9 +82,6 @@ export const Canvas = ({
80
82
  colorSpace = "p3",
81
83
  androidWarmup = false,
82
84
  ref,
83
- // Here know this is a type error but this is done on purpose to check it at runtime
84
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
85
- // @ts-expect-error
86
85
  onLayout,
87
86
  ...viewProps
88
87
  }: CanvasProps) => {
@@ -44,15 +44,14 @@ class NativeReanimatedContainer extends Container {
44
44
  return;
45
45
  }
46
46
  const recorder = new ReanimatedRecorder(this.Skia);
47
- const { nativeId, picture, Skia } = this;
47
+ const { nativeId, picture } = this;
48
48
  visit(recorder, this.root);
49
49
  const sharedValues = recorder.getSharedValues();
50
50
  const sharedRecorder = recorder.getRecorder();
51
51
  // Draw first frame
52
- Rea.executeOnUIRuntimeSync(() => {
52
+ Rea.runOnUI(() => {
53
53
  "worklet";
54
- const firstPicture = Skia.Picture.MakePicture(null)!;
55
- nativeDrawOnscreen(nativeId, sharedRecorder, firstPicture);
54
+ nativeDrawOnscreen(nativeId, sharedRecorder, picture);
56
55
  })();
57
56
  // Animate
58
57
  if (sharedValues.length > 0) {
@@ -306,8 +306,9 @@ export const drawPicture = (ctx: DrawingContext, props: PictureProps) => {
306
306
 
307
307
  export const drawAtlas = (ctx: DrawingContext, props: AtlasProps) => {
308
308
  "worklet";
309
- const { image, sprites, transforms, colors, blendMode, sampling } = props;
310
- const blend = blendMode ? BlendMode[enumKey(blendMode)] : undefined;
309
+ const { image, sprites, transforms, colors, colorBlendMode, sampling } =
310
+ props;
311
+ const blend = colorBlendMode ? BlendMode[enumKey(colorBlendMode)] : undefined;
311
312
  if (image) {
312
313
  ctx.canvas.drawAtlas(
313
314
  image,