use imagesize::blob_size;
use imagesize::image_type;
use imagesize::ImageSize;
pub use imagesize::ImageType;
use log::trace;
use std::collections::HashMap;
use std::error::Error;
use std::time::Duration;
use reqwest::blocking::Client;
use reqwest::blocking::Response;
use reqwest::header::CONTENT_TYPE;
use reqwest::header::RANGE;
use reqwest::header::USER_AGENT;
use reqwest::IntoUrl;
use reqwest::Url;
use quick_xml::events::Event;
use quick_xml::Reader;
#[derive(Debug)]
pub struct ImageLink {
pub url: Url,
pub image_type: ImageType,
pub width: usize,
pub height: usize,
}
impl ImageLink {
pub fn new<U: IntoUrl, P: AsRef<str>>(
url: U,
user_agent: P,
tcp_timeout: u64,
) -> Result<Self, Box<dyn Error>> {
let url = url.into_url()?;
let (image_size, image_type) = get_pixel_size(url.clone(), user_agent, tcp_timeout)?;
Ok(ImageLink {
url,
image_type,
width: image_size.width,
height: image_size.height,
})
}
pub fn from_website<P, Q>(
base_url: P,
user_agent: Q,
tcp_timeout: u64,
) -> Result<Vec<ImageLink>, Box<dyn Error>>
where
P: AsRef<str>,
Q: AsRef<str>,
{
let base_url = Url::parse(base_url.as_ref())?;
let response = Client::new()
.get(base_url.clone())
.timeout(Duration::new(tcp_timeout, 0))
.header(USER_AGENT, user_agent.as_ref())
.send()?;
let mut list: Vec<String> = analyze_location(response)?;
list.push(String::from("/favicon.ico"));
Ok(list
.iter()
.filter_map(|unfiltered_url| base_url.join(&unfiltered_url).ok())
.filter_map(|image_url| ImageLink::new(image_url, user_agent.as_ref(), tcp_timeout).ok())
.collect())
}
}
fn analyze_content(content: &str) -> Result<Vec<String>, Box<dyn Error>> {
let mut reader = Reader::from_str(content);
reader.trim_text(true);
reader.check_end_names(false);
let mut buf = Vec::new();
let mut list: Vec<String> = Vec::new();
loop {
match reader.read_event(&mut buf) {
Ok(Event::Empty(ref e)) => {
list.extend(check_start_elem(&reader, e));
}
Ok(Event::Start(ref e)) => {
list.extend(check_start_elem(&reader, e));
}
Ok(Event::End(_)) => {}
Ok(Event::Text(_)) => {}
Ok(Event::Eof) => break,
Err(e) => {
println!("Error at position {}: {:?}", reader.buffer_position(), e);
}
_ => (), }
buf.clear();
}
Ok(list)
}
fn get_pixel_size<U: IntoUrl, P: AsRef<str>>(
url: U,
user_agent: P,
tcp_timeout: u64,
) -> Result<(ImageSize, ImageType), Box<dyn Error>> {
let url = url.into_url()?;
let response = Client::new()
.get(url.clone())
.timeout(Duration::new(tcp_timeout, 0))
.header(RANGE, "bytes=0-99")
.header(USER_AGENT, user_agent.as_ref())
.send()?;
let data: Vec<u8> = response.bytes()?.to_vec();
let pixel_size = blob_size(&data)?;
let image_type = image_type(&data)?;
trace!(
"{}, downloaded bytes: {}, pixels: {}x{}, type: {:?}",
url,
data.len(),
pixel_size.width,
pixel_size.height,
image_type
);
Ok((pixel_size, image_type))
}
fn analyze_location(response: Response) -> Result<Vec<String>, Box<dyn Error>> {
let content_type = response.headers().get(CONTENT_TYPE);
if let Some(content_type) = content_type {
if content_type.to_str().unwrap_or("").starts_with("text/html") {
let content = response.text()?;
let list = analyze_content(&content)?;
return Ok(list);
}
}
Ok(Vec::new())
}
fn attr_to_hash(
reader: &quick_xml::Reader<&[u8]>,
e: quick_xml::events::attributes::Attributes,
) -> HashMap<String, String> {
let attrs_hashed: HashMap<String, String> = e
.filter(|x| x.is_ok())
.map(|x| x.unwrap())
.map(|x| {
(
reader.decode(x.key).map(|b| b.to_string().to_lowercase()),
reader.decode(&x.value).map(|c| c.to_string()),
)
})
.filter(|i| i.0.is_ok() && i.1.is_ok())
.map(|j| (j.0.unwrap(), j.1.unwrap()))
.collect();
attrs_hashed
}
fn extract(
attrs_hashed: &HashMap<String, String>,
names: &Vec<String>,
key_name: &str,
content: &str,
) -> Vec<String> {
let mut list: Vec<String> = vec![];
let name: Option<&String> = attrs_hashed.get(key_name);
let content = attrs_hashed.get(content);
if let Some(name) = name {
if let Some(content) = content {
let name: String = name.to_lowercase();
let content = content.to_lowercase();
if names.contains(&name) {
list.push(content.to_string());
}
}
}
list
}
fn check_start_elem(
reader: &quick_xml::Reader<&[u8]>,
e: &quick_xml::events::BytesStart<'_>,
) -> Vec<String> {
let meta_name_attrs: Vec<String> = vec![
String::from("msapplication-TileImage"),
String::from("msapplication-square70x70logo"),
String::from("msapplication-square150x150logo"),
String::from("msapplication-square310x310logo"),
String::from("msapplication-wide310x150logo"),
];
let meta_property_attrs: Vec<String> = vec![String::from("og:image")];
let link_rel_attrs: Vec<String> = vec![
String::from("apple-touch-icon"),
String::from("shortcut icon"),
String::from("icon"),
];
let mut list: Vec<String> = Vec::new();
match e.name() {
b"meta" => {
let attrs_hashed = attr_to_hash(&reader, e.attributes());
let l = extract(&attrs_hashed, &meta_name_attrs, "name", "content");
list.extend(l);
let l = extract(&attrs_hashed, &meta_property_attrs, "property", "content");
list.extend(l);
}
b"link" => {
let attrs_hashed = attr_to_hash(&reader, e.attributes());
let l = extract(&attrs_hashed, &link_rel_attrs, "rel", "href");
list.extend(l);
}
_ => {}
};
list
}