add image size metadata to the filesystem
This commit is contained in:
parent
5ce9cfc022
commit
9022fb4ce9
13 changed files with 580 additions and 56 deletions
|
@ -20,7 +20,7 @@ env_logger = "0.10.0"
|
|||
git2 = { version = "0.19.0", default-features = false, features = ["vendored-libgit2"] }
|
||||
handlebars = "4.3.7"
|
||||
http-body = "1.0.0"
|
||||
image = "0.24.8"
|
||||
image = "0.25.5"
|
||||
indexmap = { version = "2.2.6", features = ["serde"] }
|
||||
jotdown = { version = "0.4.1", default-features = false }
|
||||
log = { workspace = true }
|
||||
|
|
|
@ -10,7 +10,7 @@ use crate::{
|
|||
Syntax,
|
||||
},
|
||||
import_map::ImportRoot,
|
||||
vfs::{self, Dir, VPath, VPathBuf},
|
||||
vfs::{self, Dir, ImageSize, VPath, VPathBuf},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
|
@ -101,7 +101,7 @@ pub enum Markup {
|
|||
impl Config {
|
||||
pub fn autopopulate_emoji(&mut self, dir: &dyn Dir) -> anyhow::Result<()> {
|
||||
vfs::walk_dir_rec(dir, VPath::ROOT, &mut |path| {
|
||||
if path.extension().is_some_and(is_emoji_file) {
|
||||
if path.extension().is_some_and(is_image_file) {
|
||||
if let Some(emoji_name) = path.file_stem() {
|
||||
if !self.emoji.contains_key(emoji_name) {
|
||||
self.emoji.insert(emoji_name.to_owned(), path.to_owned());
|
||||
|
@ -117,7 +117,7 @@ impl Config {
|
|||
|
||||
pub fn autopopulate_pics(&mut self, dir: &dyn Dir) -> anyhow::Result<()> {
|
||||
vfs::walk_dir_rec(dir, VPath::ROOT, &mut |path| {
|
||||
if path.extension().is_some_and(is_pic_file) {
|
||||
if path.extension().is_some_and(is_image_file) {
|
||||
if let Some(pic_name) = path.file_stem() {
|
||||
let pic_id = pic_name
|
||||
.split_once('-')
|
||||
|
@ -151,6 +151,10 @@ impl Config {
|
|||
.expect("pics_dir is not anchored anywhere")
|
||||
}
|
||||
|
||||
pub fn pic_size(&self, pics_dir: &dyn Dir, id: &str) -> Option<ImageSize> {
|
||||
self.pics.get(id).and_then(|path| pics_dir.image_size(path))
|
||||
}
|
||||
|
||||
/// Loads all syntax definition files.
|
||||
pub fn load_syntaxes(&mut self, dir: &dyn Dir) -> anyhow::Result<()> {
|
||||
vfs::walk_dir_rec(dir, VPath::ROOT, &mut |path| {
|
||||
|
@ -185,10 +189,6 @@ impl Config {
|
|||
}
|
||||
}
|
||||
|
||||
fn is_emoji_file(extension: &str) -> bool {
|
||||
matches!(extension, "png" | "svg")
|
||||
}
|
||||
|
||||
fn is_pic_file(extension: &str) -> bool {
|
||||
pub fn is_image_file(extension: &str) -> bool {
|
||||
matches!(extension, "png" | "svg" | "jpg" | "jpeg" | "webp")
|
||||
}
|
||||
|
|
|
@ -379,19 +379,17 @@ impl<'a> Writer<'a> {
|
|||
write_attr(&pic_url, out);
|
||||
out.push('"');
|
||||
|
||||
// TODO: Image size derivation.
|
||||
// let image_size = filename.and_then(|filename| {
|
||||
// self.renderer
|
||||
// .config_derived_data
|
||||
// .image_size(&format!("static/pic/{filename}"))
|
||||
// });
|
||||
// if let Some(image_size) = image_size {
|
||||
// write!(
|
||||
// out,
|
||||
// r#" width="{}" height="{}""#,
|
||||
// image_size.width, image_size.height
|
||||
// )?;
|
||||
// }
|
||||
if let Some(image_size) = self
|
||||
.renderer
|
||||
.config
|
||||
.pic_size(&*self.renderer.dirs.pic, placeholder_pic_id)
|
||||
{
|
||||
write!(
|
||||
out,
|
||||
r#" width="{}" height="{}""#,
|
||||
image_size.width, image_size.height
|
||||
)?;
|
||||
}
|
||||
|
||||
out.push('>');
|
||||
}
|
||||
|
@ -480,6 +478,7 @@ impl<'a> Writer<'a> {
|
|||
if !src.is_empty() {
|
||||
out.push_str(r#"" src=""#);
|
||||
if let SpanLinkType::Unresolved = link_type {
|
||||
// TODO: Image size.
|
||||
if let Some(resolved) = self.resolve_link(src) {
|
||||
write_attr(&resolved, out);
|
||||
} else {
|
||||
|
@ -577,18 +576,13 @@ impl<'a> Writer<'a> {
|
|||
write_attr(&url, out);
|
||||
out.push('"');
|
||||
|
||||
// TODO: Image size derivation.
|
||||
// if let Some(image_size) = self
|
||||
// .renderer
|
||||
// .config_derived_data
|
||||
// .image_size(&format!("static/emoji/{vpath}"))
|
||||
// {
|
||||
// write!(
|
||||
// out,
|
||||
// r#" width="{}" height="{}""#,
|
||||
// image_size.width, image_size.height
|
||||
// )?;
|
||||
// }
|
||||
if let Some(image_size) = self.renderer.dirs.emoji.image_size(vpath) {
|
||||
write!(
|
||||
out,
|
||||
r#" width="{}" height="{}""#,
|
||||
image_size.width, image_size.height
|
||||
)?;
|
||||
}
|
||||
|
||||
out.push('>');
|
||||
|
||||
|
|
|
@ -9,7 +9,9 @@ use treehouse::cli::serve::serve;
|
|||
use treehouse::dirs::Dirs;
|
||||
use treehouse::generate::{self, Sources};
|
||||
use treehouse::vfs::asynch::AsyncDir;
|
||||
use treehouse::vfs::{AnchoredAtExt, Blake3ContentVersionCache, DynDir, ToDynDir, VPathBuf};
|
||||
use treehouse::vfs::{
|
||||
AnchoredAtExt, Blake3ContentVersionCache, DynDir, ImageSizeCache, ToDynDir, VPathBuf,
|
||||
};
|
||||
use treehouse::vfs::{Cd, PhysicalDir};
|
||||
use treehouse::{
|
||||
cli::{
|
||||
|
@ -43,6 +45,7 @@ fn vfs_sources() -> anyhow::Result<DynDir> {
|
|||
);
|
||||
|
||||
let root = Blake3ContentVersionCache::new(root);
|
||||
let root = ImageSizeCache::new(root);
|
||||
|
||||
Ok(root.to_dyn())
|
||||
}
|
||||
|
|
|
@ -56,6 +56,7 @@ mod content_version_cache;
|
|||
mod edit;
|
||||
mod empty;
|
||||
mod file;
|
||||
mod image_size_cache;
|
||||
mod mem_dir;
|
||||
mod overlay;
|
||||
mod path;
|
||||
|
@ -67,6 +68,7 @@ pub use content_version_cache::*;
|
|||
pub use edit::*;
|
||||
pub use empty::*;
|
||||
pub use file::*;
|
||||
pub use image_size_cache::*;
|
||||
pub use mem_dir::*;
|
||||
pub use overlay::*;
|
||||
pub use path::*;
|
||||
|
@ -77,6 +79,12 @@ pub struct DirEntry {
|
|||
pub path: VPathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct ImageSize {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
pub trait Dir: Debug {
|
||||
/// List all entries under the provided path.
|
||||
fn dir(&self, path: &VPath) -> Vec<DirEntry>;
|
||||
|
@ -90,6 +98,12 @@ pub trait Dir: Debug {
|
|||
/// Returns None if there is no content or no version string is available.
|
||||
fn content_version(&self, path: &VPath) -> Option<String>;
|
||||
|
||||
/// Returns the size of the image at the given path, or `None` if the entry is not an image
|
||||
/// (or its size cannot be known.)
|
||||
fn image_size(&self, _path: &VPath) -> Option<ImageSize> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns a path relative to `config.site` indicating where the file will be available
|
||||
/// once served.
|
||||
///
|
||||
|
@ -123,6 +137,10 @@ where
|
|||
(**self).content_version(path)
|
||||
}
|
||||
|
||||
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
|
||||
(**self).image_size(path)
|
||||
}
|
||||
|
||||
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
|
||||
(**self).anchor(path)
|
||||
}
|
||||
|
@ -150,6 +168,10 @@ impl Dir for DynDir {
|
|||
self.arc.content_version(path)
|
||||
}
|
||||
|
||||
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
|
||||
self.arc.image_size(path)
|
||||
}
|
||||
|
||||
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
|
||||
self.arc.anchor(path)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use std::fmt;
|
||||
|
||||
use super::{Dir, DirEntry, VPath, VPathBuf};
|
||||
use super::{Dir, DirEntry, EditPath, ImageSize, VPath, VPathBuf};
|
||||
|
||||
pub struct Anchored<T> {
|
||||
inner: T,
|
||||
|
@ -29,9 +29,17 @@ where
|
|||
self.inner.content_version(path)
|
||||
}
|
||||
|
||||
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
|
||||
self.inner.image_size(path)
|
||||
}
|
||||
|
||||
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
|
||||
Some(self.at.join(path))
|
||||
}
|
||||
|
||||
fn edit_path(&self, path: &VPath) -> Option<EditPath> {
|
||||
self.inner.edit_path(path)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> fmt::Debug for Anchored<T>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use std::fmt;
|
||||
|
||||
use super::{Dir, DirEntry, EditPath, VPath, VPathBuf};
|
||||
use super::{Dir, DirEntry, EditPath, ImageSize, VPath, VPathBuf};
|
||||
|
||||
pub struct Cd<T> {
|
||||
parent: T,
|
||||
|
@ -39,6 +39,10 @@ where
|
|||
self.parent.content(&self.path.join(path))
|
||||
}
|
||||
|
||||
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
|
||||
self.parent.image_size(&self.path.join(path))
|
||||
}
|
||||
|
||||
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
|
||||
self.parent.anchor(&self.path.join(path))
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ use std::fmt::{self, Debug};
|
|||
|
||||
use dashmap::DashMap;
|
||||
|
||||
use super::{Dir, DirEntry, EditPath, VPath, VPathBuf};
|
||||
use super::{Dir, DirEntry, EditPath, ImageSize, VPath, VPathBuf};
|
||||
|
||||
pub struct Blake3ContentVersionCache<T> {
|
||||
inner: T,
|
||||
|
@ -42,6 +42,10 @@ where
|
|||
.clone()
|
||||
}
|
||||
|
||||
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
|
||||
self.inner.image_size(path)
|
||||
}
|
||||
|
||||
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
|
||||
self.inner.anchor(path)
|
||||
}
|
||||
|
|
89
crates/treehouse/src/vfs/image_size_cache.rs
Normal file
89
crates/treehouse/src/vfs/image_size_cache.rs
Normal file
|
@ -0,0 +1,89 @@
|
|||
use std::{fmt, io::Cursor};
|
||||
|
||||
use anyhow::Context;
|
||||
use dashmap::DashMap;
|
||||
use log::{debug, warn};
|
||||
|
||||
use crate::config;
|
||||
|
||||
use super::{Dir, DirEntry, EditPath, ImageSize, VPath, VPathBuf};
|
||||
|
||||
pub struct ImageSizeCache<T> {
|
||||
inner: T,
|
||||
cache: DashMap<VPathBuf, Option<ImageSize>>,
|
||||
}
|
||||
|
||||
impl<T> ImageSizeCache<T> {
|
||||
pub fn new(inner: T) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
cache: DashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ImageSizeCache<T>
|
||||
where
|
||||
T: Dir,
|
||||
{
|
||||
fn compute_image_size(&self, path: &VPath) -> anyhow::Result<Option<ImageSize>> {
|
||||
if path.extension().is_some_and(config::is_image_file) {
|
||||
if let Some(content) = self.content(path) {
|
||||
let reader = image::ImageReader::new(Cursor::new(content))
|
||||
.with_guessed_format()
|
||||
.context("cannot guess image format")?;
|
||||
let (width, height) = reader.into_dimensions()?;
|
||||
debug!("image_size({path}) = ({width}, {height})");
|
||||
return Ok(Some(ImageSize { width, height }));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Dir for ImageSizeCache<T>
|
||||
where
|
||||
T: Dir,
|
||||
{
|
||||
fn dir(&self, path: &VPath) -> Vec<DirEntry> {
|
||||
self.inner.dir(path)
|
||||
}
|
||||
|
||||
fn content(&self, path: &VPath) -> Option<Vec<u8>> {
|
||||
self.inner.content(path)
|
||||
}
|
||||
|
||||
fn content_version(&self, path: &VPath) -> Option<String> {
|
||||
self.inner.content_version(path)
|
||||
}
|
||||
|
||||
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
|
||||
self.cache
|
||||
.entry(path.to_owned())
|
||||
.or_insert_with(|| {
|
||||
self.compute_image_size(path)
|
||||
.inspect_err(|err| warn!("compute_image_size({path}) failed: {err:?}"))
|
||||
.ok()
|
||||
.flatten()
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
|
||||
self.inner.anchor(path)
|
||||
}
|
||||
|
||||
fn edit_path(&self, path: &VPath) -> Option<EditPath> {
|
||||
self.inner.edit_path(path)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> fmt::Debug for ImageSizeCache<T>
|
||||
where
|
||||
T: fmt::Debug,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "ImageSizeCache({:?})", self.inner)
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
use std::{collections::HashMap, fmt};
|
||||
|
||||
use super::{Dir, DirEntry, DynDir, EditPath, VPath, VPathBuf};
|
||||
use super::{Dir, DirEntry, DynDir, EditPath, ImageSize, VPath, VPathBuf};
|
||||
|
||||
pub struct MemDir {
|
||||
mount_points: HashMap<String, DynDir>,
|
||||
|
@ -110,6 +110,17 @@ impl Dir for MemDir {
|
|||
}
|
||||
}
|
||||
|
||||
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
|
||||
match self.resolve(path) {
|
||||
Resolved::MountPoint {
|
||||
fs,
|
||||
fs_path: _,
|
||||
subpath,
|
||||
} => fs.image_size(subpath),
|
||||
Resolved::Root | Resolved::None => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
|
||||
match self.resolve(path) {
|
||||
Resolved::MountPoint {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use std::fmt;
|
||||
|
||||
use super::{Dir, DirEntry, DynDir, EditPath, VPath, VPathBuf};
|
||||
use super::{Dir, DirEntry, DynDir, EditPath, ImageSize, VPath, VPathBuf};
|
||||
|
||||
pub struct Overlay {
|
||||
base: DynDir,
|
||||
|
@ -34,6 +34,12 @@ impl Dir for Overlay {
|
|||
.or_else(|| self.base.content_version(path))
|
||||
}
|
||||
|
||||
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
|
||||
self.overlay
|
||||
.image_size(path)
|
||||
.or_else(|| self.base.image_size(path))
|
||||
}
|
||||
|
||||
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
|
||||
self.overlay.anchor(path).or_else(|| self.base.anchor(path))
|
||||
}
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use treehouse::vfs::{BufferedFile, Dir, DirEntry, MemDir, ToDynDir, VPath, VPathBuf};
|
||||
|
||||
const HEWWO: &[u8] = b"hewwo :3";
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue