Add optional feature to save SVG screenshot

memfd
Manos Pitsidianakis 2020-06-12 01:37:57 +03:00
parent bc0189ffa1
commit 0618e62ab6
Signed by: Manos Pitsidianakis
GPG Key ID: 73627C2F690DF710
5 changed files with 659 additions and 0 deletions

7
Cargo.lock generated
View File

@ -805,6 +805,7 @@ dependencies = [
"signal-hook-registry",
"smallvec",
"structopt",
"svg",
"termion",
"toml",
"unicode-segmentation",
@ -1613,6 +1614,12 @@ dependencies = [
"syn",
]
[[package]]
name = "svg"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b65a64d32a41db2a8081aa03c1ccca26f246ff681add693f8b01307b137da79"
[[package]]
name = "syn"
version = "1.0.30"

View File

@ -50,6 +50,7 @@ smallvec = { version = "1.1.0", features = ["serde", ] }
bitflags = "1.0"
pcre2 = { version = "0.2.3", optional = true }
structopt = { version = "0.3.14", default-features = false }
svg_crate = { version = "0.8.0", optional = true, package = "svg" }
[profile.release]
@ -67,6 +68,7 @@ jmap = ["melib/jmap_backend",]
sqlite3 = ["melib/sqlite3"]
regexp = ["pcre2"]
cli-docs = []
svgscreenshot = ["svg_crate"]
# Print tracing logs as meli runs in stderr
# enable for debug tracing logs: build with --features=debug-tracing

View File

@ -323,6 +323,8 @@ fn run_app(opt: Opt) -> Result<()> {
state.register_component(Box::new(EnvelopeView::new(wrapper, None, None, 0)));
} else {
state = State::new(None, sender, receiver.clone())?;
#[cfg(feature = "svgscreenshot")]
state.register_component(Box::new(components::svg::SVGScreenshotFilter::new()));
let window = Box::new(Tabbed::new(
vec![
Box::new(listing::Listing::new(&mut state.context)),

View File

@ -40,6 +40,9 @@ pub use self::utilities::*;
pub mod contacts;
pub use crate::contacts::*;
#[cfg(feature = "svgscreenshot")]
pub mod svg;
use std::fmt;
use std::fmt::{Debug, Display};

View File

@ -0,0 +1,645 @@
/*
* meli - svg screenshot
*
* Copyright Manos Pitsidianakis
*
* This file is part of meli.
*
* meli is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* meli is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use super::*;
use std::io::Write;
#[derive(Debug)]
pub struct SVGScreenshotFilter {
save_screenshot: bool,
}
impl fmt::Display for SVGScreenshotFilter {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "svg screenshot filter")
}
}
impl SVGScreenshotFilter {
pub fn new() -> Self {
SVGScreenshotFilter {
save_screenshot: false,
}
}
}
impl Component for SVGScreenshotFilter {
fn draw(&mut self, _grid: &mut CellBuffer, _area: Area, context: &mut Context) {
if !self.save_screenshot {
return;
}
self.save_screenshot = false;
let grid: &CellBuffer = _grid;
use svg_crate::node::element::{Definitions, Group, Rectangle, Style, Text, Use};
use svg_crate::node::Text as TextNode;
use svg_crate::Document;
let (width, height) = grid.size();
/*
* Format frame as follows:
* - The entire background is a big rectangle.
* - Every text piece with unified foreground color is a text element inserted into the
* `definitions` field of the svg, and then `use`ed as a reference
* - Every background piece (a slice of unified backgrund color) is a rectangle element
* inserted along with the `use` elements
*
* Each row is arbritarily set at 17px high, and each character cell is 8 pixels wide.
* Rectangle cells each have one extra pixel (so 18px * 9px) in their dimensions in order
* to cover the spacing between cells.
*/
let mut definitions = Definitions::new();
let mut rows_group = Group::new();
let mut text = String::with_capacity(width);
for (row_idx, row) in grid.bounds_iter(((0, 0), (width, height))).enumerate() {
text.clear();
let mut row_group = Group::new().set("id", format!("g{}", row_idx + 1));
let mut cur_fg = Color::Default;
let mut cur_bg = Color::Default;
let mut cur_attrs = Attr::DEFAULT;
let mut prev_x_fg = 0;
let mut is_start = true;
let mut prev_x_bg = 0;
for (x, c) in row.enumerate() {
if cur_bg != grid[c].bg() || cur_fg != grid[c].fg() || cur_attrs != grid[c].attrs()
{
if cur_bg != Color::Default {
let mut rect = Rectangle::new()
.set("x", prev_x_bg * 8)
.set("y", 17 * row_idx)
.set("width", (x - prev_x_bg) * 8 + 1)
.set("bgname", format!("{:?}", cur_bg))
.set("height", 18);
match cur_bg {
Color::Rgb(r, g, b) => {
rect = rect
.set("fill", format!("#{:02x}{:02x}{:02x}", r, g, b).as_str());
}
Color::Default => {
unreachable!();
}
c if c.as_byte() < 16 => {
rect = rect.set("class", format!("color{}", c.as_byte()).as_str());
}
c => {
let c = c.as_byte();
let (r, g, b) = XTERM_COLORS[c as usize];
rect = rect
.set("fill", format!("#{:02x}{:02x}{:02x}", r, g, b).as_str());
}
}
rows_group = rows_group.add(rect);
}
prev_x_bg = x;
cur_bg = grid[c].bg();
if !text.is_empty() {
let text_length = text.split_graphemes().len();
let mut text_el = Text::new()
.add(TextNode::new(&text))
.set("x", prev_x_fg * 8)
.set("textLength", text_length * 8)
.set("fgname", format!("{:?}", cur_fg));
if cur_attrs.intersects(Attr::BOLD) {
text_el = text_el.set("font-weight", "bold");
}
if cur_attrs.intersects(Attr::ITALICS) {
text_el = text_el.set("font-style", "italic");
}
if cur_attrs.intersects(Attr::UNDERLINE) {
text_el = text_el.set("text-decoration", "underline");
}
if cur_attrs.intersects(Attr::DIM) {
text_el = text_el.set("font-weight", "lighter");
}
if cur_attrs.intersects(Attr::HIDDEN) {
text_el = text_el.set("display", "none");
}
match cur_fg {
Color::Default if cur_attrs.intersects(Attr::REVERSE) => {
text_el = text_el.set("class", "background");
}
Color::Default => {
text_el = text_el.set("class", "foreground");
}
Color::Rgb(r, g, b) => {
text_el = text_el
.set("fill", format!("#{:02x}{:02x}{:02x}", r, g, b).as_str());
}
c if c.as_byte() < 16 => {
text_el =
text_el.set("class", format!("color{}", c.as_byte()).as_str());
}
c => {
let c = c.as_byte();
let (r, g, b) = XTERM_COLORS[c as usize];
text_el = text_el
.set("fill", format!("#{:02x}{:02x}{:02x}", r, g, b).as_str());
}
};
row_group = row_group.add(text_el);
text.clear();
}
prev_x_fg = x;
cur_fg = grid[c].fg();
cur_attrs = grid[c].attrs();
}
match grid[c].ch() {
'"' => text.push_str("&quot;"),
'&' => text.push_str("&amp;"),
'\'' => text.push_str("&apos;"),
'<' => text.push_str("&lt;"),
'>' => text.push_str("&gt;"),
' ' if is_start => {
prev_x_fg = x + 1;
}
c => text.push(c),
}
if grid[c].ch() != ' ' {
is_start = false;
}
}
if cur_bg != Color::Default {
let mut rect = Rectangle::new()
.set("x", prev_x_bg * 8)
.set("y", 17 * row_idx)
.set("width", (width - prev_x_bg) * 8 + 1)
.set("bgname", format!("{:?}", cur_bg))
.set("height", 18);
match cur_bg {
Color::Rgb(r, g, b) => {
rect = rect.set("fill", format!("#{:02x}{:02x}{:02x}", r, g, b).as_str());
}
Color::Default => {
unreachable!();
}
c if c.as_byte() < 16 => {
rect = rect.set("class", format!("color{}", c.as_byte()).as_str());
}
c => {
let c = c.as_byte();
let (r, g, b) = XTERM_COLORS[c as usize];
rect = rect.set("fill", format!("#{:02x}{:02x}{:02x}", r, g, b).as_str());
}
}
rows_group = rows_group.add(rect);
}
if !text.is_empty() {
let text_length = text.split_graphemes().len();
let mut text_el = Text::new()
.add(TextNode::new(&text))
.set("x", prev_x_fg * 8)
.set("textLength", text_length * 8)
.set("fgname", format!("{:?}", cur_fg));
if cur_attrs.intersects(Attr::BOLD) {
text_el = text_el.set("font-weight", "bold");
}
if cur_attrs.intersects(Attr::ITALICS) {
text_el = text_el.set("font-style", "italic");
}
if cur_attrs.intersects(Attr::UNDERLINE) {
text_el = text_el.set("text-decoration", "underline");
}
if cur_attrs.intersects(Attr::DIM) {
text_el = text_el.set("font-weight", "lighter");
}
if cur_attrs.intersects(Attr::HIDDEN) {
text_el = text_el.set("display", "none");
}
match cur_fg {
Color::Default if cur_attrs.intersects(Attr::REVERSE) => {
text_el = text_el.set("class", "background");
}
Color::Default => {
text_el = text_el.set("class", "foreground");
}
Color::Rgb(r, g, b) => {
text_el =
text_el.set("fill", format!("#{:02x}{:02x}{:02x}", r, g, b).as_str());
}
c if c.as_byte() < 16 => {
text_el = text_el.set("class", format!("color{}", c.as_byte()).as_str());
}
c => {
let c = c.as_byte();
let (r, g, b) = XTERM_COLORS[c as usize];
text_el =
text_el.set("fill", format!("#{:02x}{:02x}{:02x}", r, g, b).as_str());
}
}
row_group = row_group.add(text_el);
text.clear();
}
definitions = definitions.add(row_group);
rows_group = rows_group.add(
Use::new()
.set("xlink:href", format!("#g{}", row_idx + 1))
.set("y", 17 * row_idx),
);
}
let document = Document::new()
.set("viewBox", (0, 0, width * 8, height * 17 + 2))
.set("width", width * 8)
.set("height", height * 17 + 2)
.add(
Definitions::new().add(
Style::new(CSS_STYLE)
.set("id", "generated-style")
.set("type", "text/css"),
),
)
.add(
Document::new()
.set("id", "terminal")
.set("preserveAspectRatio", "xMidYMin slice")
.set("viewBox", (0, 0, width * 8, height * 17))
.set("width", width * 8)
.set("height", height * 17)
.add(
Rectangle::new()
.set("class", "background")
.set("height", "100%")
.set("width", "100%")
.set("x", 0)
.set("y", 0),
)
.add(definitions)
.add(rows_group),
)
.set("xmlns", "http://www.w3.org/2000/svg")
.set("baseProfile", "full")
.set("xmlns:xlink", "http://www.w3.org/1999/xlink")
.set("version", "1.1");
let mut s = Vec::new();
svg_crate::write(&mut s, &document).unwrap();
let mut res = Vec::new();
/*
* svg crate formats text nodes like this:
*
* <text>
* actual content
* </text>
*
* But we don't want any extra newlines before/after the tags:
*
* <text>actual content</text>
*
* So remove all new lines from SVG file.
*/
for b in s {
if b == b'\n' {
continue;
}
res.push(b);
}
let mut filename = melib::datetime::timestamp_to_string(
melib::datetime::now(),
Some("meli Screenshot - %e %h %Y %H:%M:%S.svg"),
);
while std::path::Path::new(&filename).exists() {
filename.pop();
filename.pop();
filename.pop();
filename.pop();
filename.push_str("_.svg");
}
std::fs::File::create(&filename)
.unwrap()
.write_all(&res)
.unwrap();
context.replies.push_back(UIEvent::Notification(
Some("Screenshot saved".into()),
format!("Screenshot saved to {}", filename),
None,
));
}
fn process_event(&mut self, event: &mut UIEvent, _context: &mut Context) -> bool {
if let UIEvent::Input(Key::F(6)) = event {
self.save_screenshot = true;
true
} else if let UIEvent::ExInput(Key::F(6)) = event {
self.save_screenshot = true;
true
} else if let UIEvent::EmbedInput((Key::F(6), _)) = event {
self.save_screenshot = true;
false
} else {
false
}
}
fn set_dirty(&mut self, _value: bool) {}
fn is_dirty(&self) -> bool {
self.save_screenshot
}
fn id(&self) -> ComponentId {
ComponentId::nil()
}
fn set_id(&mut self, _id: ComponentId) {}
}
const CSS_STYLE: &'static str = r#"#terminal {
font-family: 'DejaVu Sans Mono', monospace;
font-style: normal;
font-size: 14px;
}
text {
dominant-baseline: text-before-edge;
white-space: pre;
}
/* The colors defined below are the default 16 colors used for rendering text of the terminal. Adjust them as needed. xterm colors based on https://en.wikipedia.org/wiki/ANSI_escape_code#Colors */
.foreground {fill: #e5e5e5;}
.background {fill: #000000;}
.color0 {fill: #000000;}
.color1 {fill: #cd0000;}
.color2 {fill: #00cd00;}
.color3 {fill: #cdcd00;}
.color4 {fill: #0000ee;}
.color5 {fill: #cd00cd;}
.color6 {fill: #00cdcd;}
.color7 {fill: #e5e5e5;}
.color8 {fill: #7f7f7f;}
.color9 {fill: #ff0000;}
.color10 {fill: #00ff00;}
.color11 {fill: #ffff00;}
.color12 {fill: #5c5cff;}
.color13 {fill: #ff00ff;}
.color14 {fill: #00ffff;}
.color15 {fill: #ffffff;}"#;
const XTERM_COLORS: &'static [(u8, u8, u8)] = &[
/*0*/ (0, 0, 0),
/*1*/ (128, 0, 0),
/*2*/ (0, 128, 0),
/*3*/ (128, 128, 0),
/*4*/ (0, 0, 128),
/*5*/ (128, 0, 128),
/*6*/ (0, 128, 128),
/*7*/ (192, 192, 192),
/*8*/ (128, 128, 128),
/*9*/ (255, 0, 0),
/*10*/ (0, 255, 0),
/*11*/ (255, 255, 0),
/*12*/ (0, 0, 255),
/*13*/ (255, 0, 255),
/*14*/ (0, 255, 255),
/*15*/ (255, 255, 255),
/*16*/ (0, 0, 0),
/*17*/ (0, 0, 95),
/*18*/ (0, 0, 135),
/*19*/ (0, 0, 175),
/*20*/ (0, 0, 215),
/*21*/ (0, 0, 255),
/*22*/ (0, 95, 0),
/*23*/ (0, 95, 95),
/*24*/ (0, 95, 135),
/*25*/ (0, 95, 175),
/*26*/ (0, 95, 215),
/*27*/ (0, 95, 255),
/*28*/ (0, 135, 0),
/*29*/ (0, 135, 95),
/*30*/ (0, 135, 135),
/*31*/ (0, 135, 175),
/*32*/ (0, 135, 215),
/*33*/ (0, 135, 255),
/*34*/ (0, 175, 0),
/*35*/ (0, 175, 95),
/*36*/ (0, 175, 135),
/*37*/ (0, 175, 175),
/*38*/ (0, 175, 215),
/*39*/ (0, 175, 255),
/*40*/ (0, 215, 0),
/*41*/ (0, 215, 95),
/*42*/ (0, 215, 135),
/*43*/ (0, 215, 175),
/*44*/ (0, 215, 215),
/*45*/ (0, 215, 255),
/*46*/ (0, 255, 0),
/*47*/ (0, 255, 95),
/*48*/ (0, 255, 135),
/*49*/ (0, 255, 175),
/*50*/ (0, 255, 215),
/*51*/ (0, 255, 255),
/*52*/ (95, 0, 0),
/*53*/ (95, 0, 95),
/*54*/ (95, 0, 135),
/*55*/ (95, 0, 175),
/*56*/ (95, 0, 215),
/*57*/ (95, 0, 255),
/*58*/ (95, 95, 0),
/*59*/ (95, 95, 95),
/*60*/ (95, 95, 135),
/*61*/ (95, 95, 175),
/*62*/ (95, 95, 215),
/*63*/ (95, 95, 255),
/*64*/ (95, 135, 0),
/*65*/ (95, 135, 95),
/*66*/ (95, 135, 135),
/*67*/ (95, 135, 175),
/*68*/ (95, 135, 215),
/*69*/ (95, 135, 255),
/*70*/ (95, 175, 0),
/*71*/ (95, 175, 95),
/*72*/ (95, 175, 135),
/*73*/ (95, 175, 175),
/*74*/ (95, 175, 215),
/*75*/ (95, 175, 255),
/*76*/ (95, 215, 0),
/*77*/ (95, 215, 95),
/*78*/ (95, 215, 135),
/*79*/ (95, 215, 175),
/*80*/ (95, 215, 215),
/*81*/ (95, 215, 255),
/*82*/ (95, 255, 0),
/*83*/ (95, 255, 95),
/*84*/ (95, 255, 135),
/*85*/ (95, 255, 175),
/*86*/ (95, 255, 215),
/*87*/ (95, 255, 255),
/*88*/ (135, 0, 0),
/*89*/ (135, 0, 95),
/*90*/ (135, 0, 135),
/*91*/ (135, 0, 175),
/*92*/ (135, 0, 215),
/*93*/ (135, 0, 255),
/*94*/ (135, 95, 0),
/*95*/ (135, 95, 95),
/*96*/ (135, 95, 135),
/*97*/ (135, 95, 175),
/*98*/ (135, 95, 215),
/*99*/ (135, 95, 255),
/*100*/ (135, 135, 0),
/*101*/ (135, 135, 95),
/*102*/ (135, 135, 135),
/*103*/ (135, 135, 175),
/*104*/ (135, 135, 215),
/*105*/ (135, 135, 255),
/*106*/ (135, 175, 0),
/*107*/ (135, 175, 95),
/*108*/ (135, 175, 135),
/*109*/ (135, 175, 175),
/*110*/ (135, 175, 215),
/*111*/ (135, 175, 255),
/*112*/ (135, 215, 0),
/*113*/ (135, 215, 95),
/*114*/ (135, 215, 135),
/*115*/ (135, 215, 175),
/*116*/ (135, 215, 215),
/*117*/ (135, 215, 255),
/*118*/ (135, 255, 0),
/*119*/ (135, 255, 95),
/*120*/ (135, 255, 135),
/*121*/ (135, 255, 175),
/*122*/ (135, 255, 215),
/*123*/ (135, 255, 255),
/*124*/ (175, 0, 0),
/*125*/ (175, 0, 95),
/*126*/ (175, 0, 135),
/*127*/ (175, 0, 175),
/*128*/ (175, 0, 215),
/*129*/ (175, 0, 255),
/*130*/ (175, 95, 0),
/*131*/ (175, 95, 95),
/*132*/ (175, 95, 135),
/*133*/ (175, 95, 175),
/*134*/ (175, 95, 215),
/*135*/ (175, 95, 255),
/*136*/ (175, 135, 0),
/*137*/ (175, 135, 95),
/*138*/ (175, 135, 135),
/*139*/ (175, 135, 175),
/*140*/ (175, 135, 215),
/*141*/ (175, 135, 255),
/*142*/ (175, 175, 0),
/*143*/ (175, 175, 95),
/*144*/ (175, 175, 135),
/*145*/ (175, 175, 175),
/*146*/ (175, 175, 215),
/*147*/ (175, 175, 255),
/*148*/ (175, 215, 0),
/*149*/ (175, 215, 95),
/*150*/ (175, 215, 135),
/*151*/ (175, 215, 175),
/*152*/ (175, 215, 215),
/*153*/ (175, 215, 255),
/*154*/ (175, 255, 0),
/*155*/ (175, 255, 95),
/*156*/ (175, 255, 135),
/*157*/ (175, 255, 175),
/*158*/ (175, 255, 215),
/*159*/ (175, 255, 255),
/*160*/ (215, 0, 0),
/*161*/ (215, 0, 95),
/*162*/ (215, 0, 135),
/*163*/ (215, 0, 175),
/*164*/ (215, 0, 215),
/*165*/ (215, 0, 255),
/*166*/ (215, 95, 0),
/*167*/ (215, 95, 95),
/*168*/ (215, 95, 135),
/*169*/ (215, 95, 175),
/*170*/ (215, 95, 215),
/*171*/ (215, 95, 255),
/*172*/ (215, 135, 0),
/*173*/ (215, 135, 95),
/*174*/ (215, 135, 135),
/*175*/ (215, 135, 175),
/*176*/ (215, 135, 215),
/*177*/ (215, 135, 255),
/*178*/ (215, 175, 0),
/*179*/ (215, 175, 95),
/*180*/ (215, 175, 135),
/*181*/ (215, 175, 175),
/*182*/ (215, 175, 215),
/*183*/ (215, 175, 255),
/*184*/ (215, 215, 0),
/*185*/ (215, 215, 95),
/*186*/ (215, 215, 135),
/*187*/ (215, 215, 175),
/*188*/ (215, 215, 215),
/*189*/ (215, 215, 255),
/*190*/ (215, 255, 0),
/*191*/ (215, 255, 95),
/*192*/ (215, 255, 135),
/*193*/ (215, 255, 175),
/*194*/ (215, 255, 215),
/*195*/ (215, 255, 255),
/*196*/ (255, 0, 0),
/*197*/ (255, 0, 95),
/*198*/ (255, 0, 135),
/*199*/ (255, 0, 175),
/*200*/ (255, 0, 215),
/*201*/ (255, 0, 255),
/*202*/ (255, 95, 0),
/*203*/ (255, 95, 95),
/*204*/ (255, 95, 135),
/*205*/ (255, 95, 175),
/*206*/ (255, 95, 215),
/*207*/ (255, 95, 255),
/*208*/ (255, 135, 0),
/*209*/ (255, 135, 95),
/*210*/ (255, 135, 135),
/*211*/ (255, 135, 175),
/*212*/ (255, 135, 215),
/*213*/ (255, 135, 255),
/*214*/ (255, 175, 0),
/*215*/ (255, 175, 95),
/*216*/ (255, 175, 135),
/*217*/ (255, 175, 175),
/*218*/ (255, 175, 215),
/*219*/ (255, 175, 255),
/*220*/ (255, 215, 0),
/*221*/ (255, 215, 95),
/*222*/ (255, 215, 135),
/*223*/ (255, 215, 175),
/*224*/ (255, 215, 215),
/*225*/ (255, 215, 255),
/*226*/ (255, 255, 0),
/*227*/ (255, 255, 95),
/*228*/ (255, 255, 135),
/*229*/ (255, 255, 175),
/*230*/ (255, 255, 215),
/*231*/ (255, 255, 255),
/*232*/ (8, 8, 8),
/*233*/ (18, 18, 18),
/*234*/ (28, 28, 28),
/*235*/ (38, 38, 38),
/*236*/ (48, 48, 48),
/*237*/ (58, 58, 58),
/*238*/ (68, 68, 68),
/*239*/ (78, 78, 78),
/*240*/ (88, 88, 88),
/*241*/ (98, 98, 98),
/*242*/ (108, 108, 108),
/*243*/ (118, 118, 118),
/*244*/ (128, 128, 128),
/*245*/ (138, 138, 138),
/*246*/ (148, 148, 148),
/*247*/ (158, 158, 158),
/*248*/ (168, 168, 168),
/*249*/ (178, 178, 178),
/*250*/ (188, 188, 188),
/*251*/ (198, 198, 198),
/*252*/ (208, 208, 208),
/*253*/ (218, 218, 218),
/*254*/ (228, 228, 228),
/*255*/ (238, 238, 238),
];