mirror of
https://github.com/typst/typst
synced 2025-08-14 23:18:32 +08:00
parent
4993bae782
commit
9789fdb860
@ -65,6 +65,7 @@ pub fn layout_image(
|
|||||||
world: Some(engine.world),
|
world: Some(engine.world),
|
||||||
families: &families(styles).map(|f| f.as_str()).collect::<Vec<_>>(),
|
families: &families(styles).map(|f| f.as_str()).collect::<Vec<_>>(),
|
||||||
flatten_text: elem.flatten_text(styles),
|
flatten_text: elem.flatten_text(styles),
|
||||||
|
scaling: elem.scaling(styles),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.at(span)?;
|
.at(span)?;
|
||||||
|
@ -20,8 +20,8 @@ use typst_utils::LazyHash;
|
|||||||
use crate::diag::{bail, At, SourceResult, StrResult};
|
use crate::diag::{bail, At, SourceResult, StrResult};
|
||||||
use crate::engine::Engine;
|
use crate::engine::Engine;
|
||||||
use crate::foundations::{
|
use crate::foundations::{
|
||||||
cast, elem, func, scope, Bytes, Cast, Content, Dict, NativeElement, Packed, Show,
|
cast, elem, func, scope, AutoValue, Bytes, Cast, Content, Dict, NativeElement,
|
||||||
Smart, StyleChain,
|
Packed, Show, Smart, StyleChain, Value,
|
||||||
};
|
};
|
||||||
use crate::layout::{BlockElem, Length, Rel, Sizing};
|
use crate::layout::{BlockElem, Length, Rel, Sizing};
|
||||||
use crate::loading::Readable;
|
use crate::loading::Readable;
|
||||||
@ -103,6 +103,12 @@ pub struct ImageElem {
|
|||||||
/// output.
|
/// output.
|
||||||
#[default(false)]
|
#[default(false)]
|
||||||
pub flatten_text: bool,
|
pub flatten_text: bool,
|
||||||
|
|
||||||
|
/// A hint to the viewer how it should scale the image.
|
||||||
|
///
|
||||||
|
/// **Note:** This option may be ignored and results look different
|
||||||
|
/// depending on the format and viewer.
|
||||||
|
pub scaling: ImageScaling,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[scope]
|
#[scope]
|
||||||
@ -141,6 +147,12 @@ impl ImageElem {
|
|||||||
/// How the image should adjust itself to a given area.
|
/// How the image should adjust itself to a given area.
|
||||||
#[named]
|
#[named]
|
||||||
fit: Option<ImageFit>,
|
fit: Option<ImageFit>,
|
||||||
|
/// Whether text in SVG images should be converted into paths.
|
||||||
|
#[named]
|
||||||
|
flatten_text: Option<bool>,
|
||||||
|
/// How the image should be scaled by the viewer.
|
||||||
|
#[named]
|
||||||
|
scaling: Option<ImageScaling>,
|
||||||
) -> StrResult<Content> {
|
) -> StrResult<Content> {
|
||||||
let mut elem = ImageElem::new(EcoString::new(), source);
|
let mut elem = ImageElem::new(EcoString::new(), source);
|
||||||
if let Some(format) = format {
|
if let Some(format) = format {
|
||||||
@ -158,6 +170,15 @@ impl ImageElem {
|
|||||||
if let Some(fit) = fit {
|
if let Some(fit) = fit {
|
||||||
elem.push_fit(fit);
|
elem.push_fit(fit);
|
||||||
}
|
}
|
||||||
|
if let Some(fit) = fit {
|
||||||
|
elem.push_fit(fit);
|
||||||
|
}
|
||||||
|
if let Some(flatten_text) = flatten_text {
|
||||||
|
elem.push_flatten_text(flatten_text);
|
||||||
|
}
|
||||||
|
if let Some(scaling) = scaling {
|
||||||
|
elem.push_scaling(scaling);
|
||||||
|
}
|
||||||
Ok(elem.pack().spanned(span))
|
Ok(elem.pack().spanned(span))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -208,6 +229,8 @@ struct Repr {
|
|||||||
kind: ImageKind,
|
kind: ImageKind,
|
||||||
/// A text describing the image.
|
/// A text describing the image.
|
||||||
alt: Option<EcoString>,
|
alt: Option<EcoString>,
|
||||||
|
/// The scaling algorithm to use.
|
||||||
|
scaling: ImageScaling,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Image {
|
impl Image {
|
||||||
@ -247,7 +270,11 @@ impl Image {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Self(Arc::new(LazyHash::new(Repr { kind, alt: options.alt.clone() }))))
|
Ok(Self(Arc::new(LazyHash::new(Repr {
|
||||||
|
kind,
|
||||||
|
alt: options.alt.clone(),
|
||||||
|
scaling: options.scaling,
|
||||||
|
}))))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The format of the image.
|
/// The format of the image.
|
||||||
@ -291,6 +318,11 @@ impl Image {
|
|||||||
self.0.alt.as_deref()
|
self.0.alt.as_deref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The image scaling algorithm to use for this image.
|
||||||
|
pub fn scaling(&self) -> ImageScaling {
|
||||||
|
self.0.scaling
|
||||||
|
}
|
||||||
|
|
||||||
/// The decoded image.
|
/// The decoded image.
|
||||||
pub fn kind(&self) -> &ImageKind {
|
pub fn kind(&self) -> &ImageKind {
|
||||||
&self.0.kind
|
&self.0.kind
|
||||||
@ -304,6 +336,7 @@ impl Debug for Image {
|
|||||||
.field("width", &self.width())
|
.field("width", &self.width())
|
||||||
.field("height", &self.height())
|
.field("height", &self.height())
|
||||||
.field("alt", &self.alt())
|
.field("alt", &self.alt())
|
||||||
|
.field("scaling", &self.scaling())
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -384,6 +417,31 @@ cast! {
|
|||||||
v: PixmapFormat => Self::Pixmap(v),
|
v: PixmapFormat => Self::Pixmap(v),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The image scaling algorithm a viewer should use.
|
||||||
|
#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||||
|
pub enum ImageScaling {
|
||||||
|
/// Use the default scaling algorithm.
|
||||||
|
#[default]
|
||||||
|
Auto,
|
||||||
|
/// Scale photos with a smoothing algorithm such as bilinear interpolation.
|
||||||
|
Smooth,
|
||||||
|
/// Scale with nearest neighbor or similar to preserve the pixelated look
|
||||||
|
/// of the image.
|
||||||
|
Pixelated,
|
||||||
|
}
|
||||||
|
|
||||||
|
cast! {
|
||||||
|
ImageScaling,
|
||||||
|
self => match self {
|
||||||
|
ImageScaling::Auto => Value::Auto,
|
||||||
|
ImageScaling::Pixelated => "pixelated".into_value(),
|
||||||
|
ImageScaling::Smooth => "smooth".into_value(),
|
||||||
|
},
|
||||||
|
_: AutoValue => ImageScaling::Auto,
|
||||||
|
"pixelated" => ImageScaling::Pixelated,
|
||||||
|
"smooth" => ImageScaling::Smooth,
|
||||||
|
}
|
||||||
|
|
||||||
/// A kind of image.
|
/// A kind of image.
|
||||||
#[derive(Hash)]
|
#[derive(Hash)]
|
||||||
pub enum ImageKind {
|
pub enum ImageKind {
|
||||||
@ -397,6 +455,7 @@ pub enum ImageKind {
|
|||||||
|
|
||||||
pub struct ImageOptions<'a> {
|
pub struct ImageOptions<'a> {
|
||||||
pub alt: Option<EcoString>,
|
pub alt: Option<EcoString>,
|
||||||
|
pub scaling: ImageScaling,
|
||||||
pub world: Option<Tracked<'a, dyn World + 'a>>,
|
pub world: Option<Tracked<'a, dyn World + 'a>>,
|
||||||
pub families: &'a [&'a str],
|
pub families: &'a [&'a str],
|
||||||
pub flatten_text: bool,
|
pub flatten_text: bool,
|
||||||
@ -406,6 +465,7 @@ impl Default for ImageOptions<'_> {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
ImageOptions {
|
ImageOptions {
|
||||||
alt: None,
|
alt: None,
|
||||||
|
scaling: ImageScaling::Auto,
|
||||||
world: None,
|
world: None,
|
||||||
families: &[],
|
families: &[],
|
||||||
flatten_text: false,
|
flatten_text: false,
|
||||||
@ -416,6 +476,7 @@ impl Default for ImageOptions<'_> {
|
|||||||
impl Hash for ImageOptions<'_> {
|
impl Hash for ImageOptions<'_> {
|
||||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
self.alt.hash(state);
|
self.alt.hash(state);
|
||||||
|
self.scaling.hash(state);
|
||||||
self.families.hash(state);
|
self.families.hash(state);
|
||||||
self.flatten_text.hash(state);
|
self.flatten_text.hash(state);
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,9 @@ use ecow::eco_format;
|
|||||||
use image::{DynamicImage, GenericImageView, Rgba};
|
use image::{DynamicImage, GenericImageView, Rgba};
|
||||||
use pdf_writer::{Chunk, Filter, Finish, Ref};
|
use pdf_writer::{Chunk, Filter, Finish, Ref};
|
||||||
use typst_library::diag::{At, SourceResult, StrResult};
|
use typst_library::diag::{At, SourceResult, StrResult};
|
||||||
use typst_library::visualize::{ColorSpace, Image, ImageKind, RasterFormat, SvgImage};
|
use typst_library::visualize::{
|
||||||
|
ColorSpace, Image, ImageKind, ImageScaling, RasterFormat, SvgImage,
|
||||||
|
};
|
||||||
use typst_utils::Deferred;
|
use typst_utils::Deferred;
|
||||||
|
|
||||||
use crate::{color, deflate, PdfChunk, WithGlobalRefs};
|
use crate::{color, deflate, PdfChunk, WithGlobalRefs};
|
||||||
@ -36,6 +38,7 @@ pub fn write_images(
|
|||||||
height,
|
height,
|
||||||
icc_profile,
|
icc_profile,
|
||||||
alpha,
|
alpha,
|
||||||
|
interpolate,
|
||||||
} => {
|
} => {
|
||||||
let image_ref = chunk.alloc();
|
let image_ref = chunk.alloc();
|
||||||
out.insert(image.clone(), image_ref);
|
out.insert(image.clone(), image_ref);
|
||||||
@ -45,6 +48,7 @@ pub fn write_images(
|
|||||||
image.width(*width as i32);
|
image.width(*width as i32);
|
||||||
image.height(*height as i32);
|
image.height(*height as i32);
|
||||||
image.bits_per_component(i32::from(*bits_per_component));
|
image.bits_per_component(i32::from(*bits_per_component));
|
||||||
|
image.interpolate(*interpolate);
|
||||||
|
|
||||||
let mut icc_ref = None;
|
let mut icc_ref = None;
|
||||||
let space = image.color_space();
|
let space = image.color_space();
|
||||||
@ -73,6 +77,7 @@ pub fn write_images(
|
|||||||
mask.height(*height as i32);
|
mask.height(*height as i32);
|
||||||
mask.color_space().device_gray();
|
mask.color_space().device_gray();
|
||||||
mask.bits_per_component(i32::from(*bits_per_component));
|
mask.bits_per_component(i32::from(*bits_per_component));
|
||||||
|
mask.interpolate(*interpolate);
|
||||||
} else {
|
} else {
|
||||||
image.finish();
|
image.finish();
|
||||||
}
|
}
|
||||||
@ -127,6 +132,10 @@ pub fn deferred_image(
|
|||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// PDF/A does not appear to allow interpolation[^1].
|
||||||
|
// [^1]: https://github.com/typst/typst/issues/2942
|
||||||
|
let interpolate = image.scaling() == ImageScaling::Smooth && !pdfa;
|
||||||
|
|
||||||
let deferred = Deferred::new(move || match image.kind() {
|
let deferred = Deferred::new(move || match image.kind() {
|
||||||
ImageKind::Raster(raster) => {
|
ImageKind::Raster(raster) => {
|
||||||
let format = if raster.format() == RasterFormat::Jpg {
|
let format = if raster.format() == RasterFormat::Jpg {
|
||||||
@ -134,7 +143,12 @@ pub fn deferred_image(
|
|||||||
} else {
|
} else {
|
||||||
EncodeFormat::Flate
|
EncodeFormat::Flate
|
||||||
};
|
};
|
||||||
Ok(encode_raster_image(&raster.dynamic(), raster.icc_profile(), format))
|
Ok(encode_raster_image(
|
||||||
|
raster.dynamic(),
|
||||||
|
raster.icc_profile(),
|
||||||
|
format,
|
||||||
|
interpolate,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
ImageKind::Svg(svg) => {
|
ImageKind::Svg(svg) => {
|
||||||
let (chunk, id) = encode_svg(svg, pdfa)
|
let (chunk, id) = encode_svg(svg, pdfa)
|
||||||
@ -145,6 +159,7 @@ pub fn deferred_image(
|
|||||||
&pixmap.to_image(),
|
&pixmap.to_image(),
|
||||||
pixmap.icc_profile(),
|
pixmap.icc_profile(),
|
||||||
EncodeFormat::Flate,
|
EncodeFormat::Flate,
|
||||||
|
interpolate,
|
||||||
)),
|
)),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -157,6 +172,7 @@ fn encode_raster_image(
|
|||||||
image: &DynamicImage,
|
image: &DynamicImage,
|
||||||
icc_profile: Option<&[u8]>,
|
icc_profile: Option<&[u8]>,
|
||||||
format: EncodeFormat,
|
format: EncodeFormat,
|
||||||
|
interpolate: bool,
|
||||||
) -> EncodedImage {
|
) -> EncodedImage {
|
||||||
let color_space = to_color_space(image.color());
|
let color_space = to_color_space(image.color());
|
||||||
|
|
||||||
@ -192,6 +208,7 @@ fn encode_raster_image(
|
|||||||
height: image.height(),
|
height: image.height(),
|
||||||
icc_profile: compressed_icc,
|
icc_profile: compressed_icc,
|
||||||
alpha,
|
alpha,
|
||||||
|
interpolate,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -248,6 +265,8 @@ pub enum EncodedImage {
|
|||||||
icc_profile: Option<Vec<u8>>,
|
icc_profile: Option<Vec<u8>>,
|
||||||
/// The alpha channel of the image, pre-deflated, if any.
|
/// The alpha channel of the image, pre-deflated, if any.
|
||||||
alpha: Option<(Vec<u8>, Filter)>,
|
alpha: Option<(Vec<u8>, Filter)>,
|
||||||
|
/// Whether image interpolation should be enabled.
|
||||||
|
interpolate: bool,
|
||||||
},
|
},
|
||||||
/// A vector graphic.
|
/// A vector graphic.
|
||||||
///
|
///
|
||||||
|
@ -4,7 +4,7 @@ use image::imageops::FilterType;
|
|||||||
use image::{GenericImageView, Rgba};
|
use image::{GenericImageView, Rgba};
|
||||||
use tiny_skia as sk;
|
use tiny_skia as sk;
|
||||||
use typst_library::layout::Size;
|
use typst_library::layout::Size;
|
||||||
use typst_library::visualize::{Image, ImageKind};
|
use typst_library::visualize::{Image, ImageKind, ImageScaling};
|
||||||
|
|
||||||
use crate::{AbsExt, State};
|
use crate::{AbsExt, State};
|
||||||
|
|
||||||
@ -59,8 +59,10 @@ pub fn render_image(
|
|||||||
#[comemo::memoize]
|
#[comemo::memoize]
|
||||||
fn build_texture(image: &Image, w: u32, h: u32) -> Option<Arc<sk::Pixmap>> {
|
fn build_texture(image: &Image, w: u32, h: u32) -> Option<Arc<sk::Pixmap>> {
|
||||||
match image.kind() {
|
match image.kind() {
|
||||||
ImageKind::Raster(raster) => scale_image(raster.dynamic(), w, h),
|
ImageKind::Raster(raster) => scale_image(raster.dynamic(), image.scaling(), w, h),
|
||||||
ImageKind::Pixmap(raster) => scale_image(&raster.to_image(), w, h),
|
ImageKind::Pixmap(raster) => {
|
||||||
|
scale_image(&raster.to_image(), image.scaling(), w, h)
|
||||||
|
}
|
||||||
// Safety: We do not keep any references to tree nodes beyond the scope
|
// Safety: We do not keep any references to tree nodes beyond the scope
|
||||||
// of `with`.
|
// of `with`.
|
||||||
ImageKind::Svg(svg) => {
|
ImageKind::Svg(svg) => {
|
||||||
@ -78,10 +80,20 @@ fn build_texture(image: &Image, w: u32, h: u32) -> Option<Arc<sk::Pixmap>> {
|
|||||||
|
|
||||||
/// Scale a rastered image to a given size and return texture.
|
/// Scale a rastered image to a given size and return texture.
|
||||||
// TODO(frozolotl): optimize pixmap allocation
|
// TODO(frozolotl): optimize pixmap allocation
|
||||||
fn scale_image(image: &image::DynamicImage, w: u32, h: u32) -> Option<Arc<sk::Pixmap>> {
|
fn scale_image(
|
||||||
|
image: &image::DynamicImage,
|
||||||
|
scaling: ImageScaling,
|
||||||
|
w: u32,
|
||||||
|
h: u32,
|
||||||
|
) -> Option<Arc<sk::Pixmap>> {
|
||||||
let mut pixmap = sk::Pixmap::new(w, h)?;
|
let mut pixmap = sk::Pixmap::new(w, h)?;
|
||||||
let downscale = w < image.width();
|
let upscale = w > image.width();
|
||||||
let filter = if downscale { FilterType::Lanczos3 } else { FilterType::CatmullRom };
|
let filter = match scaling {
|
||||||
|
ImageScaling::Auto if upscale => FilterType::CatmullRom,
|
||||||
|
ImageScaling::Smooth if upscale => FilterType::CatmullRom,
|
||||||
|
ImageScaling::Pixelated => FilterType::Nearest,
|
||||||
|
_ => FilterType::Lanczos3, // downscale
|
||||||
|
};
|
||||||
let buf = image.resize(w, h, filter);
|
let buf = image.resize(w, h, filter);
|
||||||
for ((_, _, src), dest) in buf.pixels().zip(pixmap.pixels_mut()) {
|
for ((_, _, src), dest) in buf.pixels().zip(pixmap.pixels_mut()) {
|
||||||
let Rgba([r, g, b, a]) = src;
|
let Rgba([r, g, b, a]) = src;
|
||||||
|
@ -6,7 +6,7 @@ use image::error::UnsupportedError;
|
|||||||
use image::{codecs::png::PngEncoder, ImageEncoder};
|
use image::{codecs::png::PngEncoder, ImageEncoder};
|
||||||
use typst_library::layout::{Abs, Axes};
|
use typst_library::layout::{Abs, Axes};
|
||||||
use typst_library::visualize::{
|
use typst_library::visualize::{
|
||||||
Image, ImageFormat, ImageKind, RasterFormat, VectorFormat,
|
Image, ImageFormat, ImageKind, ImageScaling, RasterFormat, VectorFormat,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::SVGRenderer;
|
use crate::SVGRenderer;
|
||||||
@ -20,6 +20,17 @@ impl SVGRenderer {
|
|||||||
self.xml.write_attribute("width", &size.x.to_pt());
|
self.xml.write_attribute("width", &size.x.to_pt());
|
||||||
self.xml.write_attribute("height", &size.y.to_pt());
|
self.xml.write_attribute("height", &size.y.to_pt());
|
||||||
self.xml.write_attribute("preserveAspectRatio", "none");
|
self.xml.write_attribute("preserveAspectRatio", "none");
|
||||||
|
match image.scaling() {
|
||||||
|
ImageScaling::Auto => {}
|
||||||
|
ImageScaling::Smooth => {
|
||||||
|
// This is still experimental and not implemented in all major browsers[^1].
|
||||||
|
// [^1]: https://developer.mozilla.org/en-US/docs/Web/CSS/image-rendering#browser_compatibility
|
||||||
|
self.xml.write_attribute("style", "image-rendering: smooth")
|
||||||
|
}
|
||||||
|
ImageScaling::Pixelated => {
|
||||||
|
self.xml.write_attribute("style", "image-rendering: pixelated")
|
||||||
|
}
|
||||||
|
}
|
||||||
self.xml.end_element();
|
self.xml.end_element();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.2 KiB |
BIN
tests/ref/image-scaling-methods.png
Normal file
BIN
tests/ref/image-scaling-methods.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
@ -71,7 +71,7 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B
|
|||||||
data: bytes((
|
data: bytes((
|
||||||
0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF,
|
0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF,
|
||||||
0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80,
|
0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80,
|
||||||
0x80, 0x80, 0x00, 0x80, 0x80, 0x00, 0x80, 0x00, 0x80,
|
0x80, 0x80, 0x00, 0x00, 0x80, 0x80, 0x80, 0x00, 0x80,
|
||||||
)),
|
)),
|
||||||
pixel-width: 3,
|
pixel-width: 3,
|
||||||
pixel-height: 3,
|
pixel-height: 3,
|
||||||
@ -117,6 +117,29 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B
|
|||||||
width: 1cm,
|
width: 1cm,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
--- image-scaling-methods ---
|
||||||
|
#let img(scaling) = image.decode(
|
||||||
|
(
|
||||||
|
data: bytes((
|
||||||
|
0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF,
|
||||||
|
0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80,
|
||||||
|
0x80, 0x80, 0x00, 0x00, 0x80, 0x80, 0x80, 0x00, 0x80,
|
||||||
|
)),
|
||||||
|
pixel-width: 3,
|
||||||
|
pixel-height: 3,
|
||||||
|
),
|
||||||
|
format: "rgb8",
|
||||||
|
width: 1cm,
|
||||||
|
scaling: scaling,
|
||||||
|
)
|
||||||
|
#stack(
|
||||||
|
dir: ltr,
|
||||||
|
spacing: 4pt,
|
||||||
|
img(auto),
|
||||||
|
img("smooth"),
|
||||||
|
img("pixelated"),
|
||||||
|
)
|
||||||
|
|
||||||
--- image-natural-dpi-sizing ---
|
--- image-natural-dpi-sizing ---
|
||||||
// Test that images aren't upscaled.
|
// Test that images aren't upscaled.
|
||||||
// Image is just 48x80 at 220dpi. It should not be scaled to fit the page
|
// Image is just 48x80 at 220dpi. It should not be scaled to fit the page
|
||||||
|
Loading…
x
Reference in New Issue
Block a user