use std::{
collections::{HashMap, HashSet},
io::{Cursor, Read},
sync::Arc,
time::Duration,
};
use crate::error::Error;
use crate::figma_schema::{Paint, Transform};
use crate::proxy_config::ProxyConfig;
use dc_bundle::definition::EncodedImageMap;
use image::DynamicImage;
use log::{info, warn};
use serde::{Deserialize, Serialize};
use serde_bytes::ByteBuf;
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub struct VectorImageId {
stroke_hash: u64,
fill_hash: u64,
transforms: Vec<Option<Transform>>,
paints: Vec<Paint>,
}
fn http_fetch_image(
url: impl ToString,
proxy_config: &ProxyConfig,
) -> Result<(DynamicImage, Vec<u8>), Error> {
let url = url.to_string();
let mut client_builder = reqwest::blocking::Client::builder();
if let ProxyConfig::HttpProxyConfig(spec) = proxy_config {
client_builder = client_builder.proxy(reqwest::Proxy::all(spec)?);
}
let mut response = client_builder
.build()?
.get(url.as_str())
.timeout(Duration::from_secs(90))
.send()?
.error_for_status()?;
let mut response_bytes: Vec<u8> = Vec::new();
response.read_to_end(&mut response_bytes)?;
let img = image::ImageReader::new(Cursor::new(response_bytes.as_slice()))
.with_guessed_format()?
.decode()?;
Ok((img, response_bytes))
}
fn lookup_or_fetch(
client_images: &HashSet<String>,
client_used_images: &mut HashSet<String>,
referenced_images: &mut HashSet<String>,
decoded_image_sizes: &mut HashMap<String, (u32, u32)>,
network_bytes: &mut HashMap<String, Arc<serde_bytes::ByteBuf>>,
url: Option<&Option<String>>,
proxy_config: &ProxyConfig,
) -> bool {
if let Some(Some(url)) = url {
referenced_images.insert(url.clone());
if client_images.contains(url) {
client_used_images.insert(url.clone());
return true;
}
if network_bytes.contains_key(url) {
return true;
} else {
match http_fetch_image(url, proxy_config) {
Ok((dynamic_image, fetched_bytes)) => {
decoded_image_sizes
.insert(url.clone(), (dynamic_image.width(), dynamic_image.height()));
network_bytes
.insert(url.clone(), Arc::new(serde_bytes::ByteBuf::from(fetched_bytes)));
return true;
}
Err(e) => {
warn!("Unable to fetch Figma Image URL {}: {:#?}", url, e);
}
}
}
}
false
}
pub struct ImageContext {
images: HashMap<String, Option<String>>,
image_res_map: HashMap<String, String>,
vectors: HashSet<String>,
node_urls: HashMap<String, Option<String>>,
network_bytes: HashMap<String, Arc<serde_bytes::ByteBuf>>,
decoded_image_sizes: HashMap<String, (u32, u32)>,
ignored_images: HashSet<String>,
image_hash: HashMap<String, VectorImageId>,
client_images: HashSet<String>,
client_used_images: HashSet<String>,
referenced_images: HashSet<String>,
proxy_config: ProxyConfig,
}
impl ImageContext {
pub fn new(
images: HashMap<String, Option<String>>,
image_res_map: HashMap<String, String>,
proxy_config: &ProxyConfig,
) -> ImageContext {
ImageContext {
images,
image_res_map,
vectors: HashSet::new(),
node_urls: HashMap::new(),
network_bytes: HashMap::new(),
decoded_image_sizes: HashMap::new(),
ignored_images: HashSet::new(),
image_hash: HashMap::new(),
client_images: HashSet::new(),
client_used_images: HashSet::new(),
referenced_images: HashSet::new(),
proxy_config: proxy_config.clone(),
}
}
pub fn image_fill(&mut self, image_ref: impl ToString, node_name: &String) -> Option<String> {
if self.ignored_images.contains(node_name) {
None
} else {
let url = self.images.get(&image_ref.to_string());
if lookup_or_fetch(
&self.client_images,
&mut self.client_used_images,
&mut self.referenced_images,
&mut self.decoded_image_sizes,
&mut self.network_bytes,
url,
&self.proxy_config,
) {
url.unwrap_or(&None).as_ref().map(|url_string| url_string.clone())
} else {
None
}
}
}
pub fn image_res(&mut self, image_ref: impl ToString) -> Option<String> {
if self.image_res_map.contains_key(&image_ref.to_string()) {
self.image_res_map.get(&image_ref.to_string()).map(|s| s.to_string())
} else {
None
}
}
pub fn cache(&self) -> HashMap<String, String> {
let mut map = HashMap::new();
let url_map = self.node_urls.clone();
for (node, addr) in url_map {
if let Some(url) = addr {
map.insert(node, url);
}
}
map.clone()
}
pub fn update_images(&mut self, images: HashMap<String, Option<String>>) {
self.images = images;
}
pub fn encoded_image_map(&self) -> EncodedImageMap {
let mut image_bytes: HashMap<String, Arc<ByteBuf>> = HashMap::new();
for (k, v) in &self.network_bytes {
image_bytes.insert(k.clone(), v.clone());
}
for k in &self.referenced_images {
image_bytes.entry(k.clone()).or_insert_with(|| Arc::new(serde_bytes::ByteBuf::new()));
}
EncodedImageMap(image_bytes)
}
pub fn set_ignored_images(&mut self, images: Option<&HashSet<String>>) {
if let Some(images) = images {
self.ignored_images = images.clone();
} else {
self.ignored_images.clear();
}
}
}
const SESSION_VERSION: u32 = 2;
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct ImageContextSession {
#[serde(default = "default_session_version")]
version: u32,
images: HashMap<String, Option<String>>,
vectors: HashSet<String>,
image_hash: HashMap<String, VectorImageId>,
#[serde(default)]
image_bounds: HashMap<String, (u32, u32)>,
client_images: HashSet<String>,
}
fn default_session_version() -> u32 {
1
}
impl ImageContext {
pub fn as_session(&self) -> ImageContextSession {
let mut client_images = self.client_used_images.clone();
for (k, _) in &self.network_bytes {
client_images.insert(k.clone());
}
let mut image_bounds = self.decoded_image_sizes.clone();
for (k, &(width, height)) in &self.decoded_image_sizes {
if client_images.contains(k) {
image_bounds.insert(k.clone(), (width, height));
}
}
ImageContextSession {
version: SESSION_VERSION,
images: self
.images
.clone()
.into_iter()
.filter(|(k, _)| client_images.contains(k))
.collect(),
vectors: self
.vectors
.clone()
.into_iter()
.filter(|k| client_images.contains(k))
.collect(),
image_hash: self
.image_hash
.clone()
.into_iter()
.filter(|(k, _)| client_images.contains(k))
.collect(),
image_bounds,
client_images,
}
}
pub fn add_session_info(&mut self, session: ImageContextSession) {
if session.version != SESSION_VERSION {
warn!(
"ImageContextSession version mismatch: expected {}, got {}. \
Discarding cached session; images will be re-fetched.",
SESSION_VERSION, session.version
);
return;
}
for (k, v) in session.images {
self.images.insert(k, v);
}
for (k, v) in session.image_bounds {
self.decoded_image_sizes.insert(k, v);
}
for k in session.vectors {
self.vectors.insert(k);
}
for (k, v) in session.image_hash {
self.image_hash.insert(k, v);
}
for k in session.client_images {
self.client_images.insert(k);
}
}
pub fn merge_remote_images(&mut self, remote_images: HashMap<String, Option<String>>) {
let mut merged_count = 0;
for (image_ref, url) in remote_images {
if !self.images.contains_key(&image_ref) {
self.images.insert(image_ref, url);
merged_count += 1;
}
}
if merged_count > 0 {
info!("Merged {} image refs from remote document", merged_count);
}
}
}