diff --git a/Cargo.lock b/Cargo.lock index e28a6aa..f0d8aa1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,3 +1,8 @@ +[[package]] +name = "adler32" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "aho-corasick" version = "0.6.4" @@ -183,6 +188,11 @@ dependencies = [ "cc 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "color_quant" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "config" version = "0.1.0" @@ -204,6 +214,7 @@ dependencies = [ "errors 0.1.0", "front_matter 0.1.0", "globset 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "imageproc 0.1.0", "rayon 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "rendering 0.1.0", "serde 1.0.66 (registry+https://github.com/rust-lang/crates.io-index)", @@ -254,6 +265,15 @@ dependencies = [ "winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "deflate" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "adler32 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "dtoa" version = "0.4.2" @@ -290,6 +310,14 @@ dependencies = [ "strum_macros 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "enum_primitive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "error-chain" version = "0.11.0" @@ -303,6 +331,7 @@ name = "errors" version = "0.1.0" dependencies = [ "error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "image 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)", "tera 0.11.8 (registry+https://github.com/rust-lang/crates.io-index)", "toml 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -401,6 +430,15 @@ name = "getopts" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "gif" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "color_quant 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "lzw 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "glob" version = "0.2.11" @@ -500,6 +538,44 @@ dependencies = [ "unicode-normalization 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "image" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byteorder 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "enum_primitive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "gif 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", + "jpeg-decoder 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)", + "num-iter 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", + "num-rational 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)", + "png 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "scoped_threadpool 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "imageproc" +version = "0.1.0" +dependencies = [ + "errors 0.1.0", + "image 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rayon 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "tera 0.11.8 (registry+https://github.com/rust-lang/crates.io-index)", + "twox-hash 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "utils 0.1.0", +] + +[[package]] +name = "inflate" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "adler32 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "inotify" version = "0.3.0" @@ -537,6 +613,15 @@ name = "itoa" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "jpeg-decoder" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byteorder 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "rayon 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "kernel32-sys" version = "0.2.2" @@ -587,6 +672,11 @@ dependencies = [ "cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "lzw" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "mac" version = "0.1.1" @@ -810,6 +900,32 @@ dependencies = [ "num-traits 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "num-iter" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-rational" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-traits" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-traits 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "num-traits" version = "0.2.5" @@ -948,6 +1064,17 @@ dependencies = [ "typemap 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "png" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "deflate 0.7.18 (registry+https://github.com/rust-lang/crates.io-index)", + "inflate 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "num-iter 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "precomputed-hash" version = "0.1.1" @@ -999,6 +1126,16 @@ dependencies = [ "proc-macro2 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "rand" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "rand" version = "0.4.2" @@ -1009,6 +1146,15 @@ dependencies = [ "winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "rayon" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "either 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rayon-core 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "rayon" version = "1.0.1" @@ -1175,6 +1321,11 @@ dependencies = [ "pkg-config 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "scoped_threadpool" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "scopeguard" version = "0.3.3" @@ -1250,6 +1401,7 @@ dependencies = [ "errors 0.1.0", "front_matter 0.1.0", "glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "imageproc 0.1.0", "pagination 0.1.0", "rayon 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "sass-rs 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1441,6 +1593,7 @@ dependencies = [ "config 0.1.0", "content 0.1.0", "errors 0.1.0", + "imageproc 0.1.0", "lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "pulldown-cmark 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "taxonomies 0.1.0", @@ -1544,6 +1697,14 @@ name = "traitobject" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "twox-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "typeable" version = "0.1.2" @@ -1750,6 +1911,7 @@ dependencies = [ ] [metadata] +"checksum adler32 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7e522997b529f05601e05166c07ed17789691f562762c7f3b987263d2dedee5c" "checksum aho-corasick 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "d6531d44de723825aa81398a6415283229725a00fa30713812ab9323faa82fc4" "checksum ammonia 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fd4c682378117e4186a492b2252b9537990e1617f44aed9788b9a1149de45477" "checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" @@ -1773,14 +1935,17 @@ dependencies = [ "checksum chrono 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "6962c635d530328acc53ac6a955e83093fedc91c5809dfac1fa60fa470830a37" "checksum clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f0f16b89cbb9ee36d87483dc939fe9f1e13c05898d56d7b230a0d4dff033a536" "checksum cmake 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)" = "95470235c31c726d72bf2e1f421adc1e65b9d561bf5529612cbe1a72da1467b3" +"checksum color_quant 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0dbbb57365263e881e805dc77d94697c9118fd94d8da011240555aa7b23445bd" "checksum crossbeam-deque 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f739f8c5363aca78cfb059edf753d8f0d36908c348f3d8d1503f03d8b75d9cf3" "checksum crossbeam-epoch 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "927121f5407de9956180ff5e936fe3cf4324279280001cd56b669d28ee7e9150" "checksum crossbeam-utils 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2760899e32a1d58d5abb31129f8fae5de75220bc2176e77ff7c627ae45c918d9" "checksum ctrlc 3.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "630391922b1b893692c6334369ff528dcc3a9d8061ccf4c803aa8f83cb13db5e" +"checksum deflate 0.7.18 (registry+https://github.com/rust-lang/crates.io-index)" = "32c8120d981901a9970a3a1c97cf8b630e0fa8c3ca31e75b6fd6fd5f9f427b31" "checksum dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab" "checksum duct 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "166298c17c5b4fe5997b962c2f22e887c7c5adc44308eb9103ce5b66af45a423" "checksum either 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3be565ca5c557d7f59e7cfcf1844f9e3033650c929c6566f511e8005f205c1d0" "checksum elasticlunr-rs 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "4837d77a1e157489a3933b743fd774ae75074e0e390b2b7f071530048a0d87ee" +"checksum enum_primitive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "be4551092f4d519593039259a9ed8daedf0da12e5109c5280338073eaeb81180" "checksum error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ff511d5dc435d703f4971bc399647c9bc38e20cb41452e3b9feb4765419ed3f3" "checksum filetime 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "714653f3e34871534de23771ac7b26e999651a0a228f47beb324dfdf1dd4b10f" "checksum flate2 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9fac2277e84e5e858483756647a9d0aa8d9a2b7cba517fd84325a0aaa69a0909" @@ -1793,6 +1958,7 @@ dependencies = [ "checksum futf 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "7c9c1ce3fa9336301af935ab852c437817d14cd33690446569392e65170aac3b" "checksum gcc 0.3.54 (registry+https://github.com/rust-lang/crates.io-index)" = "5e33ec290da0d127825013597dbdfc28bee4964690c7ce1166cbc2a7bd08b1bb" "checksum getopts 0.2.17 (registry+https://github.com/rust-lang/crates.io-index)" = "b900c08c1939860ce8b54dc6a89e26e00c04c380fd0e09796799bd7f12861e05" +"checksum gif 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e2e41945ba23db3bf51b24756d73d81acb4f28d85c3dccc32c6fae904438c25f" "checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb" "checksum globset 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "142754da2c9b3722affd909f9e27f2a6700a7a303f362971e0a74c652005a43d" "checksum html5ever 0.22.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b04478cf718862650a0bf66acaf8f2f8c906fbc703f35c916c1f4211b069a364" @@ -1800,10 +1966,13 @@ dependencies = [ "checksum humansize 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b6cab2627acfc432780848602f3f558f7e9dd427352224b0d9324025796d2a5e" "checksum hyper 0.10.13 (registry+https://github.com/rust-lang/crates.io-index)" = "368cb56b2740ebf4230520e2b90ebb0461e69034d85d1945febd9b3971426db2" "checksum idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "014b298351066f1512874135335d62a789ffe78a9974f94b43ed5621951eaf7d" +"checksum image 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)" = "545f000e8aa4e569e93f49c446987133452e0091c2494ac3efd3606aa3d309f2" +"checksum inflate 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "f5f9f47468e9a76a6452271efadc88fe865a82be91fe75e6c0c57b87ccea59d4" "checksum inotify 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "887fcc180136e77a85e6a6128579a719027b1bab9b1c38ea4444244fe262c20c" "checksum iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dbe6e417e7d0975db6512b90796e8ce223145ac4e33c377e4a42882a0e88bb08" "checksum iron 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d8e17268922834707e1c29e8badbf9c712c9c43378e1b6a3388946baff10be2" "checksum itoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c069bbec61e1ca5a596166e55dfe4773ff745c3d16b700013bcaff9a6df2c682" +"checksum jpeg-decoder 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "c8b7d43206b34b3f94ea9445174bda196e772049b9bddbc620c9d29b2d20110d" "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" "checksum language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" "checksum lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e6412c5e2ad9584b0b8e979393122026cdd6d2a80b933f890dcd694ddbe73739" @@ -1812,6 +1981,7 @@ dependencies = [ "checksum linked-hash-map 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "70fb39025bc7cdd76305867c4eccf2f2dcf6e9a57f5b21a93e1c2d86cd03ec9e" "checksum log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" "checksum log 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6fddaa003a65722a7fb9e26b0ce95921fe4ba590542ced664d8ce2fa26f9f3ac" +"checksum lzw 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7d947cbb889ed21c2a84be6ffbaebf5b4e0f4340638cba0444907e38b56be084" "checksum mac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" "checksum maplit 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "08cbb6b4fef96b6d77bfc40ec491b1690c779e77b05cd9f07f787ed376fd4c43" "checksum markup5ever 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bfedc97d5a503e96816d10fedcd5b42f760b2e525ce2f7ec71f6a41780548475" @@ -1835,6 +2005,9 @@ dependencies = [ "checksum nodrop 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "9a2228dca57108069a5262f2ed8bd2e82496d2e074a06d1ccc7ce1687b6ae0a2" "checksum notify 4.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "5c3812da3098f210a0bb440f9c008471a031aa4c1de07a264fdd75456c95a4eb" "checksum num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "e83d528d2677f0518c570baf2b7abdcf0cd2d248860b68507bdcb3e91d4c0cea" +"checksum num-iter 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "af3fdbbc3291a5464dc57b03860ec37ca6bf915ed6ee385e7c6c052c422b2124" +"checksum num-rational 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "ee314c74bd753fc86b4780aa9475da469155f3848473a261d2d18e35245a784e" +"checksum num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" "checksum num-traits 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "630de1ef5cc79d0cdd78b7e33b81f083cbfe90de0f4b2b2f07f905867c70e9fe" "checksum num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c51a3322e4bca9d212ad9a158a02abc6934d005490c054a2778df73a70aa0a30" "checksum onig 3.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f5eeb268a4620c74ea5768c6d2ccd492d60a47a8754666b91a46bfc35cd4d1ba" @@ -1850,6 +2023,7 @@ dependencies = [ "checksum pkg-config 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)" = "110d5ee3593dbb73f56294327fe5668bcc997897097cbc76b51e7aed3f52452f" "checksum plist 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "c61ac2afed2856590ae79d6f358a24b85ece246d2aa134741a66d589519b7503" "checksum plugin 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "1a6a0dc3910bc8db877ffed8e457763b317cf880df4ae19109b9f77d277cf6e0" +"checksum png 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f0b0cabbbd20c2d7f06dbf015e06aad59b6ca3d9ed14848783e98af9aaf19925" "checksum precomputed-hash 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" "checksum proc-macro2 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "1b06e2f335f48d24442b35a19df506a835fb3547bc3c06ef27340da9acf5cae7" "checksum proc-macro2 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "effdb53b25cdad54f8f48843d67398f7ef2e14f12c1b4cb4effc549a6462a4d6" @@ -1857,7 +2031,9 @@ dependencies = [ "checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" "checksum quote 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9949cfe66888ffe1d53e6ec9d9f3b70714083854be20fd5e271b232a017401e8" "checksum quote 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e44651a0dc4cdd99f71c83b561e221f714912d11af1a4dff0631f923d53af035" +"checksum rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)" = "15a732abf9d20f0ad8eeb6f909bf6868722d9a06e1e50802b6a70351f40b4eb1" "checksum rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "eba5f8cb59cc50ed56be8880a5c7b496bfd9bd26394e176bc67884094145c2c5" +"checksum rayon 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ed02d09394c94ffbdfdc755ad62a132e94c3224a8354e78a1200ced34df12edf" "checksum rayon 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "80e811e76f1dbf68abf87a759083d34600017fc4e10b6bd5ad84a700f9dba4b1" "checksum rayon-core 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9d24ad214285a7729b174ed6d3bcfcb80177807f959d95fafd5bfc5c4f201ac8" "checksum redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "c214e91d3ecf43e9a4e41e578973adeb14b474f2bee858742d127af75a0112b1" @@ -1874,6 +2050,7 @@ dependencies = [ "checksum same-file 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "cfb6eded0b06a0b512c8ddbcf04089138c9b4362c2f696f3c3d76039d68f3637" "checksum sass-rs 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "90f8cf6e645aa843ffffcbdc1e8752b1f221dfa314c81895aeb229a77aea7e05" "checksum sass-sys 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "bae88baa915f59c39820e544cfd9296d815a6b8efc3f276a78a0505866d2b4e9" +"checksum scoped_threadpool 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" "checksum scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27" "checksum sequence_trie 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "32157204e5c9d3c04007bd7e56e96e987635ce0e8e23c085b1e403861b76c351" "checksum serde 1.0.66 (registry+https://github.com/rust-lang/crates.io-index)" = "e9a2d9a9ac5120e0f768801ca2b58ad6eec929dc9d1d616c162f208869c2ce95" @@ -1909,6 +2086,7 @@ dependencies = [ "checksum time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "d825be0eb33fda1a7e68012d51e9c7f451dc1a69391e7fdc197060bb8c56667b" "checksum toml 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "a0263c6c02c4db6c8f7681f9fd35e90de799ebd4cfdeab77a38f4ff6b3d8c0d9" "checksum traitobject 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "efd1f82c56340fdf16f2a953d7bda4f8fdffba13d93b00844c25572110b26079" +"checksum twox-hash 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "475352206e7a290c5fccc27624a163e8d0d115f7bb60ca18a64fc9ce056d7435" "checksum typeable 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1410f6f91f21d1612654e7cc69193b0334f909dcf2c790c4826254fbb86f8887" "checksum typemap 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "653be63c80a3296da5551e1bfd2cca35227e13cdd08c6668903ae2f4f77aa1f6" "checksum ucd-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "fd2be2d6639d0f8fe6cdda291ad456e23629558d466e2789d2c3e9892bda285d" diff --git a/Cargo.toml b/Cargo.toml index 117d976..bed693a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,4 +53,5 @@ members = [ "components/templates", "components/utils", "components/search", + "components/imageproc", ] diff --git a/components/content/Cargo.toml b/components/content/Cargo.toml index 80cfec1..2e7724e 100644 --- a/components/content/Cargo.toml +++ b/components/content/Cargo.toml @@ -14,6 +14,7 @@ config = { path = "../config" } utils = { path = "../utils" } rendering = { path = "../rendering" } front_matter = { path = "../front_matter" } +imageproc = { path = "../imageproc" } [dev-dependencies] tempfile = "3" diff --git a/components/content/src/lib.rs b/components/content/src/lib.rs index a0fddac..dc5cf9c 100644 --- a/components/content/src/lib.rs +++ b/components/content/src/lib.rs @@ -7,6 +7,7 @@ extern crate errors; extern crate config; extern crate front_matter; extern crate rendering; +extern crate imageproc; extern crate utils; #[cfg(test)] diff --git a/components/content/src/page.rs b/components/content/src/page.rs index d2f940d..47d2ad8 100644 --- a/components/content/src/page.rs +++ b/components/content/src/page.rs @@ -15,6 +15,7 @@ use utils::site::get_reading_analytics; use utils::templates::render_template; use front_matter::{PageFrontMatter, InsertAnchor, split_page_content}; use rendering::{RenderContext, Header, render_content}; +use imageproc; use file_info::FileInfo; @@ -162,13 +163,16 @@ impl Page { /// We need access to all pages url to render links relative to content /// so that can't happen at the same time as parsing pub fn render_markdown(&mut self, permalinks: &HashMap, tera: &Tera, config: &Config, anchor_insert: InsertAnchor) -> Result<()> { - let context = RenderContext::new( + let mut context = RenderContext::new( tera, config, &self.permalink, permalinks, anchor_insert ); + + context.teracontext.add("page", self); + let res = render_content( &self.raw_content.replacen("", "", 1), &context @@ -202,6 +206,23 @@ impl Page { render_template(&tpl_name, tera, &context, &config.theme) .chain_err(|| format!("Failed to render page '{}'", self.file.path.display())) } + + /// Creates two vectors of asset URLs. The first one contains all asset files, + /// the second one that which appear to be an image file. + fn serialize_assets(&self) -> (Vec, Vec) { + let mut assets = vec![]; + let mut images = vec![]; + for asset in self.assets.iter() { + if let Some(filename) = asset.file_name().and_then(|f| f.to_str()) { + let url = self.path.clone() + filename; + if imageproc::file_is_img(&asset) { + images.push(url.clone()); + } + assets.push(url); + } + } + (assets, images) + } } impl Default for Page { @@ -246,6 +267,9 @@ impl ser::Serialize for Page { state.serialize_field("next", &self.next)?; state.serialize_field("toc", &self.toc)?; state.serialize_field("draft", &self.is_draft())?; + let (assets, assets_imgs) = self.serialize_assets(); + state.serialize_field("assets", &assets)?; + state.serialize_field("assets_imgs", &assets_imgs)?; state.end() } } diff --git a/components/content/src/section.rs b/components/content/src/section.rs index 550acf7..b634dc9 100644 --- a/components/content/src/section.rs +++ b/components/content/src/section.rs @@ -98,13 +98,16 @@ impl Section { /// We need access to all pages url to render links relative to content /// so that can't happen at the same time as parsing pub fn render_markdown(&mut self, permalinks: &HashMap, tera: &Tera, config: &Config) -> Result<()> { - let context = RenderContext::new( + let mut context = RenderContext::new( tera, config, &self.permalink, permalinks, self.meta.insert_anchor_links, ); + + context.teracontext.add("section", self); + let res = render_content(&self.raw_content, &context) .chain_err(|| format!("Failed to render content of {}", self.file.path.display()))?; self.content = res.0; diff --git a/components/errors/Cargo.toml b/components/errors/Cargo.toml index 285bfed..c742ca4 100644 --- a/components/errors/Cargo.toml +++ b/components/errors/Cargo.toml @@ -7,3 +7,4 @@ authors = ["Vincent Prouillet "] error-chain = "0.11" tera = "0.11" toml = "0.4" +image = "0.18.0" diff --git a/components/errors/src/lib.rs b/components/errors/src/lib.rs index cc698c6..a21041d 100755 --- a/components/errors/src/lib.rs +++ b/components/errors/src/lib.rs @@ -4,6 +4,7 @@ extern crate error_chain; extern crate tera; extern crate toml; +extern crate image; error_chain! { errors {} @@ -15,6 +16,7 @@ error_chain! { foreign_links { Io(::std::io::Error); Toml(toml::de::Error); + Image(image::ImageError); } } diff --git a/components/imageproc/Cargo.toml b/components/imageproc/Cargo.toml new file mode 100644 index 0000000..571b2ed --- /dev/null +++ b/components/imageproc/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "imageproc" +version = "0.1.0" +authors = ["Vojtěch Král "] + +[dependencies] +lazy_static = "1" +regex = "0.2" +tera = "0.11.0" +image = "0.18.0" +rayon = "0.9" +twox-hash = "1.1" + +errors = { path = "../errors" } +utils = { path = "../utils" } diff --git a/components/imageproc/src/lib.rs b/components/imageproc/src/lib.rs new file mode 100644 index 0000000..78fa804 --- /dev/null +++ b/components/imageproc/src/lib.rs @@ -0,0 +1,327 @@ +#[macro_use] +extern crate lazy_static; +extern crate regex; +extern crate image; +extern crate rayon; +extern crate twox_hash; + +extern crate utils; +extern crate errors; + +use std::path::{Path, PathBuf}; +use std::hash::{Hash, Hasher}; +use std::collections::HashMap; +use std::collections::hash_map::Entry as HEntry; +use std::fs::{self, File}; + +use regex::Regex; +use image::{GenericImage, FilterType}; +use image::jpeg::JPEGEncoder; +use rayon::prelude::*; +use twox_hash::XxHash; + +use utils::fs as ufs; +use errors::{Result, ResultExt}; + + +static RESIZED_SUBDIR: &'static str = "_resized_images"; +lazy_static!{ + pub static ref RESIZED_FILENAME: Regex = Regex::new(r#"([0-9a-f]{16})([0-9a-f]{2})[.]jpg"#).unwrap(); +} + +/// Describes the precise kind of a resize operation +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResizeOp { + /// A simple scale operation that doesn't take aspect ratio into account + Scale(u32, u32), + /// Scales the image to a specified width with height computed such that aspect ratio is preserved + FitWidth(u32), + /// Scales the image to a specified height with width computed such that aspect ratio is preserved + FitHeight(u32), + /// Scales the image such that it fits within the specified width and height preserving aspect ratio. + /// Either dimension may end up being smaller, but never larger than specified. + Fit(u32, u32), + /// Scales the image such that it fills the specified width and height. Output will always have the exact dimensions specified. + /// The part of the image that doesn't fit in the thumbnail due to differing aspect ratio will be cropped away, if any. + Fill(u32, u32), +} + +impl ResizeOp { + pub fn from_args(op: &str, width: Option, height: Option) -> Result { + use ResizeOp::*; + + // Validate args: + match op { + "fitwidth" => if width.is_none() { return Err(format!("op=fitwidth requires a `width` argument").into()) }, + "fitheight" => if height.is_none() { return Err(format!("op=fitwidth requires a `height` argument").into()) }, + "scale" | "fit" | "fill" => if width.is_none() || height.is_none() { + return Err(format!("op={} requires a `width` and `height` argument", op).into()) + }, + _ => return Err(format!("Invalid image resize operation: {}", op).into()) + }; + + Ok(match op { + "scale" => Scale(width.unwrap(), height.unwrap()), + "fitwidth" => FitWidth(width.unwrap()), + "fitheight" => FitHeight(height.unwrap()), + "fit" => Fit(width.unwrap(), height.unwrap()), + "fill" => Fill(width.unwrap(), height.unwrap()), + _ => unreachable!(), + }) + } + + pub fn width(self) -> Option { + use ResizeOp::*; + + match self { + Scale(w, _) => Some(w), + FitWidth(w) => Some(w), + FitHeight(_) => None, + Fit(w, _) => Some(w), + Fill(w, _) => Some(w), + } + } + + pub fn height(self) -> Option { + use ResizeOp::*; + + match self { + Scale(_, h) => Some(h), + FitWidth(_) => None, + FitHeight(h) => Some(h), + Fit(_, h) => Some(h), + Fill(_, h) => Some(h), + } + } +} + +impl From for u8 { + fn from(op: ResizeOp) -> u8 { + use ResizeOp::*; + + match op { + Scale(_, _) => 1, + FitWidth(_) => 2, + FitHeight(_) => 3, + Fit(_, _) => 4, + Fill(_, _) => 5, + } + } +} + +impl Hash for ResizeOp { + fn hash(&self, hasher: &mut H) { + hasher.write_u8(u8::from(*self)); + if let Some(w) = self.width() { hasher.write_u32(w); } + if let Some(h) = self.height() { hasher.write_u32(h); } + } +} + +/// Holds all data needed to perform a resize operation +#[derive(Debug, PartialEq, Eq)] +pub struct ImageOp { + source: String, + op: ResizeOp, + quality: u8, + hash: u64, + collision: Option, +} + +impl ImageOp { + pub fn new(source: String, op: ResizeOp, quality: u8) -> ImageOp { + let mut hasher = XxHash::with_seed(0); + hasher.write(source.as_ref()); + op.hash(&mut hasher); + hasher.write_u8(quality); + let hash = hasher.finish(); + + ImageOp { source, op, quality, hash, collision: None } + } + + pub fn from_args(source: String, op: &str, width: Option, height: Option, quality: u8) -> Result { + let op = ResizeOp::from_args(op, width, height)?; + Ok(Self::new(source, op, quality)) + } + + fn num_colli(&self) -> u32 { self.collision.unwrap_or(0) } + + fn perform(&self, content_path: &Path, target_path: &Path) -> Result<()> { + use ResizeOp::*; + + let src_path = content_path.join(&self.source); + if !ufs::file_stale(&src_path, target_path) { + return Ok(()) + } + + let mut img = image::open(&src_path)?; + let (img_w, img_h) = img.dimensions(); + + const RESIZE_FILTER: FilterType = FilterType::Gaussian; + const RATIO_EPSILLION: f32 = 0.1; + + let img = match self.op { + Scale(w, h) => img.resize_exact(w, h, RESIZE_FILTER), + FitWidth(w) => img.resize(w, u32::max_value(), RESIZE_FILTER), + FitHeight(h) => img.resize(u32::max_value(), h, RESIZE_FILTER), + Fit(w, h) => img.resize(w, h, RESIZE_FILTER), + Fill(w, h) => { + let fw = img_w as f32 / w as f32; + let fh = img_h as f32 / h as f32; + + if (fw - fh).abs() <= RATIO_EPSILLION { + // The aspect is similar enough that there's not much point in cropping + img.resize_exact(w, h, RESIZE_FILTER) + } else { + // We perform the fill such that a crop is performed first and then resize_exact can be used, + // which should be cheaper than resizing and then cropping (smaller number of pixels to resize). + let (crop_w, crop_h) = match fw < fh { + true => (img_w, (fw * h as f32).round() as u32), + false => ((fh * w as f32).round() as u32, img_h), + }; + let (off_w, off_h) = match fw < fh { + true => (0, (img_h - crop_h) / 2), + false => ((img_w - crop_w) / 2, 0), + }; + img.crop(off_w, off_h, crop_w, crop_h).resize_exact(w, h, RESIZE_FILTER) + } + }, + }; + + let mut f = File::create(target_path)?; + let mut enc = JPEGEncoder::new_with_quality(&mut f, self.quality); + let (img_w, img_h) = img.dimensions(); + enc.encode(&img.raw_pixels(), img_w, img_h, img.color())?; + Ok(()) + } +} + + +/// A strcture into which image operations can be enqueued and then performed. +/// All output is written in a subdirectory in `static_path`, +/// taking care of file stale status based on timestamps and possible hash collisions. +#[derive(Debug)] +pub struct Processor { + content_path: PathBuf, + resized_path: PathBuf, + resized_url: String, + img_ops: HashMap, + // Hash collisions go here: + img_ops_colls: Vec, +} + +impl Processor { + pub fn new(content_path: PathBuf, static_path: &Path, base_url: &str) -> Processor { + Processor { + content_path, + resized_path: static_path.join(RESIZED_SUBDIR), + resized_url: Self::resized_url(base_url), + img_ops: HashMap::new(), + img_ops_colls: Vec::new(), + } + } + + fn resized_url(base_url: &str) -> String { + match base_url.ends_with('/') { + true => format!("{}{}", base_url, RESIZED_SUBDIR), + false => format!("{}/{}", base_url, RESIZED_SUBDIR), + } + } + + pub fn set_base_url(&mut self, base_url: &str) { + self.resized_url = Self::resized_url(base_url); + } + + pub fn source_exists(&self, source: &str) -> bool { + self.content_path.join(source).exists() + } + + pub fn num_img_ops(&self) -> usize { + self.img_ops.len() + self.img_ops_colls.len() + } + + fn insert_with_colls(&mut self, mut img_op: ImageOp) -> u32 { + match self.img_ops.entry(img_op.hash) { + HEntry::Occupied(entry) => if *entry.get() == img_op { return 0; }, + HEntry::Vacant(entry) => { + entry.insert(img_op); + return 0; + }, + } + + // If we get here, that means a hash collision. + let mut num = 1; + for op in self.img_ops_colls.iter().filter(|op| op.hash == img_op.hash) { + if *op == img_op { + return num; + } else { + num += 1; + } + } + + if num == 1 { + self.img_ops.get_mut(&img_op.hash).unwrap().collision = Some(0); + } + img_op.collision = Some(num); + self.img_ops_colls.push(img_op); + num + } + + fn op_filename(hash: u64, colli_num: u32) -> String { + // Please keep this in sync with RESIZED_FILENAME + assert!(colli_num < 256, "Unexpectedly large number of collisions: {}", colli_num); + format!("{:016x}{:02x}.jpg", hash, colli_num) + } + + fn op_url(&self, hash: u64, colli_num: u32) -> String { + format!("{}/{}", &self.resized_url, Self::op_filename(hash, colli_num)) + } + + pub fn insert(&mut self, img_op: ImageOp) -> String { + let hash = img_op.hash; + let num_colli = self.insert_with_colls(img_op); + self.op_url(hash, num_colli) + } + + pub fn prune(&self) -> Result<()> { + ufs::ensure_directory_exists(&self.resized_path)?; + let entries = fs::read_dir(&self.resized_path)?; + for entry in entries { + let entry_path = entry?.path(); + if entry_path.is_file() { + let filename = entry_path.file_name().unwrap().to_string_lossy(); + if let Some(capts) = RESIZED_FILENAME.captures(filename.as_ref()) { + let hash = u64::from_str_radix(capts.get(1).unwrap().as_str(), 16).unwrap(); + let num_colli = u32::from_str_radix(capts.get(2).unwrap().as_str(), 16).unwrap(); + if num_colli > 0 || !self.img_ops.contains_key(&hash) { + fs::remove_file(&entry_path)?; + } + } + } + } + Ok(()) + } + + pub fn do_process(&mut self) -> Result<()> { + self.img_ops.par_iter().map(|(hash, op)| { + let target = self.resized_path.join(Self::op_filename(*hash, op.num_colli())); + op.perform(&self.content_path, &target) + .chain_err(|| format!("Failed to process image: {}", op.source)) + }) + .fold(|| Ok(()), Result::and) + .reduce(|| Ok(()), Result::and) + } +} + + +/// Looks at file's extension and returns whether it's a supported image format +pub fn file_is_img>(p: P) -> bool { + p.as_ref().extension().and_then(|s| s.to_str()).map(|ext| { + match ext.to_lowercase().as_str() { + "jpg" | "jpeg" => true, + "png" => true, + "gif" => true, + "bmp" => true, + _ => false, + } + }).unwrap_or(false) +} diff --git a/components/rendering/src/context.rs b/components/rendering/src/context.rs index d8a2513..b65997a 100644 --- a/components/rendering/src/context.rs +++ b/components/rendering/src/context.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use tera::Tera; +use tera::{Tera, Context}; use front_matter::InsertAnchor; use config::Config; @@ -10,6 +10,7 @@ use config::Config; pub struct RenderContext<'a> { pub tera: &'a Tera, pub config: &'a Config, + pub teracontext: Context, pub current_page_permalink: &'a str, pub permalinks: &'a HashMap, pub insert_anchor: InsertAnchor, @@ -23,8 +24,11 @@ impl<'a> RenderContext<'a> { permalinks: &'a HashMap, insert_anchor: InsertAnchor, ) -> RenderContext<'a> { + let mut teracontext = Context::new(); + teracontext.insert("config", config); RenderContext { tera, + teracontext, current_page_permalink, permalinks, insert_anchor, diff --git a/components/rendering/src/lib.rs b/components/rendering/src/lib.rs index 00715af..9c5da90 100644 --- a/components/rendering/src/lib.rs +++ b/components/rendering/src/lib.rs @@ -34,7 +34,7 @@ pub use context::RenderContext; pub fn render_content(content: &str, context: &RenderContext) -> Result<(String, Vec
)> { // Don't do anything if there is nothing like a shortcode in the content if content.contains("{{") || content.contains("{%") { - let rendered = render_shortcodes(content, context.tera, context.config)?; + let rendered = render_shortcodes(content, context)?; return markdown_to_html(&rendered, context); } diff --git a/components/rendering/src/shortcode.rs b/components/rendering/src/shortcode.rs index e88e836..e01c860 100644 --- a/components/rendering/src/shortcode.rs +++ b/components/rendering/src/shortcode.rs @@ -1,9 +1,9 @@ use pest::Parser; use pest::iterators::Pair; -use tera::{Tera, Map, Context, Value, to_value}; +use tera::{Map, Context, Value, to_value}; use errors::{Result, ResultExt}; -use config::Config; +use ::context::RenderContext; // This include forces recompiling this source file if the grammar file changes. // Uncomment it when doing changes to the .pest file @@ -84,20 +84,20 @@ fn parse_shortcode_call(pair: Pair) -> (String, Map) { } -fn render_shortcode(name: String, args: Map, tera: &Tera, config: &Config, body: Option<&str>) -> Result { - let mut context = Context::new(); +fn render_shortcode(name: String, args: Map, context: &RenderContext, body: Option<&str>) -> Result { + let mut teracontext = Context::new(); for (key, value) in args.iter() { - context.insert(key, value); + teracontext.insert(key, value); } if let Some(ref b) = body { // Trimming right to avoid most shortcodes with bodies ending up with a HTML new line - context.insert("body", b.trim_right()); + teracontext.insert("body", b.trim_right()); } - context.insert("config", config); + teracontext.extend(context.teracontext.clone()); let tpl_name = format!("shortcodes/{}.html", name); - let res = tera - .render(&tpl_name, &context) + let res = context.tera + .render(&tpl_name, &teracontext) .chain_err(|| format!("Failed to render {} shortcode", name))?; // We trim left every single line of a shortcode to avoid the accidental @@ -105,7 +105,7 @@ fn render_shortcode(name: String, args: Map, tera: &Tera, config: Ok(res.lines().map(|s| s.trim_left()).collect()) } -pub fn render_shortcodes(content: &str, tera: &Tera, config: &Config) -> Result { +pub fn render_shortcodes(content: &str, context: &RenderContext) -> Result { let mut res = String::with_capacity(content.len()); let mut pairs = match ContentParser::parse(Rule::page, content) { @@ -138,7 +138,7 @@ pub fn render_shortcodes(content: &str, tera: &Tera, config: &Config) -> Result< Rule::text | Rule::text_in_ignored_body_sc | Rule::text_in_body_sc => res.push_str(p.into_span().as_str()), Rule::inline_shortcode => { let (name, args) = parse_shortcode_call(p); - res.push_str(&render_shortcode(name, args, tera, config, None)?); + res.push_str(&render_shortcode(name, args, context, None)?); }, Rule::shortcode_with_body => { let mut inner = p.into_inner(); @@ -146,7 +146,7 @@ pub fn render_shortcodes(content: &str, tera: &Tera, config: &Config) -> Result< // we don't care about the closing tag let (name, args) = parse_shortcode_call(inner.next().unwrap()); let body = inner.next().unwrap().into_span().as_str(); - res.push_str(&render_shortcode(name, args, tera, config, Some(body))?); + res.push_str(&render_shortcode(name, args, context, Some(body))?); }, Rule::ignored_inline_shortcode => { res.push_str( @@ -179,6 +179,10 @@ pub fn render_shortcodes(content: &str, tera: &Tera, config: &Config) -> Result< #[cfg(test)] mod tests { + use std::collections::HashMap; + use tera::Tera; + use config::Config; + use front_matter::InsertAnchor; use super::*; macro_rules! assert_lex_rule { @@ -195,6 +199,13 @@ mod tests { }; } + fn render_shortcodes(code: &str, tera: &Tera) -> String { + let config = Config::default(); + let permalinks = HashMap::new(); + let context = RenderContext::new(&tera, &config, "", &permalinks, InsertAnchor::None); + super::render_shortcodes(code, &context).unwrap() + } + #[test] fn lex_text() { let inputs = vec!["Hello world", "HEllo \n world", "Hello 1 2 true false 'hey'"]; @@ -281,26 +292,22 @@ mod tests { #[test] fn does_nothing_with_no_shortcodes() { - let res = render_shortcodes("Hello World", &Tera::default(), &Config::default()); - assert_eq!(res.unwrap(), "Hello World"); + let res = render_shortcodes("Hello World", &Tera::default()); + assert_eq!(res, "Hello World"); } #[test] fn can_unignore_inline_shortcode() { - let res = render_shortcodes( - "Hello World {{/* youtube() */}}", - &Tera::default(), - &Config::default(), - ); - assert_eq!(res.unwrap(), "Hello World {{ youtube() }}"); + let res = render_shortcodes("Hello World {{/* youtube() */}}", &Tera::default()); + assert_eq!(res, "Hello World {{ youtube() }}"); } #[test] fn can_unignore_shortcode_with_body() { let res = render_shortcodes(r#" Hello World -{%/* youtube() */%}Some body {{ hello() }}{%/* end */%}"#, &Tera::default(), &Config::default()); - assert_eq!(res.unwrap(), "\nHello World\n{% youtube() %}Some body {{ hello() }}{% end %}"); +{%/* youtube() */%}Some body {{ hello() }}{%/* end */%}"#, &Tera::default()); + assert_eq!(res, "\nHello World\n{% youtube() %}Some body {{ hello() }}{% end %}"); } #[test] @@ -343,7 +350,7 @@ Hello World fn can_render_inline_shortcodes() { let mut tera = Tera::default(); tera.add_raw_template("shortcodes/youtube.html", "Hello {{id}}").unwrap(); - let res = render_shortcodes("Inline {{ youtube(id=1) }}.", &tera, &Config::default()).unwrap(); + let res = render_shortcodes("Inline {{ youtube(id=1) }}.", &tera); assert_eq!(res, "Inline Hello 1."); } @@ -351,7 +358,7 @@ Hello World fn can_render_shortcodes_with_body() { let mut tera = Tera::default(); tera.add_raw_template("shortcodes/youtube.html", "{{body}}").unwrap(); - let res = render_shortcodes("Body\n {% youtube() %}Hey!{% end %}", &tera, &Config::default()).unwrap(); + let res = render_shortcodes("Body\n {% youtube() %}Hey!{% end %}", &tera); assert_eq!(res, "Body\n Hey!"); } } diff --git a/components/site/Cargo.toml b/components/site/Cargo.toml index eb70efa..b1abcd5 100644 --- a/components/site/Cargo.toml +++ b/components/site/Cargo.toml @@ -20,6 +20,7 @@ pagination = { path = "../pagination" } taxonomies = { path = "../taxonomies" } content = { path = "../content" } search = { path = "../search" } +imageproc = { path = "../imageproc" } [dev-dependencies] tempfile = "3" diff --git a/components/site/src/lib.rs b/components/site/src/lib.rs index 6c5fdd2..53ebc9d 100644 --- a/components/site/src/lib.rs +++ b/components/site/src/lib.rs @@ -16,6 +16,7 @@ extern crate pagination; extern crate taxonomies; extern crate content; extern crate search; +extern crate imageproc; #[cfg(test)] extern crate tempfile; @@ -24,6 +25,7 @@ use std::collections::HashMap; use std::fs::{create_dir_all, remove_dir_all, copy}; use std::mem; use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; use glob::glob; use tera::{Tera, Context}; @@ -66,9 +68,11 @@ pub struct Site { pub pages: HashMap, pub sections: HashMap, pub tera: Tera, + imageproc: Arc>, // the live reload port to be used if there is one pub live_reload: Option, pub output_path: PathBuf, + content_path: PathBuf, pub static_path: PathBuf, pub tags: Option, pub categories: Option, @@ -109,15 +113,21 @@ impl Site { // the `extend` above already does it but hey tera.build_inheritance_chains()?; + let content_path = path.join("content"); + let static_path = path.join("static"); + let imageproc = imageproc::Processor::new(content_path.clone(), &static_path, &config.base_url); + let site = Site { base_path: path.to_path_buf(), config, tera, pages: HashMap::new(), sections: HashMap::new(), + imageproc: Arc::new(Mutex::new(imageproc)), live_reload: None, output_path: path.join("public"), - static_path: path.join("static"), + content_path, + static_path, tags: None, categories: None, permalinks: HashMap::new(), @@ -128,7 +138,7 @@ impl Site { /// The index section is ALWAYS at that path pub fn index_section_path(&self) -> PathBuf { - self.base_path.join("content").join("_index.md") + self.content_path.join("_index.md") } pub fn enable_live_reload(&mut self) { @@ -153,6 +163,12 @@ impl Site { orphans } + pub fn set_base_url(&mut self, base_url: String) { + let mut imageproc = self.imageproc.lock().unwrap(); + imageproc.set_base_url(&base_url); + self.config.base_url = base_url; + } + pub fn set_output_path>(&mut self, path: P) { self.output_path = path.as_ref().to_path_buf(); } @@ -217,7 +233,7 @@ impl Site { if !self.sections.contains_key(&index_path) { let mut index_section = Section::default(); index_section.permalink = self.config.make_permalink(""); - index_section.file.parent = self.base_path.join("content"); + index_section.file.parent = self.content_path.clone(); index_section.file.relative = "_index.md".to_string(); self.sections.insert(index_path, index_section); } @@ -229,10 +245,10 @@ impl Site { self.add_page(p, false)?; } + self.register_early_global_fns(); self.render_markdown()?; self.populate_sections(); self.populate_tags_and_categories(); - self.register_tera_global_fns(); Ok(()) @@ -270,6 +286,16 @@ impl Site { Ok(()) } + /// Adds global fns that are to be available to shortcodes while rendering markdown + pub fn register_early_global_fns(&mut self) { + self.tera.register_global_function( + "get_url", global_fns::make_get_url(self.permalinks.clone(), self.config.clone()) + ); + self.tera.register_global_function( + "resize_image", global_fns::make_resize_image(self.imageproc.clone()) + ); + } + pub fn register_tera_global_fns(&mut self) { self.tera.register_global_function("trans", global_fns::make_trans(self.config.clone())); self.tera.register_global_function("get_page", global_fns::make_get_page(&self.pages)); @@ -278,10 +304,6 @@ impl Site { "get_taxonomy_url", global_fns::make_get_taxonomy_url(self.tags.clone(), self.categories.clone()) ); - self.tera.register_global_function( - "get_url", - global_fns::make_get_url(self.permalinks.clone(), self.config.clone()) - ); } /// Add a page to the site @@ -441,6 +463,17 @@ impl Site { Ok(()) } + pub fn num_img_ops(&self) -> usize { + let imageproc = self.imageproc.lock().unwrap(); + imageproc.num_img_ops() + } + + pub fn process_images(&self) -> Result<()> { + let mut imageproc = self.imageproc.lock().unwrap(); + imageproc.prune()?; + imageproc.do_process() + } + /// Deletes the `public` directory if it exists pub fn clean(&self) -> Result<()> { if self.output_path.exists() { @@ -510,6 +543,7 @@ impl Site { self.compile_sass(&self.base_path)?; } + self.process_images()?; self.copy_static_directories()?; if self.config.build_search_index { @@ -820,7 +854,7 @@ impl Site { /// Used only on reload pub fn render_index(&self) -> Result<()> { self.render_section( - &self.sections[&self.base_path.join("content").join("_index.md")], + &self.sections[&self.content_path.join("_index.md")], false ) } diff --git a/components/templates/Cargo.toml b/components/templates/Cargo.toml index 0d6ebe8..377735a 100644 --- a/components/templates/Cargo.toml +++ b/components/templates/Cargo.toml @@ -14,3 +14,4 @@ utils = { path = "../utils" } content = { path = "../content" } config = { path = "../config" } taxonomies = { path = "../taxonomies" } +imageproc = { path = "../imageproc" } diff --git a/components/templates/src/global_fns.rs b/components/templates/src/global_fns.rs index 468c910..65aeabb 100644 --- a/components/templates/src/global_fns.rs +++ b/components/templates/src/global_fns.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; -use std::path::{PathBuf}; +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; use tera::{GlobalFn, Value, from_value, to_value, Result}; @@ -7,29 +8,41 @@ use content::{Page, Section}; use config::Config; use utils::site::resolve_internal_link; use taxonomies::Taxonomy; +use imageproc; -macro_rules! required_string_arg { - ($e: expr, $err: expr) => { +macro_rules! required_arg { + ($ty: ty, $e: expr, $err: expr) => { match $e { - Some(v) => match from_value::(v.clone()) { + Some(v) => match from_value::<$ty>(v.clone()) { Ok(u) => u, Err(_) => return Err($err.into()) }, None => return Err($err.into()) - }; + } + }; +} + +macro_rules! optional_arg { + ($ty: ty, $e: expr, $err: expr) => { + match $e { + Some(v) => match from_value::<$ty>(v.clone()) { + Ok(u) => Some(u), + Err(_) => return Err($err.into()) + }, + None => None + } }; } pub fn make_trans(config: Config) -> GlobalFn { let translations_config = config.translations; - let default_lang = to_value(config.default_language).unwrap(); + let default_lang = config.default_language.clone(); Box::new(move |args| -> Result { - let key = required_string_arg!(args.get("key"), "`trans` requires a `key` argument."); - let lang_arg = args.get("lang").unwrap_or(&default_lang).clone(); - let lang = from_value::(lang_arg).unwrap(); + let key = required_arg!(String, args.get("key"), "`trans` requires a `key` argument."); + let lang = optional_arg!(String, args.get("lang"), "`trans`: `lang` must be a string.").unwrap_or(default_lang.clone()); let translations = &translations_config[lang.as_str()]; Ok(to_value(&translations[key.as_str()]).unwrap()) }) @@ -43,7 +56,7 @@ pub fn make_get_page(all_pages: &HashMap) -> GlobalFn { } Box::new(move |args| -> Result { - let path = required_string_arg!(args.get("path"), "`get_page` requires a `path` argument with a string value"); + let path = required_arg!(String, args.get("path"), "`get_page` requires a `path` argument with a string value"); match pages.get(&path) { Some(p) => Ok(to_value(p).unwrap()), None => Err(format!("Page `{}` not found.", path).into()) @@ -61,7 +74,7 @@ pub fn make_get_section(all_sections: &HashMap) -> GlobalFn { } Box::new(move |args| -> Result { - let path = required_string_arg!(args.get("path"), "`get_section` requires a `path` argument with a string value"); + let path = required_arg!(String, args.get("path"), "`get_section` requires a `path` argument with a string value"); //println!("Found {:#?}", sections.get(&path).unwrap().pages[0]); match sections.get(&path) { Some(p) => Ok(to_value(p).unwrap()), @@ -84,7 +97,7 @@ pub fn make_get_url(permalinks: HashMap, config: Config) -> Glob from_value::(c.clone()).unwrap_or(true) }); - let path = required_string_arg!(args.get("path"), "`get_url` requires a `path` argument with a string value"); + let path = required_arg!(String, args.get("path"), "`get_url` requires a `path` argument with a string value"); if path.starts_with("./") { match resolve_internal_link(&path, &permalinks) { Ok(url) => Ok(to_value(url).unwrap()), @@ -107,8 +120,8 @@ pub fn make_get_url(permalinks: HashMap, config: Config) -> Glob pub fn make_get_taxonomy_url(tags: Option, categories: Option) -> GlobalFn { Box::new(move |args| -> Result { - let kind = required_string_arg!(args.get("kind"), "`get_taxonomy_url` requires a `kind` argument with a string value"); - let name = required_string_arg!(args.get("name"), "`get_taxonomy_url` requires a `name` argument with a string value"); + let kind = required_arg!(String, args.get("kind"), "`get_taxonomy_url` requires a `kind` argument with a string value"); + let name = required_arg!(String, args.get("name"), "`get_taxonomy_url` requires a `name` argument with a string value"); let container = match kind.as_ref() { "tag" => &tags, "category" => &categories, @@ -128,6 +141,33 @@ pub fn make_get_taxonomy_url(tags: Option, categories: Option>) -> GlobalFn { + static DEFAULT_OP: &'static str = "fill"; + const DEFAULT_Q: u8 = 75; + + Box::new(move |args| -> Result { + let path = required_arg!(String, args.get("path"), "`resize_image` requires a `path` argument with a string value"); + let width = optional_arg!(u32, args.get("width"), "`resize_image`: `width` must be a non-negative integer"); + let height = optional_arg!(u32, args.get("height"), "`resize_image`: `height` must be a non-negative integer"); + let op = optional_arg!(String, args.get("op"), "`resize_image`: `op` must be a string").unwrap_or(DEFAULT_OP.to_owned()); + let quality = optional_arg!(u8, args.get("quality"), "`resize_image`: `quality` must be a number").unwrap_or(DEFAULT_Q); + if quality == 0 || quality > 100 { + return Err("`resize_image`: `quality` must be in range 1-100".to_string().into()); + } + + let mut imageproc = imageproc.lock().unwrap(); + if !imageproc.source_exists(&path) { + return Err(format!("`resize_image`: Cannot find path: {}", path).into()); + } + + let imageop = imageproc::ImageOp::from_args(path.clone(), &op, width, height, quality).map_err(|e| format!("`resize_image`: {}", e))?; + let url = imageproc.insert(imageop); + + to_value(url).map_err(|err| err.into()) + }) +} + + #[cfg(test)] mod tests { use super::{make_get_url, make_get_taxonomy_url, make_trans}; diff --git a/components/templates/src/lib.rs b/components/templates/src/lib.rs index 2e70f89..1c49403 100644 --- a/components/templates/src/lib.rs +++ b/components/templates/src/lib.rs @@ -11,6 +11,7 @@ extern crate utils; extern crate content; extern crate config; extern crate taxonomies; +extern crate imageproc; pub mod filters; pub mod global_fns; diff --git a/components/utils/src/fs.rs b/components/utils/src/fs.rs index b783f7d..4ca75f5 100644 --- a/components/utils/src/fs.rs +++ b/components/utils/src/fs.rs @@ -93,6 +93,32 @@ pub fn copy_directory(src: &PathBuf, dest: &PathBuf) -> Result<()> { Ok(()) } +/// Compares source and target files' timestamps and returns true if the source file +/// has been created _or_ updated after the target file has +pub fn file_stale(p_source: PS, p_target: PT) -> bool where PS: AsRef, PT: AsRef { + let p_source = p_source.as_ref(); + let p_target = p_target.as_ref(); + + if ! p_target.exists() { + return true; + } + + let get_time = |path: &Path| path.metadata().ok().and_then(|meta| { + Some(match (meta.created().ok(), meta.modified().ok()) { + (Some(tc), Some(tm)) => tc.max(tm), + (Some(tc), None) => tc, + (None, Some(tm)) => tm, + (None, None) => return None, + }) + }); + + let time_source = get_time(p_source); + let time_target = get_time(p_target); + + time_source.and_then(|ts| time_target.map(|tt| ts > tt)).unwrap_or(true) +} + + #[cfg(test)] mod tests { use std::fs::File; diff --git a/docs/content/documentation/content/image-resizing/index.md b/docs/content/documentation/content/image-resizing/index.md new file mode 100644 index 0000000..3055460 --- /dev/null +++ b/docs/content/documentation/content/image-resizing/index.md @@ -0,0 +1,8 @@ ++++ +title = "Image Resizing" +weight = 120 ++++ + +TODO: talk about resize_image + +{{ gallery() }} diff --git a/docs/content/documentation/templates/pages-sections.md b/docs/content/documentation/templates/pages-sections.md index 7580855..7c90a80 100644 --- a/docs/content/documentation/templates/pages-sections.md +++ b/docs/content/documentation/templates/pages-sections.md @@ -37,6 +37,7 @@ previous: Page?; next: Page?; // See the Table of contents section below for more details toc: Array
; +// TODO: add assets & assets_imgs (also draft is missing?) ``` ## Section variables diff --git a/docs/templates/shortcodes/gallery.html b/docs/templates/shortcodes/gallery.html new file mode 100644 index 0000000..cda36fb --- /dev/null +++ b/docs/templates/shortcodes/gallery.html @@ -0,0 +1,6 @@ +{% for img in page.assets_imgs %} + + + +   +{% endfor %} diff --git a/docs/templates/shortcodes/resize_image.html b/docs/templates/shortcodes/resize_image.html new file mode 100644 index 0000000..f33bc3b --- /dev/null +++ b/docs/templates/shortcodes/resize_image.html @@ -0,0 +1 @@ + diff --git a/src/cmd/serve.rs b/src/cmd/serve.rs index 20fece1..8504ccb 100644 --- a/src/cmd/serve.rs +++ b/src/cmd/serve.rs @@ -86,13 +86,13 @@ fn create_new_site(interface: &str, port: &str, output_dir: &str, base_url: &str let base_address = format!("{}:{}", base_url, port); let address = format!("{}:{}", interface, port); - - site.config.base_url = if site.config.base_url.ends_with('/') { + let base_url = if site.config.base_url.ends_with('/') { format!("http://{}/", base_address) } else { format!("http://{}", base_address) }; + site.set_base_url(base_url); site.set_output_path(output_dir); site.load()?; site.enable_live_reload(); diff --git a/src/console.rs b/src/console.rs index 9f0b5f0..5905bbe 100644 --- a/src/console.rs +++ b/src/console.rs @@ -27,10 +27,11 @@ pub fn error(message: &str) { /// Display in the console the number of pages/sections in the site pub fn notify_site_size(site: &Site) { println!( - "-> Creating {} pages ({} orphan) and {} sections", + "-> Creating {} pages ({} orphan), {} sections, and processing {} images", site.pages.len(), site.get_all_orphan_pages().len(), site.sections.len() - 1, // -1 since we do not the index as a section + site.num_img_ops(), ); }