Compare commits
5 Commits
1c82313c98
...
7ec3a11037
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ec3a11037 | |||
| 792471bf00 | |||
| d0ce89b27c | |||
| 1b292b30c9 | |||
| 233d0c4883 |
@ -40,3 +40,7 @@ load_image = "2.12.0"
|
|||||||
[[bench]]
|
[[bench]]
|
||||||
name = "image"
|
name = "image"
|
||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
|
# Build dependencies with release optimizations even in dev mode.
|
||||||
|
[profile.dev.package."*"]
|
||||||
|
opt-level = 3
|
||||||
|
|||||||
109
react-slideshow/src/App.js
vendored
109
react-slideshow/src/App.js
vendored
@ -5,9 +5,22 @@ import {
|
|||||||
Route,
|
Route,
|
||||||
useParams
|
useParams
|
||||||
} from "react-router-dom";
|
} from "react-router-dom";
|
||||||
|
import Random from './rand';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
|
||||||
|
let CONFIG;
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
CONFIG = {
|
||||||
|
sleepTimeSeconds: 5 * 60,
|
||||||
|
showUI: false,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
CONFIG = {
|
||||||
|
sleepTimeSeconds: 10,
|
||||||
|
showUI: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const IMAGE_CHUNK = 256;
|
const IMAGE_CHUNK = 256;
|
||||||
const roundup = (v, mod) => {
|
const roundup = (v, mod) => {
|
||||||
let r = v % mod;
|
let r = v % mod;
|
||||||
@ -18,6 +31,20 @@ const roundup = (v, mod) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shuffles array in place. ES6 version
|
||||||
|
* From https://stackoverflow.com/questions/6274339/how-can-i-shuffle-an-array
|
||||||
|
* @param {Array} a items An array containing the items.
|
||||||
|
*/
|
||||||
|
function shuffle(a) {
|
||||||
|
let rng = new Random(new Date().getDay());
|
||||||
|
for (let i = a.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(rng.nextFloat() * (i + 1));
|
||||||
|
[a[i], a[j]] = [a[j], a[i]];
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
class Album extends React.Component {
|
class Album extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -25,6 +52,7 @@ class Album extends React.Component {
|
|||||||
error: null,
|
error: null,
|
||||||
mediaItems: null,
|
mediaItems: null,
|
||||||
idx: 0,
|
idx: 0,
|
||||||
|
showUI: props.showUI,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -52,18 +80,44 @@ class Album extends React.Component {
|
|||||||
h = roundup(h, IMAGE_CHUNK);
|
h = roundup(h, IMAGE_CHUNK);
|
||||||
w = (h/ratio).toFixed();
|
w = (h/ratio).toFixed();
|
||||||
}
|
}
|
||||||
|
console.log(`Window size ${window.innerWidth}x${window.innerHeight} with a devicePixelRatio of ${window.devicePixelRatio} for a total size of ${w}x${h}`);
|
||||||
|
|
||||||
//let w = roundup(window.innerWidth*window.devicePixelRatio, IMAGE_CHUNK);
|
//let w = roundup(window.innerWidth*window.devicePixelRatio, IMAGE_CHUNK);
|
||||||
//let h = roundup(window.innerHeight*window.devicePixelRatio, IMAGE_CHUNK);
|
//let h = roundup(window.innerHeight*window.devicePixelRatio, IMAGE_CHUNK);
|
||||||
let {idx, error, mediaItems} = this.state;
|
let {idx, error, mediaItems, showUI} = this.state;
|
||||||
if (error !== null) {
|
if (error !== null) {
|
||||||
return <h2>Error: {JSON.stringify(error)}</h2>;
|
return <h2>Error: {JSON.stringify(error)}</h2>;
|
||||||
} else if (mediaItems !== null) {
|
} else if (mediaItems !== null) {
|
||||||
let numImages = mediaItems.length;
|
let landscapes = mediaItems.filter((mi) => {
|
||||||
|
let md = mi.mediaMetadata;
|
||||||
|
let ratio = md.width/md.height;
|
||||||
|
return ratio > 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
let portraits = mediaItems.filter((mi) => {
|
||||||
|
let md = mi.mediaMetadata;
|
||||||
|
let ratio = md.width/md.height;
|
||||||
|
return ratio <= 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`${landscapes.length} landscape photos`);
|
||||||
|
console.log(`${portraits.length} portraits photos`);
|
||||||
|
let photos;
|
||||||
|
if (ratio > 1) {
|
||||||
|
console.log('display in landscape mode');
|
||||||
|
photos = landscapes;
|
||||||
|
} else {
|
||||||
|
console.log('display in portrait mode');
|
||||||
|
photos = portraits;
|
||||||
|
}
|
||||||
|
photos = shuffle(photos);
|
||||||
|
|
||||||
|
let numImages = photos.length;
|
||||||
let nextIdx = (idx+1)%numImages;
|
let nextIdx = (idx+1)%numImages;
|
||||||
let prevIdx = (numImages+idx-1)%numImages;
|
let prevIdx = (numImages+idx-1)%numImages;
|
||||||
let image = mediaItems[idx];
|
let image = photos[idx];
|
||||||
let nextImage = mediaItems[nextIdx];
|
let nextImage = photos[nextIdx];
|
||||||
|
let prevImage = photos[prevIdx];
|
||||||
let style = {
|
let style = {
|
||||||
height: '100%',
|
height: '100%',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@ -74,26 +128,44 @@ class Album extends React.Component {
|
|||||||
backgroundSize: 'cover',
|
backgroundSize: 'cover',
|
||||||
};
|
};
|
||||||
let prefetchStyle = {
|
let prefetchStyle = {
|
||||||
// display: 'none'
|
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
width: '25%',
|
width: '25%',
|
||||||
height: '25%',
|
height: '25%',
|
||||||
|
bottom: 0,
|
||||||
};
|
};
|
||||||
console.log(`window.devicePixelRatio ${window.devicePixelRatio}`);
|
let leftPrefetchStyle = {
|
||||||
return <div style={style} onClick={(e)=>{
|
left: 0,
|
||||||
e.stopPropagation();
|
...prefetchStyle
|
||||||
this.setState({idx: nextIdx})
|
};
|
||||||
}}>
|
let rightPrefetchStyle = {
|
||||||
<img
|
right: 0,
|
||||||
style={prefetchStyle}
|
...prefetchStyle
|
||||||
|
};
|
||||||
|
let ui;
|
||||||
|
if (showUI) {
|
||||||
|
ui = <div>
|
||||||
|
<img
|
||||||
|
style={leftPrefetchStyle}
|
||||||
onClick={(e)=>{
|
onClick={(e)=>{
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this.setState({idx: prevIdx})
|
this.setState({idx: prevIdx})
|
||||||
}}
|
}}
|
||||||
|
src={`/api/image/${prevImage.id}?w=${w}&h=${h}`} alt="prefetch prev" />
|
||||||
|
<img
|
||||||
|
style={rightPrefetchStyle}
|
||||||
|
onClick={(e)=>{
|
||||||
|
e.stopPropagation();
|
||||||
|
this.setState({idx: nextIdx})
|
||||||
|
}}
|
||||||
src={`/api/image/${nextImage.id}?w=${w}&h=${h}`} alt="prefetch next" />
|
src={`/api/image/${nextImage.id}?w=${w}&h=${h}`} alt="prefetch next" />
|
||||||
</div>;
|
</div>;
|
||||||
|
}
|
||||||
|
return <div style={style} onClick={(e)=>{
|
||||||
|
e.stopPropagation();
|
||||||
|
this.setState({showUI: !showUI})
|
||||||
|
}}>
|
||||||
|
{ ui }
|
||||||
|
</div>;
|
||||||
} else {
|
} else {
|
||||||
return <h2>Loading...</h2>;
|
return <h2>Loading...</h2>;
|
||||||
}
|
}
|
||||||
@ -141,14 +213,15 @@ class AlbumIndex extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const AlbumRoute = () => {
|
const AlbumRoute = ({showUI}) => {
|
||||||
// We can use the `useParams` hook here to access
|
// We can use the `useParams` hook here to access
|
||||||
// the dynamic pieces of the URL.
|
// the dynamic pieces of the URL.
|
||||||
let { albumId } = useParams();
|
let { albumId } = useParams();
|
||||||
return <Album album={albumId} />;
|
return <Album album={albumId} showUI={showUI} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
|
let {showUI} = CONFIG;
|
||||||
return <Router>
|
return <Router>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/">
|
<Route exact path="/">
|
||||||
@ -157,7 +230,7 @@ const App = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Route>
|
</Route>
|
||||||
<Route exact path="/:albumId">
|
<Route exact path="/:albumId">
|
||||||
<AlbumRoute />
|
<AlbumRoute showUI={showUI} />
|
||||||
</Route>
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
30
react-slideshow/src/rand.js
vendored
Normal file
30
react-slideshow/src/rand.js
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// From https://gist.github.com/blixt/f17b47c62508be59987b
|
||||||
|
/**
|
||||||
|
* Creates a pseudo-random value generator. The seed must be an integer.
|
||||||
|
*
|
||||||
|
* Uses an optimized version of the Park-Miller PRNG.
|
||||||
|
* http://www.firstpr.com.au/dsp/rand31/
|
||||||
|
*/
|
||||||
|
function Random(seed) {
|
||||||
|
console.log(`Seeding prng with ${seed}`);
|
||||||
|
this._seed = seed % 2147483647;
|
||||||
|
if (this._seed <= 0) this._seed += 2147483646;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a pseudo-random value between 1 and 2^32 - 2.
|
||||||
|
*/
|
||||||
|
Random.prototype.next = function () {
|
||||||
|
return this._seed = this._seed * 16807 % 2147483647;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a pseudo-random floating point number in range [0, 1).
|
||||||
|
*/
|
||||||
|
Random.prototype.nextFloat = function (opt_minOrMax, opt_max) {
|
||||||
|
// We know that result of next() will be 1 to 2147483646 (inclusive).
|
||||||
|
return (this.next() - 1) / 2147483646;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Random;
|
||||||
@ -25,7 +25,7 @@ use rocksdb::IteratorMode;
|
|||||||
use rocksdb::DB;
|
use rocksdb::DB;
|
||||||
|
|
||||||
// Used to ensure DB is invalidated after schema changes.
|
// Used to ensure DB is invalidated after schema changes.
|
||||||
const LIBRARY_GENERATION: &'static str = "13";
|
const LIBRARY_GENERATION: &'static str = "14";
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Library {
|
pub struct Library {
|
||||||
@ -116,6 +116,25 @@ pub fn resize(
|
|||||||
img: &DynamicImage,
|
img: &DynamicImage,
|
||||||
dimensions: (Option<u32>, Option<u32>),
|
dimensions: (Option<u32>, Option<u32>),
|
||||||
filter: FilterType,
|
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 {
|
) -> DynamicImage {
|
||||||
let (w, h) = dimensions;
|
let (w, h) = dimensions;
|
||||||
let (orig_w, orig_h) = img.dimensions();
|
let (orig_w, orig_h) = img.dimensions();
|
||||||
@ -261,11 +280,13 @@ impl Library {
|
|||||||
fn generational_key(generation: &str, key: &str) -> String {
|
fn generational_key(generation: &str, key: &str) -> String {
|
||||||
format!("{}/{}", generation, key)
|
format!("{}/{}", generation, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_thumbnail(
|
pub fn generate_thumbnail(
|
||||||
&self,
|
&self,
|
||||||
media_items_id: &str,
|
media_items_id: &str,
|
||||||
dimensions: (Option<u32>, Option<u32>),
|
dimensions: (Option<u32>, Option<u32>),
|
||||||
filter: FilterType,
|
filter: FilterType,
|
||||||
|
fill: bool,
|
||||||
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
||||||
match self.original(&media_items_id) {
|
match self.original(&media_items_id) {
|
||||||
None => {
|
None => {
|
||||||
@ -275,7 +296,11 @@ impl Library {
|
|||||||
Some(path) => {
|
Some(path) => {
|
||||||
let orig_img = load_image(&path, dimensions.0, dimensions.1)?;
|
let orig_img = load_image(&path, dimensions.0, dimensions.1)?;
|
||||||
//.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
//.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
||||||
let img = resize(&orig_img, dimensions, filter);
|
let img = if fill {
|
||||||
|
resize_to_fill(&orig_img, dimensions, filter)
|
||||||
|
} else {
|
||||||
|
resize(&orig_img, dimensions, filter)
|
||||||
|
};
|
||||||
let buf = save_to_jpeg_bytes(&img)
|
let buf = save_to_jpeg_bytes(&img)
|
||||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
||||||
Ok(buf)
|
Ok(buf)
|
||||||
@ -286,6 +311,7 @@ impl Library {
|
|||||||
&self,
|
&self,
|
||||||
media_items_id: &str,
|
media_items_id: &str,
|
||||||
dimensions: (Option<u32>, Option<u32>),
|
dimensions: (Option<u32>, Option<u32>),
|
||||||
|
fill: bool,
|
||||||
) -> Option<Vec<u8>> {
|
) -> Option<Vec<u8>> {
|
||||||
fn cache_key(media_items_id: &str, dimensions: (Option<u32>, Option<u32>)) -> String {
|
fn cache_key(media_items_id: &str, dimensions: (Option<u32>, Option<u32>)) -> String {
|
||||||
let dim = match dimensions {
|
let dim = match dimensions {
|
||||||
@ -300,18 +326,15 @@ impl Library {
|
|||||||
let db = self.cache_db.clone();
|
let db = self.cache_db.clone();
|
||||||
match db.get(key.as_bytes()) {
|
match db.get(key.as_bytes()) {
|
||||||
// Cache hit, return bytes as-is.
|
// Cache hit, return bytes as-is.
|
||||||
Ok(Some(bytes)) => {
|
Ok(Some(bytes)) => Some(bytes),
|
||||||
info!("cache HIT {}", key);
|
|
||||||
Some(bytes)
|
|
||||||
}
|
|
||||||
// Cache miss, fill cache and return.
|
// Cache miss, fill cache and return.
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
info!("cache MISS {}", key);
|
info!("cache MISS {}", key);
|
||||||
let bytes = match self.generate_thumbnail(
|
let bytes = match self.generate_thumbnail(
|
||||||
media_items_id,
|
media_items_id,
|
||||||
dimensions,
|
dimensions,
|
||||||
FilterType::Nearest,
|
FilterType::Builtin(imageops::FilterType::Lanczos3),
|
||||||
//FilterType::Builtin(imageops::FilterType::Lanczos3),
|
fill,
|
||||||
) {
|
) {
|
||||||
Ok(bytes) => bytes,
|
Ok(bytes) => bytes,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|||||||
10
src/web.rs
10
src/web.rs
@ -73,6 +73,7 @@ fn album(lib: Library, id: String) -> Result<impl warp::Reply, warp::Rejection>
|
|||||||
struct ImageParams {
|
struct ImageParams {
|
||||||
w: Option<u32>,
|
w: Option<u32>,
|
||||||
h: Option<u32>,
|
h: Option<u32>,
|
||||||
|
fill: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn image(
|
fn image(
|
||||||
@ -81,7 +82,11 @@ fn image(
|
|||||||
params: ImageParams,
|
params: ImageParams,
|
||||||
) -> Result<impl warp::Reply, warp::Rejection> {
|
) -> Result<impl warp::Reply, warp::Rejection> {
|
||||||
// TODO(wathiede): add caching headers.
|
// TODO(wathiede): add caching headers.
|
||||||
match lib.thumbnail(&media_items_id, (params.w, params.h)) {
|
match lib.thumbnail(
|
||||||
|
&media_items_id,
|
||||||
|
(params.w, params.h),
|
||||||
|
params.fill.unwrap_or(false),
|
||||||
|
) {
|
||||||
None => {
|
None => {
|
||||||
warn!("Couldn't find original {}", &media_items_id);
|
warn!("Couldn't find original {}", &media_items_id);
|
||||||
Err(warp::reject::not_found())
|
Err(warp::reject::not_found())
|
||||||
@ -121,7 +126,8 @@ pub fn run(addr: SocketAddr, root: PathBuf) -> Result<(), Box<dyn Error>> {
|
|||||||
// Fallback, always keep this last.
|
// Fallback, always keep this last.
|
||||||
let api = api.or(index);
|
let api = api.or(index);
|
||||||
|
|
||||||
let api = api.with(warp::log("photosync"));
|
//let api = api.with(warp::log("photosync"));
|
||||||
|
|
||||||
// We don't want metrics & heath checking filling up the logs, so we add this handler after
|
// We don't want metrics & heath checking filling up the logs, so we add this handler after
|
||||||
// wrapping with the log filter.
|
// wrapping with the log filter.
|
||||||
let routes = metrics().or(api);
|
let routes = metrics().or(api);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user