From 568571f3326cd4e4e36eee1a75f44a805fbf67e6 Mon Sep 17 00:00:00 2001 From: JimmyDaddy Date: Mon, 1 Jan 2024 20:09:07 +0800 Subject: [PATCH] fix: 1.1.x iOS crash and icon watermark not shown correctly (#190) * fix: 1.1.x iOS crash and icon watermark not shown correctly * chore: release 1.1.12 --- CHANGELOG.md | 7 + example/ios/Podfile.lock | 8 +- example/package.json | 7 +- example/patches/react-native+0.71.11.patch | 194 +++++++++++++++++++++ ios/RCTImageMarker/ImageMarker.swift | 42 ++--- ios/RCTImageMarker/Utils.swift | 44 +++-- package.json | 2 +- 7 files changed, 253 insertions(+), 51 deletions(-) create mode 100644 example/patches/react-native+0.71.11.patch diff --git a/CHANGELOG.md b/CHANGELOG.md index a93dd26a..f02a3873 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ +## [1.1.12](https://github.com/JimmyDaddy/react-native-image-marker/compare/v1.1.11...v1.1.12) (2024-01-01) + + +### Bug Fixes + +* 1.1.x iOS crash and icon watermark not shown correctly ([1ae9cb0](https://github.com/JimmyDaddy/react-native-image-marker/commit/1ae9cb015e54bc52bf6ed27fe1c70528d7e54f2c)) + ## [1.1.11](https://github.com/JimmyDaddy/react-native-image-marker/compare/v1.1.8...v1.1.11) (2023-12-21) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index c19eac21..bfb0f32c 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -331,7 +331,7 @@ PODS: - glog - react-native-blob-util (0.19.6): - React-Core - - react-native-image-marker (1.1.10): + - react-native-image-marker (1.1.11): - React-Core - react-native-image-picker (5.7.0): - React-Core @@ -583,7 +583,7 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: - boost: 57d2868c099736d80fcd648bf211b4431e51a558 + boost: 64032b9e9b938fda23325e68a3771f0fabf414dc CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 FBLazyVector: c511d4cd0210f416cb5c289bd5ae6b36d909b048 @@ -617,7 +617,7 @@ SPEC CHECKSUMS: React-jsinspector: b6ed4cb3ffa27a041cd440300503dc512b761450 React-logger: 186dd536128ae5924bc38ed70932c00aa740cd5b react-native-blob-util: d8fa1a7f726867907a8e43163fdd8b441d4489ea - react-native-image-marker: 4414050c15944297bc24b3a37a1070d951fb57a2 + react-native-image-marker: d58c3e046b75e3302fd4f365cd7c32051badfef2 react-native-image-picker: 3269f75c251cdcd61ab51b911dd30d6fff8c6169 React-perflogger: e706562ab7eb8eb590aa83a224d26fa13963d7f2 React-RCTActionSheet: 57d4bd98122f557479a3359ad5dad8e109e20c5a @@ -638,4 +638,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 4420d5a35a7e9d1bc71140c63c7e29db0be4fdf7 -COCOAPODS: 1.12.1 +COCOAPODS: 1.14.3 diff --git a/example/package.json b/example/package.json index d5eb7414..5a54ebd9 100644 --- a/example/package.json +++ b/example/package.json @@ -6,7 +6,8 @@ "android": "react-native run-android", "ios": "react-native run-ios", "start": "react-native start", - "pods": "pod-install --quiet" + "pods": "pod-install", + "postinstall": "patch-package" }, "dependencies": { "@expo/react-native-action-sheet": "^4.0.1", @@ -23,6 +24,8 @@ "@babel/preset-env": "^7.20.0", "@babel/runtime": "^7.20.0", "babel-plugin-module-resolver": "^4.1.0", - "metro-react-native-babel-preset": "0.73.10" + "metro-react-native-babel-preset": "0.73.10", + "patch-package": "^8.0.0", + "postinstall-postinstall": "^2.1.0" } } diff --git a/example/patches/react-native+0.71.11.patch b/example/patches/react-native+0.71.11.patch new file mode 100644 index 00000000..1bd9dfaf --- /dev/null +++ b/example/patches/react-native+0.71.11.patch @@ -0,0 +1,194 @@ +diff --git a/node_modules/react-native/sdks/hermes/hermes-engine.podspec b/node_modules/react-native/sdks/hermes/hermes-engine.podspec +new file mode 100644 +index 0000000..b800547 +--- /dev/null ++++ b/node_modules/react-native/sdks/hermes/hermes-engine.podspec +@@ -0,0 +1,143 @@ ++# Copyright (c) Meta Platforms, Inc. and affiliates. ++# ++# This source code is licensed under the MIT license found in the ++# LICENSE file in the root directory of this source tree. ++ ++require "json" ++require_relative "./hermes-utils.rb" ++ ++react_native_path = File.join(__dir__, "..", "..") ++ ++# Whether Hermes is built for Release or Debug is determined by the PRODUCTION envvar. ++build_type = ENV['PRODUCTION'] == "1" ? :release : :debug ++ ++# package.json ++package = JSON.parse(File.read(File.join(react_native_path, "package.json"))) ++version = package['version'] ++ ++# sdks/.hermesversion ++hermestag_file = File.join(react_native_path, "sdks", ".hermesversion") ++isInCI = ENV['REACT_NATIVE_CI'] === 'true' ++ ++source = {} ++git = "https://github.com/facebook/hermes.git" ++ ++isInMain = version.include?('1000.0.0') ++isNightly = version.start_with?('0.0.0-') ++ ++if ENV.has_key?('HERMES_ENGINE_TARBALL_PATH') ++ if !File.exist?(ENV['HERMES_ENGINE_TARBALL_PATH']) ++ abort "[Hermes] HERMES_ENGINE_TARBALL_PATH is set, but points to a non-existing file: \"#{ENV['HERMES_ENGINE_TARBALL_PATH']}\"\nIf you don't want to use tarball, run `unset HERMES_ENGINE_TARBALL_PATH`" ++ end ++end ++ ++if ENV.has_key?('HERMES_ENGINE_TARBALL_PATH') ++ Pod::UI.puts "[Hermes] Using pre-built Hermes binaries from local path: #{ENV['HERMES_ENGINE_TARBALL_PATH']}".yellow if Object.const_defined?("Pod::UI") ++ source[:http] = "file://#{ENV['HERMES_ENGINE_TARBALL_PATH']}" ++elsif isInMain ++ Pod::UI.puts '[Hermes] Installing hermes-engine may take slightly longer, building Hermes compiler from source...'.yellow if Object.const_defined?("Pod::UI") ++ source[:git] = git ++ source[:commit] = `git ls-remote https://github.com/facebook/hermes main | cut -f 1`.strip ++elsif isNightly ++ Pod::UI.puts '[Hermes] Nightly version, download pre-built for Hermes'.yellow if Object.const_defined?("Pod::UI") ++ destination_path = download_nightly_hermes(react_native_path, version) ++ # set tarball as hermes engine ++ source[:http] = "file://#{destination_path}" ++elsif File.exist?(hermestag_file) && isInCI ++ Pod::UI.puts '[Hermes] Detected that you are on a React Native release branch, building Hermes from source but fetched from tag...'.yellow if Object.const_defined?("Pod::UI") ++ hermestag = File.read(hermestag_file).strip ++ source[:git] = git ++ source[:tag] = hermestag ++else ++ # Sample url from Maven: ++ # https://repo1.maven.org/maven2/com/facebook/react/react-native-artifacts/0.71.0/react-native-artifacts-0.71.0-hermes-ios-debug.tar.gz ++ source[:http] = "https://repo1.maven.org/maven2/com/facebook/react/react-native-artifacts/#{version}/react-native-artifacts-#{version}-hermes-ios-#{build_type.to_s}.tar.gz" ++end ++ ++Pod::Spec.new do |spec| ++ spec.name = "hermes-engine" ++ spec.version = version ++ spec.summary = "Hermes is a small and lightweight JavaScript engine optimized for running React Native." ++ spec.description = "Hermes is a JavaScript engine optimized for fast start-up of React Native apps. It features ahead-of-time static optimization and compact bytecode." ++ spec.homepage = "https://hermesengine.dev" ++ spec.license = package['license'] ++ spec.author = "Facebook" ++ spec.source = source ++ spec.platforms = { :osx => "10.13", :ios => "12.4" } ++ ++ spec.preserve_paths = '**/*.*' ++ spec.source_files = '' ++ ++ spec.xcconfig = { ++ "CLANG_CXX_LANGUAGE_STANDARD" => "c++17", ++ "CLANG_CXX_LIBRARY" => "compiler-default" ++ }.merge!(build_type == :debug ? { "GCC_PREPROCESSOR_DEFINITIONS" => "HERMES_ENABLE_DEBUGGER=1" } : {}) ++ ++ if source[:http] then ++ ++ spec.subspec 'Pre-built' do |ss| ++ ss.preserve_paths = ["destroot/bin/*"].concat(build_type == :debug ? ["**/*.{h,c,cpp}"] : []) ++ ss.source_files = "destroot/include/**/*.h" ++ ss.exclude_files = ["destroot/include/jsi/jsi/JSIDynamic.{h,cpp}", "destroot/include/jsi/jsi/jsilib-*.{h,cpp}"] ++ ss.header_mappings_dir = "destroot/include" ++ ss.ios.vendored_frameworks = "destroot/Library/Frameworks/universal/hermes.xcframework" ++ ss.osx.vendored_frameworks = "destroot/Library/Frameworks/macosx/hermes.framework" ++ end ++ ++ elsif source[:git] then ++ ++ spec.subspec 'Hermes' do |ss| ++ ss.source_files = '' ++ ss.public_header_files = 'API/hermes/*.h' ++ ss.header_dir = 'hermes' ++ end ++ ++ spec.subspec 'JSI' do |ss| ++ ss.source_files = '' ++ ss.public_header_files = 'API/jsi/jsi/*.h' ++ ss.header_dir = 'jsi' ++ end ++ ++ spec.subspec 'Public' do |ss| ++ ss.source_files = '' ++ ss.public_header_files = 'public/hermes/Public/*.h' ++ ss.header_dir = 'hermes/Public' ++ end ++ ++ hermesc_path = "" ++ ++ if ENV.has_key?('HERMES_OVERRIDE_HERMESC_PATH') && File.exist?(ENV['HERMES_OVERRIDE_HERMESC_PATH']) then ++ hermesc_path = ENV['HERMES_OVERRIDE_HERMESC_PATH'] ++ else ++ # Keep hermesc_path synchronized with .gitignore entry. ++ ENV['REACT_NATIVE_PATH'] = react_native_path ++ hermesc_path = "${REACT_NATIVE_PATH}/sdks/hermes-engine/build_host_hermesc" ++ # NOTE: Prepare command is not run if the pod is not downloaded. ++ spec.prepare_command = ". #{react_native_path}/sdks/hermes-engine/utils/build-hermesc-xcode.sh #{hermesc_path}" ++ end ++ ++ spec.user_target_xcconfig = { ++ 'FRAMEWORK_SEARCH_PATHS' => '"$(PODS_ROOT)/hermes-engine/destroot/Library/Frameworks/iphoneos" ' + ++ '"$(PODS_ROOT)/hermes-engine/destroot/Library/Frameworks/iphonesimulator" ' + ++ '"$(PODS_ROOT)/hermes-engine/destroot/Library/Frameworks/macosx" ' + ++ '"$(PODS_ROOT)/hermes-engine/destroot/Library/Frameworks/catalyst"', ++ 'OTHER_LDFLAGS' => '-framework "hermes"', ++ 'HERMES_CLI_PATH' => "#{hermesc_path}/bin/hermesc" ++ } ++ ++ spec.script_phases = [ ++ { ++ :name => 'Build Hermes', ++ :script => <<-EOS ++ . ${PODS_ROOT}/../.xcode.env ++ export CMAKE_BINARY=${CMAKE_BINARY:-#{%x(command -v cmake | tr -d '\n')}} ++ . ${REACT_NATIVE_PATH}/sdks/hermes-engine/utils/build-hermes-xcode.sh #{version} #{hermesc_path}/ImportHermesc.cmake ++ EOS ++ }, ++ { ++ :name => 'Copy Hermes Framework', ++ :script => ". ${REACT_NATIVE_PATH}/sdks/hermes-engine/utils/copy-hermes-xcode.sh" ++ } ++ ] ++ end ++end +diff --git a/node_modules/react-native/sdks/hermes/hermes-utils.rb b/node_modules/react-native/sdks/hermes/hermes-utils.rb +new file mode 100644 +index 0000000..0842496 +--- /dev/null ++++ b/node_modules/react-native/sdks/hermes/hermes-utils.rb +@@ -0,0 +1,26 @@ ++# Copyright (c) Meta Platforms, Inc. and affiliates. ++# ++# This source code is licensed under the MIT license found in the ++# LICENSE file in the root directory of this source tree. ++ ++require 'net/http' ++require 'rexml/document' ++ ++# This function downloads the nightly prebuilt version of Hermes based on the passed version ++# and save it in the node_module/react_native/sdks/downloads folder ++# It then returns the path to the hermes tarball ++# ++# Parameters ++# - react_native_path: the path to the React Native folder in node modules. It is used as root path to store the Hermes tarball ++# - version: the version of React Native that requires the Hermes tarball ++# Returns: the path to the downloaded Hermes tarball ++def download_nightly_hermes(react_native_path, version) ++ params = "r=snapshots\&g=com.facebook.react\&a=react-native-artifacts\&c=hermes-ios-debug\&e=tar.gz\&v=#{version}-SNAPSHOT" ++ tarball_url = "http://oss.sonatype.org/service/local/artifact/maven/redirect\?#{params}" ++ ++ destination_folder = "#{react_native_path}/sdks/downloads" ++ destination_path = "#{destination_folder}/hermes-ios.tar.gz" ++ ++ `mkdir -p "#{destination_folder}" && curl "#{tarball_url}" -Lo "#{destination_path}"` ++ return destination_path ++end +diff --git a/node_modules/react-native/third-party-podspecs/boost.podspec b/node_modules/react-native/third-party-podspecs/boost.podspec +index 3d9331c..b1e2c6a 100644 +--- a/node_modules/react-native/third-party-podspecs/boost.podspec ++++ b/node_modules/react-native/third-party-podspecs/boost.podspec +@@ -10,7 +10,7 @@ Pod::Spec.new do |spec| + spec.homepage = 'http://www.boost.org' + spec.summary = 'Boost provides free peer-reviewed portable C++ source libraries.' + spec.authors = 'Rene Rivera' +- spec.source = { :http => 'https://boostorg.jfrog.io/artifactory/main/release/1.76.0/source/boost_1_76_0.tar.bz2', ++ spec.source = { :http => 'https://sourceforge.net/projects/boost/files/boost/1.76.0/boost_1_76_0.tar.bz2', + :sha256 => 'f0397ba6e982c4450f27bf32a2a83292aba035b827a5623a14636ea583318c41' } + + # Pinning to the same version as React.podspec. diff --git a/ios/RCTImageMarker/ImageMarker.swift b/ios/RCTImageMarker/ImageMarker.swift index 0e603376..509fd9d0 100644 --- a/ios/RCTImageMarker/ImageMarker.swift +++ b/ios/RCTImageMarker/ImageMarker.swift @@ -116,7 +116,7 @@ public final class ImageMarker: NSObject, RCTBridgeModule { } } - func markerImgWithText(_ image: UIImage, _ opts: MarkTextOptions) -> UIImage? { + func markImgWithText(_ image: UIImage, _ opts: MarkTextOptions) -> UIImage? { var bg = image; let w = bg.size.width @@ -271,9 +271,9 @@ public final class ImageMarker: NSObject, RCTBridgeModule { return aimg } - func markeImage(with image: UIImage, waterImages: [UIImage], options: MarkImageOptions) -> UIImage? { - - var bg = image; + func markImage(with image: UIImage, waterImages: [UIImage], options: MarkImageOptions) -> UIImage? { + + let bg = image; let w = bg.size.width let h = bg.size.height UIGraphicsBeginImageContextWithOptions(bg.size, false, options.backgroundImage.scale) @@ -323,39 +323,41 @@ public final class ImageMarker: NSObject, RCTBridgeModule { case .topLeft: rect = CGRect(origin: CGPoint(x: 20, y: 20), size: size) case .topCenter: - rect = CGRect(origin: CGPoint(x: (w - size.width) / 2, y: 20), size: size) + rect = CGRect(origin: CGPoint(x: (w - ww) / 2, y: 20), size: size) case .topRight: - rect = CGRect(origin: CGPoint(x: w - size.width - 20, y: 20), size: size) + rect = CGRect(origin: CGPoint(x: w - ww - 20, y: 20), size: size) case .bottomLeft: - rect = CGRect(origin: CGPoint(x: 20, y: h - size.height - 20), size: size) + rect = CGRect(origin: CGPoint(x: 20, y: h - wh - 20), size: size) case .bottomCenter: - rect = CGRect(origin: CGPoint(x: (w - size.width) / 2, y: h - size.height - 20), size: size) + rect = CGRect(origin: CGPoint(x: (w - ww) / 2, y: h - wh - 20), size: size) case .bottomRight: - rect = CGRect(origin: CGPoint(x: w - size.width - 20, y: h - size.height - 20), size: size) + rect = CGRect(origin: CGPoint(x: w - ww - 20, y: h - wh - 20), size: size) case .center: - rect = CGRect(origin: CGPoint(x: (w - size.width) / 2, y: (h - size.height) / 2), size: size) + rect = CGRect(origin: CGPoint(x: (w - ww) / 2, y: (h - wh) / 2), size: size) default: rect = CGRect(origin: CGPoint(x: 20, y: 20), size: size) } } else { - rect = CGRect(x: Utils.parseSpreadValue(v: watermarkOptions.X, relativeTo: w) ?? 20, y: Utils.parseSpreadValue(v: watermarkOptions.Y, relativeTo: h) ?? 20, width: CGFloat(ww), height: CGFloat(wh)) + rect = CGRect(x: Utils.parseSpreadValue(v: watermarkOptions.X, relativeTo: w) ?? 20, y: Utils.parseSpreadValue(v: watermarkOptions.Y, relativeTo: h) ?? 20, width: diagonal, height: diagonal) } UIGraphicsBeginImageContextWithOptions(CGSize(width: diagonal, height: diagonal), false, 1) let markerContext = UIGraphicsGetCurrentContext() markerContext?.saveGState() - + if watermarkOptions.imageOption.alpha != 1.0 { markerContext?.beginTransparencyLayer(auxiliaryInfo: nil) markerContext?.setAlpha(watermarkOptions.imageOption.alpha) markerContext?.setBlendMode(.multiply) let markerImage = markerImg.rotatedImageWithTransform(watermarkOptions.imageOption.rotate) - markerContext?.draw(markerImage.cgImage!, in: CGRect(origin: .zero, size: CGSize(width: diagonal, height: diagonal))) + let originPoint = CGPoint(x: 0, y: rect.height - markerImage.size.height) + markerContext?.draw(markerImage.cgImage!, in: CGRect(origin: originPoint, size: CGSize(width: markerImage.size.width, height: markerImage.size.height))) markerContext?.endTransparencyLayer() } else { let markerImage = markerImg.rotatedImageWithTransform(watermarkOptions.imageOption.rotate) - markerContext?.draw(markerImage.cgImage!, in: CGRect(origin: .zero, size: CGSize(width: diagonal, height: diagonal))) + let originPoint = CGPoint(x: 0, y: rect.height - markerImage.size.height) + markerContext?.draw(markerImage.cgImage!, in: CGRect(origin: originPoint, size: CGSize(width: markerImage.size.width, height: markerImage.size.height))) } markerContext?.restoreGState() @@ -388,12 +390,12 @@ public final class ImageMarker: NSObject, RCTBridgeModule { Task(priority: .userInitiated) { do { let images = try await loadImages(with: [(markOpts?.backgroundImage)!]) - let scaledImage = self.markerImgWithText(images[0], markOpts!) + let scaledImage = self.markImgWithText(images[0], markOpts!) let res = self.saveImageForMarker(scaledImage!, with: markOpts!) resolver(res) - print("Loaded images:", images) + print("Loaded images: \(images)") } catch { - print("Failed to load images:", error) + print("Failed to load images, error: \(error).") } } } @@ -408,12 +410,12 @@ public final class ImageMarker: NSObject, RCTBridgeModule { do { let waterImages = markOpts?.watermarkImages.map { $0.imageOption } var images = try await loadImages(with: [(markOpts?.backgroundImage)!] + waterImages!) - let scaledImage = self.markeImage(with: images.remove(at: 0), waterImages: images, options: markOpts!) + let scaledImage = self.markImage(with: images.remove(at: 0), waterImages: images, options: markOpts!) let res = self.saveImageForMarker(scaledImage!, with: markOpts!) resolver(res) - print("Loaded images:", images) + print("Loaded images: \(images), waterImages: \(String(describing: waterImages))") } catch { - print("Failed to load images:", error) + print("Failed to load images, error: \(error).") } } } diff --git a/ios/RCTImageMarker/Utils.swift b/ios/RCTImageMarker/Utils.swift index 05ab23f1..4f442ec1 100644 --- a/ios/RCTImageMarker/Utils.swift +++ b/ios/RCTImageMarker/Utils.swift @@ -65,37 +65,33 @@ class Utils: NSObject { static func stringToInt(_ string: String) -> UInt32 { if string.count == 1 { let hexChar = string[string.startIndex] - var intCh: UInt32 = 0 - if hexChar >= "0" && hexChar <= "9" { - intCh = (UInt32(hexChar.asciiValue!) - 48) * 16 /* 0 的Ascll - 48 */ - } else if hexChar >= "A" && hexChar <= "F" { - intCh = (UInt32(hexChar.asciiValue!) - 55) * 16 /* A 的Ascll - 65 */ - } else { - intCh = (UInt32(hexChar.asciiValue!) - 87) * 16 /* a 的Ascll - 97 */ - } + var intCh: UInt32 = getCharInt(hexChar) return intCh * 2 } else { let hexChar1 = string[string.startIndex] - var intCh1: UInt32 = 0 - if hexChar1 >= "0" && hexChar1 <= "9" { - intCh1 = (UInt32(hexChar1.asciiValue!) - 48) * 16 /* 0 的Ascll - 48 */ - } else if hexChar1 >= "A" && hexChar1 <= "F" { - intCh1 = (UInt32(hexChar1.asciiValue!) - 55) * 16 /* A 的Ascll - 65 */ - } else { - intCh1 = (UInt32(hexChar1.asciiValue!) - 87) * 16 /* a 的Ascll - 97 */ - } + var intCh1: UInt32 = getCharInt(hexChar1) let hexChar2 = string[string.index(after: string.startIndex)] - var intCh2: UInt32 = 0 - if hexChar2 >= "0" && hexChar2 <= "9" { - intCh2 = UInt32(hexChar2.asciiValue!) - 48 /* 0 的Ascll - 48 */ - } else if hexChar1 >= "A" && hexChar1 <= "F" { - intCh2 = UInt32(hexChar2.asciiValue!) - 55 /* A 的Ascll - 65 */ - } else { - intCh2 = UInt32(hexChar2.asciiValue!) - 87 /* a 的Ascll - 97 */ - } + var intCh2: UInt32 = getCharInt(hexChar2) return intCh1 + intCh2 } } + + static func getCharInt(_ char: Character) -> UInt32 { + var charInt: UInt32 = 0 + if let asciiValue = char.asciiValue { + switch asciiValue { + case 48...57: // '0'...'9' + charInt = UInt32(asciiValue) - 48 + case 65...70: // 'A'...'F' + charInt = UInt32(asciiValue) - 55 + case 97...102: // 'a'...'f' + charInt = UInt32(asciiValue) - 87 + default: + print("Invalid hex character") + } + } + return charInt + } static func getShadowStyle(_ shadowStyle: [AnyHashable: Any]?) -> NSShadow? { if let shadowStyle = shadowStyle { diff --git a/package.json b/package.json index 381882b2..ff583b98 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-image-marker", - "version": "1.1.11", + "version": "1.1.12", "description": "Add text or icon watermark to your images", "main": "lib/commonjs/index", "module": "lib/module/index",