From f193f4fd30b52bde83e21c6242c2d04599db5ee3 Mon Sep 17 00:00:00 2001 From: Marrub Date: Fri, 9 Nov 2018 22:10:14 -0500 Subject: [PATCH] initial commit --- .gitignore | 4 + Cargo.toml | 15 +++ src/bin.rs | 62 ++++++++++ src/crc.rs | 18 +++ src/err.rs | 8 ++ src/gtkcrap.rs | 51 +++++++++ src/icon.rs | 38 +++++++ src/main.rs | 299 +++++++++++++++++++++++++++++++++++++++++++++++++ src/png.rs | 77 +++++++++++++ 9 files changed, 572 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 src/bin.rs create mode 100644 src/crc.rs create mode 100644 src/err.rs create mode 100644 src/gtkcrap.rs create mode 100644 src/icon.rs create mode 100644 src/main.rs create mode 100644 src/png.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8840279 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.png +*.swp +target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2d429de --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "grope" +version = "0.1.0" +authors = ["Alison Sanderson"] +edition = "2018" + +[dependencies] +cairo-rs = {version = "0.5", features = ["png"]} +gdk = "0.9" +gdk-pixbuf = "0.5" +glib = "0.6" +gtk = "0.5" +gtk-sys = "0.7" +libc = "0.2" +memmap = "0.7" diff --git a/src/bin.rs b/src/bin.rs new file mode 100644 index 0000000..bf61643 --- /dev/null +++ b/src/bin.rs @@ -0,0 +1,62 @@ +//! Binary data conversion utilities. + +pub type Ident = [u8; 4]; + +pub type ResultS = Result; + +pub trait BinToNum +{ + // Checked + fn c_iden(&self, i: usize) -> ResultS; + fn c_u32b(&self, i: usize) -> ResultS; + fn c_u16b(&self, i: usize) -> ResultS; + + fn c_i32b(&self, i: usize) -> ResultS + {match self.c_u32b(i) {Ok(n) => Ok(n as i32), Err(e) => Err(e)}} + fn c_i16b(&self, i: usize) -> ResultS + {match self.c_u16b(i) {Ok(n) => Ok(n as i16), Err(e) => Err(e)}} + + // Optional + fn o_iden(&self, i: usize) -> Option {self.c_iden(i).ok()} + fn o_u32b(&self, i: usize) -> Option {self.c_u32b(i).ok()} + fn o_u16b(&self, i: usize) -> Option {self.c_u16b(i).ok()} + fn o_i32b(&self, i: usize) -> Option {self.c_i32b(i).ok()} + fn o_i16b(&self, i: usize) -> Option {self.c_i16b(i).ok()} + + // Unchecked + fn b_iden(&self, i: usize) -> Ident {self.c_iden(i).unwrap()} + fn b_u32b(&self, i: usize) -> u32 {self.c_u32b(i).unwrap()} + fn b_u16b(&self, i: usize) -> u16 {self.c_u16b(i).unwrap()} + fn b_i32b(&self, i: usize) -> i32 {self.c_i32b(i).unwrap()} + fn b_i16b(&self, i: usize) -> i16 {self.c_i16b(i).unwrap()} +} + +impl BinToNum for [u8] +{ + fn c_iden(&self, i: usize) -> ResultS + { + if i + 3 >= self.len() {return Err("not enough data")} + Ok([self[i], self[i+1], self[i+2], self[i+3]]) + } + + fn c_u32b(&self, i: usize) -> ResultS + { + if i + 3 >= self.len() {return Err("not enough data")} + Ok(self[i ] as (u32) << 24 | self[i+1] as (u32) << 16 | + self[i+2] as (u32) << 8 | self[i+3] as (u32)) + } + + fn c_u16b(&self, i: usize) -> ResultS + { + if i + 1 >= self.len() {return Err("not enough data")} + Ok(self[i] as (u16) << 8 | self[i+1] as (u16)) + } +} + +pub fn d_u32b(n: u32) -> [u8; 4] {[(n >> 24) as u8, (n >> 16) as u8, + (n >> 8) as u8, (n >> 0) as u8]} +pub fn d_u16b(n: u16) -> [u8; 2] {[(n >> 8) as u8, (n >> 0) as u8]} +pub fn d_i32b(n: i32) -> [u8; 4] {d_u32b(n as u32)} +pub fn d_i16b(n: i16) -> [u8; 2] {d_u16b(n as u16)} + +// EOF diff --git a/src/crc.rs b/src/crc.rs new file mode 100644 index 0000000..1ddeb37 --- /dev/null +++ b/src/crc.rs @@ -0,0 +1,18 @@ +fn crc_init() -> [u32; 256] +{ + let mut t = [0; 256]; + for n in 0..256 + { + t[n] = (0..8).fold(n as u32, |a, _| + {if a & 1 == 1 {0xedb88320 ^ a >> 1} else {a >> 1}}); + } + t +} + +pub fn crc32(b: &[u8], s: u32) -> u32 +{ + let t = crc_init(); + !b.iter().fold(!s, |a, &o| {a >> 8 ^ t[(a & 0xff ^ o as u32) as usize]}) +} + +// EOF diff --git a/src/err.rs b/src/err.rs new file mode 100644 index 0000000..298ca4e --- /dev/null +++ b/src/err.rs @@ -0,0 +1,8 @@ +#[derive(Debug)] pub struct StrErr(pub String); + +impl From for StrErr + {fn from(e: T) -> Self {StrErr(e.to_string())}} + +impl StrErr {pub fn new(e: impl ToString) -> Self {StrErr(e.to_string())}} + +// EOF diff --git a/src/gtkcrap.rs b/src/gtkcrap.rs new file mode 100644 index 0000000..4ed4e9c --- /dev/null +++ b/src/gtkcrap.rs @@ -0,0 +1,51 @@ +use crate::err; +use glib::object::Downcast; +use glib::translate::{FromGlibPtrNone, Stash, ToGlib, ToGlibPtr}; +use gtk::{FileChooserAction, prelude::*}; +use gtk_sys::*; +use libc::{c_char, c_void}; +use std::ptr; + +pub fn file_chooser(title: &str, action: FileChooserAction, parent: >k::Window) -> gtk::FileChooserDialog +{ + unsafe + { + let title = Some(title); + let title = title.to_glib_none(); + let parent = parent.to_glib_none(); + let first = Some("_Cancel"); + let first = first.to_glib_none(); + let second = Some(match action { + FileChooserAction::Open => "_Open", + FileChooserAction::Save => "_Save", + FileChooserAction::SelectFolder => "_Select", + FileChooserAction::CreateFolder => "_Create", + FileChooserAction::__Unknown(_) => "_Choose", + }); + let second: Stash<*const c_char, Option<&str>> = second.to_glib_none(); + gtk::Widget::from_glib_none(gtk_file_chooser_dialog_new( + title.0, parent.0, + action.to_glib(), + first.0, GTK_RESPONSE_CANCEL, + second.0, GTK_RESPONSE_ACCEPT, + ptr::null() as *const c_void)).downcast_unchecked() + } +} + +pub fn err_dlg(w: >k::Window, r: Result<(), err::StrErr>) +{ + match r { + Ok(()) => (), + Err(e) => { + let dlg = gtk::MessageDialog::new(Some(w), + gtk::DialogFlags::MODAL, + gtk::MessageType::Error, + gtk::ButtonsType::Ok, + &e.0); + dlg.run(); + dlg.destroy(); + } + } +} + +// EOF diff --git a/src/icon.rs b/src/icon.rs new file mode 100644 index 0000000..0685452 --- /dev/null +++ b/src/icon.rs @@ -0,0 +1,38 @@ +pub static ICON: [&str; 35] = [ + "32 32 2 1 ", + " c black", + ". c white", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ... . . .. ... .. ", + " . . .. . . . . . . ", + " . . . . . . . .... ", + " . . . . . . . . ", + " ... . .. ... ... ", + " . . ", + " ... . ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " "]; + +// EOF diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..3a6de8d --- /dev/null +++ b/src/main.rs @@ -0,0 +1,299 @@ +#![windows_subsystem="windows"] + +#[allow(dead_code)] mod bin; +#[allow(dead_code)] mod crc; + mod err; + mod gtkcrap; + mod icon; +#[allow(dead_code)] mod png; + +use cairo::ImageSurface; +use crate::{bin::*, err::*, gtkcrap::*, png::*}; +use gdk::{EventMask, enums::key}; +use gtk::{FileChooserAction, prelude::*}; +use memmap::Mmap; +use std::{cell::RefCell, fs::File, path::{Path, PathBuf}, rc::Rc}; + +fn drag(e: &gdk::EventMotion, v: &mut Stuff) -> Inhibit +{ + if e.get_state().contains(gdk::ModifierType::BUTTON3_MASK) + { + let p = e.get_position(); + + if let Some(c) = &mut v.c + { + v.v.0 -= (c.0 - p.0) / 2.0; + v.v.1 -= (c.1 - p.1) / 2.0; + v.update(); + } + + v.c = Some(p); + + Inhibit(true) + } + else if e.get_state().contains(gdk::ModifierType::BUTTON1_MASK) + { + if let None = v.png {return Inhibit(false)} + + let p = e.get_position(); + + if let Some(c) = &mut v.c + { + v.p.0 += (c.0 - p.0) as i32; + v.p.1 += (c.1 - p.1) as i32; + v.update(); + } + + v.c = Some(p); + + Inhibit(true) + } + else + {Inhibit(false)} +} + +fn drop(_e: &gdk::EventButton, v: &mut Stuff) -> Inhibit +{ + v.c = None; + + Inhibit(false) +} + +fn draw(c: &cairo::Context, v: &mut Stuff) -> Inhibit +{ + const VW: f64 = 320.0; + const VH: f64 = 200.0; + const HW: f64 = 160.0; + const HH: f64 = 100.0; + + // scale to 320x200 (1.2x height) + c.scale(1.0, 1.2); + + // draw background + c.set_source_rgb(167.0/255.0, 107.0/255.0, 107.0/255.0); + c.rectangle(0.0, 0.0, VW, VH); + c.fill(); + + // center + c.translate(HW + v.v.0, HH + v.v.1); + + // guide lines + c.set_source_rgb(0.5, 0.0, 0.0); + c.set_line_width(1.0); + + // center + c.move_to(-VW, 0.0); c.line_to( VW, 0.0); c.stroke(); + c.move_to(0.0, -VH); c.line_to(0.0, VH); c.stroke(); + + // hud borders + c.move_to(0.0, VH); c.line_to(VW, VH); c.stroke(); + c.move_to( VW, 0.0); c.line_to(VW, VH); c.stroke(); + + // statusbar + c.move_to(0.0, VH-32.0); c.line_to(VW, VH-32.0); c.stroke(); // doom + c.move_to(0.0, VH-38.0); c.line_to(VW, VH-38.0); c.stroke(); // hexen + c.move_to(0.0, VH-42.0); c.line_to(VW, VH-42.0); c.stroke(); // heretic + + // draw image + if let Some(img) = &v.img + { + c.set_source_surface(img, 0.0 - v.p.0 as f64, 0.0 - v.p.1 as f64); + c.paint(); + } + + Inhibit(false) +} + +fn key(k: &gdk::EventKey, v: &mut Stuff) -> Inhibit +{ + if let None = v.png {return Inhibit(false)} + match k.get_keyval() { + key::Left => {v.p.0 += 1; v.update(); Inhibit(true)}, + key::Right => {v.p.0 -= 1; v.update(); Inhibit(true)}, + key::Up => {v.p.1 += 1; v.update(); Inhibit(true)}, + key::Down => {v.p.1 -= 1; v.update(); Inhibit(true)}, + _ => Inhibit(false), + } +} + +fn open(v: &mut Stuff) +{ + let fc = file_chooser("Open Image", FileChooserAction::Open, &v.win); + + if fc.run() == gtk_sys::GTK_RESPONSE_ACCEPT { + if let Some(p) = fc.get_filename() { + let r = v.set_img(&p); err_dlg(&v.win, r); + } + } + + fc.destroy(); +} + +fn save(v: &mut Stuff) + {if let Some(p) = &v.path {let r = v.save_img(&p); err_dlg(&v.win, r)}} + +fn save_as(v: &mut Stuff) +{ + let fc = file_chooser("Save Image", FileChooserAction::Save, &v.win); + + if fc.run() == gtk_sys::GTK_RESPONSE_ACCEPT { + if let Some(p) = fc.get_filename() { + let r = v.save_img(&p); err_dlg(&v.win, r); + } + } + + fc.destroy(); +} + +#[allow(dead_code)] +struct Stuff +{ + p: (i32, i32), + s: (u32, u32), + png: Option, + img: Option, + + v: (f64, f64), + c: Option<(f64, f64)>, + + path: Option, + + lx: gtk::Label, + ly: gtk::Label, + xh: gtk::Box, + area: gtk::DrawingArea, + xv: gtk::Box, + win: gtk::Window, +} + +impl Stuff +{ + fn update(&self) + { + self.lx.set_text(&(-self.p.0).to_string()); + self.ly.set_text(&(-self.p.1).to_string()); + self.area.queue_draw(); + } + + fn set_img(&mut self, fname: &Path) -> Result<(), StrErr> + { + let mut fp = File::open(fname)?; + let png = PngFile::new(&unsafe{Mmap::map(&fp)?})?; + let img = ImageSurface::create_from_png(&mut fp)?; + + let xy = + if let Some(xy) = png.chunk(*b"grAb", |c| (c.b_i32b(0), c.b_i32b(4))) + {xy} + else + {(0, 0)}; + + let wh = png.chunk(*b"IHDR", |c| (c.b_u32b(0), c.b_u32b(4))) + .ok_or(StrErr::new("no IHDR"))?; + + self.p = xy; + self.s = wh; + self.png = Some(png); + self.img = Some(img); + self.path = Some(fname.canonicalize()?); + + self.update(); + + Ok(()) + } + + fn save_img(&self, fname: &Path) -> Result<(), StrErr> + { + match &self.png { + Some(png) => { + let mut png = png.clone(); + let mut c = PngChunk{typ: *b"grAb", dat: Vec::new()}; + for b in &d_i32b(self.p.0) {c.dat.push(*b)} + for b in &d_i32b(self.p.1) {c.dat.push(*b)} + if let Some(n) = png.find_chunk(*b"grAb") {png.chnk[n] = c} + else {png.chnk[1] = c} + png.write(&mut File::create(fname)?)?; + Ok(()) + } + None => Err("no image loaded".into()) + } + } +} + +fn btn(b: >k::Button, r: Rc>, f: F) + where F: Fn(&mut Stuff) + 'static +{ + b.connect_clicked(move |_| + {if let Ok(mut v) = r.try_borrow_mut() {f(&mut v)}}); +} + +fn cb(r: Rc>, + f: impl Fn(&T, &mut Stuff) -> Inhibit + 'static) + -> impl Fn(&U, &T) -> Inhibit + 'static +{ + move |_, e| { + if let Ok(mut v) = r.try_borrow_mut() {f(e, &mut v)} + else {Inhibit(false)} + } +} + +fn main() -> Result<(), StrErr> +{ + gtk::init()?; + + let ly = gtk::Label::new(None); + let lx = gtk::Label::new(None); + + let bs = gtk::Button::new_with_mnemonic("_As"); + let bq = gtk::Button::new_with_mnemonic("_Save"); + let bo = gtk::Button::new_with_mnemonic("_Open"); + + let xh = gtk::Box::new(gtk::Orientation::Horizontal, 4); + xh.add(&bo); + xh.add(&bq); + xh.add(&bs); + xh.add(&lx); + xh.add(&ly); + + let area = gtk::DrawingArea::new(); + area.set_size_request(320, 240); + + let xv = gtk::Box::new(gtk::Orientation::Vertical, 4); + xv.add(&area); + xv.add(&xh); + + let win = gtk::Window::new(gtk::WindowType::Toplevel); + win.set_title("grAb editor"); + win.set_resizable(false); + win.set_icon(Some(&gdk_pixbuf::Pixbuf::new_from_xpm_data(&icon::ICON))); + win.add(&xv); + + let v = Rc::new(RefCell::new( + Stuff{p:(0,0),s:(0,0),v:(0.0,0.0),c:None, + path:None, png:None,img:None, lx,ly, xh,xv, area,win})); + + btn(&bo, v.clone(), open); + btn(&bq, v.clone(), save); + btn(&bs, v.clone(), save_as); + + { + let vr = &mut *v.borrow_mut(); + vr.update(); + + vr.area.connect_draw (cb(v.clone(), draw)); + vr.win.connect_key_press_event (cb(v.clone(), key)); + vr.win.connect_motion_notify_event (cb(v.clone(), drag)); + vr.win.connect_button_release_event(cb(v.clone(), drop)); + vr.win.connect_delete_event(|_, _| {gtk::main_quit(); Inhibit(false)}); + vr.win.set_events((EventMask::KEY_PRESS_MASK | + EventMask::BUTTON1_MOTION_MASK | + EventMask::BUTTON3_MOTION_MASK | + EventMask::BUTTON_RELEASE_MASK).bits() as i32); + vr.win.show_all(); + } + + gtk::main(); + + Ok(()) +} + +// EOF diff --git a/src/png.rs b/src/png.rs new file mode 100644 index 0000000..e2134ea --- /dev/null +++ b/src/png.rs @@ -0,0 +1,77 @@ +use crate::{bin::*, crc::*}; +use std::{fs::File, io::prelude::*}; + +#[derive(Clone)] +#[derive(Debug)] +pub struct PngChunk +{ + pub typ: Ident, + pub dat: Vec, +} + +#[derive(Clone)] +#[derive(Debug)] +pub struct PngFile +{ + pub chnk: Vec, +} + +impl PngFile +{ + pub fn new(b: &[u8]) -> Result + { + if &b[0..8] != b"\x89PNG\r\n\x1a\n" {return Err("invalid header")} + + let mut chnk = Vec::new(); + let mut p = 8; + + loop + { + let len = b.c_u32b(p )? as usize; + let typ = b.c_iden(p+4 )?; + let crc = b.c_u32b(p+8+len)?; + + if crc32(&b[p+4..p+8+len], 0) != crc + {return Err("invalid CRC in chunk")} + + chnk.push(PngChunk{typ, dat: b[p+8..p+8+len].to_vec()}); + + if typ == *b"IEND" {break} else {p += len + 12} + } + + Ok(PngFile{chnk}) + } + + pub fn write(&self, fp: &mut File) -> std::io::Result<()> + { + fp.write_all(b"\x89PNG\r\n\x1a\n")?; + for c in &self.chnk + { + fp.write_all(&d_u32b(c.dat.len() as u32))?; + fp.write_all(&c.typ)?; + fp.write_all(&c.dat)?; + fp.write_all(&d_u32b(crc32(&c.dat, crc32(&c.typ, 0))))?; + } + Ok(()) + } + + pub fn has_chunk(&self, t: Ident) -> bool + { + for c in &self.chnk {if c.typ == t {return true}} + false + } + + pub fn find_chunk(&self, t: Ident) -> Option + { + for i in 0..self.chnk.len() {if self.chnk[i].typ == t {return Some(i)}} + None + } + + pub fn chunk Ret>(&self, t: Ident, f: F) -> Option + { + for c in &self.chnk {if c.typ == t {return Some(f(&c.dat))}} + None + } +} + +// EOF