Initial version with image thumbnails
Signed-off-by: trivernis <trivernis@protonmail.com>imgbot
commit
cf3ad87360
@ -0,0 +1,2 @@
|
|||||||
|
/target
|
||||||
|
Cargo.lock
|
@ -0,0 +1,8 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DiscordProjectSettings">
|
||||||
|
<option name="show" value="ASK" />
|
||||||
|
<option name="description" value="" />
|
||||||
|
</component>
|
||||||
|
</project>
|
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/thumbnailer.iml" filepath="$PROJECT_DIR$/.idea/thumbnailer.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="CPP_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "thumbnailer"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
webp = "0.2.0"
|
||||||
|
mime = "0.3.16"
|
||||||
|
thiserror = "1.0.30"
|
||||||
|
rayon = "1.5.1"
|
||||||
|
|
||||||
|
[dependencies.image]
|
||||||
|
version = "0.23.14"
|
@ -0,0 +1,20 @@
|
|||||||
|
use std::io;
|
||||||
|
use mime::Mime;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
pub type ThumbResult<T> = Result<T, ThumbError>;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ThumbError {
|
||||||
|
#[error("IO Error {0}")]
|
||||||
|
IO(#[from] io::Error),
|
||||||
|
|
||||||
|
#[error("Image Error {0}")]
|
||||||
|
Image(#[from] image::error::ImageError),
|
||||||
|
|
||||||
|
#[error("Failed to decode image")]
|
||||||
|
Decode,
|
||||||
|
|
||||||
|
#[error("Unsupported media type {0}")]
|
||||||
|
Unsupported(Mime),
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
use std::io::{BufRead, Read, Seek};
|
||||||
|
use image::{DynamicImage, ImageFormat};
|
||||||
|
use mime::Mime;
|
||||||
|
use image::io::Reader as ImageReader;
|
||||||
|
use webp::Decoder as WebpDecoder;
|
||||||
|
use crate::error::{ThumbError, ThumbResult};
|
||||||
|
|
||||||
|
const IMAGE_WEBP_MIME: &str = "image/webp";
|
||||||
|
|
||||||
|
/// Reads an image with a known mime type
|
||||||
|
pub fn read_image<R: BufRead + Seek>(reader: R, mime: Mime) -> ThumbResult<DynamicImage> {
|
||||||
|
match mime.essence_str() {
|
||||||
|
IMAGE_WEBP_MIME => read_webp_image(reader),
|
||||||
|
_ => read_generic_image(reader, mime_to_image_format(mime)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads a webp image
|
||||||
|
fn read_webp_image<R: Read>(mut reader: R) -> ThumbResult<DynamicImage> {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
reader.read_to_end(&mut buf)?;
|
||||||
|
let webp_image = WebpDecoder::new(&buf).decode().ok_or_else(|| ThumbError::Decode)?;
|
||||||
|
|
||||||
|
Ok(webp_image.to_image())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads a generic image
|
||||||
|
fn read_generic_image<R: BufRead + Seek>(reader: R, format: Option<ImageFormat>) -> ThumbResult<DynamicImage> {
|
||||||
|
let reader = if let Some(format) = format {
|
||||||
|
ImageReader::with_format(reader, format)
|
||||||
|
} else {
|
||||||
|
ImageReader::new(reader).with_guessed_format()?
|
||||||
|
};
|
||||||
|
let image = reader.decode()?;
|
||||||
|
|
||||||
|
Ok(image)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mime_to_image_format(mime: Mime) -> Option<ImageFormat> {
|
||||||
|
match mime.subtype().as_str() {
|
||||||
|
"png" => Some(ImageFormat::Png),
|
||||||
|
"jpeg" => Some(ImageFormat::Jpeg),
|
||||||
|
"bmp" => Some(ImageFormat::Bmp),
|
||||||
|
"gif" => Some(ImageFormat::Gif),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
use crate::error::{ThumbError, ThumbResult};
|
||||||
|
use crate::formats::image_format::read_image;
|
||||||
|
use image::DynamicImage;
|
||||||
|
use mime::Mime;
|
||||||
|
use std::io::{BufRead, Seek};
|
||||||
|
|
||||||
|
pub mod image_format;
|
||||||
|
|
||||||
|
/// Reads the buffer content into an image that can be used for thumbnail generation
|
||||||
|
pub fn get_base_image<R: BufRead + Seek>(reader: R, mime: Mime) -> ThumbResult<DynamicImage> {
|
||||||
|
match mime.type_() {
|
||||||
|
mime::IMAGE => read_image(reader, mime),
|
||||||
|
_ => Err(ThumbError::Unsupported(mime)),
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
use crate::error::ThumbResult;
|
||||||
|
use image;
|
||||||
|
use image::imageops::FilterType;
|
||||||
|
use image::{DynamicImage, ImageOutputFormat};
|
||||||
|
use mime::Mime;
|
||||||
|
use rayon::prelude::*;
|
||||||
|
use std::io::{BufRead, Seek, Write};
|
||||||
|
|
||||||
|
use crate::formats::get_base_image;
|
||||||
|
pub use size::ThumbnailSize;
|
||||||
|
|
||||||
|
pub mod error;
|
||||||
|
mod formats;
|
||||||
|
mod size;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Thumbnail {
|
||||||
|
inner: DynamicImage,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Thumbnail {
|
||||||
|
/// Writes the bytes of the image in a png format
|
||||||
|
pub fn write_png<W: Write>(self, writer: &mut W) -> ThumbResult<()> {
|
||||||
|
self.inner.write_to(writer, ImageOutputFormat::Png)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes the bytes of the image in a jpeg format
|
||||||
|
pub fn write_jpeg<W: Write>(self, writer: &mut W, compression: u8) -> ThumbResult<()> {
|
||||||
|
self.inner
|
||||||
|
.write_to(writer, ImageOutputFormat::Jpeg(compression))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates thumbnails of the requested sizes for the given reader providing the content as bytes and
|
||||||
|
/// the mime describing the contents type
|
||||||
|
pub fn create_thumbnails<R: BufRead + Seek, I: IntoIterator<Item = ThumbnailSize>>(
|
||||||
|
reader: R,
|
||||||
|
mime: Mime,
|
||||||
|
sizes: I,
|
||||||
|
) -> ThumbResult<Vec<Thumbnail>> {
|
||||||
|
let image = get_base_image(reader, mime)?;
|
||||||
|
let sizes: Vec<ThumbnailSize> = sizes.into_iter().collect();
|
||||||
|
let thumbnails = resize_images(image, &sizes)
|
||||||
|
.into_iter()
|
||||||
|
.map(|image| Thumbnail { inner: image })
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(thumbnails)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resize_images(image: DynamicImage, sizes: &[ThumbnailSize]) -> Vec<DynamicImage> {
|
||||||
|
sizes
|
||||||
|
.into_par_iter()
|
||||||
|
.map(|size| {
|
||||||
|
let (width, height) = size.dimensions();
|
||||||
|
image.resize(width, height, FilterType::Nearest)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
|
||||||
|
/// Represents fixed sizes of a thumbnail
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub enum ThumbnailSize {
|
||||||
|
Icon,
|
||||||
|
Small,
|
||||||
|
Medium,
|
||||||
|
Large,
|
||||||
|
Larger,
|
||||||
|
Custom((u32, u32))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ThumbnailSize {
|
||||||
|
pub fn dimensions(&self) -> (u32, u32) {
|
||||||
|
match self {
|
||||||
|
ThumbnailSize::Icon => (64, 64),
|
||||||
|
ThumbnailSize::Small => (128, 128),
|
||||||
|
ThumbnailSize::Medium => (256, 256),
|
||||||
|
ThumbnailSize::Large => (512, 512),
|
||||||
|
ThumbnailSize::Larger => (1024, 1024),
|
||||||
|
ThumbnailSize::Custom(size) => *size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 33 KiB |
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
After Width: | Height: | Size: 6.4 KiB |
@ -0,0 +1,79 @@
|
|||||||
|
const PNG_BYTES: &'static [u8] = include_bytes!("assets/test.png");
|
||||||
|
const JPG_BYTES: &'static [u8] = include_bytes!("assets/test.jpg");
|
||||||
|
const WEBP_BYTES: &'static [u8] = include_bytes!("assets/test.webp");
|
||||||
|
|
||||||
|
use crate::ImageType::{Jpeg, Png, Webp};
|
||||||
|
use mime::Mime;
|
||||||
|
use std::io::Cursor;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use thumbnailer::error::ThumbResult;
|
||||||
|
use thumbnailer::{create_thumbnails, Thumbnail, ThumbnailSize};
|
||||||
|
|
||||||
|
enum ImageType {
|
||||||
|
Png,
|
||||||
|
Jpeg,
|
||||||
|
Webp,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_creates_small_thumbnails_for_png() {
|
||||||
|
create_thumbnail(Png, ThumbnailSize::Small).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_creates_medium_thumbnails_for_png() {
|
||||||
|
create_thumbnail(Png, ThumbnailSize::Medium).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_creates_large_thumbnails_for_png() {
|
||||||
|
create_thumbnail(Png, ThumbnailSize::Large).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_creates_small_thumbnails_for_jpeg() {
|
||||||
|
create_thumbnail(Jpeg, ThumbnailSize::Small).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_creates_medium_thumbnails_for_jpeg() {
|
||||||
|
create_thumbnail(Jpeg, ThumbnailSize::Medium).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_creates_large_thumbnails_for_jpeg() {
|
||||||
|
create_thumbnail(Jpeg, ThumbnailSize::Large).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_creates_small_thumbnails_for_webp() {
|
||||||
|
create_thumbnail(Webp, ThumbnailSize::Small).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_creates_medium_thumbnails_for_webp() {
|
||||||
|
create_thumbnail(Webp, ThumbnailSize::Medium).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_creates_large_thumbnails_for_webp() {
|
||||||
|
create_thumbnail(Webp, ThumbnailSize::Large).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_thumbnail(image_type: ImageType, size: ThumbnailSize) -> ThumbResult<Vec<Thumbnail>> {
|
||||||
|
match image_type {
|
||||||
|
ImageType::Png => {
|
||||||
|
let reader = Cursor::new(PNG_BYTES);
|
||||||
|
create_thumbnails(reader, mime::IMAGE_PNG, [size])
|
||||||
|
}
|
||||||
|
ImageType::Jpeg => {
|
||||||
|
let reader = Cursor::new(JPG_BYTES);
|
||||||
|
create_thumbnails(reader, mime::IMAGE_JPEG, [size])
|
||||||
|
}
|
||||||
|
ImageType::Webp => {
|
||||||
|
let reader = Cursor::new(WEBP_BYTES);
|
||||||
|
let webp_mime = Mime::from_str("image/webp").unwrap();
|
||||||
|
create_thumbnails(reader, webp_mime, [size])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,81 @@
|
|||||||
|
use mime::Mime;
|
||||||
|
use std::io::Cursor;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use thumbnailer::error::ThumbResult;
|
||||||
|
use thumbnailer::{create_thumbnails, ThumbnailSize};
|
||||||
|
|
||||||
|
const PNG_BYTES: &'static [u8] = include_bytes!("assets/test.png");
|
||||||
|
const JPG_BYTES: &'static [u8] = include_bytes!("assets/test.jpg");
|
||||||
|
const WEBP_BYTES: &'static [u8] = include_bytes!("assets/test.webp");
|
||||||
|
|
||||||
|
enum SourceFormat {
|
||||||
|
Png,
|
||||||
|
Jpeg,
|
||||||
|
Webp,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TargetFormat {
|
||||||
|
Png,
|
||||||
|
Jpeg,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_converts_png_thumbnails_for_png() {
|
||||||
|
write_thumbnail(SourceFormat::Png, TargetFormat::Png).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_converts_jpeg_thumbnails_for_png() {
|
||||||
|
write_thumbnail(SourceFormat::Png, TargetFormat::Jpeg).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_converts_png_thumbnails_for_jpeg() {
|
||||||
|
write_thumbnail(SourceFormat::Jpeg, TargetFormat::Png).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_converts_jpeg_thumbnails_for_jpeg() {
|
||||||
|
write_thumbnail(SourceFormat::Jpeg, TargetFormat::Jpeg).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_converts_png_thumbnails_for_webp() {
|
||||||
|
write_thumbnail(SourceFormat::Webp, TargetFormat::Png).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_converts_jpeg_thumbnails_for_webp() {
|
||||||
|
write_thumbnail(SourceFormat::Webp, TargetFormat::Jpeg).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_thumbnail(
|
||||||
|
source_format: SourceFormat,
|
||||||
|
target_format: TargetFormat,
|
||||||
|
) -> ThumbResult<Vec<u8>> {
|
||||||
|
let thumb = match source_format {
|
||||||
|
SourceFormat::Png => {
|
||||||
|
let reader = Cursor::new(PNG_BYTES);
|
||||||
|
create_thumbnails(reader, mime::IMAGE_PNG, [ThumbnailSize::Medium]).unwrap()
|
||||||
|
}
|
||||||
|
SourceFormat::Jpeg => {
|
||||||
|
let reader = Cursor::new(JPG_BYTES);
|
||||||
|
create_thumbnails(reader, mime::IMAGE_JPEG, [ThumbnailSize::Medium]).unwrap()
|
||||||
|
}
|
||||||
|
SourceFormat::Webp => {
|
||||||
|
let reader = Cursor::new(WEBP_BYTES);
|
||||||
|
let webp_mime = Mime::from_str("image/webp").unwrap();
|
||||||
|
create_thumbnails(reader, webp_mime, [ThumbnailSize::Medium]).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pop()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
match target_format {
|
||||||
|
TargetFormat::Png => thumb.write_png(&mut buf)?,
|
||||||
|
TargetFormat::Jpeg => thumb.write_jpeg(&mut buf, 8)?,
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(buf)
|
||||||
|
}
|
Loading…
Reference in New Issue