mirror of
https://github.com/typst/typst
synced 2025-06-08 13:16:24 +08:00
more
This commit is contained in:
parent
c14cddd55a
commit
db412f4387
@ -3,12 +3,12 @@ use crate::link::handle_link;
|
|||||||
use crate::metadata::build_metadata;
|
use crate::metadata::build_metadata;
|
||||||
use crate::outline::build_outline;
|
use crate::outline::build_outline;
|
||||||
use crate::page::PageLabelExt;
|
use crate::page::PageLabelExt;
|
||||||
|
use crate::shape::handle_shape;
|
||||||
use crate::text::handle_text;
|
use crate::text::handle_text;
|
||||||
use crate::util::{build_path, display_font, AbsExt, TransformExt};
|
use crate::util::{convert_path, display_font, AbsExt, TransformExt};
|
||||||
use crate::{paint, PdfOptions};
|
use crate::{paint, PdfOptions};
|
||||||
use krilla::destination::{NamedDestination, XyzDestination};
|
use krilla::destination::{NamedDestination, XyzDestination};
|
||||||
use krilla::error::KrillaError;
|
use krilla::error::KrillaError;
|
||||||
use krilla::geom::Rect;
|
|
||||||
use krilla::page::PageLabel;
|
use krilla::page::PageLabel;
|
||||||
use krilla::path::PathBuilder;
|
use krilla::path::PathBuilder;
|
||||||
use krilla::surface::Surface;
|
use krilla::surface::Surface;
|
||||||
@ -54,11 +54,11 @@ impl State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn size(&mut self, size: Size) {
|
pub(crate) fn size(&mut self, size: Size) {
|
||||||
self.size = size;
|
self.size = size;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn transform(&mut self, transform: Transform) {
|
pub(crate) fn transform(&mut self, transform: Transform) {
|
||||||
self.transform = self.transform.pre_concat(transform);
|
self.transform = self.transform.pre_concat(transform);
|
||||||
self.transform_chain = self.transform_chain.pre_concat(transform);
|
self.transform_chain = self.transform_chain.pre_concat(transform);
|
||||||
}
|
}
|
||||||
@ -68,7 +68,7 @@ impl State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Creates the [`Transforms`] structure for the current item.
|
/// Creates the [`Transforms`] structure for the current item.
|
||||||
pub fn transforms(&self, size: Size) -> Transforms {
|
pub(crate) fn transforms(&self, size: Size) -> Transforms {
|
||||||
Transforms {
|
Transforms {
|
||||||
transform_chain_: self.transform_chain,
|
transform_chain_: self.transform_chain,
|
||||||
container_transform_chain: self.container_transform_chain,
|
container_transform_chain: self.container_transform_chain,
|
||||||
@ -84,26 +84,26 @@ pub(crate) struct FrameContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FrameContext {
|
impl FrameContext {
|
||||||
pub fn new(size: Size) -> Self {
|
pub(crate) fn new(size: Size) -> Self {
|
||||||
Self {
|
Self {
|
||||||
states: vec![State::new(size, Transform::identity(), Transform::identity())],
|
states: vec![State::new(size, Transform::identity(), Transform::identity())],
|
||||||
annotations: vec![],
|
annotations: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push(&mut self) {
|
pub(crate) fn push(&mut self) {
|
||||||
self.states.push(self.states.last().unwrap().clone());
|
self.states.push(self.states.last().unwrap().clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pop(&mut self) {
|
pub(crate) fn pop(&mut self) {
|
||||||
self.states.pop();
|
self.states.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn state(&self) -> &State {
|
pub(crate) fn state(&self) -> &State {
|
||||||
self.states.last().unwrap()
|
self.states.last().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn state_mut(&mut self) -> &mut State {
|
pub(crate) fn state_mut(&mut self) -> &mut State {
|
||||||
self.states.last_mut().unwrap()
|
self.states.last_mut().unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -112,16 +112,16 @@ impl FrameContext {
|
|||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub(super) struct Transforms {
|
pub(super) struct Transforms {
|
||||||
/// The full transform chain.
|
/// The full transform chain.
|
||||||
pub transform_chain_: Transform,
|
pub(crate) transform_chain_: Transform,
|
||||||
/// The transform of first hard frame in the hierarchy.
|
/// The transform of first hard frame in the hierarchy.
|
||||||
pub container_transform_chain: Transform,
|
pub(crate) container_transform_chain: Transform,
|
||||||
/// The size of the first hard frame in the hierarchy.
|
/// The size of the first hard frame in the hierarchy.
|
||||||
pub container_size: Size,
|
pub(crate) container_size: Size,
|
||||||
/// The size of the item.
|
/// The size of the item.
|
||||||
pub size: Size,
|
pub(crate) size: Size,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct GlobalContext<'a> {
|
pub(crate) struct GlobalContext<'a> {
|
||||||
/// Cache the conversion between krilla and Typst fonts (forward and backward).
|
/// Cache the conversion between krilla and Typst fonts (forward and backward).
|
||||||
pub(crate) fonts_forward: HashMap<Font, krilla::font::Font>,
|
pub(crate) fonts_forward: HashMap<Font, krilla::font::Font>,
|
||||||
pub(crate) fonts_backward: HashMap<krilla::font::Font, Font>,
|
pub(crate) fonts_backward: HashMap<krilla::font::Font, Font>,
|
||||||
@ -265,7 +265,7 @@ pub fn pdf(
|
|||||||
let mut page = document.start_page_with(settings);
|
let mut page = document.start_page_with(settings);
|
||||||
let mut surface = page.surface();
|
let mut surface = page.surface();
|
||||||
let mut fc = FrameContext::new(typst_page.frame.size());
|
let mut fc = FrameContext::new(typst_page.frame.size());
|
||||||
process_frame(
|
handle_frame(
|
||||||
&mut fc,
|
&mut fc,
|
||||||
&typst_page.frame,
|
&typst_page.frame,
|
||||||
typst_page.fill_or_transparent(),
|
typst_page.fill_or_transparent(),
|
||||||
@ -286,6 +286,82 @@ pub fn pdf(
|
|||||||
finish(document, gc)
|
finish(document, gc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn handle_frame(
|
||||||
|
fc: &mut FrameContext,
|
||||||
|
frame: &Frame,
|
||||||
|
fill: Option<Paint>,
|
||||||
|
surface: &mut Surface,
|
||||||
|
gc: &mut GlobalContext,
|
||||||
|
) -> SourceResult<()> {
|
||||||
|
fc.push();
|
||||||
|
|
||||||
|
if frame.kind().is_hard() {
|
||||||
|
fc.state_mut().set_container_transform();
|
||||||
|
fc.state_mut().size(frame.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(fill) = fill {
|
||||||
|
let shape = Geometry::Rect(frame.size()).filled(fill);
|
||||||
|
handle_shape(fc, &shape, surface, gc)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (point, item) in frame.items() {
|
||||||
|
fc.push();
|
||||||
|
fc.state_mut().transform(Transform::translate(point.x, point.y));
|
||||||
|
|
||||||
|
match item {
|
||||||
|
FrameItem::Group(g) => handle_group(fc, g, surface, gc)?,
|
||||||
|
FrameItem::Text(t) => handle_text(fc, t, surface, gc)?,
|
||||||
|
FrameItem::Shape(s, _) => handle_shape(fc, s, surface, gc)?,
|
||||||
|
FrameItem::Image(image, size, span) => {
|
||||||
|
handle_image(gc, fc, image, *size, surface, *span)?
|
||||||
|
}
|
||||||
|
FrameItem::Link(d, s) => handle_link(fc, gc, d, *s),
|
||||||
|
FrameItem::Tag(_) => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
fc.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
fc.pop();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn handle_group(
|
||||||
|
fc: &mut FrameContext,
|
||||||
|
group: &GroupItem,
|
||||||
|
surface: &mut Surface,
|
||||||
|
context: &mut GlobalContext,
|
||||||
|
) -> SourceResult<()> {
|
||||||
|
fc.push();
|
||||||
|
fc.state_mut().transform(group.transform);
|
||||||
|
|
||||||
|
let clip_path = group
|
||||||
|
.clip_path
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|p| {
|
||||||
|
let mut builder = PathBuilder::new();
|
||||||
|
convert_path(p, &mut builder);
|
||||||
|
builder.finish()
|
||||||
|
})
|
||||||
|
.and_then(|p| p.transform(fc.state().transform.to_krilla()));
|
||||||
|
|
||||||
|
if let Some(clip_path) = &clip_path {
|
||||||
|
surface.push_clip_path(clip_path, &krilla::path::FillRule::NonZero);
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_frame(fc, &group.frame, None, surface, context)?;
|
||||||
|
|
||||||
|
if clip_path.is_some() {
|
||||||
|
surface.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
fc.pop();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Finish a krilla document and handle export errors.
|
/// Finish a krilla document and handle export errors.
|
||||||
fn finish(document: Document, gc: GlobalContext) -> SourceResult<Vec<u8>> {
|
fn finish(document: Document, gc: GlobalContext) -> SourceResult<Vec<u8>> {
|
||||||
match document.finish() {
|
match document.finish() {
|
||||||
@ -430,157 +506,3 @@ fn get_version(options: &PdfOptions) -> SourceResult<PdfVersion> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn process_frame(
|
|
||||||
fc: &mut FrameContext,
|
|
||||||
frame: &Frame,
|
|
||||||
fill: Option<Paint>,
|
|
||||||
surface: &mut Surface,
|
|
||||||
gc: &mut GlobalContext,
|
|
||||||
) -> SourceResult<()> {
|
|
||||||
fc.push();
|
|
||||||
|
|
||||||
if frame.kind().is_hard() {
|
|
||||||
fc.state_mut().set_container_transform();
|
|
||||||
fc.state_mut().size(frame.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(fill) = fill {
|
|
||||||
let shape = Geometry::Rect(frame.size()).filled(fill);
|
|
||||||
handle_shape(fc, &shape, surface, gc)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (point, item) in frame.items() {
|
|
||||||
fc.push();
|
|
||||||
fc.state_mut().transform(Transform::translate(point.x, point.y));
|
|
||||||
|
|
||||||
match item {
|
|
||||||
FrameItem::Group(g) => handle_group(fc, g, surface, gc)?,
|
|
||||||
FrameItem::Text(t) => handle_text(fc, t, surface, gc)?,
|
|
||||||
FrameItem::Shape(s, _) => handle_shape(fc, s, surface, gc)?,
|
|
||||||
FrameItem::Image(image, size, span) => {
|
|
||||||
handle_image(gc, fc, image, *size, surface, *span)?
|
|
||||||
}
|
|
||||||
FrameItem::Link(d, s) => handle_link(fc, gc, d, *s),
|
|
||||||
FrameItem::Tag(_) => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
fc.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
fc.pop();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_group(
|
|
||||||
fc: &mut FrameContext,
|
|
||||||
group: &GroupItem,
|
|
||||||
surface: &mut Surface,
|
|
||||||
context: &mut GlobalContext,
|
|
||||||
) -> SourceResult<()> {
|
|
||||||
fc.push();
|
|
||||||
fc.state_mut().transform(group.transform);
|
|
||||||
|
|
||||||
let clip_path = group
|
|
||||||
.clip_path
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|p| {
|
|
||||||
let mut builder = PathBuilder::new();
|
|
||||||
build_path(p, &mut builder);
|
|
||||||
builder.finish()
|
|
||||||
})
|
|
||||||
.and_then(|p| p.transform(fc.state().transform.to_krilla()));
|
|
||||||
|
|
||||||
if let Some(clip_path) = &clip_path {
|
|
||||||
surface.push_clip_path(clip_path, &krilla::path::FillRule::NonZero);
|
|
||||||
}
|
|
||||||
|
|
||||||
process_frame(fc, &group.frame, None, surface, context)?;
|
|
||||||
|
|
||||||
if clip_path.is_some() {
|
|
||||||
surface.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
fc.pop();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_shape(
|
|
||||||
fc: &mut FrameContext,
|
|
||||||
shape: &Shape,
|
|
||||||
surface: &mut Surface,
|
|
||||||
gc: &mut GlobalContext,
|
|
||||||
) -> SourceResult<()> {
|
|
||||||
let mut path_builder = PathBuilder::new();
|
|
||||||
|
|
||||||
match &shape.geometry {
|
|
||||||
Geometry::Line(l) => {
|
|
||||||
path_builder.move_to(0.0, 0.0);
|
|
||||||
path_builder.line_to(l.x.to_f32(), l.y.to_f32());
|
|
||||||
}
|
|
||||||
Geometry::Rect(size) => {
|
|
||||||
let w = size.x.to_f32();
|
|
||||||
let h = size.y.to_f32();
|
|
||||||
let rect = if w < 0.0 || h < 0.0 {
|
|
||||||
// Skia doesn't normally allow for negative dimensions, but
|
|
||||||
// Typst supports them, so we apply a transform if needed
|
|
||||||
// Because this operation is expensive according to tiny-skia's
|
|
||||||
// docs, we prefer to not apply it if not needed
|
|
||||||
let transform =
|
|
||||||
krilla::geom::Transform::from_scale(w.signum(), h.signum());
|
|
||||||
Rect::from_xywh(0.0, 0.0, w.abs(), h.abs())
|
|
||||||
.and_then(|rect| rect.transform(transform))
|
|
||||||
} else {
|
|
||||||
Rect::from_xywh(0.0, 0.0, w, h)
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(rect) = rect {
|
|
||||||
path_builder.push_rect(rect);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Geometry::Path(p) => {
|
|
||||||
build_path(p, &mut path_builder);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
surface.push_transform(&fc.state().transform.to_krilla());
|
|
||||||
|
|
||||||
if let Some(path) = path_builder.finish() {
|
|
||||||
if let Some(paint) = &shape.fill {
|
|
||||||
let fill = paint::fill(
|
|
||||||
gc,
|
|
||||||
&paint,
|
|
||||||
shape.fill_rule,
|
|
||||||
false,
|
|
||||||
surface,
|
|
||||||
fc.state().transforms(shape.geometry.bbox_size()),
|
|
||||||
)?;
|
|
||||||
surface.fill_path(&path, fill);
|
|
||||||
}
|
|
||||||
|
|
||||||
let stroke = shape.stroke.as_ref().and_then(|stroke| {
|
|
||||||
if stroke.thickness.to_f32() > 0.0 {
|
|
||||||
Some(stroke)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(stroke) = &stroke {
|
|
||||||
let stroke = paint::stroke(
|
|
||||||
gc,
|
|
||||||
stroke,
|
|
||||||
false,
|
|
||||||
surface,
|
|
||||||
fc.state().transforms(shape.geometry.bbox_size()),
|
|
||||||
)?;
|
|
||||||
surface.stroke_path(&path, stroke);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
surface.pop();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
@ -7,6 +7,7 @@ mod metadata;
|
|||||||
mod outline;
|
mod outline;
|
||||||
mod page;
|
mod page;
|
||||||
mod paint;
|
mod paint;
|
||||||
|
mod shape;
|
||||||
mod text;
|
mod text;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
//! Convert paint types from typst to krilla.
|
//! Convert paint types from typst to krilla.
|
||||||
|
|
||||||
use crate::krilla::{process_frame, FrameContext, GlobalContext, Transforms};
|
use crate::krilla::{handle_frame, FrameContext, GlobalContext, Transforms};
|
||||||
use crate::util::{AbsExt, ColorExt, FillRuleExt, LineCapExt, LineJoinExt, TransformExt};
|
use crate::util::{AbsExt, ColorExt, FillRuleExt, LineCapExt, LineJoinExt, TransformExt};
|
||||||
use krilla::geom::NormalizedF32;
|
use krilla::geom::NormalizedF32;
|
||||||
use krilla::paint::SpreadMethod;
|
use krilla::paint::SpreadMethod;
|
||||||
@ -127,7 +127,7 @@ pub(crate) fn convert_pattern(
|
|||||||
let mut stream_builder = surface.stream_builder();
|
let mut stream_builder = surface.stream_builder();
|
||||||
let mut surface = stream_builder.surface();
|
let mut surface = stream_builder.surface();
|
||||||
let mut fc = FrameContext::new(pattern.frame().size());
|
let mut fc = FrameContext::new(pattern.frame().size());
|
||||||
process_frame(&mut fc, pattern.frame(), None, &mut surface, gc)?;
|
handle_frame(&mut fc, pattern.frame(), None, &mut surface, gc)?;
|
||||||
surface.finish();
|
surface.finish();
|
||||||
let stream = stream_builder.finish();
|
let stream = stream_builder.finish();
|
||||||
let pattern = krilla::paint::Pattern {
|
let pattern = krilla::paint::Pattern {
|
||||||
|
92
crates/typst-pdf/src/shape.rs
Normal file
92
crates/typst-pdf/src/shape.rs
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
use crate::krilla::{FrameContext, GlobalContext};
|
||||||
|
use crate::paint;
|
||||||
|
use crate::util::{convert_path, AbsExt, TransformExt};
|
||||||
|
use krilla::geom::Rect;
|
||||||
|
use krilla::path::{Path, PathBuilder};
|
||||||
|
use krilla::surface::Surface;
|
||||||
|
use typst_library::diag::SourceResult;
|
||||||
|
use typst_library::visualize::{Geometry, Shape};
|
||||||
|
|
||||||
|
pub(crate) fn handle_shape(
|
||||||
|
fc: &mut FrameContext,
|
||||||
|
shape: &Shape,
|
||||||
|
surface: &mut Surface,
|
||||||
|
gc: &mut GlobalContext,
|
||||||
|
) -> SourceResult<()> {
|
||||||
|
surface.push_transform(&fc.state().transform.to_krilla());
|
||||||
|
|
||||||
|
if let Some(path) = convert_geometry(&shape.geometry) {
|
||||||
|
if let Some(paint) = &shape.fill {
|
||||||
|
let fill = paint::fill(
|
||||||
|
gc,
|
||||||
|
&paint,
|
||||||
|
shape.fill_rule,
|
||||||
|
false,
|
||||||
|
surface,
|
||||||
|
fc.state().transforms(shape.geometry.bbox_size()),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
surface.fill_path(&path, fill);
|
||||||
|
}
|
||||||
|
|
||||||
|
let stroke = shape.stroke.as_ref().and_then(|stroke| {
|
||||||
|
if stroke.thickness.to_f32() > 0.0 {
|
||||||
|
Some(stroke)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(stroke) = &stroke {
|
||||||
|
let stroke = paint::stroke(
|
||||||
|
gc,
|
||||||
|
stroke,
|
||||||
|
false,
|
||||||
|
surface,
|
||||||
|
fc.state().transforms(shape.geometry.bbox_size()),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
surface.stroke_path(&path, stroke);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
surface.pop();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_geometry(geometry: &Geometry) -> Option<Path> {
|
||||||
|
let mut path_builder = PathBuilder::new();
|
||||||
|
|
||||||
|
match geometry {
|
||||||
|
Geometry::Line(l) => {
|
||||||
|
path_builder.move_to(0.0, 0.0);
|
||||||
|
path_builder.line_to(l.x.to_f32(), l.y.to_f32());
|
||||||
|
}
|
||||||
|
Geometry::Rect(size) => {
|
||||||
|
let w = size.x.to_f32();
|
||||||
|
let h = size.y.to_f32();
|
||||||
|
let rect = if w < 0.0 || h < 0.0 {
|
||||||
|
// Skia doesn't normally allow for negative dimensions, but
|
||||||
|
// Typst supports them, so we apply a transform if needed
|
||||||
|
// Because this operation is expensive according to tiny-skia's
|
||||||
|
// docs, we prefer to not apply it if not needed
|
||||||
|
let transform =
|
||||||
|
krilla::geom::Transform::from_scale(w.signum(), h.signum());
|
||||||
|
Rect::from_xywh(0.0, 0.0, w.abs(), h.abs())
|
||||||
|
.and_then(|rect| rect.transform(transform))
|
||||||
|
} else {
|
||||||
|
Rect::from_xywh(0.0, 0.0, w, h)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(rect) = rect {
|
||||||
|
path_builder.push_rect(rect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Geometry::Path(p) => {
|
||||||
|
convert_path(p, &mut path_builder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
path_builder.finish()
|
||||||
|
}
|
@ -121,7 +121,7 @@ pub(crate) fn display_font(font: &Font) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build a typst path using a path builder.
|
/// Build a typst path using a path builder.
|
||||||
pub(crate) fn build_path(path: &Path, builder: &mut PathBuilder) {
|
pub(crate) fn convert_path(path: &Path, builder: &mut PathBuilder) {
|
||||||
for item in &path.0 {
|
for item in &path.0 {
|
||||||
match item {
|
match item {
|
||||||
PathItem::MoveTo(p) => builder.move_to(p.x.to_f32(), p.y.to_f32()),
|
PathItem::MoveTo(p) => builder.move_to(p.x.to_f32(), p.y.to_f32()),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user