Initial commit

This commit is contained in:
Fathy Boundjadj 2022-12-27 17:26:32 +01:00
parent 761e4e0716
commit a69e8b6096
45 changed files with 5110 additions and 1469 deletions

3
.cargo/config.toml Normal file
View File

@ -0,0 +1,3 @@
[build]
rustflags = ["-L", "/Users/fathy/Git/carbonyl/chromium/src/out/Default/obj/headless"]
target-dir = "build"

5
.gitignore vendored
View File

@ -5,5 +5,6 @@ node_modules/
/.git_cache
/.vscode
/build
/electron/*
!/electron/.gclient
/chromium/*
!/chromium/.gclient
/Cargo.lock

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "libsixel"]
path = libsixel
url = https://github.com/saitoha/libsixel

14
Cargo.toml Normal file
View File

@ -0,0 +1,14 @@
[package]
name = "carbonyl"
version = "0.0.1"
edition = "2021"
[dependencies]
libc = "0.2"
unicode-width = "0.1.10"
unicode-segmentation = "1.10.0"
[lib]
name = "carbonyl"
path = "src/lib.rs"
crate-type = ["staticlib"]

View File

@ -21,7 +21,7 @@ RUN apt-get update && \
# Release binaries
# ================
FROM --platform=$BUILDPLATFORM debian:11 AS html2svg-binaries
FROM --platform=$BUILDPLATFORM debian:11 AS carbonyl-binaries
RUN apt-get update && apt-get install -y unzip
@ -31,7 +31,7 @@ RUN unzip /runtime.zip -d /runtime
# TypeScript build
# ================
FROM --platform=$BUILDPLATFORM node:18 AS html2svg-js
FROM --platform=$BUILDPLATFORM node:18 AS carbonyl-js
WORKDIR /app
COPY package.json yarn.lock /app/
@ -55,8 +55,8 @@ WORKDIR /app
COPY package.json yarn.lock /app/
RUN yarn --production
COPY --from=html2svg-js /app/build /app/build
COPY --from=html2svg-binaries /runtime /app/build/runtime
COPY --from=carbonyl-js /app/build /app/build
COPY --from=carbonyl-binaries /runtime /app/build/runtime
COPY /scripts/docker-entrypoint.sh /app/scripts/docker-entrypoint.sh
ENTRYPOINT ["/app/scripts/docker-entrypoint.sh"]

11
chromium/.gclient Normal file
View File

@ -0,0 +1,11 @@
solutions = [
{
"name": "src",
"url": "https://chromium.googlesource.com/chromium/src.git@111.0.5539.1",
"managed": False,
"custom_deps": {},
"custom_vars": {
"use_rust": True,
}
},
]

View File

@ -1,10 +0,0 @@
solutions = [
{ "name" : 'src/electron',
"url" : 'https://github.com/electron/electron',
"deps_file" : 'DEPS',
"managed" : False,
"custom_deps" : {
},
"custom_vars": {},
},
]

View File

@ -1,5 +1,5 @@
{
"name": "html2svg",
"name": "carbonyl",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",

144
readme.md
View File

@ -1,133 +1,43 @@
# `html2svg`
# `carbonyl`
Convert HTML and `<canvas>` to vector (SVG, PDF) or bitmap (PNG, JPEG, WebP) images using Chromium. [Read the blog post](https://fathy.fr/html2svg).
Carbonyl is a Chromium based browser built to run in a terminal. [Read the blog post](https://fathy.fr/carbonyl).
It supports pretty much all Web APIs including WebGL, WebGPU, audio and video playback, animations, etc..
It's snappy, starts in less than a second, runs at 60 FPS, and idles at 0% CPU usage. It does not requires a window server (ie. works in a safe-mode console), and even runs through SSH.
Carbonyl originally started as [`html2svg`](https://github.com/fathyb/html2svg) and is now the runtime behind it.
## Usage
```shell
# Export to SVG
$ docker run fathyb/html2svg https://google.com > google.svg
$ docker run fathyb/html2svg https://google.com --format svg > google.svg
# Export to PDF
$ docker run fathyb/html2svg https://google.com --format pdf > google.pdf
# Export to PNG
$ docker run fathyb/html2svg https://google.com --format png > google.png
# Display help
$ docker run fathyb/html2svg --help
Usage: html2svg [options] [command] <url>
Arguments:
url URL to the web page to render
Options:
-f, --full capture the entire page
-W, --wait <seconds> set the amount of seconds to wait between the page loaded event and taking the screenshot (default: 1)
-w, --width <width> set the viewport width in pixels (default: 1920)
-h, --height <height> set the viewport height in pixels (default: 1080)
-f, --format <format> set the output format, should one of these values: svg, pdf, png, jpg, webp (default: "svg")
--help display help for command
Commands:
serve [options]
```
### Server
An HTTP server is also provided, all CLI options are supported:
> Currently building...
```shell
# Start a server on port 8080
$ docker run -p 8080:8080 fathyb/html2svg serve
# Export to SVG
$ curl -d http://google.fr http://localhost:8080 > google.svg
$ curl -d '{"url": "http://google.fr", "format": "svg"}' http://localhost:8080 > google.svg
# Export to PDF
$ curl -d '{"url": "http://google.fr", "format": "pdf"}' http://localhost:8080 > google.pdf
# Export to PNG
$ curl -d '{"url": "http://google.fr", "format": "png"}' http://localhost:8080 > google.png
# Display help
$ docker run fathyb/html2svg serve --help
Usage: html2svg serve [options]
Options:
-H, --host <hostname> set the hostname to listen on (default: "localhost")
-p, --port <hostname> set the port to listen on (default: 8080)
-u, --unix <path> set the unix socket to listen on
-h, --help display help for command
# Watch YouTube inside a Docker container
$ docker run fathyb/carbonyl youtube.com
```
## Development
Building Chromium is only officially supported on AMD64. If you'd like to target ARM64, cross-compile from AMD64 instead.
### Fetch
### Local
You'll need to install all tools required to build Chromium: https://www.chromium.org/developers/how-tos/get-the-code/
If you're running Linux, you can use [the Docker build instructions](#docker) to generate binaries.
1. Fetch dependencies:
```shell
$ yarn
```
2. Clone Electron.js and Chromium using `gclient`:
```shell
$ yarn gclient
```
3. Configure the build system using `gn` using one of these commands:
```shell
# for local developement
$ yarn gn testing
# or for releasing
$ yarn gn release
# add --ide=xcode if you'd like to generate an Xcode project on macOS
$ yarn gn release --ide=xcode
```
4. Build using `ninja` using one of these commands:
```shell
# make a testing build
$ yarn ninja testing
# make a release build
$ yarn ninja release
```
### Docker
We use `docker run` instead of `Dockerfile` for building Chromium to support incremental building.
```shell
# Create the build environment
$ docker build . --build-arg "WORKDIR=$(pwd)" --target build-env --tag html2svg-build-env
# Clone the Chromium/Electron code
$ docker run -ti -v $(pwd):$(pwd) html2svg-build-env scripts/gclient.sh --revision "src/electron@cb22573c3e76e09df9fbad36dc372080c04d349e"
# Apply html2svg patches
$ docker run -ti -v $(pwd):$(pwd) html2svg-build-env scripts/patch.sh
# Install build dependencies
$ docker run -ti -v $(pwd):$(pwd) html2svg-build-env electron/src/build/install-build-deps.sh
```console
$ cd chromium
$ gclient sync
```
Now you'll have to build binaries, steps differs depending on the platform you'd like to target:
- AMD64:
```shell
# Fetch compiler files
$ docker run -ti -v $(pwd):$(pwd) html2svg-build-env electron/src/build/linux/sysroot_scripts/install-sysroot.py --arch=amd64
# Generate build files
$ docker run -ti -v $(pwd):$(pwd) --workdir $(pwd)/electron/src html2svg-build-env gn gen "out/release-amd64" --args="import(\"//electron/build/args/release.gn\") cc_wrapper=\"ccache\""
# Build binaries
$ docker run -ti -v $(pwd):$(pwd) html2svg-build-env scripts/build.sh release-amd64
```
- ARM64:
```shell
# Fetch compiler files
$ docker run -ti -v $(pwd):$(pwd) html2svg-build-env electron/src/build/linux/sysroot_scripts/install-sysroot.py --arch=arm64
# Generate build files
$ docker run -ti -v $(pwd):$(pwd) --workdir $(pwd)/electron/src html2svg-build-env gn gen "out/release-arm64" --args="import(\"//electron/build/args/release.gn\") cc_wrapper=\"ccache\" target_cpu=\"arm64\""
# Build binaries
$ docker run -ti -v $(pwd):$(pwd) html2svg-build-env scripts/build.sh release-arm64 --target-cpu=arm64
```
### Configure
Finally, build the Docker image:
```shell
docker build .
> You need to disable `lld` on macOS because of a linking bug related to Rust and `compact_unwind`
```console
$ cd chromium/src
$ gn gen out/Default
```
### Build
```console
$ cd chromium/src
$ ninja -C out/Default headless:headless_shell
```

View File

@ -5,4 +5,4 @@ set -e
export DISPLAY=:99
Xvfb $DISPLAY -screen 0 1920x1080x24 &
node /app/build/html2svg.cli.js "$@"
node /app/build/carbonyl.cli.js "$@"

3
src/browser.rs Normal file
View File

@ -0,0 +1,3 @@
mod ffi;
pub use ffi::*;

189
src/browser/ffi.rs Normal file
View File

@ -0,0 +1,189 @@
use std::ffi::CStr;
use std::io::{stderr, Write};
use std::process::{self, Command, Stdio};
use std::{env, io};
use libc::{c_char, c_int, c_uchar, c_uint, size_t};
use crate::gfx::{Color, Point, Rect, Size};
use crate::terminal::output::Renderer;
use crate::terminal::{input, output};
/// This file bridges the C++ code with Rust.
/// "C-unwind" combined with .unwrap() is used to allow catching Rust panics
/// using C++ exception handling.
#[repr(C)]
pub struct CSize {
width: c_uint,
height: c_uint,
}
#[repr(C)]
pub struct CPoint {
x: c_uint,
y: c_uint,
}
#[repr(C)]
pub struct CRect {
origin: CPoint,
size: CSize,
}
#[repr(C)]
pub struct CColor {
r: u8,
g: u8,
b: u8,
}
#[repr(C)]
pub struct BrowserDelegate {
shutdown: extern "C" fn(),
scroll: extern "C" fn(c_int),
key_press: extern "C" fn(c_char),
mouse_up: extern "C" fn(c_uint, c_uint),
mouse_down: extern "C" fn(c_uint, c_uint),
mouse_move: extern "C" fn(c_uint, c_uint),
}
fn main() -> io::Result<()> {
const CARBONYL_INSIDE_SHELL: &str = "CARBONYL_INSIDE_SHELL";
if env::vars().find(|(key, value)| key == CARBONYL_INSIDE_SHELL && value == "1") != None {
return Ok(());
}
input::setup()?;
Renderer::setup()?;
let output = Command::new(env::current_exe()?)
.args(env::args().skip(1))
.arg("--disable-threaded-scrolling")
.arg("--disable-threaded-animation")
.env(CARBONYL_INSIDE_SHELL, "1")
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::piped())
.output()?;
Renderer::teardown()?;
stderr().write_all(&output.stderr)?;
if let Some(code) = output.status.code() {
process::exit(code);
} else {
process::exit(127);
}
}
#[no_mangle]
pub extern "C-unwind" fn carbonyl_shell_main() {
main().unwrap()
}
#[no_mangle]
pub extern "C-unwind" fn carbonyl_renderer_create() -> *mut Renderer {
let mut renderer = Box::new(Renderer::new());
let src = output::size().unwrap();
renderer.set_size(Size::new(7, 14), src);
Box::into_raw(renderer)
}
#[no_mangle]
pub extern "C-unwind" fn carbonyl_renderer_clear_text(renderer: *mut Renderer) {
let renderer = unsafe { &mut *renderer };
renderer.clear_text()
}
#[no_mangle]
pub extern "C-unwind" fn carbonyl_renderer_draw_text(
renderer: *mut Renderer,
utf8: *const c_char,
rect: *const CRect,
color: *const CColor,
) {
let (renderer, string, rect, color) =
unsafe { (&mut *renderer, CStr::from_ptr(utf8), &*rect, &*color) };
renderer.draw_text(
string.to_str().unwrap(),
Point::new(rect.origin.x as i32, rect.origin.y as i32),
Size::new(rect.size.width as u32, rect.size.height as u32),
Color::new(color.r, color.g, color.b),
)
}
#[no_mangle]
pub extern "C-unwind" fn carbonyl_renderer_draw_background(
renderer: *mut Renderer,
pixels: *mut c_uchar,
pixels_size: size_t,
rect: *const CRect,
) {
let (renderer, pixels, rect) = unsafe {
(
&mut *renderer,
std::slice::from_raw_parts_mut(pixels, pixels_size),
&*rect,
)
};
renderer
.draw_background(
pixels,
Rect {
origin: Point::new(rect.origin.x as i32, rect.origin.y as i32),
size: Size::new(rect.size.width, rect.size.height),
},
)
.unwrap()
}
#[no_mangle]
pub extern "C-unwind" fn carbonyl_output_get_size(size: *mut CSize) {
let dst = unsafe { &mut *size };
let src = output::size().unwrap().cast::<c_uint>();
dst.width = src.width * 7;
dst.height = src.height * 14;
}
/// Function called by the C++ code to listen for input events.
///
/// This will block so the calling code should start and own a dedicated thread.
/// It will panic if there is any error.
#[no_mangle]
pub extern "C-unwind" fn carbonyl_input_listen(delegate: *mut BrowserDelegate) {
let char_width = 7;
let char_height = 14;
let BrowserDelegate {
shutdown,
scroll,
key_press,
mouse_up,
mouse_down,
mouse_move,
} = unsafe { &*delegate };
input::listen(|event| {
match event {
input::Event::Exit => return Some(shutdown()),
input::Event::KeyPress { key } => key_press(key as c_char),
input::Event::Scroll { delta } => scroll(delta as c_int * char_height as c_int),
input::Event::MouseUp { col, row } => {
mouse_up(col as c_uint * char_width, row as c_uint * char_height)
}
input::Event::MouseDown { col, row } => {
mouse_down(col as c_uint * char_width, row as c_uint * char_height)
}
input::Event::MouseMove { col, row } => {
mouse_move(col as c_uint * char_width, row as c_uint * char_height)
}
}
None
})
.unwrap()
}

View File

@ -0,0 +1,64 @@
// Copyright (c) 2019 GitHub, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "headless/lib/browser/headless_host_display_client.h"
#include <utility>
#include "components/viz/common/resources/resource_format.h"
#include "components/viz/common/resources/resource_sizes.h"
#include "mojo/public/cpp/system/platform_handle.h"
#include "skia/ext/platform_canvas.h"
#include "third_party/skia/include/core/SkColor.h"
#include "third_party/skia/include/core/SkRect.h"
#include "third_party/skia/src/core/SkDevice.h"
#include "ui/gfx/skia_util.h"
#if BUILDFLAG(IS_WIN)
#include "skia/ext/skia_utils_win.h"
#endif
#include "headless/app/carbonyl_rust_bridge.h"
namespace carbonyl {
LayeredWindowUpdater::LayeredWindowUpdater(
mojo::PendingReceiver<viz::mojom::LayeredWindowUpdater> receiver)
: receiver_(this, std::move(receiver)) {}
LayeredWindowUpdater::~LayeredWindowUpdater() = default;
void LayeredWindowUpdater::OnAllocatedSharedMemory(
const gfx::Size& pixel_size,
base::UnsafeSharedMemoryRegion region) {
if (region.IsValid())
shm_mapping_ = region.Map();
}
void LayeredWindowUpdater::Draw(const gfx::Rect& damage_rect,
DrawCallback draw_callback) {
Renderer::Main()->DrawBackgrond(
shm_mapping_.GetMemoryAs<uint8_t>(),
shm_mapping_.size()
);
std::move(draw_callback).Run();
}
HostDisplayClient::HostDisplayClient()
: viz::HostDisplayClient(gfx::kNullAcceleratedWidget) {}
HostDisplayClient::~HostDisplayClient() = default;
void HostDisplayClient::CreateLayeredWindowUpdater(
mojo::PendingReceiver<viz::mojom::LayeredWindowUpdater> receiver) {
layered_window_updater_ =
std::make_unique<LayeredWindowUpdater>(std::move(receiver));
}
#if BUILDFLAG(IS_LINUX) && !BUILDFLAG(IS_CHROMEOS)
void HostDisplayClient::DidCompleteSwapWithNewSize(
const gfx::Size& size) {}
#endif
} // namespace carbonyl

View File

@ -0,0 +1,72 @@
// Copyright (c) 2019 GitHub, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef HEADLESS_LIB_BROWSER_HEADLESS_FOCUS_HOST_DISPLAY_CLIENT_H_
#define HEADLESS_LIB_BROWSER_HEADLESS_FOCUS_HOST_DISPLAY_CLIENT_H_
#include <memory>
#include "base/callback.h"
#include "base/memory/shared_memory_mapping.h"
#include "components/viz/host/host_display_client.h"
#include "services/viz/privileged/mojom/compositing/layered_window_updater.mojom.h"
#include "ui/gfx/native_widget_types.h"
namespace carbonyl {
typedef base::RepeatingCallback<void(const gfx::Rect&, const SkBitmap&)>
OnPaintCallback;
class LayeredWindowUpdater : public viz::mojom::LayeredWindowUpdater {
public:
explicit LayeredWindowUpdater(
mojo::PendingReceiver<viz::mojom::LayeredWindowUpdater> receiver);
~LayeredWindowUpdater() override;
// disable copy
LayeredWindowUpdater(const LayeredWindowUpdater&) = delete;
LayeredWindowUpdater& operator=(const LayeredWindowUpdater&) = delete;
// viz::mojom::LayeredWindowUpdater implementation.
void OnAllocatedSharedMemory(const gfx::Size& pixel_size,
base::UnsafeSharedMemoryRegion region) override;
void Draw(const gfx::Rect& damage_rect, DrawCallback draw_callback) override;
private:
mojo::Receiver<viz::mojom::LayeredWindowUpdater> receiver_;
base::WritableSharedMemoryMapping shm_mapping_;
};
class HostDisplayClient : public viz::HostDisplayClient {
public:
explicit HostDisplayClient();
~HostDisplayClient() override;
// disable copy
HostDisplayClient(const HostDisplayClient&) = delete;
HostDisplayClient& operator=(const HostDisplayClient&) =
delete;
private:
#if BUILDFLAG(IS_MAC)
void OnDisplayReceivedCALayerParams(
const gfx::CALayerParams& ca_layer_params) override;
#endif
void CreateLayeredWindowUpdater(
mojo::PendingReceiver<viz::mojom::LayeredWindowUpdater> receiver)
override;
#if BUILDFLAG(IS_LINUX) && !BUILDFLAG(IS_CHROMEOS)
void DidCompleteSwapWithNewSize(const gfx::Size& size) override;
#endif
std::unique_ptr<LayeredWindowUpdater> layered_window_updater_;
OnPaintCallback callback_;
bool active_ = false;
};
} // namespace carbonyl
#endif // HEADLESS_LIB_BROWSER_HEADLESS_FOCUS_HOST_DISPLAY_CLIENT_H_

File diff suppressed because it is too large Load Diff

11
src/gfx.rs Normal file
View File

@ -0,0 +1,11 @@
mod color;
mod point;
mod rect;
mod size;
mod vector;
pub use color::*;
pub use point::*;
pub use rect::*;
pub use size::*;
pub use vector::*;

26
src/gfx/color.rs Normal file
View File

@ -0,0 +1,26 @@
use super::Vector3;
use crate::impl_vector_overload;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Color<T: Copy = u8> {
pub r: T,
pub g: T,
pub b: T,
}
impl Color {
pub fn from_iter<'a, T>(iter: &mut T) -> Option<Color>
where
T: Iterator<Item = &'a u8>,
{
let (b, g, r, _) = (iter.next(), iter.next(), iter.next(), iter.next());
Some(Color::<u8>::new(*r?, *g?, *b?))
}
pub fn black() -> Color {
Color::<u8>::new(0, 0, 0)
}
}
impl_vector_overload!(Color r g b);

10
src/gfx/point.rs Normal file
View File

@ -0,0 +1,10 @@
use super::Vector2;
use crate::impl_vector_overload;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Point<T: Copy = i32> {
pub x: T,
pub y: T,
}
impl_vector_overload!(Point x y);

7
src/gfx/rect.rs Normal file
View File

@ -0,0 +1,7 @@
use super::{Point, Size};
// #[derive(Clone, Copy, Debug, PartialEq)]
pub struct Rect<P: Copy = i32, S: Copy = u32> {
pub origin: Point<P>,
pub size: Size<S>,
}

10
src/gfx/size.rs Normal file
View File

@ -0,0 +1,10 @@
use super::Vector2;
use crate::impl_vector_overload;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Size<T: Copy = u32> {
pub width: T,
pub height: T,
}
impl_vector_overload!(Size width height);

381
src/gfx/vector.rs Normal file
View File

@ -0,0 +1,381 @@
pub trait Vector2<T>
where
T: Copy,
{
fn x(&self) -> T;
fn y(&self) -> T;
}
pub trait Vector3<T>
where
T: Copy,
{
fn x(&self) -> T;
fn y(&self) -> T;
fn z(&self) -> T;
}
pub trait Cast<T> {
fn cast(self) -> T;
}
pub trait ToIntUnchecked<T> {
unsafe fn to_int_unchecked(self) -> T;
}
macro_rules! impl_cast_trait {
() => {
impl_cast_trait!(u8 as int);
impl_cast_trait!(i8 as int);
impl_cast_trait!(u16 as int);
impl_cast_trait!(i16 as int);
impl_cast_trait!(u32 as int);
impl_cast_trait!(i32 as int);
impl_cast_trait!(u64 as int);
impl_cast_trait!(i64 as int);
impl_cast_trait!(f32 as float);
impl_cast_trait!(f64 as float);
impl_cast_trait!(usize as int);
impl_cast_trait!(isize as int);
};
($from:ty as int) => {
impl_cast_trait!($from);
impl ToIntUnchecked<$from> for f32 {
unsafe fn to_int_unchecked(self) -> $from {
f32::to_int_unchecked(self)
}
}
impl ToIntUnchecked<$from> for f64 {
unsafe fn to_int_unchecked(self) -> $from {
f64::to_int_unchecked(self)
}
}
};
($from:ty as float) => {
impl_cast_trait!($from);
};
($from:ty) => {
impl_cast_trait!($from => u8);
impl_cast_trait!($from => i8);
impl_cast_trait!($from => u16);
impl_cast_trait!($from => i16);
impl_cast_trait!($from => u32);
impl_cast_trait!($from => i32);
impl_cast_trait!($from => u64);
impl_cast_trait!($from => i64);
impl_cast_trait!($from => f32);
impl_cast_trait!($from => f64);
impl_cast_trait!($from => usize);
impl_cast_trait!($from => isize);
};
($from:ty => $to:ty) => {
impl Cast<$to> for $from {
fn cast(self) -> $to {
self as $to
}
}
};
}
impl_cast_trait!();
#[macro_export]
macro_rules! impl_vector_overload {
($struct:ident $x:ident $y:ident) => (
impl<T: Copy> $struct<T> {
pub const fn new($x: T, $y: T) -> $struct<T> {
$struct { $x, $y }
}
pub const fn splat(value: T) -> Self {
Self::new(value, value)
}
pub const fn to_array(&self) -> [T; 2] {
[self.$x, self.$y]
}
pub fn iter(&self) -> std::array::IntoIter<T, 2> {
self.to_array().into_iter()
}
}
impl<T: Copy> Vector2<T> for $struct<T> {
fn x(&self) -> T {
self.$x
}
fn y(&self) -> T {
self.$y
}
}
impl<T: Copy> std::iter::FromIterator<T> for $struct<T> {
fn from_iter<I>(iter: I) -> $struct<T>
where
I: IntoIterator<Item = T>
{
let mut iter = iter.into_iter();
let expect = "initialized a vector with a small iter";
Self::new(
iter.next().expect(expect),
iter.next().expect(expect)
)
}
}
impl<T: Copy> From<T> for $struct<T> {
fn from(value: T) -> Self {
Self::new(value, value)
}
}
impl<T: Copy> From<(T, T)> for $struct<T> {
fn from((x, y): (T, T)) -> Self {
Self::new(x, y)
}
}
impl<T: Copy> From<[T; 2]> for $struct<T> {
fn from(array: [T; 2]) -> Self {
Self::new(array[0], array[1])
}
}
crate::impl_vector_traits!($struct Vector2);
);
($struct:ident $x:ident $y:ident $z:ident) => (
impl<T: Copy> $struct<T> {
pub const fn new($x: T, $y: T, $z: T) -> $struct<T> {
$struct { $x, $y, $z }
}
pub const fn splat(value: T) -> Self {
Self::new(value, value, value)
}
pub const fn to_array(&self) -> [T; 3] {
[self.$x, self.$y, self.$z]
}
pub fn iter(&self) -> std::array::IntoIter<T, 3> {
self.to_array().into_iter()
}
}
impl<T: Copy> Vector3<T> for $struct<T> {
fn x(&self) -> T {
self.$x
}
fn y(&self) -> T {
self.$y
}
fn z(&self) -> T {
self.$z
}
}
impl<T: Copy> std::iter::FromIterator<T> for $struct<T> {
fn from_iter<I>(iter: I) -> $struct<T>
where
I: IntoIterator<Item = T>
{
let mut iter = iter.into_iter();
let expect = "initialized a vector with a small iter";
Self::new(
iter.next().expect(expect),
iter.next().expect(expect),
iter.next().expect(expect)
)
}
}
impl<T: Copy> From<T> for $struct<T> {
fn from(value: T) -> Self {
Self::new(value, value, value)
}
}
impl<T: Copy> From<(T, T, T)> for $struct<T> {
fn from((x, y, z): (T, T, T)) -> Self {
Self::new(x, y, z)
}
}
impl<T: Copy> From<[T; 3]> for $struct<T> {
fn from(array: [T; 3]) -> Self {
Self::new(array[0], array[1], array[2])
}
}
crate::impl_vector_traits!($struct Vector3);
);
}
#[macro_export]
macro_rules! impl_vector_traits {
($struct:ident $vector:ident) => {
impl<T: Copy> $struct<T> {
pub fn dot<U>(&self, rhs: U) -> T
where
U: Into<$struct<T>>,
T: std::ops::Mul<T, Output = T> + std::iter::Sum,
{
(self * rhs.into()).sum()
}
pub fn sum(&self) -> T
where
T: std::iter::Sum,
{
self.iter().sum::<T>()
}
pub fn cast<U>(&self) -> $struct<U>
where
T: super::Cast<U>,
U: Copy
{
self.map(|v| v.cast())
}
pub fn map<U, F>(&self, f: F) -> $struct<U>
where
U: Copy,
F: FnMut(T) -> U
{
self.iter().map(f).collect()
}
}
crate::impl_vector_traits!($struct $vector i8);
crate::impl_vector_traits!($struct $vector u8);
crate::impl_vector_traits!($struct $vector i16);
crate::impl_vector_traits!($struct $vector u16);
crate::impl_vector_traits!($struct $vector i32);
crate::impl_vector_traits!($struct $vector u32);
crate::impl_vector_traits!($struct $vector i64);
crate::impl_vector_traits!($struct $vector u64);
crate::impl_vector_traits!($struct $vector isize);
crate::impl_vector_traits!($struct $vector usize);
crate::impl_vector_traits!($struct $vector f32 float);
crate::impl_vector_traits!($struct $vector f64 float);
crate::impl_vector_traits!($struct $vector Add add);
crate::impl_vector_traits!($struct $vector Sub sub);
crate::impl_vector_traits!($struct $vector Mul mul);
crate::impl_vector_traits!($struct $vector Div div);
crate::impl_vector_traits!($struct $vector BitOr bitor);
crate::impl_vector_traits!($struct $vector BitXor bitxor);
crate::impl_vector_traits!($struct $vector BitAnd bitand);
};
($struct:ident $vector:ident $type:ident) => (
impl $struct<$type> {
pub fn avg_with<T>(&self, rhs: T) -> Self
where
T: Into<$struct<$type>>
{
let rhs = rhs.into();
(self & rhs) + (self ^ rhs) / 2
}
}
);
($struct:ident $vector:ident $type:ident float) => (
impl $struct<$type> {
pub unsafe fn to_int_unchecked<U>(&self) -> $struct<U>
where
$type: super::ToIntUnchecked<U>,
U: Copy
{
self.map(|x| <$type as super::ToIntUnchecked<U>>::to_int_unchecked(x))
}
pub fn mul_add<M, A>(&self, mul: M, add: A) -> Self
where
M: Into<$struct<$type>>,
A: Into<$struct<$type>>,
{
self.iter()
.zip(mul.into().iter())
.zip(add.into().iter())
.map(|((x, y), z)| x.mul_add(y, z))
.collect()
}
pub fn round(&self) -> Self {
self.map(|v| v.round())
}
pub fn min<U>(&self, min: U) -> Self
where
U: Into<$struct<$type>>
{
self.iter()
.zip(min.into().iter())
.map(|(x, y)| x.min(y))
.collect()
}
pub fn max<U>(&self, max: U) -> Self
where
U: Into<$struct<$type>>
{
self.iter()
.zip(max.into().iter())
.map(|(x, y)| x.max(y))
.collect()
}
pub fn clamp<U>(&self, min: U, max: U) -> Self
where
U: Into<$struct<$type>>
{
self.iter()
.zip(min.into().iter())
.zip(max.into().iter())
.map(|((x, y), z)| x.clamp(y, z))
.collect()
}
}
);
($struct:ident $vector:ident $trait:ident $name:ident) => {
impl<T: Copy> $struct<T> {
pub fn $name<U>(&self, rhs: U) -> Self
where
T: std::ops::$trait<T, Output = T>,
U: Copy + Into<$struct<T>>
{
self.iter()
.zip(rhs.into().iter())
.map(|(x, y)| x.$name(y))
.collect()
}
}
impl<T, U> std::ops::$trait<U> for $struct<T>
where
T: Copy + std::ops::$trait<T, Output = T>,
U: Copy + Into<$struct<T>>,
{
type Output = $struct<T>;
fn $name(self, rhs: U) -> Self::Output {
$struct::$name(&self, rhs)
}
}
impl<'a, T, U> std::ops::$trait<U> for &'a $struct<T>
where
T: Copy + std::ops::$trait<T, Output = T>,
U: Copy + Into<$struct<T>>,
{
type Output = $struct<T>;
fn $name(self, rhs: U) -> Self::Output {
$struct::$name(&self, rhs)
}
}
};
}

477
src/gfx/vector.simd.rs Normal file
View File

@ -0,0 +1,477 @@
use std::simd::{self};
pub trait Vector2<T>
where
T: Copy,
{
fn x(&self) -> T;
fn y(&self) -> T;
}
pub trait Vector3<T>
where
T: Copy,
{
fn x(&self) -> T;
fn y(&self) -> T;
fn z(&self) -> T;
}
pub trait VectorElement {
type Vector2;
type Vector3;
}
impl VectorElement for u8 {
type Vector2 = simd::u8x4;
type Vector3 = simd::u8x4;
}
impl VectorElement for i8 {
type Vector2 = simd::i8x4;
type Vector3 = simd::i8x4;
}
impl VectorElement for u16 {
type Vector2 = simd::u16x2;
type Vector3 = simd::u16x4;
}
impl VectorElement for i16 {
type Vector2 = simd::i16x2;
type Vector3 = simd::i16x4;
}
impl VectorElement for u32 {
type Vector2 = simd::u32x2;
type Vector3 = simd::u32x4;
}
impl VectorElement for i32 {
type Vector2 = simd::i32x2;
type Vector3 = simd::i32x4;
}
impl VectorElement for u64 {
type Vector2 = simd::u64x2;
type Vector3 = simd::u64x4;
}
impl VectorElement for i64 {
type Vector2 = simd::i64x2;
type Vector3 = simd::i64x4;
}
impl VectorElement for f32 {
type Vector2 = simd::f32x2;
type Vector3 = simd::f32x4;
}
impl VectorElement for f64 {
type Vector2 = simd::f64x2;
type Vector3 = simd::f64x4;
}
impl VectorElement for usize {
type Vector2 = simd::usizex2;
type Vector3 = simd::usizex4;
}
impl VectorElement for isize {
type Vector2 = simd::isizex2;
type Vector3 = simd::isizex4;
}
pub trait CreateVector2<T> {
fn create(a: T, b: T) -> Self;
}
pub trait CreateVector3<T> {
fn create(a: T, b: T, z: T) -> Self;
}
#[macro_export]
macro_rules! impl_vector_overload {
($struct:ident $x:ident $y:ident = $default:ty) => (
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct $struct<T: Copy + super::VectorElement = $default> {
simd: T::Vector2
}
impl<T: Copy + super::VectorElement> $struct<T> {
pub fn new($x: T, $y: T) -> $struct<T>
where
$struct<T>: super::Vector2<T>,
$struct<T>: super::CreateVector2<T>,
{
<$struct<T> as super::CreateVector2<T>>::create($x, $y)
}
}
crate::impl_simd2_overload!($struct $x $y u8 4);
crate::impl_simd2_overload!($struct $x $y i8 4);
crate::impl_simd2_overload!($struct $x $y u16 2);
crate::impl_simd2_overload!($struct $x $y i16 2);
crate::impl_simd2_overload!($struct $x $y u32 2);
crate::impl_simd2_overload!($struct $x $y i32 2);
crate::impl_simd2_overload!($struct $x $y u64 2);
crate::impl_simd2_overload!($struct $x $y i64 2);
crate::impl_simd2_overload!($struct $x $y f32 2);
crate::impl_simd2_overload!($struct $x $y f64 2);
crate::impl_simd2_overload!($struct $x $y usize 2);
crate::impl_simd2_overload!($struct $x $y isize 2);
crate::impl_vector_traits!($struct Vector2);
);
($struct:ident $x:ident $y:ident $z:ident = $default:ty) => (
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct $struct<T: Copy = $default>
where
T: super::VectorElement
{
simd: T::Vector3
}
impl<T: Copy + super::VectorElement> $struct<T> {
pub fn new($x: T, $y: T, $z: T) -> $struct<T>
where
$struct<T>: super::Vector3<T>,
$struct<T>: super::CreateVector3<T>,
{
<$struct<T> as super::CreateVector3<T>>::create($x, $y, $z)
}
}
crate::impl_simd3_overload!($struct $x $y $z u8 4);
crate::impl_simd3_overload!($struct $x $y $z i8 4);
crate::impl_simd3_overload!($struct $x $y $z u16 4);
crate::impl_simd3_overload!($struct $x $y $z i16 4);
crate::impl_simd3_overload!($struct $x $y $z u32 4);
crate::impl_simd3_overload!($struct $x $y $z i32 4);
crate::impl_simd3_overload!($struct $x $y $z u64 4);
crate::impl_simd3_overload!($struct $x $y $z i64 4);
crate::impl_simd3_overload!($struct $x $y $z f32 4);
crate::impl_simd3_overload!($struct $x $y $z f64 4);
crate::impl_simd3_overload!($struct $x $y $z usize 4);
crate::impl_simd3_overload!($struct $x $y $z isize 4);
crate::impl_vector_traits!($struct Vector3);
);
}
#[macro_export]
macro_rules! impl_vector_traits {
($struct:ident $vector:ident) => {
crate::impl_vector_traits!($struct $vector i8);
crate::impl_vector_traits!($struct $vector u8);
crate::impl_vector_traits!($struct $vector i16);
crate::impl_vector_traits!($struct $vector u16);
crate::impl_vector_traits!($struct $vector i32);
crate::impl_vector_traits!($struct $vector u32);
crate::impl_vector_traits!($struct $vector i64);
crate::impl_vector_traits!($struct $vector u64);
crate::impl_vector_traits!($struct $vector isize);
crate::impl_vector_traits!($struct $vector usize);
crate::impl_vector_traits!($struct $vector f32 float);
crate::impl_vector_traits!($struct $vector f64 float);
};
($struct:ident $vector:ident $type:ident) => (
crate::impl_vector_traits!($struct $vector $type ops);
crate::impl_vector_traits!($struct $vector BitOr bitor $type);
crate::impl_vector_traits!($struct $vector BitXor bitxor $type);
crate::impl_vector_traits!($struct $vector BitAnd bitand $type);
impl $struct<$type>
where
$type: super::VectorElement
{
pub fn avg_with<T>(self, rhs: T) -> Self
where
T: Into<$struct<$type>>
{
let rhs = rhs.into();
(self & rhs) + (self ^ rhs) / 2
}
}
);
($struct:ident $vector:ident $type:ident float) => (
crate::impl_vector_traits!($struct $vector $type ops);
impl $struct<$type>
where
$type: super::VectorElement
{
pub fn mul_add<A, B>(self, a: A, b: B) -> Self
where
A: Into<$struct<$type>>,
B: Into<$struct<$type>>,
{
std::simd::StdFloat::mul_add(
self.to_simd(),
a.into().to_simd(),
b.into().to_simd(),
).into()
}
pub fn round(&self) -> Self {
std::simd::StdFloat::round(
self.to_simd()
).into()
}
pub fn min<T>(&self, min: T) -> Self
where
T: Into<$struct<$type>>
{
std::simd::SimdFloat::simd_min(
self.to_simd(),
min.into().to_simd()
).into()
}
pub fn max<T>(&self, max: T) -> Self
where
T: Into<$struct<$type>>
{
std::simd::SimdFloat::simd_max(
self.to_simd(),
max.into().to_simd()
).into()
}
pub fn clamp<T>(&self, min: T, max: T) -> Self
where
T: Into<$struct<$type>>
{
std::simd::SimdFloat::simd_clamp(
self.to_simd(),
min.into().to_simd(),
max.into().to_simd()
).into()
}
}
);
($struct:ident $vector:ident $type:ident ops) => (
crate::impl_vector_traits!($struct $vector Add add $type);
crate::impl_vector_traits!($struct $vector Sub sub $type);
crate::impl_vector_traits!($struct $vector Mul mul $type);
crate::impl_vector_traits!($struct $vector Div div $type);
impl $struct<$type>
where
$type: super::VectorElement
{
pub fn dot<T>(self, rhs: T) -> $type
where
T: Into<$struct<$type>>
{
(self * rhs).sum()
}
}
);
($struct:ident $vector:ident $trait:ident $name:ident $type:ident) => {
impl<T> std::ops::$trait<T> for $struct<$type>
where
T: Into<$struct<$type>>
{
type Output = $struct<$type>;
fn $name(self, rhs: T) -> Self::Output {
self.to_simd().$name(rhs.into().to_simd()).into()
}
}
};
}
#[macro_export]
macro_rules! impl_simd2_overload {
($struct:ident $x:ident $y:ident $type:ident $lanes:expr) => {
impl super::CreateVector2<$type> for $struct<$type> {
fn create($x: $type, $y: $type) -> $struct<$type> {
$struct::<$type>::create($x, $y)
}
}
impl $struct<$type> {
pub const fn create($x: $type, $y: $type) -> $struct<$type> {
let mut array = [1 as $type; $lanes];
array[0] = $x;
array[1] = $y;
$struct {
simd: std::simd::Simd::from_array(array),
}
}
pub const fn splat(value: $type) -> Self {
Self::create(value, value)
}
pub const fn $x(self) -> $type {
self.simd.as_array()[0]
}
pub const fn $y(self) -> $type {
self.simd.as_array()[1]
}
pub fn sum(self) -> $type
where
$type: std::ops::Add<$type, Output = $type>,
{
self.$x() + self.$y()
}
}
impl Vector2<$type> for $struct<$type> {
fn x(&self) -> $type {
$struct::<$type>::$x(*self)
}
fn y(&self) -> $type {
$struct::<$type>::$y(*self)
}
}
impl From<$type> for $struct<$type> {
fn from(value: $type) -> $struct<$type> {
$struct::<$type>::create(value, value)
}
}
impl From<[$type; $lanes]> for $struct<$type> {
fn from(array: [$type; $lanes]) -> $struct<$type> {
$struct::<$type>::create(array[0], array[1])
}
}
impl From<($type, $type)> for $struct<$type> {
fn from(tuple: ($type, $type)) -> $struct<$type> {
$struct::<$type>::create(tuple.0, tuple.1)
}
}
impl Into<std::simd::Simd<$type, $lanes>> for $struct<$type> {
fn into(self) -> std::simd::Simd<$type, $lanes> {
self.to_simd()
}
}
impl Into<$struct<$type>> for std::simd::Simd<$type, $lanes> {
fn into(self) -> $struct<$type> {
$struct::<$type>::from_simd(self)
}
}
impl $struct<$type> {
pub const fn from_simd(simd: std::simd::Simd<$type, $lanes>) -> Self {
$struct { simd }
}
pub const fn to_simd(&self) -> std::simd::Simd<$type, $lanes> {
self.simd
}
pub fn cast<U>(&self) -> $struct<U>
where
U: std::simd::SimdElement + super::VectorElement,
std::simd::Simd<U, $lanes>: Into<$struct<U>>,
{
self.to_simd().cast().into()
}
}
};
}
#[macro_export]
macro_rules! impl_simd3_overload {
($struct:ident $x:ident $y:ident $z:ident $type:ident $lanes:expr) => {
impl super::CreateVector3<$type> for $struct<$type> {
fn create($x: $type, $y: $type, $z: $type) -> $struct<$type> {
$struct::<$type>::create($x, $y, $z)
}
}
impl $struct<$type> {
pub const fn create($x: $type, $y: $type, $z: $type) -> $struct<$type> {
let mut array = [1 as $type; $lanes];
array[0] = $x;
array[1] = $y;
array[2] = $z;
$struct {
simd: std::simd::Simd::from_array(array),
}
}
pub const fn splat(value: $type) -> Self {
Self::create(value, value, value)
}
pub const fn $x(self) -> $type {
self.simd.as_array()[0]
}
pub const fn $y(self) -> $type {
self.simd.as_array()[1]
}
pub const fn $z(self) -> $type {
self.simd.as_array()[2]
}
pub fn sum(self) -> $type
where
$type: std::ops::Add<$type, Output = $type>,
{
self.$x() + self.$y() + self.$z()
}
}
impl Vector3<$type> for $struct<$type> {
fn x(&self) -> $type {
self.$x()
}
fn y(&self) -> $type {
self.$y()
}
fn z(&self) -> $type {
self.$z()
}
}
impl From<$type> for $struct<$type> {
fn from(value: $type) -> $struct<$type> {
$struct::<$type>::create(value, value, value)
}
}
impl From<[$type; $lanes]> for $struct<$type> {
fn from(array: [$type; $lanes]) -> $struct<$type> {
$struct::<$type>::create(array[0], array[1], array[2])
}
}
impl From<($type, $type, $type)> for $struct<$type> {
fn from(tuple: ($type, $type, $type)) -> $struct<$type> {
$struct::<$type>::create(tuple.0, tuple.1, tuple.2)
}
}
impl Into<std::simd::Simd<$type, $lanes>> for $struct<$type> {
fn into(self) -> std::simd::Simd<$type, $lanes> {
self.to_simd()
}
}
impl Into<$struct<$type>> for std::simd::Simd<$type, $lanes> {
fn into(self) -> $struct<$type> {
$struct::<$type>::from_simd(self)
}
}
impl $struct<$type> {
pub const fn from_simd(simd: std::simd::Simd<$type, $lanes>) -> Self {
$struct { simd }
}
pub const fn to_simd(self) -> std::simd::Simd<$type, $lanes> {
self.simd
}
pub fn cast<U>(self) -> $struct<U>
where
U: std::simd::SimdElement + super::VectorElement,
std::simd::Simd<U, $lanes>: Into<$struct<U>>,
{
self.to_simd().cast().into()
}
}
};
}

View File

@ -1,187 +0,0 @@
import { join } from 'path'
import { tmpdir } from 'os'
import { program } from 'commander'
import { pipeline } from 'stream/promises'
import { mkdir, rm } from 'fs/promises'
import { randomBytes } from 'crypto'
import { ListenOptions } from 'net'
import { ChildProcess, spawn } from 'child_process'
import { IncomingMessage, request } from 'http'
import { Options } from './html2svg'
if (require.main === module) {
const entry = process.argv.find((a) => a.endsWith(__filename))
const index = entry ? process.argv.indexOf(entry) : -1
const args = process.argv.slice(Math.max(2, index + 1))
cli(args)
.then(() => process.exit(0))
.catch((error) => {
console.error(error)
process.exit(1)
})
}
export async function cli(args: string[]) {
program
.name('html2svg')
.showHelpAfterError()
.showSuggestionAfterError()
.argument('<url>', 'URL to the web page to render')
.option('-f, --full', 'capture the entire page')
.option(
'-W, --wait <seconds>',
'set the amount of seconds to wait between the page loaded event and taking the screenshot',
validateInt,
1,
)
.option(
'-w, --width <width>',
'set the viewport width in pixels',
validateInt,
1920,
)
.option(
'-h, --height <height>',
'set the viewport height in pixels',
validateInt,
1080,
)
.option(
'-f, --format <format>',
'set the output format, should one of these values: svg, pdf, png, jpg, webp',
'svg',
)
.action(async (url, options) => {
const id = Array.from(randomBytes(16))
.map((x) => x.toString(36).padStart(2, '0'))
.join('')
const dir = join(tmpdir(), 'html2svg-server')
const path = join(dir, `${id}.sock`)
await mkdir(dir, { recursive: true })
try {
const server = serve({ path, log: false })
await Promise.all([
server.wait(),
callServer(url, options, server.process, path),
])
} finally {
await rm(path, { force: true })
}
})
program
.command('serve')
.option(
'-H, --host <hostname>',
'set the hostname to listen on',
'0.0.0.0',
)
.option(
'-p, --port <hostname>',
'set the port to listen on',
validateInt,
8080,
)
.option('-u, --unix <path>', 'set the unix socket to listen on')
.action(
async ({ host, port, unix }) =>
await serve(unix ? { path: unix } : { host, port }).wait(),
)
await program.parseAsync(args, { from: 'user' })
}
async function callServer(
url: string,
options: Options,
server: ChildProcess,
socketPath: string,
) {
const start = Date.now()
while (Date.now() - start < 10_000) {
const done = await new Promise<boolean>((resolve, reject) =>
request({ method: 'POST', socketPath })
.on('error', (error: any) => {
if (error?.code === 'ENOENT') {
resolve(false)
} else {
reject(error)
}
})
.on('response', (res) =>
printRequest(res)
.then(() => resolve(true))
.catch(reject),
)
.end(JSON.stringify({ url, ...options })),
)
if (done) {
return server.kill()
} else {
await sleep(100)
}
}
throw new Error('Timed out waiting for server to start')
}
async function printRequest(res: IncomingMessage) {
if (res.statusCode !== 200) {
throw new Error(`Server error ${res.statusCode}`)
}
await pipeline(res, process.stdout)
}
function validateInt(string: string) {
const number = parseInt(string, 10)
if (Number.isNaN(number)) {
throw new Error(`Invalid number value: ${string}`)
}
return number
}
async function sleep(ms: number) {
await new Promise<void>((resolve) => setTimeout(resolve, ms))
}
function serve(options: ListenOptions & { log?: boolean }) {
const child = spawn(
require.resolve('./runtime/electron'),
['--no-sandbox', require.resolve('./html2svg.server')],
{
stdio: 'inherit',
env: {
...process.env,
HTML2SVG_SERVER_OPTIONS: JSON.stringify(options),
},
},
)
return {
process: child,
async wait() {
await new Promise<void>((resolve, reject) =>
child.on('error', reject).on('close', (code, signal) => {
if (signal) {
reject(new Error(`Server quit with signal ${signal}`))
} else if (code !== 0) {
reject(new Error(`Server quit with code ${code}`))
} else {
resolve()
}
}),
)
},
}
}

View File

@ -1,88 +0,0 @@
import { createServer } from 'http'
import { ListenOptions } from 'net'
import { readStream } from './read-stream'
import { html2svg, Options } from './html2svg'
if (require.main === module) {
const options = JSON.parse(process.env.HTML2SVG_SERVER_OPTIONS ?? '{}')
const { path, host, port, log } = options
server(options)
.then(() => {
if (log !== false) {
process.stderr.write(
`Listening on ${
path ? `unix socket ${path}` : `${host}:${port}`
}\n`,
)
}
})
.catch((error) => {
console.error(error)
process.exit(1)
})
}
export async function server(listen: ListenOptions) {
const server = createServer((req, res) => {
const { url } = req
if (url !== '/') {
return res.writeHead(404).end('Not Found')
}
readStream(req)
.then(async (data) => {
const body = parseOptions(parseJSON(data.toString('utf-8')))
if (!body) {
return res.writeHead(400).end('Invalid request params')
}
const buffer = await html2svg(body.url, body.options)
res.writeHead(200).end(buffer)
})
.catch((error) => {
console.error('Internal server error', error)
res.writeHead(500).end('Internal Server Error')
})
})
await new Promise<void>((resolve, reject) =>
server.on('error', reject).on('listening', resolve).listen(listen),
)
}
function parseOptions(data: any): null | { url: string; options?: Options } {
if (!data) {
return null
}
if (typeof data === 'string') {
return { url: data }
}
if (typeof data !== 'object') {
return null
}
const { url, ...options } = data
if (typeof url !== 'string') {
return null
}
return { url, options }
}
function parseJSON(data: string) {
try {
return JSON.parse(data)
} catch {
return data
}
}

View File

@ -1,129 +0,0 @@
import { app, BrowserWindow } from 'electron'
export interface Options {
full?: boolean
wait?: number
width?: number
height?: number
format?: 'svg' | 'pdf' | 'png' | 'jpg' | 'webp'
}
app.dock?.hide()
app.disableHardwareAcceleration()
app.commandLine.appendSwitch('no-sandbox')
app.on('window-all-closed', () => {})
export async function html2svg(
url: string,
{ full, wait, format, width = 1920, height = 1080 }: Options = {},
) {
const mode = getMode(format ?? 'svg')
await app.whenReady()
const args = [
'--mute-audio',
'--disable-audio-output',
'--disable-dev-shm-usage',
'--force-color-profile=srgb',
]
if (mode === 0) {
args.push('--html2svg-svg-mode', '--disable-remote-fonts')
}
const page = new BrowserWindow({
width,
height,
show: false,
webPreferences: {
sandbox: false,
offscreen: true,
additionalArguments: args,
},
})
try {
await new Promise<void>((resolve, reject) =>
Promise.resolve()
.then(async () => {
const timeout = setTimeout(() => {
page.webContents.off('did-finish-load', listener)
reject(new Error('timeout'))
}, 10_000)
const listener = () => {
clearTimeout(timeout)
resolve()
}
page.webContents.once('did-finish-load', listener)
await page.loadURL(url)
})
.catch(reject),
)
await page.webContents.executeJavaScriptInIsolatedWorld(1, [
{
code: `
new Promise(resolve => {
const style = document.createElement('style')
style.innerHTML = trustedTypes
.createPolicy('html2svg/scrollbar-css', { createHTML: x => x })
.createHTML(\`
*::-webkit-scrollbar,
*::-webkit-scrollbar-track,
*::-webkit-scrollbar-thumb {
display: none;
}
\`)
document.head.appendChild(style)
scrollTo({ top: document.body.scrollHeight })
requestAnimationFrame(() => {
scrollTo({ top: 0 })
requestAnimationFrame(() =>
setTimeout(resolve, ${(wait ?? 0) * 1000})
)
})
})
`,
},
])
const buffer: ArrayBuffer = await page.webContents.executeJavaScript(`
getPageContentsAsSVG(
${full ? 0 : height} * devicePixelRatio,
${mode},
document.title,
)
`)
return Buffer.from(buffer)
} finally {
page.destroy()
}
}
function getMode(format: string) {
switch (format) {
case 'svg':
return 0
case 'pdf':
return 1
case 'png':
return 2
case 'jpg':
case 'jpeg':
return 3
case 'webp':
return 4
default:
throw new Error(`Unsupported output format: ${format}`)
}
}

4
src/lib.rs Normal file
View File

@ -0,0 +1,4 @@
#![feature(c_unwind)]
pub mod browser;
pub mod gfx;
pub mod terminal;

10
src/mojom/BUILD.gn Normal file
View File

@ -0,0 +1,10 @@
import("//mojo/public/tools/bindings/mojom.gni")
mojom("mojom") {
sources = [ "carbonyl.mojom" ]
deps = [
"//ui/gfx/geometry/mojom",
"//skia/public/mojom",
]
}

14
src/mojom/carbonyl.mojom Normal file
View File

@ -0,0 +1,14 @@
module carbonyl.mojom;
import "ui/gfx/geometry/mojom/geometry.mojom";
import "skia/public/mojom/skcolor.mojom";
struct TextData {
string contents;
gfx.mojom.RectF bounds;
skia.mojom.SkColor color;
};
interface CarbonylRenderService {
DrawText(array<TextData> data);
};

View File

@ -1,10 +0,0 @@
export function readStream(stream: NodeJS.ReadableStream) {
const chunks: Buffer[] = []
return new Promise<Buffer>((resolve, reject) =>
stream
.on('data', (chunk) => chunks.push(chunk))
.on('error', (error) => reject(error))
.on('end', () => resolve(Buffer.concat(chunks))),
)
}

View File

@ -1,708 +1,15 @@
diff --git a/src/svg/SkSVGDevice.cpp b/src/svg/SkSVGDevice.cpp
index dcb51de458..9049072b19 100644
--- a/src/svg/SkSVGDevice.cpp
+++ b/src/svg/SkSVGDevice.cpp
@@ -48,6 +48,7 @@
#include "src/core/SkClipStack.h"
#include "src/core/SkDevice.h"
#include "src/core/SkFontPriv.h"
+#include "src/core/SkMaskFilterBase.h"
#include "src/core/SkTLazy.h"
#include "src/image/SkImage_Base.h"
#include "src/shaders/SkShaderBase.h"
@@ -57,6 +58,9 @@
#include <memory>
#include <string>
#include <utility>
+#include <iostream>
+
+#include "src/utils/SkUTF.h"
#if SK_SUPPORT_GPU
class SkMesh;
@@ -185,6 +189,7 @@ struct Resources {
SkString fPaintServer;
SkString fColorFilter;
+ SkString fMaskFilter;
};
// Determine if the paint requires us to reset the viewport.
@@ -241,7 +246,9 @@ public:
, fPathCount(0)
, fImageCount(0)
, fPatternCount(0)
- , fColorFilterCount(0) {}
+ , fColorFilterCount(0)
+ , fGroupCount(0)
+ , fFilterCount(0) {}
SkString addLinearGradient() {
return SkStringPrintf("gradient_%d", fGradientCount++);
@@ -261,12 +268,22 @@ public:
return SkStringPrintf("pattern_%d", fPatternCount++);
}
+ SkString addGroup() {
+ return SkStringPrintf("group_%d", fGroupCount++);
+ }
+
+ SkString addFilter() {
+ return SkStringPrintf("filter_%d", fFilterCount++);
+ }
+
private:
uint32_t fGradientCount;
uint32_t fPathCount;
uint32_t fImageCount;
uint32_t fPatternCount;
uint32_t fColorFilterCount;
+ uint32_t fGroupCount;
+ uint32_t fFilterCount;
};
struct SkSVGDevice::MxCp {
@@ -296,6 +313,14 @@ public:
svgdev->syncClipStack(*mc.fClipStack);
Resources res = this->addResources(mc, paint);
+ if (!svgdev->fClipStack.empty()) {
+ const auto clip_path = SkStringPrintf("url(#cl_%x)", svgdev->fClipStack.back());
+
+ fCloseGroup = true;
+ fWriter->startElement("g");
+ fWriter->addAttribute("clip-path", clip_path.c_str());
+ }
+
fWriter->startElement(name);
this->addPaint(paint, res);
@@ -307,6 +332,10 @@ public:
~AutoElement() {
fWriter->endElement();
+
+ if (fCloseGroup) {
+ fWriter->endElement();
+ }
}
void addAttribute(const char name[], const char val[]) {
@@ -351,6 +380,7 @@ private:
SkXMLWriter* fWriter;
ResourceBucket* fResourceBucket;
+ bool fCloseGroup = false;
};
void SkSVGDevice::AutoElement::addPaint(const SkPaint& paint, const Resources& resources) {
@@ -366,18 +396,25 @@ void SkSVGDevice::AutoElement::addPaint(const SkPaint& paint, const Resources& r
static constexpr char kDefaultFill[] = "black";
if (!resources.fPaintServer.equals(kDefaultFill)) {
this->addAttribute("fill", resources.fPaintServer);
+ }
- if (SK_AlphaOPAQUE != SkColorGetA(paint.getColor())) {
- this->addAttribute("fill-opacity", svg_opacity(paint.getColor()));
- }
+ if (SK_AlphaOPAQUE != SkColorGetA(paint.getColor())) {
+ this->addAttribute("fill-opacity", svg_opacity(paint.getColor()));
}
} else {
SkASSERT(style == SkPaint::kStroke_Style);
this->addAttribute("fill", "none");
}
- if (!resources.fColorFilter.isEmpty()) {
- this->addAttribute("filter", resources.fColorFilter.c_str());
+ if (!resources.fColorFilter.isEmpty() || !resources.fMaskFilter.isEmpty()) {
+ this->addAttribute(
+ "filter",
+ SkStringPrintf(
+ "%s %s",
+ resources.fColorFilter.isEmpty() ? "" : resources.fColorFilter.c_str(),
+ resources.fMaskFilter.isEmpty() ? "" : resources.fMaskFilter.c_str()
+ )
+ );
}
if (style == SkPaint::kStroke_Style || style == SkPaint::kStrokeAndFill_Style) {
@@ -416,16 +453,29 @@ Resources SkSVGDevice::AutoElement::addResources(const MxCp& mc, const SkPaint&
Resources resources(paint);
if (paint.getShader()) {
- AutoElement defs("defs", fWriter);
-
this->addShaderResources(paint, &resources);
}
if (const SkColorFilter* cf = paint.getColorFilter()) {
// TODO: Implement skia color filters for blend modes other than SrcIn
- SkBlendMode mode;
- if (cf->asAColorMode(nullptr, &mode) && mode == SkBlendMode::kSrcIn) {
- this->addColorFilterResources(*cf, &resources);
+ this->addColorFilterResources(*cf, &resources);
+ }
+
+ if (const SkMaskFilter* mf = paint.getMaskFilter()) {
+ SkMaskFilterBase::BlurRec maskBlur;
+
+ if (as_MFB(mf)->asABlur(&maskBlur) && maskBlur.fStyle == kNormal_SkBlurStyle) {
+ SkString maskfilterID = fResourceBucket->addColorFilter();
+
+ AutoElement filterElement("filter", fWriter);
+
+ filterElement.addAttribute("id", maskfilterID);
+
+ AutoElement floodElement("feGaussianBlur", fWriter);
+
+ floodElement.addAttribute("stdDeviation", maskBlur.fSigma);
+
+ resources.fMaskFilter.printf("url(#%s)", maskfilterID.c_str());
}
}
@@ -464,6 +514,14 @@ void SkSVGDevice::AutoElement::addGradientShaderResources(const SkShader* shader
void SkSVGDevice::AutoElement::addColorFilterResources(const SkColorFilter& cf,
Resources* resources) {
SkString colorfilterID = fResourceBucket->addColorFilter();
+ SkColor filterColor;
+ SkBlendMode mode;
+ bool asAColorMode = cf.asAColorMode(&filterColor, &mode);
+
+ if (!asAColorMode) {
+ return;
+ }
+
{
AutoElement filterElement("filter", fWriter);
filterElement.addAttribute("id", colorfilterID);
@@ -472,12 +530,6 @@ void SkSVGDevice::AutoElement::addColorFilterResources(const SkColorFilter& cf,
filterElement.addAttribute("width", "100%");
filterElement.addAttribute("height", "100%");
- SkColor filterColor;
- SkBlendMode mode;
- bool asAColorMode = cf.asAColorMode(&filterColor, &mode);
- SkAssertResult(asAColorMode);
- SkASSERT(mode == SkBlendMode::kSrcIn);
-
{
// first flood with filter color
AutoElement floodElement("feFlood", fWriter);
@@ -490,7 +542,7 @@ void SkSVGDevice::AutoElement::addColorFilterResources(const SkColorFilter& cf,
// apply the transform to filter color
AutoElement compositeElement("feComposite", fWriter);
compositeElement.addAttribute("in", "flood");
- compositeElement.addAttribute("operator", "in");
+ compositeElement.addAttribute("operator", mode == SkBlendMode::kSrcIn ? "atop" : "out");
}
}
resources->fColorFilter.printf("url(#%s)", colorfilterID.c_str());
@@ -707,12 +759,16 @@ void SkSVGDevice::AutoElement::addTextAttributes(const SkFont& font) {
continue;
}
familySet.add(familyString.fString);
- familyName.appendf((familyName.isEmpty() ? "%s" : ", %s"), familyString.fString.c_str());
+ familyName.appendf((familyName.isEmpty() ? "'%s'" : ", '%s'"), familyString.fString.c_str());
}
}
- if (!familyName.isEmpty()) {
- this->addAttribute("font-family", familyName);
- }
+
+ familyName.appendf(
+ (familyName.isEmpty() ? "%s" : ", %s"),
+ "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'"
+ );
+
+ this->addAttribute("font-family", familyName);
diff --git a/src/core/SkBitmapDevice.cpp b/src/core/SkBitmapDevice.cpp
index b497d690f7..5e4ee2b1ce 100644
--- a/src/core/SkBitmapDevice.cpp
+++ b/src/core/SkBitmapDevice.cpp
@@ -522,8 +522,8 @@ void SkBitmapDevice::onDrawGlyphRunList(SkCanvas* canvas,
const sktext::GlyphRunList& glyphRunList,
const SkPaint& initialPaint,
const SkPaint& drawingPaint) {
- SkASSERT(!glyphRunList.hasRSXForm());
- LOOP_TILER( drawGlyphRunList(canvas, &fGlyphPainter, glyphRunList, drawingPaint), nullptr )
+ // SkASSERT(!glyphRunList.hasRSXForm());
+ // LOOP_TILER( drawGlyphRunList(canvas, &fGlyphPainter, glyphRunList, drawingPaint), nullptr )
}
sk_sp<SkBaseDevice> SkSVGDevice::Make(const SkISize& size, std::unique_ptr<SkXMLWriter> writer,
@@ -721,12 +777,18 @@ sk_sp<SkBaseDevice> SkSVGDevice::Make(const SkISize& size, std::unique_ptr<SkXML
: nullptr;
}
+
SkSVGDevice::SkSVGDevice(const SkISize& size, std::unique_ptr<SkXMLWriter> writer, uint32_t flags)
+ : SkSVGDevice(size, std::move(writer), flags, nullptr)
+{}
+
+SkSVGDevice::SkSVGDevice(const SkISize& size, std::unique_ptr<SkXMLWriter> writer, uint32_t flags, SkSVGDevice* root)
: INHERITED(SkImageInfo::MakeUnknown(size.fWidth, size.fHeight),
SkSurfaceProps(0, kUnknown_SkPixelGeometry))
, fWriter(std::move(writer))
- , fResourceBucket(new ResourceBucket)
+ , fResourceBucket(root ? nullptr : new ResourceBucket)
, fFlags(flags)
+ , fRootDevice(root ? root : this)
{
SkASSERT(fWriter);
@@ -739,15 +801,44 @@ SkSVGDevice::SkSVGDevice(const SkISize& size, std::unique_ptr<SkXMLWriter> write
fRootElement->addAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
fRootElement->addAttribute("width", size.width());
fRootElement->addAttribute("height", size.height());
+
+ fRootDefsElement = std::make_unique<AutoElement>("defs", fWriter);
+ fRootGroupElement = std::make_unique<AutoElement>("g", fWriter);
+ fRootElementID = bucket()->addGroup();
+
+ fRootGroupElement->addAttribute("id", fRootElementID);
}
-SkSVGDevice::~SkSVGDevice() {
+void SkSVGDevice::closeClipStack() {
// Pop order is important.
while (!fClipStack.empty()) {
fClipStack.pop_back();
}
}
+void SkSVGDevice::closeWriter() {
+ closeClipStack();
+
+ if (!fRootElement) {
+ return;
+ }
+
+ fRootGroupElement.reset();
+ fRootDefsElement.reset();
+
+ if (fWriteUseElement) {
+ AutoElement elem("use", fWriter);
+
+ elem.addAttribute("href", SkStringPrintf("#%s", fRootElementID.c_str()));
+ }
+
+ fRootElement.reset();
+}
+
+SkSVGDevice::~SkSVGDevice() {
+ closeWriter();
+}
+
SkParsePath::PathEncoding SkSVGDevice::pathEncoding() const {
return (fFlags & SkSVGCanvas::kRelativePathEncoding_Flag)
? SkParsePath::PathEncoding::Relative
@@ -762,7 +853,7 @@ void SkSVGDevice::syncClipStack(const SkClipStack& cs) {
// First, find/preserve the common bottom.
while ((elem = iter.next()) && (rec_idx < fClipStack.size())) {
- if (fClipStack[SkToInt(rec_idx)].fGenID != elem->getGenID()) {
+ if (fClipStack[SkToInt(rec_idx)] != elem->getGenID()) {
break;
}
rec_idx++;
@@ -779,6 +870,13 @@ void SkSVGDevice::syncClipStack(const SkClipStack& cs) {
AutoElement clip_path("clipPath", fWriter);
clip_path.addAttribute("id", cid);
+ if (!fClipStack.empty()) {
+ clip_path.addAttribute(
+ "clip-path",
+ SkStringPrintf("url(#cl_%x)", fClipStack.back())
+ );
+ }
+
// TODO: handle non-intersect clips.
switch (e->getDeviceSpaceType()) {
@@ -812,25 +910,25 @@ void SkSVGDevice::syncClipStack(const SkClipStack& cs) {
// TODO: handle shader clipping, perhaps rasterize and apply as a mask image?
break;
}
-
- return cid;
};
// Rebuild the top.
while (elem) {
- const auto cid = define_clip(elem);
-
- auto clip_grp = std::make_unique<AutoElement>("g", fWriter);
- clip_grp->addAttribute("clip-path", SkStringPrintf("url(#%s)", cid.c_str()));
-
- fClipStack.push_back({ std::move(clip_grp), elem->getGenID() });
+ if (
+ elem->getDeviceSpaceType() != SkClipStack::Element::DeviceSpaceType::kEmpty &&
+ elem->getOp() == SkClipOp::kIntersect
+ ) {
+ define_clip(elem);
+
+ fClipStack.push_back(elem->getGenID());
+ }
elem = iter.next();
}
}
void SkSVGDevice::drawPaint(const SkPaint& paint) {
- AutoElement rect("rect", this, fResourceBucket.get(), MxCp(this), paint);
+ AutoElement rect("rect", this, bucket(), MxCp(this), paint);
rect.addRectAttributes(SkRect::MakeWH(SkIntToScalar(this->width()),
SkIntToScalar(this->height())));
}
@@ -890,11 +988,11 @@ void SkSVGDevice::drawPoints(SkCanvas::PointMode mode, size_t count,
void SkSVGDevice::drawRect(const SkRect& r, const SkPaint& paint) {
std::unique_ptr<AutoElement> svg;
if (RequiresViewportReset(paint)) {
- svg = std::make_unique<AutoElement>("svg", this, fResourceBucket.get(), MxCp(this), paint);
+ svg = std::make_unique<AutoElement>("svg", this, bucket(), MxCp(this), paint);
svg->addRectAttributes(r);
}
- AutoElement rect("rect", this, fResourceBucket.get(), MxCp(this), paint);
+ AutoElement rect("rect", this, bucket(), MxCp(this), paint);
if (svg) {
rect.addAttribute("x", 0);
@@ -907,7 +1005,7 @@ void SkSVGDevice::drawRect(const SkRect& r, const SkPaint& paint) {
}
void SkSVGDevice::drawOval(const SkRect& oval, const SkPaint& paint) {
- AutoElement ellipse("ellipse", this, fResourceBucket.get(), MxCp(this), paint);
+ AutoElement ellipse("ellipse", this, bucket(), MxCp(this), paint);
ellipse.addAttribute("cx", oval.centerX());
ellipse.addAttribute("cy", oval.centerY());
ellipse.addAttribute("rx", oval.width() / 2);
@@ -915,7 +1013,7 @@ void SkSVGDevice::drawOval(const SkRect& oval, const SkPaint& paint) {
}
void SkSVGDevice::drawRRect(const SkRRect& rr, const SkPaint& paint) {
- AutoElement elem("path", this, fResourceBucket.get(), MxCp(this), paint);
+ AutoElement elem("path", this, bucket(), MxCp(this), paint);
elem.addPathAttributes(SkPath::RRect(rr), this->pathEncoding());
}
@@ -948,7 +1046,7 @@ void SkSVGDevice::drawPath(const SkPath& path, const SkPaint& paint, bool pathIs
}
// Create path element.
- AutoElement elem("path", this, fResourceBucket.get(), MxCp(this), *path_paint);
+ AutoElement elem("path", this, bucket(), MxCp(this), *path_paint);
elem.addPathAttributes(*pathPtr, this->pathEncoding());
// TODO: inverse fill types?
@@ -975,7 +1073,7 @@ void SkSVGDevice::drawBitmapCommon(const MxCp& mc, const SkBitmap& bm, const SkP
SkString svgImageData("data:image/png;base64,");
svgImageData.append(b64Data.get(), b64Size);
- SkString imageID = fResourceBucket->addImage();
+ SkString imageID = bucket()->addImage();
{
AutoElement defs("defs", fWriter);
{
@@ -988,7 +1086,7 @@ void SkSVGDevice::drawBitmapCommon(const MxCp& mc, const SkBitmap& bm, const SkP
}
{
- AutoElement imageUse("use", this, fResourceBucket.get(), mc, paint);
+ AutoElement imageUse("use", this, bucket(), mc, paint);
imageUse.addAttribute("xlink:href", SkStringPrintf("#%s", imageID.c_str()));
}
}
@@ -1023,18 +1121,60 @@ public:
SkAutoSTArray<64, SkUnichar> unichars(runSize);
SkFontPriv::GlyphsToUnichars(glyphRun.font(), glyphRun.glyphsIDs().data(),
runSize, unichars.get());
- auto positions = glyphRun.positions();
+ auto position = fOrigin;
+
+ if (runSize > 0) {
+ position += glyphRun.positions()[0];
+ }
+
+ fPosXStr.appendf("%.8g", position.fX);
+ fPosYStr.appendf("%.8g", position.fY);
+
+ auto input = std::make_unique<char[]>(runSize);
+
for (size_t i = 0; i < runSize; ++i) {
- this->appendUnichar(unichars[i], positions[i]);
+ input[i] = unichars[i];
+ }
+
+ size_t size = 0;
+ auto error = SkBase64::Decode(input.get(), runSize, nullptr, &size);
+
+ if (error != SkBase64::kNoError) {
+ std::cerr << "Failed to decode SVG base64 text data, size=" << runSize << std::endl;
+
+ return;
+ }
+
+ auto utf8 = std::make_unique<char[]>(size);
+
+ error = SkBase64::Decode(input.get(), runSize, utf8.get(), &size);
+
+ if (error != SkBase64::kNoError) {
+ std::cerr << "Failed to decode SVG base64 text data, size=" << runSize << std::endl;
+
+ return;
+ }
+
+ auto* utf8_start = static_cast<const char*>(utf8.get());
+ auto* utf8_end = utf8_start + std::strlen(utf8_start);
+
+ while (utf8_start < utf8_end) {
+ SkUnichar unichar = SkUTF::NextUTF8(&utf8_start, utf8_end);
+
+ if (unichar == '\0') {
+ break;
+ }
+
+ this->appendUnichar(unichar);
}
}
const SkString& text() const { return fText; }
const SkString& posX() const { return fPosXStr; }
- const SkString& posY() const { return fHasConstY ? fConstYStr : fPosYStr; }
+ const SkString& posY() const { return fPosYStr; }
private:
- void appendUnichar(SkUnichar c, SkPoint position) {
+ void appendUnichar(SkUnichar c) {
bool discardPos = false;
bool isWhitespace = false;
@@ -1077,31 +1217,13 @@ private:
}
fLastCharWasWhitespace = isWhitespace;
-
- if (discardPos) {
- return;
- }
-
- position += fOrigin;
- fPosXStr.appendf("%.8g, ", position.fX);
- fPosYStr.appendf("%.8g, ", position.fY);
-
- if (fConstYStr.isEmpty()) {
- fConstYStr = fPosYStr;
- fConstY = position.fY;
- } else {
- fHasConstY &= SkScalarNearlyEqual(fConstY, position.fY);
- }
}
const SkPoint fOrigin;
SkString fText,
- fPosXStr, fPosYStr,
- fConstYStr;
- SkScalar fConstY;
- bool fLastCharWasWhitespace = true, // start off in whitespace mode to strip leading space
- fHasConstY = true;
+ fPosXStr, fPosYStr;
+ bool fLastCharWasWhitespace = true; // start off in whitespace mode to strip leading space
};
void SkSVGDevice::onDrawGlyphRunList(SkCanvas* canvas,
@@ -1126,7 +1248,7 @@ void SkSVGDevice::onDrawGlyphRunList(SkCanvas* canvas,
// Emit one <text> element for each run.
for (auto& glyphRun : glyphRunList) {
- AutoElement elem("text", this, fResourceBucket.get(), MxCp(this), drawingPaint);
+ AutoElement elem("text", this, bucket(), MxCp(this), drawingPaint);
elem.addTextAttributes(glyphRun.font());
SVGTextBuilder builder(glyphRunList.origin(), glyphRun);
@@ -1145,3 +1267,106 @@ void SkSVGDevice::drawMesh(const SkMesh&, sk_sp<SkBlender>, const SkPaint&) {
// todo
}
#endif
+
+SkBaseDevice* SkSVGDevice::onCreateDevice(const CreateInfo& info, const SkPaint* paint) {
+ return fLayers.emplace_back(
+ std::make_unique<Layer>(
+ SkISize::Make(info.fInfo.width(), info.fInfo.height()),
+ fFlags,
+ fRootDevice
+ )
+ )->fDevice;
+}
+
+void SkSVGDevice::drawDevice(SkBaseDevice* baseDevice, const SkSamplingOptions& options, const SkPaint& paint) {
+ for (auto& layerPtr : fLayers) {
+ auto* layer = layerPtr.get();
+ auto* device = layer->fDevice;
+
+ if (device == baseDevice) {
+ auto blendMode = paint.getBlendMode_or(SkBlendMode::kClear);
+
+ SkASSERT(device->fRootDevice == fRootDevice);
+ SkASSERT(device->bucket() == bucket());
+
+ if (layer->fNode == nullptr) {
+ if(blendMode == SkBlendMode::kDstIn) {
+ device->fWriteUseElement = false;
+ }
+
+ device->closeWriter();
+ layer->fDevice = nullptr;
+ layer->fNode = layer->fDom.finishParsing();
+ }
+
+ bool skip_root = true; // the root element is <svg>, skip it
+ SkMatrix matrix = device->getRelativeTransform(*this);
+
+ if (blendMode == SkBlendMode::kDstIn) {
+ const auto srcID = device->fRootElementID;
+ const auto dstID = fRootElementID;
+ const auto filterID = bucket()->addFilter();
+
+ {
+ closeClipStack();
+
+ fRootGroupElement.reset();
+ fRootGroupElement = std::make_unique<AutoElement>("g", fWriter);
+ fRootElementID = bucket()->addGroup();
+ fRootGroupElement->addAttribute("id", fRootElementID);
+ }
+
+ fWriter->writeDOM(layer->fDom, layer->fNode, skip_root);
+
+ {
+ AutoElement filter("filter", fWriter);
+
+ filter.addAttribute("x", "0");
+ filter.addAttribute("y", "0");
+ filter.addAttribute("width", "100%");
+ filter.addAttribute("height", "100%");
+ filter.addAttribute("id", filterID);
+ filter.addAttribute("filterUnits", "userSpaceOnUse");
+
+ {
+ AutoElement srcImage("feImage", fWriter);
+
+ srcImage.addAttribute("href", SkStringPrintf("#%s", srcID.c_str()));
+ srcImage.addAttribute("result", "src");
+ }
+
+ {
+ AutoElement dstImage("feImage", fWriter);
+
+ dstImage.addAttribute("href", SkStringPrintf("#%s", dstID.c_str()));
+ dstImage.addAttribute("result", "dst");
+ }
+
+ {
+ AutoElement feComposite("feComposite", fWriter);
+
+ feComposite.addAttribute("in", "dst");
+ feComposite.addAttribute("in2", "src");
+ feComposite.addAttribute("operator", "in");
+ }
+ }
+
+ {
+ AutoElement rect("rect", fWriter);
+
+ rect.addAttribute("x", "0");
+ rect.addAttribute("y", "0");
+ rect.addAttribute("width", "100%");
+ rect.addAttribute("height", "100%");
+ rect.addAttribute("filter", SkStringPrintf("url(#%s)", filterID.c_str()));
+ }
+ } else {
+ AutoElement group("g", this, bucket(), MxCp(&matrix, &cs()), paint);
+
+ fWriter->writeDOM(layer->fDom, layer->fNode, skip_root);
+ }
+
+ return;
+ }
+ }
+}
diff --git a/src/svg/SkSVGDevice.h b/src/svg/SkSVGDevice.h
index 8705ad5066..470962c068 100644
--- a/src/svg/SkSVGDevice.h
+++ b/src/svg/SkSVGDevice.h
@@ -14,6 +14,9 @@
#include "include/private/SkTArray.h"
#include "include/utils/SkParsePath.h"
#include "src/core/SkClipStackDevice.h"
+#include "src/core/SkDraw.h"
+#include "src/xml/SkXMLParser.h"
+#include "src/xml/SkXMLWriter.h"
#include <cstddef>
#include <cstdint>
@@ -34,6 +37,7 @@ class SkPath;
class SkRRect;
class SkVertices;
class SkXMLWriter;
+class SkSVGDevice;
struct SkISize;
struct SkPoint;
struct SkRect;
@@ -70,15 +74,20 @@ protected:
#ifdef SK_ENABLE_SKSL
void drawMesh(const SkMesh&, sk_sp<SkBlender>, const SkPaint&) override;
#endif
+
+ SkBaseDevice* onCreateDevice(const CreateInfo&, const SkPaint*) override;
+ virtual void drawDevice(SkBaseDevice*, const SkSamplingOptions&, const SkPaint&) override;
private:
SkSVGDevice(const SkISize& size, std::unique_ptr<SkXMLWriter>, uint32_t);
+ SkSVGDevice(const SkISize& size, std::unique_ptr<SkXMLWriter>, uint32_t, SkSVGDevice*);
~SkSVGDevice() override;
struct MxCp;
void drawBitmapCommon(const MxCp&, const SkBitmap& bm, const SkPaint& paint);
+ void closeWriter();
+ void closeClipStack();
void syncClipStack(const SkClipStack&);
-
SkParsePath::PathEncoding pathEncoding() const;
class AutoElement;
@@ -88,13 +97,38 @@ private:
const std::unique_ptr<ResourceBucket> fResourceBucket;
const uint32_t fFlags;
- struct ClipRec {
- std::unique_ptr<AutoElement> fClipPathElem;
- uint32_t fGenID;
+ SkSVGDevice* fRootDevice;
+ std::unique_ptr<AutoElement> fRootElement;
+ std::unique_ptr<AutoElement> fRootDefsElement;
+ std::unique_ptr<AutoElement> fRootGroupElement;
+ SkTArray<uint32_t> fClipStack;
+ SkString fRootElementID;
+ SkString fClipPathID;
+ bool fWriteUseElement = true;
+
+ ResourceBucket* bucket() {
+ return fRootDevice->fResourceBucket.get();
+ }
+
+ class Layer {
+ public:
+ Layer(const SkISize& size, uint32_t flags, SkSVGDevice* root)
+ : fDevice(
+ new SkSVGDevice(
+ size,
+ std::make_unique<SkXMLParserWriter>(fDom.beginParsing()),
+ flags,
+ root
+ )
+ )
+ {}
+
+ SkDOM fDom;
+ SkSVGDevice* fDevice;
+ const SkDOMNode* fNode = nullptr;
};
- std::unique_ptr<AutoElement> fRootElement;
- SkTArray<ClipRec> fClipStack;
+ std::vector<std::unique_ptr<Layer>> fLayers;
using INHERITED = SkClipStackDevice;
};
void SkBitmapDevice::drawVertices(const SkVertices* vertices,

2
src/terminal.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod input;
pub mod output;

10
src/terminal/input.rs Normal file
View File

@ -0,0 +1,10 @@
mod event;
mod listen;
mod mouse;
mod parser;
mod raw_tty;
pub use event::*;
pub use listen::*;
pub use mouse::*;
pub use parser::*;

View File

@ -0,0 +1,59 @@
use std::ops::BitAnd;
use super::Mouse;
#[derive(Debug)]
pub enum Event {
KeyPress { key: u8 },
MouseUp { row: usize, col: usize },
MouseDown { row: usize, col: usize },
MouseMove { row: usize, col: usize },
Scroll { delta: isize },
Exit,
}
impl Event {
pub fn from(mouse: &mut Mouse, release: bool) -> Option<Event> {
if !mouse.read() {
return None;
}
match (mouse.btn, mouse.col, mouse.row) {
(Some(btn), Some(col), Some(row)) => Some({
if Mask::ScrollDown & btn {
Event::Scroll { delta: -1 }
} else if Mask::ScrollUp & btn {
Event::Scroll { delta: 1 }
} else {
let col = col as usize - 1;
let row = row as usize - 1;
if release {
Event::MouseUp { row, col }
} else if Mask::MouseMove & btn {
Event::MouseMove { row, col }
} else {
Event::MouseDown { row, col }
}
}
}),
_ => None,
}
}
}
enum Mask {
MouseMove = 0x20,
ScrollUp = 0x40,
ScrollDown = 0x41,
}
impl BitAnd<u32> for Mask {
type Output = bool;
fn bitand(self, rhs: u32) -> bool {
let mask = self as u32;
mask & rhs == mask
}
}

View File

@ -0,0 +1,40 @@
use std::io::{self, Read};
use crate::terminal::input::*;
pub fn setup() -> io::Result<()> {
raw_tty::setup()
}
/// Listen for input events in stdin.
/// This will block, so it should run from a dedicated thread.
pub fn listen<T, F>(mut callback: F) -> io::Result<T>
where
F: FnMut(Event) -> Option<T>,
{
let mut buf = [0u8; 1024];
let mut stdin = io::stdin();
let mut parser = Parser::new();
loop {
// Wait for some input
let size = stdin.read(&mut buf)?;
let mut scroll = 0;
// Parse the input for xterm commands
for event in parser.parse(&buf[0..size]) {
// Allow the callback to return early (ie. handle ctrl+c)
if let Event::Scroll { delta } = event {
scroll += delta;
} else if let Some(result) = callback(event) {
return Ok(result);
}
}
if scroll != 0 {
if let Some(result) = callback(Event::Scroll { delta: scroll }) {
return Ok(result);
}
}
}
}

View File

@ -0,0 +1,65 @@
use super::Event;
#[derive(Debug)]
pub struct Mouse {
pub buf: Option<Vec<u8>>,
pub btn: Option<u32>,
pub col: Option<u32>,
pub row: Option<u32>,
}
impl Mouse {
pub fn start(&mut self) {
self.buf = Some(Vec::new())
}
pub fn end(&mut self, key: u8, events: &mut Vec<Event>) {
if let Some(event) = Event::from(self, key == 0x6d) {
events.push(event)
}
}
pub fn reset(&mut self) {
self.buf = None;
self.btn = None;
self.col = None;
self.row = None;
}
pub fn read(&mut self) -> bool {
if self.parse() == None {
self.reset();
false
} else {
true
}
}
fn parse(&mut self) -> Option<()> {
if let Some(ref buf) = self.buf {
let string = std::str::from_utf8(buf).ok()?;
let data = string.parse().ok()?;
self.update(data)
} else {
None
}
}
fn update(&mut self, data: u32) -> Option<()> {
if self.btn == None {
self.btn = Some(data)
} else if self.col == None {
self.col = Some(data)
} else if self.row == None {
self.row = Some(data)
} else {
return None;
}
self.buf = Some(Vec::new());
return Some(());
}
}

View File

@ -0,0 +1,93 @@
use crate::terminal::input::*;
pub struct Parser {
esc: bool,
csi: bool,
mouse: Mouse,
}
impl Parser {
pub fn new() -> Parser {
Parser {
esc: false,
csi: false,
mouse: Mouse {
buf: None,
btn: None,
col: None,
row: None,
},
}
}
pub fn parse(&mut self, input: &[u8]) -> Vec<Event> {
let mut events = Vec::new();
let Parser {
mut esc,
mut csi,
ref mut mouse,
} = self;
for &key in input {
if esc {
// Inside an escape sequence
if let Some(ref mut buf) = mouse.buf {
// Process mouse input sequence
match key {
// Delimiter: concat characters, parse number, and clear buffer
0x3b => esc = mouse.read(),
// Terminator: emit mouse move, movement, or release based on terminator
0x4d | 0x6d => {
mouse.end(key, &mut events);
esc = false
}
// Consider anything else part of the value
_ => buf.push(key),
}
} else if csi {
// Inside a control sequence
match key {
// Mouse input
0x3c => mouse.start(),
// Map arrow keys events to key codes
0x41..=0x44 => events.push(Event::KeyPress {
key: [0x26, 0x28, 0x27, 0x25][(key - 0x41) as usize],
}),
// Ignore anything else
_ => esc = false,
}
} else if key == 0x5b {
// [ character, start a CSI sequence
csi = true
} else {
// Unrecognized sequence, emit an ESC keypress
events.push(Event::KeyPress { key: 0x1b });
if key != 0x1b {
// Cancel the sequence only if this isn't an ESC character
esc = false;
events.push(Event::KeyPress { key });
}
}
} else if key == 0x1b {
// ESC character, start an escape sequence
esc = true;
csi = false;
mouse.reset();
} else if key == 0x03 {
// CTRL-C pressed
events.push(Event::Exit)
} else {
// Any other character should be parser as text input
events.push(Event::KeyPress { key })
}
}
self.esc = esc;
self.csi = csi;
events
}
}

View File

@ -0,0 +1,40 @@
use std::os::unix::prelude::AsRawFd;
use std::{fs, io};
/// Setup the input stream to operate in raw mode.
/// Allows for reading characters without waiting for the return key to be pressed.
pub fn setup() -> io::Result<()> {
unsafe {
let tty;
let fd = if libc::isatty(libc::STDIN_FILENO) == 1 {
libc::STDIN_FILENO
} else {
// Use /dev/tty in the input stream is not a terminal.
// Happens if something is piped to stdin.
tty = fs::File::open("/dev/tty")?;
tty.as_raw_fd()
};
let mut ptr = core::mem::MaybeUninit::uninit();
// Load the terminal parameters
if libc::tcgetattr(fd, ptr.as_mut_ptr()) == 0 {
let mut termios = ptr.assume_init();
let c_oflag = termios.c_oflag;
// Set the terminal to raw mode
libc::cfmakeraw(&mut termios);
// Restore output flags, ensures carriage returns are consistent
termios.c_oflag = c_oflag;
// Save the terminal parameters
if libc::tcsetattr(fd, libc::TCSANOW, &termios) == 0 {
return Ok(());
}
}
}
Err(io::Error::last_os_error())
}

12
src/terminal/output.rs Normal file
View File

@ -0,0 +1,12 @@
// mod kd_tree;
// mod quantizer;
mod cell;
mod painter;
mod renderer;
mod size;
mod xterm;
pub use cell::*;
pub use painter::*;
pub use renderer::*;
pub use size::*;

View File

@ -0,0 +1,35 @@
use std::rc::Rc;
use crate::gfx::{Color, Point};
#[derive(Clone, PartialEq)]
pub struct Grapheme {
/// Unicode character in UTF-8, might contain multiple code points (Emoji, CJK).
pub char: String,
pub index: usize,
pub width: usize,
pub color: Color,
}
/// Terminal cell with `height = width * 2`
#[derive(PartialEq)]
pub struct Cell {
/// Top pixel color value
pub top: Color,
/// Bottom pixel color value
pub bottom: Color,
pub cursor: Point<u32>,
/// Text grapheme if any
pub grapheme: Option<Rc<Grapheme>>,
}
impl Cell {
pub fn new(x: u32, y: u32) -> Cell {
Cell {
top: Color::black(),
bottom: Color::black(),
cursor: Point::new(x, y),
grapheme: None,
}
}
}

View File

@ -0,0 +1,65 @@
use std::ops::Mul;
use crate::gfx::Color;
struct KDNode {
left: Option<Box<KDNode>>,
right: Option<Box<KDNode>>,
normal: Color<f64>,
middle: (usize, Color<f64>),
}
impl KDNode {
fn new(colors: &[Color]) {
let (sum, sum_squared) = colors.iter().fold(
(Color::black(), Color::black()),
|(sum, sum_squared), color| (sum + color, sum_squared + color * color),
);
}
fn nearest(&self, color: Color<f64>, mut limit: f64) -> Option<(usize, f64)> {
let diff = color - self.middle.1;
let distance = diff.mul(&diff).sum().sqrt();
let mut result = None;
if distance < limit {
limit = distance;
}
let dot = diff.mul(self.normal).sum();
if dot <= 0.0 {
if let Some(ref left) = self.left {
if let Some(nearest) = left.nearest(color, limit) {
limit = nearest.1;
result = Some(nearest);
}
}
if -dot < limit {
if let Some(ref right) = self.right {
if let Some(nearest) = right.nearest(color, limit) {
result = Some(nearest);
}
}
}
} else {
if let Some(ref right) = self.right {
if let Some(nearest) = right.nearest(color, limit) {
limit = nearest.1;
result = Some(nearest);
}
}
if dot < limit {
if let Some(ref left) = self.left {
if let Some(nearest) = left.nearest(color, limit) {
result = Some(nearest);
}
}
}
}
result
}
}

View File

@ -0,0 +1,154 @@
use std::io::{self, Stdout, Write};
use crate::gfx::{Color, Point};
use super::Cell;
#[derive(PartialEq)]
enum PaintMode {
Text,
Bitmap,
}
pub struct Painter {
mode: PaintMode,
output: Stdout,
buffer: Vec<u8>,
cursor: Option<Point<u32>>,
true_color: bool,
background: Option<Color>,
foreground: Option<Color>,
background_code: Option<u8>,
foreground_code: Option<u8>,
}
impl Painter {
pub fn new() -> Painter {
Painter {
mode: PaintMode::Text,
buffer: Vec::new(),
cursor: None,
output: io::stdout(),
true_color: if let Ok(value) = std::env::var("COLORTERM") {
match value.as_str() {
"truecolor" | "24bit" => true,
_ => false,
}
} else {
false
},
background: None,
foreground: None,
background_code: None,
foreground_code: None,
}
}
pub fn true_color(&self) -> bool {
self.true_color
}
pub fn flush(&mut self) -> io::Result<()> {
self.output.write(self.buffer.as_slice())?;
self.output.flush()?;
self.buffer.clear();
Ok(())
}
pub fn paint(&mut self, cell: &Cell) -> io::Result<()> {
let &Cell {
cursor,
ref grapheme,
top: mut background,
bottom: mut foreground,
} = cell;
let (char, width, escape) = if let Some(grapheme) = grapheme {
if grapheme.index > 0 {
return Ok(());
}
background = background.avg_with(foreground);
foreground = grapheme.color;
(
grapheme.char.as_str(),
grapheme.width as u32,
if self.mode == PaintMode::Bitmap {
self.mode = PaintMode::Text;
Some("\x1b[22m\x1b[24m")
} else {
None
},
)
} else {
(
"",
1,
if self.mode == PaintMode::Text {
self.mode = PaintMode::Bitmap;
Some("\x1b[1m\x1b[4m")
} else {
None
},
)
};
if self.cursor != Some(cursor) {
write!(self.buffer, "\x1b[{};{}H", cursor.y + 1, cursor.x + 1)?;
};
self.cursor = Some(cursor + Point::new(width, 0));
if self.background != Some(background) {
self.background = Some(background);
if self.true_color {
write!(
self.buffer,
"\x1b[48;2;{};{};{}m",
background.r, background.g, background.b,
)?
} else {
let code = background.to_xterm();
if self.background_code != Some(code) {
self.background_code = Some(code);
write!(self.buffer, "\x1b[48;5;{}m", code)?
}
}
}
if self.foreground != Some(foreground) {
self.foreground = Some(foreground);
if self.true_color {
write!(
self.buffer,
"\x1b[38;2;{};{};{}m",
foreground.r, foreground.g, foreground.b,
)?
} else {
let code = foreground.to_xterm();
if self.foreground_code != Some(code) {
self.foreground_code = Some(code);
write!(self.buffer, "\x1b[38;5;{}m", code)?
}
}
}
if let Some(escape) = escape {
self.buffer.write_all(escape.as_bytes())?
}
self.buffer.write_all(char.as_bytes())?;
Ok(())
}
}

View File

@ -0,0 +1,111 @@
use crate::gfx::Color;
#[derive(Clone, Copy)]
enum Channel {
R,
G,
B,
}
const COLOR_BUCKETS: usize = 8;
const COLORS: usize = 2_usize.pow(COLOR_BUCKETS as u32);
/// Find the closest color to `color` on `palette` using a binary search
pub fn palette_color(palette: &[Color; COLORS], color: Color) {
let mut size = palette.len() / 2;
let mut iter = palette.iter();
let mut prev = iter.next();
}
fn distance(a: Color, b: Color) {}
pub fn quantize(pixels: &[u8]) -> [Color; COLORS] {
let mut min = Color::black();
let mut max = Color::black();
let mut bucket = Vec::<Color>::new();
let mut pixels_iter = pixels.iter();
let mut bucket_iter = bucket.iter_mut();
// Step 1: find the dominant channel
loop {
match (
bucket_iter.next(),
pixels_iter.next(),
pixels_iter.next(),
pixels_iter.next(),
pixels_iter.next(),
) {
(Some(color), Some(r), Some(g), Some(b), Some(_)) => {
// Save the color in a bucket
color.set_r(*r);
color.set_g(*g);
color.set_b(*b);
min.set_r(min.r().min(color.r()));
min.set_g(min.g().min(color.g()));
min.set_b(min.b().min(color.b()));
max.set_r(max.r().max(color.r()));
max.set_g(max.g().max(color.g()));
max.set_b(max.b().max(color.b()));
}
_ => break,
}
}
let ranges = [
(Channel::R, max.r() - min.r()),
(Channel::G, max.g() - min.g()),
(Channel::B, max.b() - min.b()),
];
let (channel, _) = ranges
.iter()
.reduce(|a, b| if a.1 > b.1 { a } else { b })
.unwrap();
// Step 2: perform median-cut
for i in 1..=COLOR_BUCKETS {
let buckets = 2_usize.pow(i as u32);
let size = bucket.len() / buckets;
for j in 0..buckets {
let start = j * size;
let end = start + size;
let slice = &mut bucket[start..end];
slice.sort_unstable_by(match channel {
Channel::R => |a: &Color, b: &Color| a.r().cmp(&b.r()),
Channel::G => |a: &Color, b: &Color| a.g().cmp(&b.g()),
Channel::B => |a: &Color, b: &Color| a.b().cmp(&b.b()),
});
}
}
// Step 3: get the average color in each bucket
let mut palette = [Color::black(); COLORS];
let size = bucket.len() / palette.len();
for (i, color) in palette.iter_mut().enumerate() {
let start = i * size;
let end = start + size;
let slice = &bucket[start..end];
let mut sum = None;
for color in slice.into_iter() {
sum = Some(match sum {
None => color.cast(),
Some(sum) => color.cast() + sum,
})
}
if let Some(sum) = sum {
let avg = sum / size as u32;
color.set_r(avg.r() as u8);
color.set_g(avg.g() as u8);
color.set_b(avg.b() as u8);
}
}
palette
}

View File

@ -0,0 +1,231 @@
use std::{
io::{self, Write as _},
rc::Rc,
};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use crate::{
gfx::{Color, Point, Rect, Size},
terminal,
};
use super::{Cell, Grapheme, Painter};
struct Dimensions {
/// Size of a terminal cell in pixels
cell: Size,
/// Size of the browser window in pixels
browser: Size,
/// Size of the terminal window in cells
terminal: Size,
}
pub struct Renderer {
cells: Vec<(Cell, Cell)>,
dimensions: Dimensions,
painter: Painter,
}
const SEQUENCES: [(u32, bool); 4] = [(1049, true), (1003, true), (1006, true), (25, false)];
impl Renderer {
pub fn new() -> Renderer {
Renderer {
cells: Vec::with_capacity(0),
painter: Painter::new(),
dimensions: Dimensions {
cell: Size::new(7, 14),
browser: Size::new(0, 0),
terminal: Size::new(0, 0),
},
}
}
pub fn setup() -> io::Result<()> {
terminal::input::setup()?;
let mut out = io::stdout();
for (sequence, enable) in SEQUENCES {
write!(out, "\x1b[?{}{}", sequence, if enable { "h" } else { "l" })?;
}
out.flush()?;
Ok(())
}
pub fn teardown() -> io::Result<()> {
let mut out = io::stdout();
for (sequence, enable) in SEQUENCES {
write!(out, "\x1b[?{}{}", sequence, if enable { "l" } else { "h" })?;
}
out.flush()?;
Ok(())
}
pub fn set_size(&mut self, cell: Size, terminal: Size) {
let size = (terminal.width * terminal.height) as usize;
self.dimensions.cell = cell;
self.dimensions.terminal = terminal;
self.dimensions.browser = cell * terminal;
let mut x = 0;
let mut y = 0;
let bound = terminal.width - 1;
self.cells.resize_with(size, || {
let cell = (Cell::new(x, y), Cell::new(x, y));
if x < bound {
x += 1;
} else {
x = 0;
y += 1;
}
cell
});
}
pub fn render(&mut self) -> io::Result<()> {
for (previous, current) in self.cells.iter_mut() {
if current == previous {
continue;
}
previous.top = current.top;
previous.bottom = current.bottom;
previous.grapheme = current.grapheme.clone();
self.painter.paint(current)?;
}
self.painter.flush()?;
Ok(())
}
/// Draw the background from a pixel array encoded in RGBA8888
pub fn draw_background(&mut self, pixels: &mut [u8], rect: Rect) -> io::Result<()> {
let viewport = self.dimensions.terminal.cast::<usize>();
let pixels_row = viewport.width * 4;
if pixels.len() != pixels_row * viewport.height * 2 {
return Ok(());
}
let pos = rect.origin.cast::<usize>() / Point::new(1, 2);
let size = rect.size.cast::<usize>() / Size::new(1, 2);
let pixels_left = pos.x * 4;
let pixels_width = size.width * 4;
// Iterate over each row
for y in pos.y..pos.y + size.height {
// Terminal chars have an aspect ratio of 2:1.
// In order to display perfectly squared pixels, we
// render a unicode glyph taking the bottom half of the cell
// using a foreground representing the bottom pixel,
// and a background representing the top pixel.
// This means that the pixel input buffer should be twice the size
// of the terminal cell buffer (two pixels take one terminal cell).
let left = pixels_left + y * 2 * pixels_row;
let right = left + pixels_width;
// Get a slice pointing to the top pixel row
let mut top_row = pixels[left..right].iter();
// Get a slice pointing to the bottom pixel row
let mut bottom_row = pixels[left + pixels_row..right + pixels_row].iter();
let cells_left = y * viewport.width + pos.x;
let cells = self.cells[cells_left..].iter_mut();
// Iterate over each column
for (_, cell) in cells {
match (
Color::from_iter(&mut top_row),
Color::from_iter(&mut bottom_row),
) {
(Some(top), Some(bottom)) => {
cell.top = top;
cell.bottom = bottom;
}
_ => break,
}
}
}
self.render()
}
pub fn clear_text(&mut self) {
for (_, cell) in self.cells.iter_mut() {
cell.grapheme = None
}
}
/// Render some text into the terminal output
pub fn draw_text(&mut self, string: &str, origin: Point, size: Size, color: Color) {
// Get an iterator starting at the text origin
let len = self.cells.len();
let viewport = &self.dimensions.terminal;
if size.width > 2 && size.height > 2 {
let (scaled_origin, scaled_size) = (origin + 0, size - 0);
let x = scaled_origin.x.max(0).min(viewport.width as i32);
let y = scaled_origin.y.max(0).min(viewport.height as i32 * 2);
let y_end = y + scaled_size.height as i32;
for y in y..y_end {
let index = x + y / 2 * (viewport.width as i32);
let start = len.min(index as usize);
let end = len.min(start + scaled_size.width as usize);
for (_, cell) in self.cells[start..end].iter_mut() {
cell.grapheme = None
}
}
} else {
// Compute the buffer index based on the position
let index = origin.x + origin.y / 2 * (self.dimensions.terminal.width as i32);
let mut iter = self.cells[len.min(index as usize)..].iter_mut();
// Get every Unicode grapheme in the input string
for grapheme in UnicodeSegmentation::graphemes(string, true) {
let width = grapheme.width();
for index in 0..width {
// Get the next terminal cell at the given position
match iter.next() {
// Stop if we're at the end of the buffer
None => return,
// Set the cell to the current grapheme
Some((_, cell)) => {
let next = Grapheme {
// Create a new shared reference to the text
color,
index,
width,
// Export the set of unicode code points for this graphene into an UTF-8 string
char: grapheme.to_string(),
};
if match cell.grapheme {
None => true,
Some(ref previous) => {
previous.color != next.color || previous.char != next.char
}
} {
cell.grapheme = Some(Rc::new(next))
}
}
}
}
}
}
}
}

View File

@ -0,0 +1,18 @@
use core::mem::MaybeUninit;
use std::io;
use crate::gfx::Size;
pub fn size() -> io::Result<Size> {
let mut ptr = MaybeUninit::<libc::winsize>::uninit();
unsafe {
if libc::ioctl(libc::STDOUT_FILENO, libc::TIOCGWINSZ, ptr.as_mut_ptr()) == 0 {
let size = ptr.assume_init();
Ok(Size::new(size.ws_col as u32, size.ws_row as u32))
} else {
Err(io::Error::last_os_error())
}
}
}

View File

@ -0,0 +1,17 @@
use crate::gfx::Color;
impl Color {
pub fn to_xterm(&self) -> u8 {
if self.r == self.g && self.g == self.b && self.r > 4 && self.r < 239 {
232 + (self.r - 8) / 10
} else {
(16.0
+ self
.cast::<f32>()
.mul_add(5.0 / 200.0, -(55.0 * (5.0 / 200.0)))
.max(0.0)
.round()
.dot((36.0, 6.0, 1.0))) as u8
}
}
}