From 0d4c725c04ce1fd18a236d0301359c49307ca147 Mon Sep 17 00:00:00 2001 From: danvleju-rdx <163979791+danvleju-rdx@users.noreply.github.com> Date: Thu, 26 Sep 2024 11:28:28 +0300 Subject: [PATCH] Image service URL construction (#221) * wip * bump to 1.1.18 * split tests * address feedback * wip * bump to 1.1.18 * split tests * address feedback * Create kotlin extensions * Bump version * add test --------- Co-authored-by: micbakos-rdx --- Cargo.lock | 2 +- apple/Sources/Sargon/Util/URL+image.swift | 17 +++ .../TestCases/Prelude/ImageURLTests.swift | 37 ++++++ crates/sargon/Cargo.toml | 2 +- .../sargon/src/core/utils/image_url_utils.rs | 120 ++++++++++++++++++ .../core/utils/image_url_utils_uniffi_fn.rs | 55 ++++++++ crates/sargon/src/core/utils/mod.rs | 4 + crates/sargon/src/core/utils/string_utils.rs | 20 +++ crates/sargon/src/types/mod.rs | 4 + crates/sargon/src/types/vector_image_type.rs | 64 ++++++++++ .../src/types/vector_image_type_uniffi_fn.rs | 49 +++++++ .../com/radixdlt/sargon/extensions/Url.kt | 30 +++++ .../sargon/extensions/VectorImageType.kt | 11 ++ .../sargon/samples/VectorImageTypeSample.kt | 14 ++ .../test/java/com/radixdlt/sargon/UrlTest.kt | 117 +++++++++++++++++ .../radixdlt/sargon/VectorImageTypeTest.kt | 25 ++++ 16 files changed, 569 insertions(+), 2 deletions(-) create mode 100644 apple/Sources/Sargon/Util/URL+image.swift create mode 100644 apple/Tests/TestCases/Prelude/ImageURLTests.swift create mode 100644 crates/sargon/src/core/utils/image_url_utils.rs create mode 100644 crates/sargon/src/core/utils/image_url_utils_uniffi_fn.rs create mode 100644 crates/sargon/src/types/vector_image_type.rs create mode 100644 crates/sargon/src/types/vector_image_type_uniffi_fn.rs create mode 100644 jvm/sargon-android/src/main/java/com/radixdlt/sargon/extensions/Url.kt create mode 100644 jvm/sargon-android/src/main/java/com/radixdlt/sargon/extensions/VectorImageType.kt create mode 100644 jvm/sargon-android/src/main/java/com/radixdlt/sargon/samples/VectorImageTypeSample.kt create mode 100644 jvm/sargon-android/src/test/java/com/radixdlt/sargon/UrlTest.kt create mode 100644 jvm/sargon-android/src/test/java/com/radixdlt/sargon/VectorImageTypeTest.kt diff --git a/Cargo.lock b/Cargo.lock index 0c40f6cb4..e8dc1cc57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2617,7 +2617,7 @@ dependencies = [ [[package]] name = "sargon" -version = "1.1.16" +version = "1.1.19" dependencies = [ "actix-rt", "aes-gcm", diff --git a/apple/Sources/Sargon/Util/URL+image.swift b/apple/Sources/Sargon/Util/URL+image.swift new file mode 100644 index 000000000..859c91c47 --- /dev/null +++ b/apple/Sources/Sargon/Util/URL+image.swift @@ -0,0 +1,17 @@ +import Foundation +import SargonUniFFI + +extension URL { + public func isVectorImage(type: VectorImageType) -> Bool { + imageUrlUtilsIsVectorImage(url: self.absoluteString, imageType: type) + } + + public func imageURL(imageServiceURL: URL, size: CGSize) throws -> URL { + try imageUrlUtilsMakeImageUrl( + url: self.absoluteString, + imageServiceUrl: imageServiceURL.absoluteString, + width: UInt32(size.width), + height: UInt32(size.height) + ) + } +} diff --git a/apple/Tests/TestCases/Prelude/ImageURLTests.swift b/apple/Tests/TestCases/Prelude/ImageURLTests.swift new file mode 100644 index 000000000..fb81571d4 --- /dev/null +++ b/apple/Tests/TestCases/Prelude/ImageURLTests.swift @@ -0,0 +1,37 @@ +@testable import Sargon + +import CustomDump +import Foundation +import Sargon +import SargonUniFFI +import XCTest + +final class ImageURLTests: XCTestCase { + func test_is_vector_image() { + let svgURL = URL(string: "https://svgshare.com/i/U7z.svg")! + + XCTAssert(svgURL.isVectorImage(type: .svg)) + } + + func test_image_url() throws { + let size = CGSize(width: 1024, height: 1024) + let imageServiceURL = URL(string: "https://image-service-dev.extratools.works")! + let svgURL = URL(string: "https://svgshare.com/i/U7z.svg")! + + XCTAssertEqual( + try svgURL.imageURL(imageServiceURL: imageServiceURL, size: size), + URL(string: "https://image-service-dev.extratools.works/?imageOrigin=https%3A%2F%2Fsvgshare.com%2Fi%2FU7z.svg&imageSize=1024x1024&format=png") + ) + } + + func test_image_url_with_data_url() throws { + let size = CGSize(width: 1024, height: 1024) + let imageServiceURL = URL(string: "https://image-service-dev.extratools.works")! + let svgDataURL = URL(string: "data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%201000%201000%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpolygon%20fill%3D%22hsla%2890%2C99%25%2C52%25%2C1%29%22%20points%3D%220%2C%200%2C%201000%2C%201000%2C%200%2C%201000%22%20transform%3D%22scale%28-1%2C1%29%20translate%28-1000%29%22%2F%3E%0A%3Cpolygon%20fill%3D%22hsla%28199%2C90%25%2C64%25%2C1%29%22%20points%3D%221000%2C%201000%2C%201000%2C%200%2C%200%2C%200%22%20transform%3D%22scale%28-1%2C1%29%20translate%28-1000%29%22%2F%3E%0A%3Cpath%20d%3D%22M1000%2C229%20A1000%2C1000%2C0%2C0%2C0%2C229%2C1000%20L1000%2C1000%20z%22%20fill%3D%22hsla%28140%2C98%25%2C61%25%2C1%29%22%2F%3E%0A%3Cpath%20d%3D%22M392%2C500%20L608%2C500%20M500%2C392%20L500%2C608%22%20stroke%3D%22hsla%2847%2C92%25%2C61%25%2C1%29%22%20stroke-width%3D%2272%22%2F%3E%0A%3C%2Fsvg%3E")! + + XCTAssertEqual( + try svgDataURL.imageURL(imageServiceURL: imageServiceURL, size: size), + URL(string: "https://image-service-dev.extratools.works/?imageOrigin=data%3Aimage%2Fsvg%2Bxml%2C%253Csvg%2520viewBox%253D%25220%25200%25201000%25201000%2522%2520xmlns%253D%2522http%253A%252F%252Fwww.w3.org%252F2000%252Fsvg%2522%253E%250A%253Cpolygon%2520fill%253D%2522hsla%252890%252C99%2525%252C52%2525%252C1%2529%2522%2520points%253D%25220%252C%25200%252C%25201000%252C%25201000%252C%25200%252C%25201000%2522%2520transform%253D%2522scale%2528-1%252C1%2529%2520translate%2528-1000%2529%2522%252F%253E%250A%253Cpolygon%2520fill%253D%2522hsla%2528199%252C90%2525%252C64%2525%252C1%2529%2522%2520points%253D%25221000%252C%25201000%252C%25201000%252C%25200%252C%25200%252C%25200%2522%2520transform%253D%2522scale%2528-1%252C1%2529%2520translate%2528-1000%2529%2522%252F%253E%250A%253Cpath%2520d%253D%2522M1000%252C229%2520A1000%252C1000%252C0%252C0%252C0%252C229%252C1000%2520L1000%252C1000%2520z%2522%2520fill%253D%2522hsla%2528140%252C98%2525%252C61%2525%252C1%2529%2522%252F%253E%250A%253Cpath%2520d%253D%2522M392%252C500%2520L608%252C500%2520M500%252C392%2520L500%252C608%2522%2520stroke%253D%2522hsla%252847%252C92%2525%252C61%2525%252C1%2529%2522%2520stroke-width%253D%252272%2522%252F%253E%250A%253C%252Fsvg%253E&imageSize=1024x1024&format=png") + ) + } +} \ No newline at end of file diff --git a/crates/sargon/Cargo.toml b/crates/sargon/Cargo.toml index 2239f4833..d849618a2 100644 --- a/crates/sargon/Cargo.toml +++ b/crates/sargon/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sargon" -version = "1.1.16" +version = "1.1.19" edition = "2021" build = "build.rs" diff --git a/crates/sargon/src/core/utils/image_url_utils.rs b/crates/sargon/src/core/utils/image_url_utils.rs new file mode 100644 index 000000000..48ec9a264 --- /dev/null +++ b/crates/sargon/src/core/utils/image_url_utils.rs @@ -0,0 +1,120 @@ +use crate::prelude::*; +use crate::types::*; + +pub fn is_vector_image(url: &str, image_type: VectorImageType) -> bool { + let parsed_url = match parse_url(url) { + Ok(parsed) => parsed, + Err(_) => return false, + }; + let query_parameters = parsed_url + .query_pairs() + .into_owned() + .collect::>(); + let image_url_string = query_parameters + .get("imageOrigin") + .map(|s| s.as_str()) + .unwrap_or(url); + + image_url_string + .starts_with(&format!("data:image/{}", image_type.data_url_type())) + || image_url_string + .to_lowercase() + .ends_with(image_type.url_extension()) +} + +pub fn make_image_url( + url: &str, + image_service_url: &str, + width: u32, + height: u32, +) -> Result { + const MIN_SIZE: u32 = 64; + + let image_origin = url_encode(url); + let image_size = + format!("{}x{}", width.max(MIN_SIZE), height.max(MIN_SIZE)); + let mut query = + format!("imageOrigin={}&imageSize={}", image_origin, image_size); + + if is_vector_image(url, VectorImageType::Svg) { + query.push_str("&format=png"); + } + + let mut parsed_image_service_url = parse_url(image_service_url)?; + parsed_image_service_url.set_query(Some(&query)); + + Ok(parsed_image_service_url) +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_is_vector_image() { + let svg_url = "https://svgshare.com/i/U7z.svg"; + let pdf_url = "https://example.com/image.pdf"; + let svg_data_url = "data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%201000%201000%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpolygon%20fill%3D%22hsla%2890%2C99%25%2C52%25%2C1%29%22%20points%3D%220%2C%200%2C%201000%2C%201000%2C%200%2C%201000%22%20transform%3D%22scale%28-1%2C1%29%20translate%28-1000%29%22%2F%3E%0A%3Cpolygon%20fill%3D%22hsla%28199%2C90%25%2C64%25%2C1%29%22%20points%3D%221000%2C%201000%2C%201000%2C%200%2C%200%2C%200%22%20transform%3D%22scale%28-1%2C1%29%20translate%28-1000%29%22%2F%3E%0A%3Cpath%20d%3D%22M1000%2C229%20A1000%2C1000%2C0%2C0%2C0%2C229%2C1000%20L1000%2C1000%20z%22%20fill%3D%22hsla%28140%2C98%25%2C61%25%2C1%29%22%2F%3E%0A%3Cpath%20d%3D%22M392%2C500%20L608%2C500%20M500%2C392%20L500%2C608%22%20stroke%3D%22hsla%2847%2C92%25%2C61%25%2C1%29%22%20stroke-width%3D%2272%22%2F%3E%0A%3C%2Fsvg%3E"; + let pdf_data_url = "data:image/pdf,dummydata"; + + assert!(is_vector_image(svg_url, VectorImageType::Svg)); + assert!(is_vector_image(pdf_url, VectorImageType::Pdf)); + assert!(is_vector_image(svg_data_url, VectorImageType::Svg)); + assert!(is_vector_image(pdf_data_url, VectorImageType::Pdf)); + } + + #[test] + fn test_is_vector_image_invalid_url() { + let url = "invalid"; + + assert_eq!(is_vector_image(url, VectorImageType::sample()), false); + } + + #[test] + fn test_is_vector_image_with_image_origin_url() { + let url = "https://image-service-dev.extratools.works/?imageOrigin=https%3A%2F%2Fsvgshare.com%2Fi%2FU7z.svg&imageSize=1024x1024&format=png"; + + assert!(is_vector_image(url, VectorImageType::Svg)); + } + + #[test] + fn test_is_vector_image_with_image_origin_data_url() { + let data_url = "https://image-service-dev.extratools.works/?imageOrigin=data%3Aimage%2Fsvg%2Bxml%2C%253Csvg%2520viewBox%253D%25220%25200%25201000%25201000%2522%2520xmlns%253D%2522http%253A%252F%252Fwww.w3.org%252F2000%252Fsvg%2522%253E%250A%253Cpolygon%2520fill%253D%2522hsla%252890%252C99%2525%252C52%2525%252C1%2529%2522%2520points%253D%25220%252C%25200%252C%25201000%252C%25201000%252C%25200%252C%25201000%2522%2520transform%253D%2522scale%2528-1%252C1%2529%2520translate%2528-1000%2529%2522%252F%253E%250A%253Cpolygon%2520fill%253D%2522hsla%2528199%252C90%2525%252C64%2525%252C1%2529%2522%2520points%253D%25221000%252C%25201000%252C%25201000%252C%25200%252C%25200%252C%25200%2522%2520transform%253D%2522scale%2528-1%252C1%2529%2520translate%2528-1000%2529%2522%252F%253E%250A%253Cpath%2520d%253D%2522M1000%252C229%2520A1000%252C1000%252C0%252C0%252C0%252C229%252C1000%2520L1000%252C1000%2520z%2522%2520fill%253D%2522hsla%2528140%252C98%2525%252C61%2525%252C1%2529%2522%252F%253E%250A%253Cpath%2520d%253D%2522M392%252C500%2520L608%252C500%2520M500%252C392%2520L500%252C608%2522%2520stroke%253D%2522hsla%252847%252C92%2525%252C61%2525%252C1%2529%2522%2520stroke-width%253D%252272%2522%252F%253E%250A%253C%252Fsvg%253E&imageSize=1024x1024&format=png"; + + assert!(is_vector_image(data_url, VectorImageType::Svg)); + } + + #[test] + fn test_make_image_url_svg_url() { + let image_service_url = "https://image-service-dev.extratools.works/"; + let image_origin_url = "https://svgshare.com/i/U7z.svg"; + + pretty_assertions::assert_eq!( + make_image_url(image_origin_url, image_service_url, 1024, 1024).unwrap().to_string(), + "https://image-service-dev.extratools.works/?imageOrigin=https%3A%2F%2Fsvgshare.com%2Fi%2FU7z.svg&imageSize=1024x1024&format=png".to_string() + ); + } + + #[test] + fn test_make_image_url_svg_data_url() { + let image_service_url = "https://image-service-dev.extratools.works/"; + let image_origin_data_url = "data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%201000%201000%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpolygon%20fill%3D%22hsla%2890%2C99%25%2C52%25%2C1%29%22%20points%3D%220%2C%200%2C%201000%2C%201000%2C%200%2C%201000%22%20transform%3D%22scale%28-1%2C1%29%20translate%28-1000%29%22%2F%3E%0A%3Cpolygon%20fill%3D%22hsla%28199%2C90%25%2C64%25%2C1%29%22%20points%3D%221000%2C%201000%2C%201000%2C%200%2C%200%2C%200%22%20transform%3D%22scale%28-1%2C1%29%20translate%28-1000%29%22%2F%3E%0A%3Cpath%20d%3D%22M1000%2C229%20A1000%2C1000%2C0%2C0%2C0%2C229%2C1000%20L1000%2C1000%20z%22%20fill%3D%22hsla%28140%2C98%25%2C61%25%2C1%29%22%2F%3E%0A%3Cpath%20d%3D%22M392%2C500%20L608%2C500%20M500%2C392%20L500%2C608%22%20stroke%3D%22hsla%2847%2C92%25%2C61%25%2C1%29%22%20stroke-width%3D%2272%22%2F%3E%0A%3C%2Fsvg%3E"; + + pretty_assertions::assert_eq!( + make_image_url(image_origin_data_url, image_service_url, 1024, 1024).unwrap().to_string(), + "https://image-service-dev.extratools.works/?imageOrigin=data%3Aimage%2Fsvg%2Bxml%2C%253Csvg%2520viewBox%253D%25220%25200%25201000%25201000%2522%2520xmlns%253D%2522http%253A%252F%252Fwww.w3.org%252F2000%252Fsvg%2522%253E%250A%253Cpolygon%2520fill%253D%2522hsla%252890%252C99%2525%252C52%2525%252C1%2529%2522%2520points%253D%25220%252C%25200%252C%25201000%252C%25201000%252C%25200%252C%25201000%2522%2520transform%253D%2522scale%2528-1%252C1%2529%2520translate%2528-1000%2529%2522%252F%253E%250A%253Cpolygon%2520fill%253D%2522hsla%2528199%252C90%2525%252C64%2525%252C1%2529%2522%2520points%253D%25221000%252C%25201000%252C%25201000%252C%25200%252C%25200%252C%25200%2522%2520transform%253D%2522scale%2528-1%252C1%2529%2520translate%2528-1000%2529%2522%252F%253E%250A%253Cpath%2520d%253D%2522M1000%252C229%2520A1000%252C1000%252C0%252C0%252C0%252C229%252C1000%2520L1000%252C1000%2520z%2522%2520fill%253D%2522hsla%2528140%252C98%2525%252C61%2525%252C1%2529%2522%252F%253E%250A%253Cpath%2520d%253D%2522M392%252C500%2520L608%252C500%2520M500%252C392%2520L500%252C608%2522%2520stroke%253D%2522hsla%252847%252C92%2525%252C61%2525%252C1%2529%2522%2520stroke-width%253D%252272%2522%252F%253E%250A%253C%252Fsvg%253E&imageSize=1024x1024&format=png".to_string() + ); + } + + #[test] + fn test_make_image_url_not_svg() { + let image_service_url = "https://image-service-dev.extratools.works/"; + let image_origin_url = "https://sgo4bmuvgu4t24bvdcfbndmnxigspezdsnzoevon2jb5odru7auq.arweave.net/kZ3AspU1OT1wNRiKFo2Nug0nkyOTcuJVzdJD1w40-Ck"; + + pretty_assertions::assert_eq!( + make_image_url(image_origin_url, image_service_url, 1024, 1024).unwrap().to_string(), + "https://image-service-dev.extratools.works/?imageOrigin=https%3A%2F%2Fsgo4bmuvgu4t24bvdcfbndmnxigspezdsnzoevon2jb5odru7auq.arweave.net%2FkZ3AspU1OT1wNRiKFo2Nug0nkyOTcuJVzdJD1w40-Ck&imageSize=1024x1024".to_string() + ); + } +} diff --git a/crates/sargon/src/core/utils/image_url_utils_uniffi_fn.rs b/crates/sargon/src/core/utils/image_url_utils_uniffi_fn.rs new file mode 100644 index 000000000..722938cd1 --- /dev/null +++ b/crates/sargon/src/core/utils/image_url_utils_uniffi_fn.rs @@ -0,0 +1,55 @@ +use crate::prelude::*; +use crate::types::*; + +#[uniffi::export] +pub fn image_url_utils_is_vector_image( + url: &str, + image_type: VectorImageType, +) -> bool { + is_vector_image(url, image_type) +} + +#[uniffi::export] +pub fn image_url_utils_make_image_url( + url: &str, + image_service_url: &str, + width: u32, + height: u32, +) -> Result { + make_image_url(url, image_service_url, width, height) +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_image_url_utils_is_vector_image() { + let url = "https://svgshare.com/i/U7z.svg"; + let image_type = VectorImageType::Svg; + + assert_eq!( + is_vector_image(url, image_type), + image_url_utils_is_vector_image(&url, image_type) + ) + } + + #[test] + fn test_image_url_utils_make_image_url() { + let url = "https://svgshare.com/i/U7z.svg"; + let image_service_url = "https://image-service-dev.extratools.works"; + let width = 1024; + let height = 1024; + + assert_eq!( + make_image_url(url, image_service_url, width, height), + image_url_utils_make_image_url( + url, + image_service_url, + width, + height + ) + ) + } +} diff --git a/crates/sargon/src/core/utils/mod.rs b/crates/sargon/src/core/utils/mod.rs index 10c882b02..847fafc44 100644 --- a/crates/sargon/src/core/utils/mod.rs +++ b/crates/sargon/src/core/utils/mod.rs @@ -1,9 +1,13 @@ mod factory; +mod image_url_utils; +mod image_url_utils_uniffi_fn; mod logged_panic; mod serialization; mod string_utils; pub use factory::*; +pub use image_url_utils::*; +pub use image_url_utils_uniffi_fn::*; pub use logged_panic::*; pub use serialization::*; pub use string_utils::*; diff --git a/crates/sargon/src/core/utils/string_utils.rs b/crates/sargon/src/core/utils/string_utils.rs index 6e84a6db3..0c65dcf1b 100644 --- a/crates/sargon/src/core/utils/string_utils.rs +++ b/crates/sargon/src/core/utils/string_utils.rs @@ -1,4 +1,5 @@ use crate::CommonError; +use url::form_urlencoded; use url::Url; /// Returns the last `n` chars of the &str `s`. If `s` is shorter than `n` @@ -42,6 +43,10 @@ pub fn parse_url(s: impl AsRef) -> Result { }) } +pub fn url_encode(s: impl AsRef) -> String { + form_urlencoded::byte_serialize(s.as_ref().as_bytes()).collect() +} + #[cfg(test)] mod tests { use super::*; @@ -104,4 +109,19 @@ mod tests { fn test_parse_url_invalid() { assert!(parse_url("https/radixdlt").is_err()); } + + #[test] + fn test_url_encode() { + let url = "https://svgshare.com/i/U7z.svg"; + let data_url = "data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%201000%201000%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpolygon%20fill%3D%22hsla%2890%2C99%25%2C52%25%2C1%29%22%20points%3D%220%2C%200%2C%201000%2C%201000%2C%200%2C%201000%22%20transform%3D%22scale%28-1%2C1%29%20translate%28-1000%29%22%2F%3E%0A%3Cpolygon%20fill%3D%22hsla%28199%2C90%25%2C64%25%2C1%29%22%20points%3D%221000%2C%201000%2C%201000%2C%200%2C%200%2C%200%22%20transform%3D%22scale%28-1%2C1%29%20translate%28-1000%29%22%2F%3E%0A%3Cpath%20d%3D%22M1000%2C229%20A1000%2C1000%2C0%2C0%2C0%2C229%2C1000%20L1000%2C1000%20z%22%20fill%3D%22hsla%28140%2C98%25%2C61%25%2C1%29%22%2F%3E%0A%3Cpath%20d%3D%22M392%2C500%20L608%2C500%20M500%2C392%20L500%2C608%22%20stroke%3D%22hsla%2847%2C92%25%2C61%25%2C1%29%22%20stroke-width%3D%2272%22%2F%3E%0A%3C%2Fsvg%3E"; + + pretty_assertions::assert_eq!( + url_encode(url), + "https%3A%2F%2Fsvgshare.com%2Fi%2FU7z.svg" + ); + pretty_assertions::assert_eq!( + url_encode(data_url), + "data%3Aimage%2Fsvg%2Bxml%2C%253Csvg%2520viewBox%253D%25220%25200%25201000%25201000%2522%2520xmlns%253D%2522http%253A%252F%252Fwww.w3.org%252F2000%252Fsvg%2522%253E%250A%253Cpolygon%2520fill%253D%2522hsla%252890%252C99%2525%252C52%2525%252C1%2529%2522%2520points%253D%25220%252C%25200%252C%25201000%252C%25201000%252C%25200%252C%25201000%2522%2520transform%253D%2522scale%2528-1%252C1%2529%2520translate%2528-1000%2529%2522%252F%253E%250A%253Cpolygon%2520fill%253D%2522hsla%2528199%252C90%2525%252C64%2525%252C1%2529%2522%2520points%253D%25221000%252C%25201000%252C%25201000%252C%25200%252C%25200%252C%25200%2522%2520transform%253D%2522scale%2528-1%252C1%2529%2520translate%2528-1000%2529%2522%252F%253E%250A%253Cpath%2520d%253D%2522M1000%252C229%2520A1000%252C1000%252C0%252C0%252C0%252C229%252C1000%2520L1000%252C1000%2520z%2522%2520fill%253D%2522hsla%2528140%252C98%2525%252C61%2525%252C1%2529%2522%252F%253E%250A%253Cpath%2520d%253D%2522M392%252C500%2520L608%252C500%2520M500%252C392%2520L500%252C608%2522%2520stroke%253D%2522hsla%252847%252C92%2525%252C61%2525%252C1%2529%2522%2520stroke-width%253D%252272%2522%252F%253E%250A%253C%252Fsvg%253E" + ); + } } diff --git a/crates/sargon/src/types/mod.rs b/crates/sargon/src/types/mod.rs index c136ba1f5..d6c267adc 100644 --- a/crates/sargon/src/types/mod.rs +++ b/crates/sargon/src/types/mod.rs @@ -1,3 +1,7 @@ mod ffi_url; +mod vector_image_type; +mod vector_image_type_uniffi_fn; pub use ffi_url::*; +pub use vector_image_type::*; +pub use vector_image_type_uniffi_fn::*; diff --git a/crates/sargon/src/types/vector_image_type.rs b/crates/sargon/src/types/vector_image_type.rs new file mode 100644 index 000000000..0e8f5688b --- /dev/null +++ b/crates/sargon/src/types/vector_image_type.rs @@ -0,0 +1,64 @@ +use crate::prelude::*; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, uniffi::Enum)] +pub enum VectorImageType { + Svg, + Pdf, +} + +impl VectorImageType { + pub fn url_extension(&self) -> &str { + match self { + VectorImageType::Svg => ".svg", + VectorImageType::Pdf => ".pdf", + } + } + + pub fn data_url_type(&self) -> &str { + match self { + VectorImageType::Svg => "svg+xml", + VectorImageType::Pdf => "pdf", + } + } +} + +impl HasSampleValues for VectorImageType { + fn sample() -> Self { + Self::Svg + } + + fn sample_other() -> Self { + Self::Pdf + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = VectorImageType; + + #[test] + fn inequality() { + assert_ne!(SUT::sample(), SUT::sample_other()); + } + + #[test] + fn equality() { + assert_eq!(SUT::sample(), SUT::sample()); + assert_eq!(SUT::sample_other(), SUT::sample_other()); + } + + #[test] + fn test_url_extension() { + assert_eq!(SUT::sample().url_extension(), ".svg"); + assert_eq!(SUT::sample_other().url_extension(), ".pdf"); + } + + #[test] + fn test_data_url_type() { + assert_eq!(SUT::sample().data_url_type(), "svg+xml"); + assert_eq!(SUT::sample_other().data_url_type(), "pdf"); + } +} diff --git a/crates/sargon/src/types/vector_image_type_uniffi_fn.rs b/crates/sargon/src/types/vector_image_type_uniffi_fn.rs new file mode 100644 index 000000000..796314517 --- /dev/null +++ b/crates/sargon/src/types/vector_image_type_uniffi_fn.rs @@ -0,0 +1,49 @@ +use crate::prelude::*; +use crate::types::*; + +#[uniffi::export] +pub fn vector_image_type_url_extension(image_type: VectorImageType) -> String { + image_type.url_extension().to_string() +} + +#[uniffi::export] +pub fn vector_image_type_data_url_type(image_type: VectorImageType) -> String { + image_type.data_url_type().to_string() +} + +#[uniffi::export] +pub fn new_vector_image_type_sample() -> VectorImageType { + VectorImageType::sample() +} + +#[uniffi::export] +pub fn new_vector_image_type_sample_other() -> VectorImageType { + VectorImageType::sample_other() +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = VectorImageType; + + #[test] + fn equality_samples() { + assert_eq!(SUT::sample(), new_vector_image_type_sample()); + assert_eq!(SUT::sample_other(), new_vector_image_type_sample_other()); + } + + #[test] + fn test_vector_image_type_url_extension() { + let sut = SUT::sample(); + assert_eq!(sut.url_extension(), vector_image_type_url_extension(sut)); + } + + #[test] + fn test_vector_image_type_data_url_type() { + let sut = SUT::sample(); + assert_eq!(sut.data_url_type(), vector_image_type_data_url_type(sut)); + } +} diff --git a/jvm/sargon-android/src/main/java/com/radixdlt/sargon/extensions/Url.kt b/jvm/sargon-android/src/main/java/com/radixdlt/sargon/extensions/Url.kt new file mode 100644 index 000000000..9c1525956 --- /dev/null +++ b/jvm/sargon-android/src/main/java/com/radixdlt/sargon/extensions/Url.kt @@ -0,0 +1,30 @@ +package com.radixdlt.sargon.extensions + +import android.net.Uri +import android.util.Size +import com.radixdlt.sargon.CommonException +import com.radixdlt.sargon.Url +import com.radixdlt.sargon.VectorImageType +import com.radixdlt.sargon.imageUrlUtilsIsVectorImage +import com.radixdlt.sargon.imageUrlUtilsMakeImageUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull + +fun String.toUrl() = toHttpUrl() +fun String.toUrlOrNull() = toHttpUrlOrNull() + +fun Uri.isVectorImage(imageType: VectorImageType): Boolean = imageUrlUtilsIsVectorImage( + url = toString(), + imageType = imageType +) + +@Throws(CommonException::class) +fun Uri.intoImageUrl( + imageServiceUrl: Url, + size: Size +): Url = imageUrlUtilsMakeImageUrl( + url = toString(), + imageServiceUrl = imageServiceUrl.toString(), + width = size.width.toUInt(), + height = size.height.toUInt() +) \ No newline at end of file diff --git a/jvm/sargon-android/src/main/java/com/radixdlt/sargon/extensions/VectorImageType.kt b/jvm/sargon-android/src/main/java/com/radixdlt/sargon/extensions/VectorImageType.kt new file mode 100644 index 000000000..21f0292c4 --- /dev/null +++ b/jvm/sargon-android/src/main/java/com/radixdlt/sargon/extensions/VectorImageType.kt @@ -0,0 +1,11 @@ +package com.radixdlt.sargon.extensions + +import com.radixdlt.sargon.VectorImageType +import com.radixdlt.sargon.vectorImageTypeDataUrlType +import com.radixdlt.sargon.vectorImageTypeUrlExtension + +val VectorImageType.urlExtension: String + get() = vectorImageTypeUrlExtension(imageType = this) + +val VectorImageType.dataUrlType: String + get() = vectorImageTypeDataUrlType(imageType = this) diff --git a/jvm/sargon-android/src/main/java/com/radixdlt/sargon/samples/VectorImageTypeSample.kt b/jvm/sargon-android/src/main/java/com/radixdlt/sargon/samples/VectorImageTypeSample.kt new file mode 100644 index 000000000..fcf9bfd14 --- /dev/null +++ b/jvm/sargon-android/src/main/java/com/radixdlt/sargon/samples/VectorImageTypeSample.kt @@ -0,0 +1,14 @@ +package com.radixdlt.sargon.samples + +import com.radixdlt.sargon.VectorImageType +import com.radixdlt.sargon.annotation.UsesSampleValues +import com.radixdlt.sargon.newVectorImageTypeSample +import com.radixdlt.sargon.newVectorImageTypeSampleOther + +@UsesSampleValues +val VectorImageType.Companion.sample: Sample + get() = object : Sample { + override fun invoke(): VectorImageType = newVectorImageTypeSample() + + override fun other(): VectorImageType = newVectorImageTypeSampleOther() + } \ No newline at end of file diff --git a/jvm/sargon-android/src/test/java/com/radixdlt/sargon/UrlTest.kt b/jvm/sargon-android/src/test/java/com/radixdlt/sargon/UrlTest.kt new file mode 100644 index 000000000..51a5c0f5f --- /dev/null +++ b/jvm/sargon-android/src/test/java/com/radixdlt/sargon/UrlTest.kt @@ -0,0 +1,117 @@ +package com.radixdlt.sargon + +import android.net.Uri +import android.util.Size +import androidx.core.net.toUri +import com.radixdlt.sargon.extensions.intoImageUrl +import com.radixdlt.sargon.extensions.isVectorImage +import com.radixdlt.sargon.extensions.toUrl +import com.radixdlt.sargon.extensions.toUrlOrNull +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkConstructor +import io.mockk.slot +import okhttp3.HttpUrl.Companion.toHttpUrl +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.net.URI + +class UrlTest { + + @Test + fun testToUrl() { + assertEquals( + "https://svgshare.com/i/U7z.svg", + "https://svgshare.com/i/U7z.svg".toUrl().toString(), + ) + } + + @Test + fun testToUrlOrNull() { + assertEquals( + "https://svgshare.com/i/U7z.svg", + "https://svgshare.com/i/U7z.svg".toUrlOrNull().toString(), + ) + + assertNull( + "data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%201000%201000%22%20".toUrlOrNull(), + ) + } + + @Test + fun testVectorImage() { + val svgUrl = mockUri("https://svgshare.com/i/U7z.svg") + + assertTrue(svgUrl.isVectorImage(imageType = VectorImageType.SVG)) + } + + @Test + fun testImageUrl() { + val size = mockSize(width = 1024, height = 1024) + val imageServiceURL = "https://image-service-dev.extratools.works".toUrl() + val svgURL = mockUri("https://svgshare.com/i/U7z.svg") + + assertEquals( + "https://image-service-dev.extratools.works/?imageOrigin=https%3A%2F%2Fsvgshare.com%2Fi%2FU7z.svg&imageSize=1024x1024&format=png", + svgURL.intoImageUrl( + imageServiceUrl = imageServiceURL, + size = size + ).toString() + ) + } + + @Test + fun testImageUrlWithDataUrl() { + val size = mockSize(width = 1024, height = 1024) + val imageServiceURL = "https://image-service-dev.extratools.works".toUrl() + val svgDataUrl = mockUri("data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%201000%201000%22%20" + + "xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpolygon%20fill%3" + + "D%22hsla%2890%2C99%25%2C52%25%2C1%29%22%20points%3D%220%2C%200%2C%201000%2C" + + "%201000%2C%200%2C%201000%22%20transform%3D%22scale%28-1%2C1%29%20translate%" + + "28-1000%29%22%2F%3E%0A%3Cpolygon%20fill%3D%22hsla%28199%2C90%25%2C64%25%2C1" + + "%29%22%20points%3D%221000%2C%201000%2C%201000%2C%200%2C%200%2C%200%22%20tra" + + "nsform%3D%22scale%28-1%2C1%29%20translate%28-1000%29%22%2F%3E%0A%3Cpath%20d" + + "%3D%22M1000%2C229%20A1000%2C1000%2C0%2C0%2C0%2C229%2C1000%20L1000%2C1000%20" + + "z%22%20fill%3D%22hsla%28140%2C98%25%2C61%25%2C1%29%22%2F%3E%0A%3Cpath%20d%3" + + "D%22M392%2C500%20L608%2C500%20M500%2C392%20L500%2C608%22%20stroke%3D%22hsla" + + "%2847%2C92%25%2C61%25%2C1%29%22%20stroke-width%3D%2272%22%2F%3E%0A%3C%2Fsvg" + + "%3E") + + assertEquals( + "https://image-service-dev.extratools.works/?imageOrigin=data%3Aimage%2Fsvg" + + "%2Bxml%2C%253Csvg%2520viewBox%253D%25220%25200%25201000%25201000%2522%2520x" + + "mlns%253D%2522http%253A%252F%252Fwww.w3.org%252F2000%252Fsvg%2522%253E%250A" + + "%253Cpolygon%2520fill%253D%2522hsla%252890%252C99%2525%252C52%2525%252C1%25" + + "29%2522%2520points%253D%25220%252C%25200%252C%25201000%252C%25201000%252C%2" + + "5200%252C%25201000%2522%2520transform%253D%2522scale%2528-1%252C1%2529%2520" + + "translate%2528-1000%2529%2522%252F%253E%250A%253Cpolygon%2520fill%253D%2522" + + "hsla%2528199%252C90%2525%252C64%2525%252C1%2529%2522%2520points%253D%252210" + + "00%252C%25201000%252C%25201000%252C%25200%252C%25200%252C%25200%2522%2520tr" + + "ansform%253D%2522scale%2528-1%252C1%2529%2520translate%2528-1000%2529%2522%" + + "252F%253E%250A%253Cpath%2520d%253D%2522M1000%252C229%2520A1000%252C1000%252" + + "C0%252C0%252C0%252C229%252C1000%2520L1000%252C1000%2520z%2522%2520fill%253D" + + "%2522hsla%2528140%252C98%2525%252C61%2525%252C1%2529%2522%252F%253E%250A%25" + + "3Cpath%2520d%253D%2522M392%252C500%2520L608%252C500%2520M500%252C392%2520L5" + + "00%252C608%2522%2520stroke%253D%2522hsla%252847%252C92%2525%252C61%2525%252" + + "C1%2529%2522%2520stroke-width%253D%252272%2522%252F%253E%250A%253C%252Fsvg%" + + "253E&imageSize=1024x1024&format=png", + svgDataUrl.intoImageUrl(imageServiceUrl = imageServiceURL, size = size).toString() + ) + } + + private fun mockUri(urlString: String): Uri { + val uri = mockk() + every { uri.toString() } returns urlString + return uri + } + + private fun mockSize(width: Int, height: Int): Size { + val size = mockk() + every { size.width } returns width + every { size.height } returns height + return size + } +} \ No newline at end of file diff --git a/jvm/sargon-android/src/test/java/com/radixdlt/sargon/VectorImageTypeTest.kt b/jvm/sargon-android/src/test/java/com/radixdlt/sargon/VectorImageTypeTest.kt new file mode 100644 index 000000000..3790b9de5 --- /dev/null +++ b/jvm/sargon-android/src/test/java/com/radixdlt/sargon/VectorImageTypeTest.kt @@ -0,0 +1,25 @@ +package com.radixdlt.sargon + +import com.radixdlt.sargon.extensions.dataUrlType +import com.radixdlt.sargon.extensions.urlExtension +import com.radixdlt.sargon.samples.Sample +import com.radixdlt.sargon.samples.sample +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class VectorImageTypeTest: SampleTestable { + override val samples: List> + get() = listOf(VectorImageType.sample) + + @Test + fun testExtension() { + assertEquals(".svg", VectorImageType.sample().urlExtension) + assertEquals(".pdf", VectorImageType.sample.other().urlExtension) + } + + @Test + fun testDataUrlType() { + assertEquals("svg+xml", VectorImageType.sample().dataUrlType) + assertEquals("pdf", VectorImageType.sample.other().dataUrlType) + } +} \ No newline at end of file