Compare commits
3 Commits
24240c5f68
...
7e2cf1b956
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e2cf1b956 | |||
| b61e65bd83 | |||
| 0799708109 |
22
Cargo.lock
generated
22
Cargo.lock
generated
@ -448,9 +448,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deflate"
|
name = "deflate"
|
||||||
version = "0.7.20"
|
version = "0.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "707b6a7b384888a70c8d2e8650b3e60170dfc6a67bb4aa67b6dfca57af4bedb4"
|
checksum = "050ef6de42a33903b30a7497b76b40d3d58691d4d3eec355348c122444a388f0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"adler32",
|
"adler32",
|
||||||
"byteorder 1.3.4",
|
"byteorder 1.3.4",
|
||||||
@ -1043,9 +1043,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "image"
|
name = "image"
|
||||||
version = "0.23.0"
|
version = "0.23.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ef4e336ec01a678e7ab692914c641181528e8656451e6252f8f9e33728882eaf"
|
checksum = "9062b90712d25bc6bb165d110aa59c6b47c849246e341e7b86a98daff9d49f60"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"byteorder 1.3.4",
|
"byteorder 1.3.4",
|
||||||
@ -1059,6 +1059,15 @@ dependencies = [
|
|||||||
"tiff",
|
"tiff",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "imageutils"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "git+https://git.z.xinu.tv/wathiede/imageutils#d6804fa0f8e0afe2deb54354acc983a1ab59d794"
|
||||||
|
dependencies = [
|
||||||
|
"image",
|
||||||
|
"jpeg-decoder",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "imgref"
|
name = "imgref"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
@ -1678,6 +1687,7 @@ dependencies = [
|
|||||||
"google_api_auth",
|
"google_api_auth",
|
||||||
"hexihasher",
|
"hexihasher",
|
||||||
"image",
|
"image",
|
||||||
|
"imageutils",
|
||||||
"jpeg-decoder",
|
"jpeg-decoder",
|
||||||
"lazy_static 1.4.0",
|
"lazy_static 1.4.0",
|
||||||
"load_image",
|
"load_image",
|
||||||
@ -1750,9 +1760,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "png"
|
name = "png"
|
||||||
version = "0.15.3"
|
version = "0.16.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ef859a23054bbfee7811284275ae522f0434a3c8e7f4b74bd4a35ae7e1c4a283"
|
checksum = "46060468187c21c00ffa2a920690b29997d7fd543f5a4d400461e4a7d4fccde8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"crc32fast",
|
"crc32fast",
|
||||||
|
|||||||
@ -21,11 +21,12 @@ structopt = "0.3.9"
|
|||||||
yup-oauth2 = "^3.1"
|
yup-oauth2 = "^3.1"
|
||||||
warp = "0.1"
|
warp = "0.1"
|
||||||
serde = { version = "1.0.104", features = ["derive"] }
|
serde = { version = "1.0.104", features = ["derive"] }
|
||||||
image = { version = "0.23.0" } #, default-features = false, features = ["jpeg"] }
|
image = { version = "0.23.2" } #, default-features = false, features = ["jpeg"] }
|
||||||
rust-embed = "5.2.0"
|
rust-embed = "5.2.0"
|
||||||
mime_guess = "2.0.1"
|
mime_guess = "2.0.1"
|
||||||
rocksdb = "0.13.0"
|
rocksdb = "0.13.0"
|
||||||
jpeg-decoder = "0.1.18"
|
jpeg-decoder = "0.1.18"
|
||||||
|
imageutils = { git = "https://git.z.xinu.tv/wathiede/imageutils" }
|
||||||
|
|
||||||
[dependencies.prometheus]
|
[dependencies.prometheus]
|
||||||
features = ["process"]
|
features = ["process"]
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import {
|
|||||||
Route,
|
Route,
|
||||||
useParams
|
useParams
|
||||||
} from "react-router-dom";
|
} from "react-router-dom";
|
||||||
import 'animate.css';
|
|
||||||
|
|
||||||
import Random from './rand';
|
import Random from './rand';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
@ -44,7 +43,7 @@ const roundup = (v: number, mod: number) => {
|
|||||||
* From https://stackoverflow.com/questions/6274339/how-can-i-shuffle-an-array
|
* From https://stackoverflow.com/questions/6274339/how-can-i-shuffle-an-array
|
||||||
* @param {Array} a items An array containing the items.
|
* @param {Array} a items An array containing the items.
|
||||||
*/
|
*/
|
||||||
function shuffle(a: Array<MediaItem>) {
|
function shuffle<T>(a: Array<T>) {
|
||||||
let rng = new Random(new Date().getDate());
|
let rng = new Random(new Date().getDate());
|
||||||
for (let i = a.length - 1; i > 0; i--) {
|
for (let i = a.length - 1; i > 0; i--) {
|
||||||
const j = Math.floor(rng.nextFloat() * (i + 1));
|
const j = Math.floor(rng.nextFloat() * (i + 1));
|
||||||
@ -83,15 +82,19 @@ class Slide {
|
|||||||
return this.items.map(img => `/api/image/${img.id}?w=${w}&h=${h}`);
|
return this.items.map(img => `/api/image/${img.id}?w=${w}&h=${h}`);
|
||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
const imgs = this.imageUrls.map(url => {
|
let urls = this.imageUrls;
|
||||||
|
let frac = 100 / urls.length;
|
||||||
|
let imgs = urls.map(url => {
|
||||||
|
// TODO(wathiede): make this landscape/portrait aware.
|
||||||
let style: React.CSSProperties = {
|
let style: React.CSSProperties = {
|
||||||
height: '100%',
|
height: '100%',
|
||||||
width: '100%',
|
width: frac + '%',
|
||||||
backgroundColor: 'black',
|
backgroundColor: 'black',
|
||||||
backgroundImage: `url(${url})`,
|
backgroundImage: `url(${url})`,
|
||||||
backgroundRepeat: 'no-repeat',
|
backgroundRepeat: 'no-repeat',
|
||||||
backgroundPosition: 'center center',
|
backgroundPosition: 'center center',
|
||||||
backgroundSize: 'cover',
|
backgroundSize: 'cover',
|
||||||
|
float: 'left',
|
||||||
};
|
};
|
||||||
return <div key={url} style={style}></div>;
|
return <div key={url} style={style}></div>;
|
||||||
});
|
});
|
||||||
@ -103,6 +106,16 @@ class Slide {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function makePairs<T>(items: Array<T>) {
|
||||||
|
const half = Math.floor(items.length/2);
|
||||||
|
console.log(`items ${items.length} half ${half}`)
|
||||||
|
let pairs = [];
|
||||||
|
for (let i = 0; i < half; i++) {
|
||||||
|
pairs.push([items[2*i], items[2*i+1]]);
|
||||||
|
}
|
||||||
|
return pairs;
|
||||||
|
}
|
||||||
|
|
||||||
type MediaMetadata = {
|
type MediaMetadata = {
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
@ -157,18 +170,24 @@ class Album extends React.Component<AlbumProps, AlbumState> {
|
|||||||
|
|
||||||
console.log(`${landscapes.length} landscape photos`);
|
console.log(`${landscapes.length} landscape photos`);
|
||||||
console.log(`${portraits.length} portraits photos`);
|
console.log(`${portraits.length} portraits photos`);
|
||||||
let photos;
|
let slides: Array<Slide>;
|
||||||
if (ratio > 1) {
|
if (ratio > 1) {
|
||||||
console.log('display in landscape mode');
|
console.log('display in landscape mode');
|
||||||
photos = landscapes;
|
slides = landscapes.map((p)=>{
|
||||||
|
return new Slide([p]);
|
||||||
|
});
|
||||||
|
let pairs = makePairs(shuffle(portraits));
|
||||||
|
slides = slides.concat(pairs.map((p, i) => new Slide(p)));
|
||||||
} else {
|
} else {
|
||||||
console.log('display in portrait mode');
|
console.log('display in portrait mode');
|
||||||
photos = portraits;
|
slides = portraits.map((p)=>{
|
||||||
|
return new Slide([p]);
|
||||||
|
});
|
||||||
|
// TODO(wathiede): fix Slide::render before adding landscapes
|
||||||
|
// to slides here.
|
||||||
}
|
}
|
||||||
photos = shuffle(photos);
|
slides = shuffle(slides);
|
||||||
let slides = photos.map((p)=>{
|
console.log(`${slides.length} slides`);
|
||||||
return new Slide([p]);
|
|
||||||
});
|
|
||||||
let numSlides = slides.length;
|
let numSlides = slides.length;
|
||||||
slides.forEach((p, idx)=>{
|
slides.forEach((p, idx)=>{
|
||||||
let nextIdx = (idx+1)%numSlides;
|
let nextIdx = (idx+1)%numSlides;
|
||||||
@ -197,7 +216,6 @@ class Album extends React.Component<AlbumProps, AlbumState> {
|
|||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
// TODO(wathiede): fade transition.
|
// TODO(wathiede): fade transition.
|
||||||
// TODO(wathiede): pair-up portrait orientation images.
|
|
||||||
let {curSlide, error, showUI} = this.state;
|
let {curSlide, error, showUI} = this.state;
|
||||||
if (error !== null) {
|
if (error !== null) {
|
||||||
return <h2>Error: {JSON.stringify(error)}</h2>;
|
return <h2>Error: {JSON.stringify(error)}</h2>;
|
||||||
@ -230,7 +248,7 @@ class Album extends React.Component<AlbumProps, AlbumState> {
|
|||||||
this.setState({curSlide: curSlide?.prevSlide})
|
this.setState({curSlide: curSlide?.prevSlide})
|
||||||
}}>{ prevSlide?.render() }</div>
|
}}>{ prevSlide?.render() }</div>
|
||||||
{/* TODO(wathiede): make this work with multiple items. */}
|
{/* TODO(wathiede): make this work with multiple items. */}
|
||||||
<div className="meta">{curSlide?.items[0].filename}</div>
|
<div className="meta">{curSlide?.items.map(i=>i.filename).join(' | ')}</div>
|
||||||
<div
|
<div
|
||||||
style={rightPrefetchStyle}
|
style={rightPrefetchStyle}
|
||||||
onClick={(e)=>{
|
onClick={(e)=>{
|
||||||
|
|||||||
211
src/library.rs
211
src/library.rs
@ -1,20 +1,13 @@
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::io::BufReader;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use google_photoslibrary1 as photos;
|
use google_photoslibrary1 as photos;
|
||||||
use image::imageops;
|
use image::imageops;
|
||||||
use image::DynamicImage;
|
use imageutils::{load_image, resize, resize_to_fill, save_to_jpeg_bytes, FilterType};
|
||||||
use image::GenericImage;
|
|
||||||
use image::GenericImageView;
|
|
||||||
use image::ImageBuffer;
|
|
||||||
use image::ImageFormat;
|
|
||||||
use image::ImageResult;
|
|
||||||
use jpeg_decoder::Decoder;
|
|
||||||
use log::error;
|
use log::error;
|
||||||
use log::info;
|
use log::info;
|
||||||
use log::warn;
|
use log::warn;
|
||||||
@ -34,128 +27,6 @@ pub struct Library {
|
|||||||
cache_db: Arc<DB>,
|
cache_db: Arc<DB>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_image<P>(
|
|
||||||
path: P,
|
|
||||||
width_hint: Option<u32>,
|
|
||||||
height_hint: Option<u32>,
|
|
||||||
) -> Result<DynamicImage, Box<dyn std::error::Error>>
|
|
||||||
where
|
|
||||||
P: AsRef<Path>,
|
|
||||||
{
|
|
||||||
// TODO(wathiede): fall back to image::load_image when jpeg decoding fails.
|
|
||||||
let file = File::open(path).expect("failed to open file");
|
|
||||||
let mut decoder = Decoder::new(BufReader::new(file));
|
|
||||||
let (w, h) = match (width_hint, height_hint) {
|
|
||||||
(Some(w), Some(h)) => {
|
|
||||||
let got = decoder.scale(w as u16, h as u16)?;
|
|
||||||
//info!("Hinted at {}x{}, got {}x{}", w, h, got.0, got.1);
|
|
||||||
(got.0 as u32, got.1 as u32)
|
|
||||||
}
|
|
||||||
// TODO(wathiede): handle partial hints by grabbing info and then computing the absent
|
|
||||||
// dimenison.
|
|
||||||
_ => {
|
|
||||||
decoder.read_info()?;
|
|
||||||
let info = decoder.info().unwrap();
|
|
||||||
(info.width as u32, info.height as u32)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let pixels = decoder.decode().expect("failed to decode image");
|
|
||||||
Ok(DynamicImage::ImageRgb8(
|
|
||||||
ImageBuffer::from_raw(w, h, pixels).expect("pixels to small for given dimensions"),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
|
||||||
pub enum FilterType {
|
|
||||||
Builtin(imageops::FilterType),
|
|
||||||
Nearest,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// fill_size computes the largest rectangle that fits in src with the aspect ratio of dst.
|
|
||||||
fn fill_size(src: (u32, u32), dst: (u32, u32)) -> (u32, u32) {
|
|
||||||
debug_assert!(src.0 >= dst.0);
|
|
||||||
debug_assert!(src.1 >= dst.1);
|
|
||||||
let x_scale = src.0 as f32 / dst.0 as f32;
|
|
||||||
let y_scale = src.1 as f32 / dst.1 as f32;
|
|
||||||
if x_scale > y_scale {
|
|
||||||
// Height will fill, width will crop.
|
|
||||||
(
|
|
||||||
(dst.0 as f32 * y_scale) as u32,
|
|
||||||
(dst.1 as f32 * y_scale) as u32,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// Width will fill, height will crop.
|
|
||||||
(
|
|
||||||
(dst.0 as f32 * x_scale) as u32,
|
|
||||||
(dst.1 as f32 * x_scale) as u32,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resize_to_fill_nearest(w: u32, h: u32, img: &DynamicImage) -> DynamicImage {
|
|
||||||
let mut dst = DynamicImage::new_rgb8(w, h);
|
|
||||||
let (src_w, src_h) = img.dimensions();
|
|
||||||
let (crop_w, crop_h) = fill_size((src_w, src_h), (w, h));
|
|
||||||
let off_x = (src_w - crop_w) / 2;
|
|
||||||
let off_y = (src_h - crop_h) / 2;
|
|
||||||
let src = img.view(off_x, off_y, crop_w, crop_h);
|
|
||||||
let x_scale = crop_w as f32 / w as f32;
|
|
||||||
let y_scale = crop_h as f32 / h as f32;
|
|
||||||
|
|
||||||
for y in 0..h {
|
|
||||||
for x in 0..w {
|
|
||||||
let x_idx = (x as f32 * x_scale).round() as u32;
|
|
||||||
let y_idx = (y as f32 * y_scale).round() as u32;
|
|
||||||
dst.put_pixel(x, y, src.get_pixel(x_idx, y_idx))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dst
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn resize(
|
|
||||||
img: &DynamicImage,
|
|
||||||
dimensions: (Option<u32>, Option<u32>),
|
|
||||||
filter: FilterType,
|
|
||||||
) -> DynamicImage {
|
|
||||||
let (w, h) = dimensions;
|
|
||||||
let (orig_w, orig_h) = img.dimensions();
|
|
||||||
let (w, h) = match (w, h) {
|
|
||||||
(Some(w), Some(h)) => (w, h),
|
|
||||||
(Some(w), None) => (w, orig_h * w / orig_w),
|
|
||||||
(None, Some(h)) => (orig_w * h / orig_h, h),
|
|
||||||
(None, None) => (orig_w, orig_h),
|
|
||||||
};
|
|
||||||
match filter {
|
|
||||||
FilterType::Builtin(filter) => img.resize(w, h, filter),
|
|
||||||
FilterType::Nearest => unimplemented!(), //resize_to_fill_nearest(w, h, img),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn resize_to_fill(
|
|
||||||
img: &DynamicImage,
|
|
||||||
dimensions: (Option<u32>, Option<u32>),
|
|
||||||
filter: FilterType,
|
|
||||||
) -> DynamicImage {
|
|
||||||
let (w, h) = dimensions;
|
|
||||||
let (orig_w, orig_h) = img.dimensions();
|
|
||||||
let (w, h) = match (w, h) {
|
|
||||||
(Some(w), Some(h)) => (w, h),
|
|
||||||
(Some(w), None) => (w, orig_h * w / orig_w),
|
|
||||||
(None, Some(h)) => (orig_w * h / orig_h, h),
|
|
||||||
(None, None) => (orig_w, orig_h),
|
|
||||||
};
|
|
||||||
match filter {
|
|
||||||
FilterType::Builtin(filter) => img.resize_to_fill(w, h, filter),
|
|
||||||
FilterType::Nearest => resize_to_fill_nearest(w, h, img),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn save_to_jpeg_bytes(img: &DynamicImage) -> ImageResult<Vec<u8>> {
|
|
||||||
let mut buf = Vec::new();
|
|
||||||
img.write_to(&mut buf, ImageFormat::Jpeg)?;
|
|
||||||
Ok(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Library {
|
impl Library {
|
||||||
pub fn new(root: PathBuf) -> Result<Library, Box<dyn std::error::Error>> {
|
pub fn new(root: PathBuf) -> Result<Library, Box<dyn std::error::Error>> {
|
||||||
let db = DB::open_default(root.join("cache"))?;
|
let db = DB::open_default(root.join("cache"))?;
|
||||||
@ -364,86 +235,6 @@ mod test {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use tempdir::TempDir;
|
use tempdir::TempDir;
|
||||||
|
|
||||||
fn compare_images(lhs: DynamicImage, rhs: DynamicImage) {
|
|
||||||
let lhs = lhs.to_rgb();
|
|
||||||
let rhs = rhs.to_rgb();
|
|
||||||
// Based on https://en.wikipedia.org/wiki/Peak_signal-to-noise_ratio#Definition
|
|
||||||
//
|
|
||||||
let mut mse: [i64; 3] = [0, 0, 0];
|
|
||||||
for (l, r) in lhs.pixels().zip(rhs.pixels()) {
|
|
||||||
let image::Rgb(l_pix) = l;
|
|
||||||
let image::Rgb(r_pix) = r;
|
|
||||||
|
|
||||||
{
|
|
||||||
for i in 0..3 {
|
|
||||||
let d = l_pix[i] as i64 - r_pix[i] as i64;
|
|
||||||
let d2 = d * d;
|
|
||||||
mse[i] += d2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// assert_eq!(l_pix, r_pix, "{:?} != {:?} @ {} x {} ", l_pix, r_pix, x, y);
|
|
||||||
}
|
|
||||||
let (w, h) = lhs.dimensions();
|
|
||||||
let mn = (w * h) as i64;
|
|
||||||
mse.iter_mut().for_each(|i| *i = *i / mn);
|
|
||||||
let psnr: Vec<_> = mse
|
|
||||||
.iter()
|
|
||||||
.map(|i| 20. * 255_f32.log10() - 10. * (*i as f32).log10())
|
|
||||||
.collect();
|
|
||||||
// Uncomment to explore differences
|
|
||||||
/*
|
|
||||||
lhs.save("/tmp/lhs.png").expect("failed to write lhs.png");
|
|
||||||
rhs.save("/tmp/rhs.png").expect("failed to write rhs.png");
|
|
||||||
assert!(false, "MSE {:?} PSNR {:?} dB", mse, psnr);
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fill_sizes() {
|
|
||||||
let srcs = vec![(400, 300), (300, 400)];
|
|
||||||
|
|
||||||
let dsts = vec![(225, 300), (300, 225), (100, 100)];
|
|
||||||
|
|
||||||
let want = vec![
|
|
||||||
(225, 300),
|
|
||||||
(400, 300),
|
|
||||||
(300, 300),
|
|
||||||
(300, 400),
|
|
||||||
(300, 225),
|
|
||||||
(300, 300),
|
|
||||||
];
|
|
||||||
let mut i = 0;
|
|
||||||
for s in &srcs {
|
|
||||||
for d in &dsts {
|
|
||||||
let w = want[i];
|
|
||||||
dbg!(s, d, w);
|
|
||||||
let got = fill_size(*s, *d);
|
|
||||||
assert_eq!(
|
|
||||||
got, w,
|
|
||||||
"{}. src {:?} dst {:?} want {:?} got {:?}",
|
|
||||||
i, s, d, w, got
|
|
||||||
);
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn resize_to_fill_nearest() {
|
|
||||||
let w = 256;
|
|
||||||
let h = 256;
|
|
||||||
const TEST_IMAGE_PATH: &'static str = "testdata/image.jpg";
|
|
||||||
let img = load_image(TEST_IMAGE_PATH).expect("failed to load test image");
|
|
||||||
let reference = resize(
|
|
||||||
&img,
|
|
||||||
(Some(w), Some(h)),
|
|
||||||
FilterType::Builtin(imageops::FilterType::Nearest),
|
|
||||||
);
|
|
||||||
let got = resize(&img, (Some(w), Some(h)), FilterType::Nearest);
|
|
||||||
compare_images(reference, got);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn clean_db() {
|
fn clean_db() {
|
||||||
let td = TempDir::new("photosync_test").expect("failed to create temporary directory");
|
let td = TempDir::new("photosync_test").expect("failed to create temporary directory");
|
||||||
|
|||||||
BIN
testdata/image.jpg
vendored
BIN
testdata/image.jpg
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 3.1 MiB |
Loading…
x
Reference in New Issue
Block a user