mirror of
https://github.com/typst/typst
synced 2025-05-23 21:45:29 +08:00
Make file system loader serializable 📚
This commit is contained in:
parent
6292d25afb
commit
3e03667c37
@ -17,7 +17,7 @@ const CASES: &[&str] = &["full/coma.typ", "text/basic.typ"];
|
|||||||
|
|
||||||
fn benchmarks(c: &mut Criterion) {
|
fn benchmarks(c: &mut Criterion) {
|
||||||
let mut loader = FsLoader::new();
|
let mut loader = FsLoader::new();
|
||||||
loader.search_dir(FONT_DIR);
|
loader.search_path(FONT_DIR);
|
||||||
|
|
||||||
let mut env = Env::new(loader);
|
let mut env = Env::new(loader);
|
||||||
|
|
||||||
|
39
src/env/fs.rs
vendored
39
src/env/fs.rs
vendored
@ -5,6 +5,7 @@ use std::path::{Path, PathBuf};
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use memmap2::Mmap;
|
use memmap2::Mmap;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use ttf_parser::{name_id, Face};
|
use ttf_parser::{name_id, Face};
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
@ -14,10 +15,11 @@ use crate::font::{FaceInfo, FontStretch, FontStyle, FontVariant, FontWeight};
|
|||||||
/// Loads fonts and resources from the local file system.
|
/// Loads fonts and resources from the local file system.
|
||||||
///
|
///
|
||||||
/// _This is only available when the `fs` feature is enabled._
|
/// _This is only available when the `fs` feature is enabled._
|
||||||
#[derive(Default, Debug, Clone)]
|
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct FsLoader {
|
pub struct FsLoader {
|
||||||
faces: Vec<FaceInfo>,
|
faces: Vec<FaceInfo>,
|
||||||
paths: Vec<PathBuf>,
|
files: Vec<PathBuf>,
|
||||||
|
#[serde(skip)]
|
||||||
cache: FileCache,
|
cache: FileCache,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,7 +32,7 @@ impl FsLoader {
|
|||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
faces: vec![],
|
faces: vec![],
|
||||||
paths: vec![],
|
files: vec![],
|
||||||
cache: HashMap::new(),
|
cache: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -64,19 +66,22 @@ impl FsLoader {
|
|||||||
let windir =
|
let windir =
|
||||||
std::env::var("WINDIR").unwrap_or_else(|_| "C:\\Windows".to_string());
|
std::env::var("WINDIR").unwrap_or_else(|_| "C:\\Windows".to_string());
|
||||||
|
|
||||||
self.search_dir(Path::new(&windir).join("Fonts"));
|
self.search_path(Path::new(&windir).join("Fonts"));
|
||||||
|
|
||||||
if let Some(roaming) = dirs::config_dir() {
|
if let Some(roaming) = dirs::config_dir() {
|
||||||
self.search_dir(roaming.join("Microsoft\\Windows\\Fonts"));
|
self.search_path(roaming.join("Microsoft\\Windows\\Fonts"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(local) = dirs::cache_dir() {
|
if let Some(local) = dirs::cache_dir() {
|
||||||
self.search_dir(local.join("Microsoft\\Windows\\Fonts"));
|
self.search_path(local.join("Microsoft\\Windows\\Fonts"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Search for all fonts in a directory.
|
/// Search for all fonts at a path.
|
||||||
pub fn search_dir(&mut self, dir: impl AsRef<Path>) {
|
///
|
||||||
|
/// If the path is a directory, all contained fonts will be searched for
|
||||||
|
/// recursively.
|
||||||
|
pub fn search_path(&mut self, dir: impl AsRef<Path>) {
|
||||||
let walk = WalkDir::new(dir)
|
let walk = WalkDir::new(dir)
|
||||||
.follow_links(true)
|
.follow_links(true)
|
||||||
.sort_by(|a, b| a.file_name().cmp(b.file_name()))
|
.sort_by(|a, b| a.file_name().cmp(b.file_name()))
|
||||||
@ -102,8 +107,9 @@ impl FsLoader {
|
|||||||
///
|
///
|
||||||
/// The file may form a font collection and contain multiple font faces,
|
/// The file may form a font collection and contain multiple font faces,
|
||||||
/// which will then all be indexed.
|
/// which will then all be indexed.
|
||||||
pub fn search_file(&mut self, path: impl AsRef<Path>) -> io::Result<()> {
|
fn search_file(&mut self, path: impl AsRef<Path>) -> io::Result<()> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
|
let path = path.strip_prefix(".").unwrap_or(path);
|
||||||
|
|
||||||
let file = File::open(path)?;
|
let file = File::open(path)?;
|
||||||
let mmap = unsafe { Mmap::map(&file)? };
|
let mmap = unsafe { Mmap::map(&file)? };
|
||||||
@ -149,10 +155,15 @@ impl FsLoader {
|
|||||||
|
|
||||||
// Merge with an existing entry for the same family name.
|
// Merge with an existing entry for the same family name.
|
||||||
self.faces.push(FaceInfo { family, variant, index });
|
self.faces.push(FaceInfo { family, variant, index });
|
||||||
self.paths.push(path.to_owned());
|
self.files.push(path.to_owned());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Paths to font files, parallel to [`faces()`](Self::faces).
|
||||||
|
pub fn files(&self) -> &[PathBuf] {
|
||||||
|
&self.files
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Loader for FsLoader {
|
impl Loader for FsLoader {
|
||||||
@ -161,7 +172,7 @@ impl Loader for FsLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn load_face(&mut self, idx: usize) -> Option<Buffer> {
|
fn load_face(&mut self, idx: usize) -> Option<Buffer> {
|
||||||
load(&mut self.cache, &self.paths[idx])
|
load(&mut self.cache, &self.files[idx])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_file(&mut self, url: &str) -> Option<Buffer> {
|
fn load_file(&mut self, url: &str) -> Option<Buffer> {
|
||||||
@ -169,6 +180,7 @@ impl Loader for FsLoader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Load from the file system using a cache.
|
||||||
fn load(cache: &mut FileCache, path: &Path) -> Option<Buffer> {
|
fn load(cache: &mut FileCache, path: &Path) -> Option<Buffer> {
|
||||||
match cache.entry(path.to_owned()) {
|
match cache.entry(path.to_owned()) {
|
||||||
Entry::Occupied(entry) => entry.get().clone(),
|
Entry::Occupied(entry) => entry.get().clone(),
|
||||||
@ -186,10 +198,9 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_index_font_dir() {
|
fn test_index_font_dir() {
|
||||||
let mut loader = FsLoader::new();
|
let mut loader = FsLoader::new();
|
||||||
loader.search_dir("fonts");
|
loader.search_path("fonts");
|
||||||
loader.paths.sort();
|
|
||||||
|
|
||||||
assert_eq!(loader.paths, &[
|
assert_eq!(loader.files, &[
|
||||||
Path::new("fonts/EBGaramond-Bold.ttf"),
|
Path::new("fonts/EBGaramond-Bold.ttf"),
|
||||||
Path::new("fonts/EBGaramond-BoldItalic.ttf"),
|
Path::new("fonts/EBGaramond-BoldItalic.ttf"),
|
||||||
Path::new("fonts/EBGaramond-Italic.ttf"),
|
Path::new("fonts/EBGaramond-Italic.ttf"),
|
||||||
|
@ -25,7 +25,7 @@ impl Face {
|
|||||||
// SAFETY:
|
// SAFETY:
|
||||||
// - The slices's location is stable in memory:
|
// - The slices's location is stable in memory:
|
||||||
// - We don't move the underlying vector
|
// - We don't move the underlying vector
|
||||||
// - Nobody else can move it since we haved a strong ref to the `Rc`.
|
// - Nobody else can move it since we have a strong ref to the `Rc`.
|
||||||
// - The internal static lifetime is not leaked because its rewritten
|
// - The internal static lifetime is not leaked because its rewritten
|
||||||
// to the self-lifetime in `ttf()`.
|
// to the self-lifetime in `ttf()`.
|
||||||
let slice: &'static [u8] =
|
let slice: &'static [u8] =
|
||||||
@ -151,19 +151,20 @@ impl Em {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Properties of a single font face.
|
/// Properties of a single font face.
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct FaceInfo {
|
pub struct FaceInfo {
|
||||||
/// The typographic font family this face is part of.
|
/// The typographic font family this face is part of.
|
||||||
pub family: String,
|
pub family: String,
|
||||||
/// Properties that distinguish this face from other faces in the same
|
/// Properties that distinguish this face from other faces in the same
|
||||||
/// family.
|
/// family.
|
||||||
|
#[serde(flatten)]
|
||||||
pub variant: FontVariant,
|
pub variant: FontVariant,
|
||||||
/// The collection index in the font file.
|
/// The collection index in the font file.
|
||||||
pub index: u32,
|
pub index: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Properties that distinguish a face from other faces in the same family.
|
/// Properties that distinguish a face from other faces in the same family.
|
||||||
#[derive(Default, Debug, Copy, Clone, PartialEq)]
|
#[derive(Default, Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct FontVariant {
|
pub struct FontVariant {
|
||||||
/// The style of the face (normal / italic / oblique).
|
/// The style of the face (normal / italic / oblique).
|
||||||
pub style: FontStyle,
|
pub style: FontStyle,
|
||||||
@ -183,6 +184,7 @@ impl FontVariant {
|
|||||||
/// The style of a font face.
|
/// The style of a font face.
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub enum FontStyle {
|
pub enum FontStyle {
|
||||||
/// The default style.
|
/// The default style.
|
||||||
Normal,
|
Normal,
|
||||||
@ -347,6 +349,7 @@ impl Debug for FontWeight {
|
|||||||
|
|
||||||
/// The width of a font face.
|
/// The width of a font face.
|
||||||
#[derive(Copy, Clone, PartialEq, PartialOrd, Serialize, Deserialize)]
|
#[derive(Copy, Clone, PartialEq, PartialOrd, Serialize, Deserialize)]
|
||||||
|
#[serde(transparent)]
|
||||||
pub struct FontStretch(f32);
|
pub struct FontStretch(f32);
|
||||||
|
|
||||||
impl FontStretch {
|
impl FontStretch {
|
||||||
|
@ -35,7 +35,7 @@ fn main() -> anyhow::Result<()> {
|
|||||||
let src = fs::read_to_string(src_path).context("Failed to read from source file.")?;
|
let src = fs::read_to_string(src_path).context("Failed to read from source file.")?;
|
||||||
|
|
||||||
let mut loader = FsLoader::new();
|
let mut loader = FsLoader::new();
|
||||||
loader.search_dir("fonts");
|
loader.search_path("fonts");
|
||||||
loader.search_system();
|
loader.search_system();
|
||||||
|
|
||||||
let mut env = Env::new(loader);
|
let mut env = Env::new(loader);
|
||||||
|
@ -63,7 +63,7 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut loader = FsLoader::new();
|
let mut loader = FsLoader::new();
|
||||||
loader.search_dir(FONT_DIR);
|
loader.search_path(FONT_DIR);
|
||||||
|
|
||||||
let mut env = Env::new(loader);
|
let mut env = Env::new(loader);
|
||||||
|
|
||||||
@ -122,6 +122,12 @@ impl Args {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct Panic {
|
||||||
|
pos: Pos,
|
||||||
|
lhs: Option<Value>,
|
||||||
|
rhs: Option<Value>,
|
||||||
|
}
|
||||||
|
|
||||||
fn test(
|
fn test(
|
||||||
env: &mut Env,
|
env: &mut Env,
|
||||||
src_path: &Path,
|
src_path: &Path,
|
||||||
@ -302,12 +308,6 @@ fn parse_metadata(src: &str, map: &LineMap) -> (Option<bool>, DiagSet) {
|
|||||||
(compare_ref, diags)
|
(compare_ref, diags)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Panic {
|
|
||||||
pos: Pos,
|
|
||||||
lhs: Option<Value>,
|
|
||||||
rhs: Option<Value>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn register_helpers(scope: &mut Scope, panics: Rc<RefCell<Vec<Panic>>>) {
|
fn register_helpers(scope: &mut Scope, panics: Rc<RefCell<Vec<Panic>>>) {
|
||||||
pub fn args(_: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
pub fn args(_: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
||||||
let repr = pretty(args);
|
let repr = pretty(args);
|
||||||
@ -344,7 +344,7 @@ fn print_diag(diag: &Diag, map: &LineMap, lines: u32) {
|
|||||||
println!("{}: {}-{}: {}", diag.level, start, end, diag.message);
|
println!("{}: {}-{}: {}", diag.level, start, end, diag.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(env: &Env, frames: &[Frame], pixel_per_pt: f32) -> Pixmap {
|
fn draw(env: &Env, frames: &[Frame], dpi: f32) -> Pixmap {
|
||||||
let pad = Length::pt(5.0);
|
let pad = Length::pt(5.0);
|
||||||
|
|
||||||
let height = pad + frames.iter().map(|l| l.size.height + pad).sum::<Length>();
|
let height = pad + frames.iter().map(|l| l.size.height + pad).sum::<Length>();
|
||||||
@ -355,8 +355,8 @@ fn draw(env: &Env, frames: &[Frame], pixel_per_pt: f32) -> Pixmap {
|
|||||||
.max_by(|a, b| a.partial_cmp(&b).unwrap())
|
.max_by(|a, b| a.partial_cmp(&b).unwrap())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let pixel_width = (pixel_per_pt * width.to_pt() as f32) as u32;
|
let pixel_width = (dpi * width.to_pt() as f32) as u32;
|
||||||
let pixel_height = (pixel_per_pt * height.to_pt() as f32) as u32;
|
let pixel_height = (dpi * height.to_pt() as f32) as u32;
|
||||||
if pixel_width > 4000 || pixel_height > 4000 {
|
if pixel_width > 4000 || pixel_height > 4000 {
|
||||||
panic!(
|
panic!(
|
||||||
"overlarge image: {} by {} ({} x {})",
|
"overlarge image: {} by {} ({} x {})",
|
||||||
@ -365,7 +365,7 @@ fn draw(env: &Env, frames: &[Frame], pixel_per_pt: f32) -> Pixmap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut canvas = Pixmap::new(pixel_width, pixel_height).unwrap();
|
let mut canvas = Pixmap::new(pixel_width, pixel_height).unwrap();
|
||||||
let ts = Transform::from_scale(pixel_per_pt, pixel_per_pt);
|
let ts = Transform::from_scale(dpi, dpi);
|
||||||
canvas.fill(Color::BLACK);
|
canvas.fill(Color::BLACK);
|
||||||
|
|
||||||
let mut origin = Point::new(pad, pad);
|
let mut origin = Point::new(pad, pad);
|
||||||
@ -391,15 +391,9 @@ fn draw(env: &Env, frames: &[Frame], pixel_per_pt: f32) -> Pixmap {
|
|||||||
let y = pos.y.to_pt() as f32;
|
let y = pos.y.to_pt() as f32;
|
||||||
let ts = ts.pre_translate(x, y);
|
let ts = ts.pre_translate(x, y);
|
||||||
match element {
|
match element {
|
||||||
Element::Text(shaped) => {
|
Element::Text(shaped) => draw_text(&mut canvas, env, ts, shaped),
|
||||||
draw_text(&mut canvas, env, ts, shaped);
|
Element::Image(image) => draw_image(&mut canvas, env, ts, image),
|
||||||
}
|
Element::Geometry(geom) => draw_geometry(&mut canvas, ts, geom),
|
||||||
Element::Image(image) => {
|
|
||||||
draw_image(&mut canvas, env, ts, image);
|
|
||||||
}
|
|
||||||
Element::Geometry(geom) => {
|
|
||||||
draw_geometry(&mut canvas, ts, geom);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -517,32 +511,41 @@ fn convert_typst_fill(fill: Fill) -> Paint<'static> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn convert_typst_path(path: &geom::Path) -> tiny_skia::Path {
|
fn convert_typst_path(path: &geom::Path) -> tiny_skia::Path {
|
||||||
let f = |length: Length| length.to_pt() as f32;
|
|
||||||
let mut builder = tiny_skia::PathBuilder::new();
|
let mut builder = tiny_skia::PathBuilder::new();
|
||||||
|
let f = |v: Length| v.to_pt() as f32;
|
||||||
for elem in &path.0 {
|
for elem in &path.0 {
|
||||||
match elem {
|
match elem {
|
||||||
geom::PathElement::MoveTo(p) => builder.move_to(f(p.x), f(p.y)),
|
geom::PathElement::MoveTo(p) => {
|
||||||
geom::PathElement::LineTo(p) => builder.line_to(f(p.x), f(p.y)),
|
builder.move_to(f(p.x), f(p.y));
|
||||||
geom::PathElement::CubicTo(p1, p2, p3) => {
|
}
|
||||||
builder.cubic_to(f(p1.x), f(p1.y), f(p2.x), f(p2.y), f(p3.x), f(p3.y))
|
geom::PathElement::LineTo(p) => {
|
||||||
|
builder.line_to(f(p.x), f(p.y));
|
||||||
|
}
|
||||||
|
geom::PathElement::CubicTo(p1, p2, p3) => {
|
||||||
|
builder.cubic_to(f(p1.x), f(p1.y), f(p2.x), f(p2.y), f(p3.x), f(p3.y));
|
||||||
|
}
|
||||||
|
geom::PathElement::ClosePath => {
|
||||||
|
builder.close();
|
||||||
}
|
}
|
||||||
geom::PathElement::ClosePath => builder.close(),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
builder.finish().unwrap()
|
builder.finish().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn convert_usvg_transform(transform: usvg::Transform) -> Transform {
|
||||||
|
let g = |v: f64| v as f32;
|
||||||
|
let usvg::Transform { a, b, c, d, e, f } = transform;
|
||||||
|
Transform::from_row(g(a), g(b), g(c), g(d), g(e), g(f))
|
||||||
|
}
|
||||||
|
|
||||||
fn convert_usvg_fill(fill: &usvg::Fill) -> (Paint<'static>, FillRule) {
|
fn convert_usvg_fill(fill: &usvg::Fill) -> (Paint<'static>, FillRule) {
|
||||||
let mut paint = Paint::default();
|
let mut paint = Paint::default();
|
||||||
paint.anti_alias = true;
|
paint.anti_alias = true;
|
||||||
|
|
||||||
match fill.paint {
|
match fill.paint {
|
||||||
usvg::Paint::Color(color) => paint.set_color_rgba8(
|
usvg::Paint::Color(usvg::Color { red, green, blue }) => {
|
||||||
color.red,
|
paint.set_color_rgba8(red, green, blue, fill.opacity.to_u8())
|
||||||
color.green,
|
}
|
||||||
color.blue,
|
|
||||||
fill.opacity.to_u8(),
|
|
||||||
),
|
|
||||||
usvg::Paint::Link(_) => {}
|
usvg::Paint::Link(_) => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -555,29 +558,27 @@ fn convert_usvg_fill(fill: &usvg::Fill) -> (Paint<'static>, FillRule) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn convert_usvg_path(path: &usvg::PathData) -> tiny_skia::Path {
|
fn convert_usvg_path(path: &usvg::PathData) -> tiny_skia::Path {
|
||||||
let f = |v: f64| v as f32;
|
|
||||||
let mut builder = tiny_skia::PathBuilder::new();
|
let mut builder = tiny_skia::PathBuilder::new();
|
||||||
|
let f = |v: f64| v as f32;
|
||||||
for seg in path.iter() {
|
for seg in path.iter() {
|
||||||
match *seg {
|
match *seg {
|
||||||
usvg::PathSegment::MoveTo { x, y } => builder.move_to(f(x), f(y)),
|
usvg::PathSegment::MoveTo { x, y } => {
|
||||||
|
builder.move_to(f(x), f(y));
|
||||||
|
}
|
||||||
usvg::PathSegment::LineTo { x, y } => {
|
usvg::PathSegment::LineTo { x, y } => {
|
||||||
builder.line_to(f(x), f(y));
|
builder.line_to(f(x), f(y));
|
||||||
}
|
}
|
||||||
usvg::PathSegment::CurveTo { x1, y1, x2, y2, x, y } => {
|
usvg::PathSegment::CurveTo { x1, y1, x2, y2, x, y } => {
|
||||||
builder.cubic_to(f(x1), f(y1), f(x2), f(y2), f(x), f(y))
|
builder.cubic_to(f(x1), f(y1), f(x2), f(y2), f(x), f(y));
|
||||||
|
}
|
||||||
|
usvg::PathSegment::ClosePath => {
|
||||||
|
builder.close();
|
||||||
}
|
}
|
||||||
usvg::PathSegment::ClosePath => builder.close(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
builder.finish().unwrap()
|
builder.finish().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn convert_usvg_transform(transform: usvg::Transform) -> Transform {
|
|
||||||
let g = |v: f64| v as f32;
|
|
||||||
let usvg::Transform { a, b, c, d, e, f } = transform;
|
|
||||||
Transform::from_row(g(a), g(b), g(c), g(d), g(e), g(f))
|
|
||||||
}
|
|
||||||
|
|
||||||
struct WrappedPathBuilder(tiny_skia::PathBuilder);
|
struct WrappedPathBuilder(tiny_skia::PathBuilder);
|
||||||
|
|
||||||
impl OutlineBuilder for WrappedPathBuilder {
|
impl OutlineBuilder for WrappedPathBuilder {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user