├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── main.rs ├── texture-fs.glsl └── texture-vs.glsl /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | *.png 4 | *.jpeg 5 | *.jpg 6 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "adler" 7 | version = "1.0.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 10 | 11 | [[package]] 12 | name = "adler32" 13 | version = "1.2.0" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" 16 | 17 | [[package]] 18 | name = "argh" 19 | version = "0.1.7" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "dbb41d85d92dfab96cb95ab023c265c5e4261bb956c0fb49ca06d90c570f1958" 22 | dependencies = [ 23 | "argh_derive", 24 | "argh_shared", 25 | ] 26 | 27 | [[package]] 28 | name = "argh_derive" 29 | version = "0.1.7" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | checksum = "be69f70ef5497dd6ab331a50bd95c6ac6b8f7f17a7967838332743fbd58dc3b5" 32 | dependencies = [ 33 | "argh_shared", 34 | "heck", 35 | "proc-macro2", 36 | "quote", 37 | "syn", 38 | ] 39 | 40 | [[package]] 41 | name = "argh_shared" 42 | version = "0.1.7" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "e6f8c380fa28aa1b36107cd97f0196474bb7241bb95a453c5c01a15ac74b2eac" 45 | 46 | [[package]] 47 | name = "autocfg" 48 | version = "1.1.0" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 51 | 52 | [[package]] 53 | name = "bitflags" 54 | version = "1.3.2" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 57 | 58 | [[package]] 59 | name = "bytemuck" 60 | version = "1.9.1" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "cdead85bdec19c194affaeeb670c0e41fe23de31459efd1c174d049269cf02cc" 63 | 64 | [[package]] 65 | name = "byteorder" 66 | version = "1.4.3" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 69 | 70 | [[package]] 71 | name = "cc" 72 | version = "1.0.73" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" 75 | 76 | [[package]] 77 | name = "cfg-if" 78 | version = "1.0.0" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 81 | 82 | [[package]] 83 | name = "cmake" 84 | version = "0.1.48" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "e8ad8cef104ac57b68b89df3208164d228503abbdce70f6880ffa3d970e7443a" 87 | dependencies = [ 88 | "cc", 89 | ] 90 | 91 | [[package]] 92 | name = "color_quant" 93 | version = "1.1.0" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" 96 | 97 | [[package]] 98 | name = "crc32fast" 99 | version = "1.3.2" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" 102 | dependencies = [ 103 | "cfg-if", 104 | ] 105 | 106 | [[package]] 107 | name = "cty" 108 | version = "0.2.2" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" 111 | 112 | [[package]] 113 | name = "deflate" 114 | version = "1.0.0" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "c86f7e25f518f4b81808a2cf1c50996a61f5c2eb394b2393bd87f2a4780a432f" 117 | dependencies = [ 118 | "adler32", 119 | ] 120 | 121 | [[package]] 122 | name = "gl" 123 | version = "0.14.0" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "a94edab108827d67608095e269cf862e60d920f144a5026d3dbcfd8b877fb404" 126 | dependencies = [ 127 | "gl_generator", 128 | ] 129 | 130 | [[package]] 131 | name = "gl_generator" 132 | version = "0.14.0" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" 135 | dependencies = [ 136 | "khronos_api", 137 | "log", 138 | "xml-rs", 139 | ] 140 | 141 | [[package]] 142 | name = "glfw" 143 | version = "0.44.0" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "103c29fa9d30c5371187895a10948d4f54b4111450328b4e439133d88243c5e3" 146 | dependencies = [ 147 | "bitflags", 148 | "glfw-sys", 149 | "objc", 150 | "raw-window-handle", 151 | "winapi", 152 | ] 153 | 154 | [[package]] 155 | name = "glfw-sys" 156 | version = "3.3.5" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "5b0cbb12e2dfac494316e91f6bf7cf070ccd9c61501a02f4af14d71222c9f67f" 159 | dependencies = [ 160 | "cmake", 161 | ] 162 | 163 | [[package]] 164 | name = "heck" 165 | version = "0.3.3" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" 168 | dependencies = [ 169 | "unicode-segmentation", 170 | ] 171 | 172 | [[package]] 173 | name = "image" 174 | version = "0.24.1" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "db207d030ae38f1eb6f240d5a1c1c88ff422aa005d10f8c6c6fc5e75286ab30e" 177 | dependencies = [ 178 | "bytemuck", 179 | "byteorder", 180 | "color_quant", 181 | "jpeg-decoder", 182 | "num-iter", 183 | "num-rational", 184 | "num-traits", 185 | "png", 186 | ] 187 | 188 | [[package]] 189 | name = "jpeg-decoder" 190 | version = "0.2.4" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "744c24117572563a98a7e9168a5ac1ee4a1ca7f702211258797bbe0ed0346c3c" 193 | 194 | [[package]] 195 | name = "khronos_api" 196 | version = "3.1.0" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" 199 | 200 | [[package]] 201 | name = "libc" 202 | version = "0.2.124" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "21a41fed9d98f27ab1c6d161da622a4fa35e8a54a8adc24bbf3ddd0ef70b0e50" 205 | 206 | [[package]] 207 | name = "log" 208 | version = "0.4.16" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8" 211 | dependencies = [ 212 | "cfg-if", 213 | ] 214 | 215 | [[package]] 216 | name = "luminance" 217 | version = "0.47.0" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "edb83392d826ec88cb18dd9caa28cb99e2e5845d9ffbd5c391b118d65046ad1d" 220 | dependencies = [ 221 | "luminance-derive", 222 | ] 223 | 224 | [[package]] 225 | name = "luminance-derive" 226 | version = "0.10.0" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "4bb00ddc3077a19eeb0a4980e8decbfe315626ea6338f0f0347c46d91d3af7de" 229 | dependencies = [ 230 | "proc-macro2", 231 | "quote", 232 | "syn", 233 | ] 234 | 235 | [[package]] 236 | name = "luminance-gl" 237 | version = "0.19.1" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "e22f32fc6bf80347379063f47ceb4c028a89e03e5bc9fad95a37fe6335afc856" 240 | dependencies = [ 241 | "gl", 242 | "luminance", 243 | "luminance-std140", 244 | ] 245 | 246 | [[package]] 247 | name = "luminance-glfw" 248 | version = "0.18.2" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "84bc41f9709dbfa41eba6808688a4dbd8efcd70bdc5aaf1994a9394fa0f481c1" 251 | dependencies = [ 252 | "gl", 253 | "glfw", 254 | "luminance", 255 | "luminance-gl", 256 | ] 257 | 258 | [[package]] 259 | name = "luminance-std140" 260 | version = "0.2.1" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "86dde6bf0af98691bd79163f086e2f207e77bb641b89854c2a330babc38253f9" 263 | dependencies = [ 264 | "luminance", 265 | ] 266 | 267 | [[package]] 268 | name = "malloc_buf" 269 | version = "0.0.6" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" 272 | dependencies = [ 273 | "libc", 274 | ] 275 | 276 | [[package]] 277 | name = "miniz_oxide" 278 | version = "0.5.1" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "d2b29bd4bc3f33391105ebee3589c19197c4271e3e5a9ec9bfe8127eeff8f082" 281 | dependencies = [ 282 | "adler", 283 | ] 284 | 285 | [[package]] 286 | name = "motsu" 287 | version = "0.1.0" 288 | dependencies = [ 289 | "argh", 290 | "glfw", 291 | "image", 292 | "libc", 293 | "luminance", 294 | "luminance-derive", 295 | "luminance-glfw", 296 | ] 297 | 298 | [[package]] 299 | name = "num-integer" 300 | version = "0.1.44" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" 303 | dependencies = [ 304 | "autocfg", 305 | "num-traits", 306 | ] 307 | 308 | [[package]] 309 | name = "num-iter" 310 | version = "0.1.42" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" 313 | dependencies = [ 314 | "autocfg", 315 | "num-integer", 316 | "num-traits", 317 | ] 318 | 319 | [[package]] 320 | name = "num-rational" 321 | version = "0.4.0" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a" 324 | dependencies = [ 325 | "autocfg", 326 | "num-integer", 327 | "num-traits", 328 | ] 329 | 330 | [[package]] 331 | name = "num-traits" 332 | version = "0.2.14" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" 335 | dependencies = [ 336 | "autocfg", 337 | ] 338 | 339 | [[package]] 340 | name = "objc" 341 | version = "0.2.7" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" 344 | dependencies = [ 345 | "malloc_buf", 346 | ] 347 | 348 | [[package]] 349 | name = "png" 350 | version = "0.17.5" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "dc38c0ad57efb786dd57b9864e5b18bae478c00c824dc55a38bbc9da95dde3ba" 353 | dependencies = [ 354 | "bitflags", 355 | "crc32fast", 356 | "deflate", 357 | "miniz_oxide", 358 | ] 359 | 360 | [[package]] 361 | name = "proc-macro2" 362 | version = "1.0.37" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1" 365 | dependencies = [ 366 | "unicode-xid", 367 | ] 368 | 369 | [[package]] 370 | name = "quote" 371 | version = "1.0.18" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" 374 | dependencies = [ 375 | "proc-macro2", 376 | ] 377 | 378 | [[package]] 379 | name = "raw-window-handle" 380 | version = "0.4.3" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "b800beb9b6e7d2df1fe337c9e3d04e3af22a124460fb4c30fcc22c9117cefb41" 383 | dependencies = [ 384 | "cty", 385 | ] 386 | 387 | [[package]] 388 | name = "syn" 389 | version = "1.0.91" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "b683b2b825c8eef438b77c36a06dc262294da3d5a5813fac20da149241dcd44d" 392 | dependencies = [ 393 | "proc-macro2", 394 | "quote", 395 | "unicode-xid", 396 | ] 397 | 398 | [[package]] 399 | name = "unicode-segmentation" 400 | version = "1.9.0" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" 403 | 404 | [[package]] 405 | name = "unicode-xid" 406 | version = "0.2.2" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 409 | 410 | [[package]] 411 | name = "winapi" 412 | version = "0.3.9" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 415 | dependencies = [ 416 | "winapi-i686-pc-windows-gnu", 417 | "winapi-x86_64-pc-windows-gnu", 418 | ] 419 | 420 | [[package]] 421 | name = "winapi-i686-pc-windows-gnu" 422 | version = "0.4.0" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 425 | 426 | [[package]] 427 | name = "winapi-x86_64-pc-windows-gnu" 428 | version = "0.4.0" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 431 | 432 | [[package]] 433 | name = "xml-rs" 434 | version = "0.8.4" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" 437 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "motsu" 3 | version = "0.1.0" 4 | authors = ["Sam Barr "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | libc = "*" 11 | luminance = "*" 12 | luminance-derive = "*" 13 | luminance-glfw = "*" 14 | glfw = "0.44.0" 15 | argh = "*" 16 | 17 | [dependencies.image] 18 | version = "*" 19 | default-features = false 20 | features = [ "jpeg", "png" ] 21 | 22 | [profile.release] 23 | lto = "fat" 24 | codegen-units = 1 25 | panic = "abort" 26 | strip = true 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License (MIT) 2 | 3 | Copyright (c) 2020. Sam Barr 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Motsu 2 | 3 | Motsu is a simple image viewer and cropper. 4 | 5 | ## Usage 6 | 7 | Run `motsu --help` for information on command line options. 8 | 9 | Currently, only cropping is supported. Cropping is done with arrow-keys, and uncropping is done by holding shift and an arrow key. 10 | Hold ctrl while cropping to corp faster. 11 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use argh::FromArgs; 2 | use glfw::{ 3 | Action, Context as _, Key, Modifiers, MouseButton, SwapInterval, WindowEvent, WindowMode, 4 | }; 5 | use image::RgbaImage; 6 | use luminance::blending::{Blending, Equation, Factor}; 7 | use luminance::context::GraphicsContext; 8 | use luminance::pipeline::{PipelineState, TextureBinding}; 9 | use luminance::pixel::{NormRGBA8UI, NormUnsigned}; 10 | use luminance::render_state::RenderState; 11 | use luminance::shader::Uniform; 12 | use luminance::tess::{Mode, Tess, TessBuilder}; 13 | use luminance::texture::{Dim2, Sampler, TexelUpload, Texture}; 14 | use luminance_derive::{Semantics, UniformInterface, Vertex}; 15 | use luminance_glfw::{GL33Context, GlfwSurface, GlfwSurfaceError}; 16 | 17 | use std::cmp::{max, min}; 18 | use std::process::exit; 19 | 20 | #[derive(Clone, Copy, Default)] 21 | struct Crop { 22 | left: u32, 23 | right: u32, 24 | top: u32, 25 | bottom: u32, 26 | } 27 | 28 | const VS: &str = include_str!("texture-vs.glsl"); 29 | const FS: &str = include_str!("texture-fs.glsl"); 30 | type GlfwBackend = ::Backend; 31 | 32 | #[derive(Copy, Clone, Debug, Semantics)] 33 | pub enum VertexSemantics { 34 | #[sem(name = "position", repr = "[f32; 2]", wrapper = "VertexPosition")] 35 | Position, 36 | 37 | #[sem(name = "crop_left", repr = "f32", wrapper = "CropLeft")] 38 | CropLeft, 39 | 40 | #[sem(name = "crop_right", repr = "f32", wrapper = "CropRight")] 41 | CropRight, 42 | 43 | #[sem(name = "crop_top", repr = "f32", wrapper = "CropTop")] 44 | CropTop, 45 | 46 | #[sem(name = "crop_bottom", repr = "f32", wrapper = "CropBottom")] 47 | CropBottom, 48 | } 49 | 50 | #[derive(Copy, Clone, Vertex, Debug)] 51 | #[vertex(sem = "VertexSemantics")] 52 | pub struct Vertex(VertexPosition, CropLeft, CropRight, CropTop, CropBottom); 53 | 54 | #[derive(UniformInterface)] 55 | struct ShaderInterface { 56 | tex: Uniform>, 57 | } 58 | 59 | #[derive(FromArgs, Debug)] 60 | /// Image viewer and cropper. Use hjkl keys to crop image. 61 | /// 62 | /// Hold CTRL to increase cropping speed, hold shift to uncrop a side. 63 | /// 64 | /// Press q or escape to quit, and r to undo all cropping. 65 | /// 66 | /// You may also click twice on the image to crop with the bounding rectangle 67 | /// of the two mouse clicks. 68 | struct PNGArgs { 69 | /// don't display the input image 70 | #[argh(switch, short = 'q')] 71 | quiet: bool, 72 | 73 | /// save to input file 74 | #[argh(switch, short = 'i')] 75 | in_place: bool, 76 | 77 | /// output file 78 | #[argh(option, short = 'o')] 79 | output: Option, 80 | 81 | /// crop left 82 | #[argh(option, short = 'l')] 83 | crop_left: Option, 84 | 85 | /// crop right 86 | #[argh(option, short = 'r')] 87 | crop_right: Option, 88 | 89 | /// crop top 90 | #[argh(option, short = 't')] 91 | crop_top: Option, 92 | 93 | /// crop bottom 94 | #[argh(option, short = 'b')] 95 | crop_bottom: Option, 96 | 97 | /// scale 98 | #[argh(option, short = 's')] 99 | scale: Option, 100 | 101 | #[argh(positional)] 102 | input: String, 103 | } 104 | 105 | fn crop_image(image: &mut RgbaImage, crop: Crop) -> RgbaImage { 106 | let width = image.width() - crop.left - crop.right; 107 | let height = image.height() - crop.top - crop.bottom; 108 | image::imageops::crop(image, crop.left, crop.top, width, height).to_image() 109 | } 110 | 111 | fn main() { 112 | let mut args: PNGArgs = argh::from_env(); 113 | 114 | if args.in_place { 115 | match args.output { 116 | None => args.output = Some(args.input.clone()), 117 | Some(_) => { 118 | eprintln!("Cannot specify both --in-place and --output"); 119 | exit(1); 120 | } 121 | } 122 | } 123 | 124 | let mut image: RgbaImage = match image::open(&args.input) { 125 | Ok(im) => im.into_rgba8(), 126 | Err(e) => { 127 | eprintln!("{}", e); 128 | exit(1); 129 | } 130 | }; 131 | 132 | image = crop_image( 133 | &mut image, 134 | Crop { 135 | left: args.crop_left.unwrap_or(0), 136 | right: args.crop_right.unwrap_or(0), 137 | top: args.crop_top.unwrap_or(0), 138 | bottom: args.crop_bottom.unwrap_or(0), 139 | }, 140 | ); 141 | 142 | let output_image = if args.quiet { 143 | image 144 | } else { 145 | let surface = GlfwSurface::new(|glfw| { 146 | let (mut window, events) = glfw.with_primary_monitor(|glfw, mon| { 147 | let (width, height) = mon 148 | .and_then(|m| m.get_video_mode()) 149 | .map_or((500, 500), |v| (v.width / 2, v.height / 2)); 150 | glfw.create_window(width, height, "motsu", WindowMode::Windowed) 151 | .ok_or(GlfwSurfaceError::UserError("Couldn't Open Window")) 152 | })?; 153 | window.make_current(); 154 | window.set_all_polling(true); 155 | glfw.set_swap_interval(SwapInterval::Sync(1)); 156 | Ok((window, events)) 157 | }); 158 | match surface { 159 | Ok(surface) => main_loop(surface, image), 160 | Err(e) => { 161 | eprintln!("cannot create graphics surface:\n{}", e); 162 | exit(1); 163 | } 164 | } 165 | }; 166 | 167 | let output_image = if let Some(scale) = args.scale { 168 | let width = output_image.width() as f64; 169 | let height = output_image.height() as f64; 170 | image::imageops::resize( 171 | &output_image, 172 | (width * scale) as u32, 173 | (height * scale) as u32, 174 | image::imageops::FilterType::Lanczos3, 175 | ) 176 | } else { 177 | output_image 178 | }; 179 | 180 | if let Some(outfile) = args.output { 181 | if let Err(e) = output_image.save(outfile) { 182 | eprintln!("{}", e); 183 | exit(1); 184 | } 185 | } 186 | } 187 | 188 | fn calculate_vertices( 189 | image_width: u32, 190 | image_height: u32, 191 | buffer_width: u32, 192 | buffer_height: u32, 193 | crop: Crop, 194 | ) -> [Vertex; 4] { 195 | let crop_left: f32 = crop.left as f32; 196 | let crop_right: f32 = crop.right as f32; 197 | let crop_top: f32 = crop.top as f32; 198 | let crop_bottom: f32 = crop.bottom as f32; 199 | let image_width: f32 = image_width as f32; 200 | let image_height: f32 = image_height as f32; 201 | let buffer_width: f32 = buffer_width as f32; 202 | let buffer_height: f32 = buffer_height as f32; 203 | 204 | let cropped_width = image_width - crop_left - crop_right; 205 | let cropped_height = image_height - crop_top - crop_bottom; 206 | 207 | let width = if cropped_width <= buffer_width { 208 | cropped_width / buffer_width 209 | } else { 210 | 1.0 211 | }; 212 | 213 | let height = if cropped_height <= buffer_height { 214 | cropped_height / buffer_height 215 | } else { 216 | 1.0 217 | }; 218 | 219 | let cl = CropLeft::new(crop_left / image_width); 220 | let cr = CropRight::new(1.0 - crop_right / image_width); 221 | let ct = CropTop::new(crop_top / image_height); 222 | let cb = CropBottom::new(1.0 - crop_bottom / image_height); 223 | 224 | [ 225 | Vertex(VertexPosition::new([-width, -height]), cl, cr, ct, cb), 226 | Vertex(VertexPosition::new([-width, height]), cl, cr, ct, cb), 227 | Vertex(VertexPosition::new([width, height]), cl, cr, ct, cb), 228 | Vertex(VertexPosition::new([width, -height]), cl, cr, ct, cb), 229 | ] 230 | } 231 | 232 | fn make_texture( 233 | surface: &mut GlfwSurface, 234 | image: &RgbaImage, 235 | ) -> Texture { 236 | let tex = surface 237 | .context 238 | .new_texture_raw( 239 | [image.width() as u32, image.height() as u32], 240 | Sampler::default(), 241 | TexelUpload::BaseLevel { 242 | texels: image.as_raw(), 243 | mipmaps: 0, 244 | }, 245 | ) 246 | .expect("luminance texture creation failed"); 247 | tex 248 | } 249 | 250 | fn make_tess( 251 | surface: &mut GlfwSurface, 252 | image: &RgbaImage, 253 | crop: Crop, 254 | ) -> Tess { 255 | let (width, height) = surface.context.window.get_size(); 256 | TessBuilder::new(&mut surface.context) 257 | .set_vertices(calculate_vertices( 258 | image.width(), 259 | image.height(), 260 | width as u32, 261 | height as u32, 262 | crop, 263 | )) 264 | .set_mode(Mode::TriangleFan) 265 | .build() 266 | .unwrap() 267 | } 268 | 269 | fn calculate_delta(modifiers: Modifiers) -> u32 { 270 | if modifiers.contains(Modifiers::Control) { 271 | 10 272 | } else { 273 | 1 274 | } 275 | } 276 | 277 | fn main_loop(mut surface: GlfwSurface, mut image: RgbaImage) -> RgbaImage { 278 | // setup for loop 279 | let mut redraw = true; 280 | let mut crop: Crop = Default::default(); 281 | let mut mouse_position: (u32, u32) = (0, 0); 282 | let mut mouse_click: Option<(u32, u32)> = None; 283 | 284 | let mut program = surface 285 | .context 286 | .new_shader_program::<(), (), ShaderInterface>() 287 | .from_strings(VS, None, None, FS) 288 | .expect("Program failed") 289 | .ignore_warnings(); 290 | let render_st = RenderState::default().set_blending(Blending { 291 | equation: Equation::Additive, 292 | src: Factor::SrcAlpha, 293 | dst: Factor::Zero, 294 | }); 295 | let pipeline_st = PipelineState::default().set_clear_color([1.0, 1.0, 1.0, 1.0]); 296 | 297 | let mut tex = make_texture(&mut surface, &image); 298 | 299 | 'app: loop { 300 | surface.context.window.glfw.poll_events(); 301 | for (_, event) in surface.events_rx.try_iter() { 302 | // Nothing needs to happen on key release 303 | if let WindowEvent::Key(_, _, Action::Release, _) = event { 304 | continue; 305 | } 306 | 307 | match event { 308 | WindowEvent::Close | WindowEvent::Key(Key::Escape | Key::Q, _, _, _) => break 'app, 309 | WindowEvent::Pos(_, _) | WindowEvent::Size(_, _) | WindowEvent::Focus(_) => { 310 | redraw = true; 311 | } 312 | WindowEvent::Key(Key::K | Key::Up, _, _, modifiers) => { 313 | let delta = calculate_delta(modifiers); 314 | if modifiers.contains(Modifiers::Shift) { 315 | crop.top -= min(delta, crop.top); 316 | } else { 317 | crop.bottom += min(delta, image.height() - crop.top - crop.bottom - 1); 318 | } 319 | redraw = true; 320 | } 321 | WindowEvent::Key(Key::J | Key::Down, _, _, modifiers) => { 322 | let delta = calculate_delta(modifiers); 323 | if modifiers.contains(Modifiers::Shift) { 324 | crop.bottom -= min(delta, crop.bottom); 325 | } else { 326 | crop.top += min(delta, image.height() - crop.top - crop.bottom - 1); 327 | } 328 | redraw = true; 329 | } 330 | WindowEvent::Key(Key::H | Key::Left, _, _, modifiers) => { 331 | let delta = calculate_delta(modifiers); 332 | if modifiers.contains(Modifiers::Shift) { 333 | crop.left -= min(delta, crop.left); 334 | } else { 335 | crop.right += min(delta, image.width() - crop.left - crop.right - 1); 336 | } 337 | redraw = true; 338 | } 339 | WindowEvent::Key(Key::L | Key::Right, _, _, modifiers) => { 340 | let delta = calculate_delta(modifiers); 341 | if modifiers.contains(Modifiers::Shift) { 342 | crop.right -= min(delta, crop.right); 343 | } else { 344 | crop.left += min(delta, image.width() - crop.left - crop.right - 1); 345 | } 346 | redraw = true; 347 | } 348 | WindowEvent::Key(Key::R, _, Action::Press, _) => { 349 | crop = Default::default(); 350 | mouse_click = None; 351 | redraw = true; 352 | } 353 | WindowEvent::CursorPos(x, y) => { 354 | mouse_position = (x as u32, y as u32); 355 | } 356 | WindowEvent::MouseButton(MouseButton::Button1, Action::Press, _) => { 357 | let (width, height) = surface.context.window.get_size(); 358 | let im_width = (image.width() - crop.left - crop.right) as i32; 359 | let im_height = (image.height() - crop.top - crop.bottom) as i32; 360 | let disp_width = min(im_width, width); 361 | let disp_height = min(im_height, height); 362 | match mouse_click { 363 | None => mouse_click = Some(mouse_position), 364 | Some(mc) => { 365 | if mc == mouse_position { 366 | continue; 367 | } 368 | let x1: i32 = mc.0 as i32 - width / 2 + disp_width / 2; 369 | let y1: i32 = mc.1 as i32 - height / 2 + disp_height / 2; 370 | let x2: i32 = mouse_position.0 as i32 - width / 2 + disp_width / 2; 371 | let y2: i32 = mouse_position.1 as i32 - height / 2 + disp_height / 2; 372 | 373 | if x1 < 0 374 | || x2 < 0 375 | || y1 < 0 376 | || y2 < 0 377 | || x1 > disp_width 378 | || x2 > disp_width 379 | || y1 > disp_height 380 | || y2 > disp_height 381 | { 382 | mouse_click = None; 383 | continue; 384 | } 385 | 386 | let (x1, x2) = if width < im_width { 387 | (x1 * im_width / width, x2 * im_width / width) 388 | } else { 389 | (x1, x2) 390 | }; 391 | 392 | let (y1, y2) = if height < im_height { 393 | (y1 * im_height / height, y2 * im_height / height) 394 | } else { 395 | (y1, y2) 396 | }; 397 | 398 | crop.left += min(x1, x2) as u32; 399 | crop.right += (im_width - max(x1, x2)) as u32; 400 | crop.top += min(y1, y2) as u32; 401 | crop.bottom += (im_height - max(y1, y2)) as u32; 402 | mouse_click = None; 403 | redraw = true; 404 | } 405 | } 406 | } 407 | _ => {} 408 | } 409 | } 410 | 411 | if redraw { 412 | let back_buffer = surface.context.back_buffer().unwrap(); 413 | let tess = make_tess(&mut surface, &image, crop); 414 | redraw = false; 415 | 416 | surface 417 | .context 418 | .new_pipeline_gate() 419 | .pipeline(&back_buffer, &pipeline_st, |pipeline, mut shd_gate| { 420 | let bound_tex = pipeline.bind_texture(&mut tex)?; 421 | shd_gate.shade(&mut program, |mut iface, uni, mut rdr_gate| { 422 | iface.set(&uni.tex, bound_tex.binding()); 423 | rdr_gate.render(&render_st, |mut tess_gate| tess_gate.render(&tess)) 424 | }) 425 | }) 426 | .assume(); 427 | surface.context.window.swap_buffers(); 428 | } 429 | } 430 | 431 | crop_image(&mut image, crop) 432 | } 433 | -------------------------------------------------------------------------------- /src/texture-fs.glsl: -------------------------------------------------------------------------------- 1 | in vec2 v_uv; 2 | out vec4 frag; 3 | 4 | uniform sampler2D tex; 5 | 6 | void main() { 7 | frag = texture(tex, v_uv); 8 | } 9 | -------------------------------------------------------------------------------- /src/texture-vs.glsl: -------------------------------------------------------------------------------- 1 | in vec2 position; 2 | in float crop_left; 3 | in float crop_right; 4 | in float crop_top; 5 | in float crop_bottom; 6 | 7 | out vec2 v_uv; 8 | 9 | void main() { 10 | gl_Position = vec4(position, 0., 1.); 11 | 12 | float x = position.x > 0 ? crop_right : crop_left; 13 | float y = position.y > 0 ? crop_top : crop_bottom; 14 | 15 | v_uv = vec2(x, y); 16 | } 17 | --------------------------------------------------------------------------------