├── .gitignore ├── LICENSE.txt ├── README.md ├── deno.json ├── src ├── index.css ├── index.html ├── index.js ├── lib │ ├── get-scale-flag.js │ ├── preload-image.js │ └── quote-filename.js └── vendor │ ├── check.svg │ ├── copy.svg │ ├── error.svg │ └── pure-min.css └── test ├── get-scale-flags.test.ts ├── helpers.ts └── quote-filename.test.ts /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Icons by [Tabler Icons](https://tabler-icons.io/), licensed under the 2 | MIT License. 3 | 4 | Copyright (c) 2020-2022 Paweł Kuna 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | ************ 25 | 26 | This is free and unencumbered software released into the public domain. 27 | 28 | Anyone is free to copy, modify, publish, use, compile, sell, or 29 | distribute this software, either in source code form or as a compiled 30 | binary, for any purpose, commercial or non-commercial, and by any 31 | means. 32 | 33 | In jurisdictions that recognize copyright laws, the author or authors 34 | of this software dedicate any and all copyright interest in the 35 | software to the public domain. We make this dedication for the benefit 36 | of the public at large and to the detriment of our heirs and 37 | successors. We intend this dedication to be an overt act of 38 | relinquishment in perpetuity of all present and future rights to this 39 | software under copyright law. 40 | 41 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 42 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 43 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 44 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 45 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 46 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 47 | OTHER DEALINGS IN THE SOFTWARE. 48 | 49 | For more information, please refer to 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ffmpeg buddy 2 | 3 | I love [ffmpeg](https://ffmpeg.org/) and always forget how to use it. This little webpage hopes to help you build ffmpeg commands! 4 | 5 | [Try it out online.](https://evanhahn.github.io/ffmpeg-buddy/) 6 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "test": "deno check **/*.js **/*.ts && deno fmt --check && deno lint && deno test" 4 | }, 5 | "compilerOptions": { 6 | "lib": ["deno.ns", "es2022", "dom"] 7 | }, 8 | "fmt": { 9 | "proseWrap": "preserve" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body { 6 | margin: 0 auto; 7 | padding-bottom: 1em; 8 | width: 95%; 9 | max-width: 768px; 10 | font-family: "Open Sans", "Segoe UI", Avenir, Ubuntu, Tahoma, Verdana, 11 | Helvetica, sans-serif; 12 | font-size: 16pt; 13 | } 14 | 15 | a { 16 | color: blue; 17 | text-decoration: none; 18 | } 19 | 20 | h1, 21 | h2, 22 | footer { 23 | text-align: center; 24 | font-weight: 100; 25 | } 26 | 27 | h2 { 28 | font-size: 16pt; 29 | } 30 | 31 | #outputContainer { 32 | display: flex; 33 | margin: 1rem; 34 | position: relative; 35 | } 36 | 37 | #outputEl { 38 | margin: 0; 39 | flex-grow: 1; 40 | background-color: #eee; 41 | padding: 2rem; 42 | font-family: Inconsolata, Monaco, Consolas, monospace; 43 | white-space: pre-wrap; 44 | word-wrap: break-word; 45 | word-break: break-word; 46 | } 47 | 48 | #copyButtonEl { 49 | position: absolute; 50 | top: 0; 51 | right: 0; 52 | border: 0; 53 | width: 2rem; 54 | height: 2rem; 55 | padding: 3px; 56 | display: flex; 57 | } 58 | 59 | #copyButtonEl:before { 60 | width: 100%; 61 | height: 100%; 62 | content: ""; 63 | display: block; 64 | background-position: center center; 65 | background-size: cover; 66 | } 67 | 68 | #copyButtonEl.waiting:before { 69 | background-image: url(vendor/copy.svg); 70 | } 71 | 72 | #copyButtonEl.copied:before { 73 | background-image: url(vendor/check.svg); 74 | } 75 | 76 | #copyButtonEl.failed:before { 77 | background-image: url(vendor/error.svg); 78 | } 79 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ffmpeg buddy 6 | 7 | 8 | 9 | 10 | 11 |

ffmpeg buddy

12 |

13 | a friend to help you build 14 | ffmpeg commands 15 |

16 | 17 |
18 |
# loading...
19 | 24 |
25 | 26 |
27 |
28 |
29 | 30 | 31 |
32 | 33 |
34 | 35 | 36 |
37 | 38 |
39 | 40 | 41 |
42 | 43 |
44 | 45 | 46 |
47 | 48 |
49 | 53 | 54 |
55 | 56 |
57 | 61 | 62 |
63 | 64 |
65 | 66 | 67 |
68 | 69 |
70 | 71 | 77 |
78 | 79 |
80 | 81 | 86 |
87 | 88 |
89 | 90 | 91 |
92 | 93 |
94 | 95 | 96 |
97 |
98 |
99 | 100 | 101 | 102 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import quoteFilename from "./lib/quote-filename.js"; 2 | import getScaleFlag from "./lib/get-scale-flag.js"; 3 | import preloadImage from "./lib/preload-image.js"; 4 | 5 | let copyButtonState = "waiting"; 6 | let copyButtonTimeout = null; 7 | 8 | function render() { 9 | if (copyButtonState === "copying") { 10 | copyButtonEl.className = "waiting"; 11 | } else { 12 | copyButtonEl.className = copyButtonState; 13 | } 14 | 15 | const inputFilename = inputFilenameEl.value.trim() || "input.mp4"; 16 | const outputFilename = outputFilenameEl.value.trim() || "output.gif"; 17 | 18 | const hasVideo = !disableVideoEl.checked; 19 | const hasAudio = !disableAudioEl.checked; 20 | 21 | const videoFlags = []; 22 | if (hasVideo) { 23 | const scaleFlag = getScaleFlag(scaleWidthEl.value, scaleHeightEl.value); 24 | const framerate = parseInt(framerateEl.value.trim(), 10) || null; 25 | const rotation = rotateEl.value; 26 | const flip = flipEl.value; 27 | if (scaleFlag) { 28 | videoFlags.push(scaleFlag); 29 | } 30 | if (framerate) { 31 | videoFlags.push(`-r ${framerate}`); 32 | } 33 | if (rotation !== "no rotation") { 34 | videoFlags.push(rotation); 35 | } 36 | if (flip !== "no flip") { 37 | videoFlags.push(flip); 38 | } 39 | } else { 40 | videoFlags.push("-vn"); 41 | } 42 | 43 | const startAt = startAtEl.value.trim(); 44 | const endAt = endAtEl.value.trim(); 45 | 46 | outputEl.innerText = [ 47 | "ffmpeg", 48 | "-i", 49 | quoteFilename(inputFilename), 50 | ...videoFlags, 51 | ...(hasAudio ? [] : ["-an"]), 52 | ...(startAt ? [`-ss '${startAt}'`] : []), 53 | ...(endAt ? [`-to '${endAt}'`] : []), 54 | quoteFilename(outputFilename), 55 | ].join(" "); 56 | 57 | for (const el of document.getElementsByClassName("needs-video")) { 58 | if (hasVideo) { 59 | el.removeAttribute("hidden"); 60 | } else { 61 | el.setAttribute("hidden", "hidden"); 62 | } 63 | } 64 | } 65 | 66 | function onFormChange() { 67 | copyButtonState = "waiting"; 68 | clearTimeout(copyButtonTimeout); 69 | render(); 70 | } 71 | 72 | formEl.addEventListener("input", onFormChange); 73 | formEl.addEventListener("change", onFormChange); 74 | formEl.addEventListener("submit", (event) => { 75 | event.preventDefault(); 76 | render(); 77 | }); 78 | 79 | copyButtonEl.addEventListener("click", async (event) => { 80 | event.preventDefault(); 81 | 82 | if (copyButtonState === "copying") { 83 | return; 84 | } 85 | copyButtonState = "copying"; 86 | 87 | clearTimeout(copyButtonTimeout); 88 | 89 | let newState = "copied"; 90 | try { 91 | await navigator.clipboard.writeText(outputEl.textContent); 92 | } catch (err) { 93 | console.error(err); 94 | newState = "failed"; 95 | } 96 | 97 | copyButtonState = newState; 98 | render(); 99 | 100 | copyButtonTimeout = setTimeout(() => { 101 | copyButtonState = "waiting"; 102 | render(); 103 | }, 3000); 104 | }); 105 | 106 | render(); 107 | 108 | preloadImage("vendor/check.svg"); 109 | -------------------------------------------------------------------------------- /src/lib/get-scale-flag.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {string} n 5 | * @returns {number} 6 | */ 7 | function percent(n) { 8 | return parseFloat(n) / 100; 9 | } 10 | 11 | /** 12 | * @param {string} w 13 | * @param {string} h 14 | * @returns {null | string} 15 | */ 16 | export default function getScaleFlag(w, h) { 17 | w = w.trim() || "-1"; 18 | h = h.trim() || "-1"; 19 | 20 | if ((w === "100%" || w === "-1") && (h === "100%" || h === "-1")) { 21 | return null; 22 | } 23 | 24 | if (w.endsWith("%")) { 25 | w = "iw*" + percent(w); 26 | } 27 | if (h.endsWith("%")) { 28 | h = "ih*" + percent(h); 29 | } 30 | 31 | return `-vf 'scale=${w}:${h}'`; 32 | } 33 | -------------------------------------------------------------------------------- /src/lib/preload-image.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {string} src 5 | * @returns {void} 6 | */ 7 | export default function preloadImage(src) { 8 | const img = new Image(); 9 | img.src = src; 10 | } 11 | -------------------------------------------------------------------------------- /src/lib/quote-filename.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {string} filename 5 | * @returns {string} 6 | */ 7 | export default function quoteFilename(filename) { 8 | if (filename.indexOf(" ") === -1) { 9 | return filename.replace(/'/g, "\\'"); 10 | } else { 11 | return "'" + filename.replace(/'/g, "'\\''") + "'"; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/vendor/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/vendor/copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/vendor/error.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/vendor/pure-min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | Pure v2.1.0 3 | Copyright 2013 Yahoo! 4 | Licensed under the BSD License. 5 | https://github.com/pure-css/pure/blob/master/LICENSE 6 | */ 7 | /*! 8 | normalize.css v | MIT License | git.io/normalize 9 | Copyright (c) Nicolas Gallagher and Jonathan Neal 10 | */ 11 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}html{font-family:sans-serif}.hidden,[hidden]{display:none!important}.pure-img{max-width:100%;height:auto;display:block}.pure-g{letter-spacing:-.31em;text-rendering:optimizespeed;font-family:FreeSans,Arimo,"Droid Sans",Helvetica,Arial,sans-serif;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-line-pack:start;align-content:flex-start}@media all and (-ms-high-contrast:none),(-ms-high-contrast:active){table .pure-g{display:block}}.opera-only :-o-prefocus,.pure-g{word-spacing:-0.43em}.pure-u{display:inline-block;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-g [class*=pure-u]{font-family:sans-serif}.pure-u-1,.pure-u-1-1,.pure-u-1-12,.pure-u-1-2,.pure-u-1-24,.pure-u-1-3,.pure-u-1-4,.pure-u-1-5,.pure-u-1-6,.pure-u-1-8,.pure-u-10-24,.pure-u-11-12,.pure-u-11-24,.pure-u-12-24,.pure-u-13-24,.pure-u-14-24,.pure-u-15-24,.pure-u-16-24,.pure-u-17-24,.pure-u-18-24,.pure-u-19-24,.pure-u-2-24,.pure-u-2-3,.pure-u-2-5,.pure-u-20-24,.pure-u-21-24,.pure-u-22-24,.pure-u-23-24,.pure-u-24-24,.pure-u-3-24,.pure-u-3-4,.pure-u-3-5,.pure-u-3-8,.pure-u-4-24,.pure-u-4-5,.pure-u-5-12,.pure-u-5-24,.pure-u-5-5,.pure-u-5-6,.pure-u-5-8,.pure-u-6-24,.pure-u-7-12,.pure-u-7-24,.pure-u-7-8,.pure-u-8-24,.pure-u-9-24{display:inline-block;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-1-24{width:4.1667%}.pure-u-1-12,.pure-u-2-24{width:8.3333%}.pure-u-1-8,.pure-u-3-24{width:12.5%}.pure-u-1-6,.pure-u-4-24{width:16.6667%}.pure-u-1-5{width:20%}.pure-u-5-24{width:20.8333%}.pure-u-1-4,.pure-u-6-24{width:25%}.pure-u-7-24{width:29.1667%}.pure-u-1-3,.pure-u-8-24{width:33.3333%}.pure-u-3-8,.pure-u-9-24{width:37.5%}.pure-u-2-5{width:40%}.pure-u-10-24,.pure-u-5-12{width:41.6667%}.pure-u-11-24{width:45.8333%}.pure-u-1-2,.pure-u-12-24{width:50%}.pure-u-13-24{width:54.1667%}.pure-u-14-24,.pure-u-7-12{width:58.3333%}.pure-u-3-5{width:60%}.pure-u-15-24,.pure-u-5-8{width:62.5%}.pure-u-16-24,.pure-u-2-3{width:66.6667%}.pure-u-17-24{width:70.8333%}.pure-u-18-24,.pure-u-3-4{width:75%}.pure-u-19-24{width:79.1667%}.pure-u-4-5{width:80%}.pure-u-20-24,.pure-u-5-6{width:83.3333%}.pure-u-21-24,.pure-u-7-8{width:87.5%}.pure-u-11-12,.pure-u-22-24{width:91.6667%}.pure-u-23-24{width:95.8333%}.pure-u-1,.pure-u-1-1,.pure-u-24-24,.pure-u-5-5{width:100%}.pure-button{display:inline-block;line-height:normal;white-space:nowrap;vertical-align:middle;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-box-sizing:border-box;box-sizing:border-box}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button-group{letter-spacing:-.31em;text-rendering:optimizespeed}.opera-only :-o-prefocus,.pure-button-group{word-spacing:-0.43em}.pure-button-group .pure-button{letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-button{font-family:inherit;font-size:100%;padding:.5em 1em;color:rgba(0,0,0,.8);border:none transparent;background-color:#e6e6e6;text-decoration:none;border-radius:2px}.pure-button-hover,.pure-button:focus,.pure-button:hover{background-image:-webkit-gradient(linear,left top,left bottom,from(transparent),color-stop(40%,rgba(0,0,0,.05)),to(rgba(0,0,0,.1)));background-image:linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1))}.pure-button:focus{outline:0}.pure-button-active,.pure-button:active{-webkit-box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset;box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset;border-color:#000}.pure-button-disabled,.pure-button-disabled:active,.pure-button-disabled:focus,.pure-button-disabled:hover,.pure-button[disabled]{border:none;background-image:none;opacity:.4;cursor:not-allowed;-webkit-box-shadow:none;box-shadow:none;pointer-events:none}.pure-button-hidden{display:none}.pure-button-primary,.pure-button-selected,a.pure-button-primary,a.pure-button-selected{background-color:#0078e7;color:#fff}.pure-button-group .pure-button{margin:0;border-radius:0;border-right:1px solid rgba(0,0,0,.2)}.pure-button-group .pure-button:first-child{border-top-left-radius:2px;border-bottom-left-radius:2px}.pure-button-group .pure-button:last-child{border-top-right-radius:2px;border-bottom-right-radius:2px;border-right:none}.pure-form input[type=color],.pure-form input[type=date],.pure-form input[type=datetime-local],.pure-form input[type=datetime],.pure-form input[type=email],.pure-form input[type=month],.pure-form input[type=number],.pure-form input[type=password],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=text],.pure-form input[type=time],.pure-form input[type=url],.pure-form input[type=week],.pure-form select,.pure-form textarea{padding:.5em .6em;display:inline-block;border:1px solid #ccc;-webkit-box-shadow:inset 0 1px 3px #ddd;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;vertical-align:middle;-webkit-box-sizing:border-box;box-sizing:border-box}.pure-form input:not([type]){padding:.5em .6em;display:inline-block;border:1px solid #ccc;-webkit-box-shadow:inset 0 1px 3px #ddd;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;-webkit-box-sizing:border-box;box-sizing:border-box}.pure-form input[type=color]{padding:.2em .5em}.pure-form input[type=color]:focus,.pure-form input[type=date]:focus,.pure-form input[type=datetime-local]:focus,.pure-form input[type=datetime]:focus,.pure-form input[type=email]:focus,.pure-form input[type=month]:focus,.pure-form input[type=number]:focus,.pure-form input[type=password]:focus,.pure-form input[type=search]:focus,.pure-form input[type=tel]:focus,.pure-form input[type=text]:focus,.pure-form input[type=time]:focus,.pure-form input[type=url]:focus,.pure-form input[type=week]:focus,.pure-form select:focus,.pure-form textarea:focus{outline:0;border-color:#129fea}.pure-form input:not([type]):focus{outline:0;border-color:#129fea}.pure-form input[type=checkbox]:focus,.pure-form input[type=file]:focus,.pure-form input[type=radio]:focus{outline:thin solid #129FEA;outline:1px auto #129FEA}.pure-form .pure-checkbox,.pure-form .pure-radio{margin:.5em 0;display:block}.pure-form input[type=color][disabled],.pure-form input[type=date][disabled],.pure-form input[type=datetime-local][disabled],.pure-form input[type=datetime][disabled],.pure-form input[type=email][disabled],.pure-form input[type=month][disabled],.pure-form input[type=number][disabled],.pure-form input[type=password][disabled],.pure-form input[type=search][disabled],.pure-form input[type=tel][disabled],.pure-form input[type=text][disabled],.pure-form input[type=time][disabled],.pure-form input[type=url][disabled],.pure-form input[type=week][disabled],.pure-form select[disabled],.pure-form textarea[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input:not([type])[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input[readonly],.pure-form select[readonly],.pure-form textarea[readonly]{background-color:#eee;color:#777;border-color:#ccc}.pure-form input:focus:invalid,.pure-form select:focus:invalid,.pure-form textarea:focus:invalid{color:#b94a48;border-color:#e9322d}.pure-form input[type=checkbox]:focus:invalid:focus,.pure-form input[type=file]:focus:invalid:focus,.pure-form input[type=radio]:focus:invalid:focus{outline-color:#e9322d}.pure-form select{height:2.25em;border:1px solid #ccc;background-color:#fff}.pure-form select[multiple]{height:auto}.pure-form label{margin:.5em 0 .2em}.pure-form fieldset{margin:0;padding:.35em 0 .75em;border:0}.pure-form legend{display:block;width:100%;padding:.3em 0;margin-bottom:.3em;color:#333;border-bottom:1px solid #e5e5e5}.pure-form-stacked input[type=color],.pure-form-stacked input[type=date],.pure-form-stacked input[type=datetime-local],.pure-form-stacked input[type=datetime],.pure-form-stacked input[type=email],.pure-form-stacked input[type=file],.pure-form-stacked input[type=month],.pure-form-stacked input[type=number],.pure-form-stacked input[type=password],.pure-form-stacked input[type=search],.pure-form-stacked input[type=tel],.pure-form-stacked input[type=text],.pure-form-stacked input[type=time],.pure-form-stacked input[type=url],.pure-form-stacked input[type=week],.pure-form-stacked label,.pure-form-stacked select,.pure-form-stacked textarea{display:block;margin:.25em 0}.pure-form-stacked input:not([type]){display:block;margin:.25em 0}.pure-form-aligned input,.pure-form-aligned select,.pure-form-aligned textarea,.pure-form-message-inline{display:inline-block;vertical-align:middle}.pure-form-aligned textarea{vertical-align:top}.pure-form-aligned .pure-control-group{margin-bottom:.5em}.pure-form-aligned .pure-control-group label{text-align:right;display:inline-block;vertical-align:middle;width:10em;margin:0 1em 0 0}.pure-form-aligned .pure-controls{margin:1.5em 0 0 11em}.pure-form .pure-input-rounded,.pure-form input.pure-input-rounded{border-radius:2em;padding:.5em 1em}.pure-form .pure-group fieldset{margin-bottom:10px}.pure-form .pure-group input,.pure-form .pure-group textarea{display:block;padding:10px;margin:0 0 -1px;border-radius:0;position:relative;top:-1px}.pure-form .pure-group input:focus,.pure-form .pure-group textarea:focus{z-index:3}.pure-form .pure-group input:first-child,.pure-form .pure-group textarea:first-child{top:1px;border-radius:4px 4px 0 0;margin:0}.pure-form .pure-group input:first-child:last-child,.pure-form .pure-group textarea:first-child:last-child{top:1px;border-radius:4px;margin:0}.pure-form .pure-group input:last-child,.pure-form .pure-group textarea:last-child{top:-2px;border-radius:0 0 4px 4px;margin:0}.pure-form .pure-group button{margin:.35em 0}.pure-form .pure-input-1{width:100%}.pure-form .pure-input-3-4{width:75%}.pure-form .pure-input-2-3{width:66%}.pure-form .pure-input-1-2{width:50%}.pure-form .pure-input-1-3{width:33%}.pure-form .pure-input-1-4{width:25%}.pure-form-message-inline{display:inline-block;padding-left:.3em;color:#666;vertical-align:middle;font-size:.875em}.pure-form-message{display:block;color:#666;font-size:.875em}@media only screen and (max-width :480px){.pure-form button[type=submit]{margin:.7em 0 0}.pure-form input:not([type]),.pure-form input[type=color],.pure-form input[type=date],.pure-form input[type=datetime-local],.pure-form input[type=datetime],.pure-form input[type=email],.pure-form input[type=month],.pure-form input[type=number],.pure-form input[type=password],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=text],.pure-form input[type=time],.pure-form input[type=url],.pure-form input[type=week],.pure-form label{margin-bottom:.3em;display:block}.pure-group input:not([type]),.pure-group input[type=color],.pure-group input[type=date],.pure-group input[type=datetime-local],.pure-group input[type=datetime],.pure-group input[type=email],.pure-group input[type=month],.pure-group input[type=number],.pure-group input[type=password],.pure-group input[type=search],.pure-group input[type=tel],.pure-group input[type=text],.pure-group input[type=time],.pure-group input[type=url],.pure-group input[type=week]{margin-bottom:0}.pure-form-aligned .pure-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.pure-form-aligned .pure-controls{margin:1.5em 0 0 0}.pure-form-message,.pure-form-message-inline{display:block;font-size:.75em;padding:.2em 0 .8em}}.pure-menu{-webkit-box-sizing:border-box;box-sizing:border-box}.pure-menu-fixed{position:fixed;left:0;top:0;z-index:3}.pure-menu-item,.pure-menu-list{position:relative}.pure-menu-list{list-style:none;margin:0;padding:0}.pure-menu-item{padding:0;margin:0;height:100%}.pure-menu-heading,.pure-menu-link{display:block;text-decoration:none;white-space:nowrap}.pure-menu-horizontal{width:100%;white-space:nowrap}.pure-menu-horizontal .pure-menu-list{display:inline-block}.pure-menu-horizontal .pure-menu-heading,.pure-menu-horizontal .pure-menu-item,.pure-menu-horizontal .pure-menu-separator{display:inline-block;vertical-align:middle}.pure-menu-item .pure-menu-item{display:block}.pure-menu-children{display:none;position:absolute;left:100%;top:0;margin:0;padding:0;z-index:3}.pure-menu-horizontal .pure-menu-children{left:0;top:auto;width:inherit}.pure-menu-active>.pure-menu-children,.pure-menu-allow-hover:hover>.pure-menu-children{display:block;position:absolute}.pure-menu-has-children>.pure-menu-link:after{padding-left:.5em;content:"\25B8";font-size:small}.pure-menu-horizontal .pure-menu-has-children>.pure-menu-link:after{content:"\25BE"}.pure-menu-scrollable{overflow-y:scroll;overflow-x:hidden}.pure-menu-scrollable .pure-menu-list{display:block}.pure-menu-horizontal.pure-menu-scrollable .pure-menu-list{display:inline-block}.pure-menu-horizontal.pure-menu-scrollable{white-space:nowrap;overflow-y:hidden;overflow-x:auto;padding:.5em 0}.pure-menu-horizontal .pure-menu-children .pure-menu-separator,.pure-menu-separator{background-color:#ccc;height:1px;margin:.3em 0}.pure-menu-horizontal .pure-menu-separator{width:1px;height:1.3em;margin:0 .3em}.pure-menu-horizontal .pure-menu-children .pure-menu-separator{display:block;width:auto}.pure-menu-heading{text-transform:uppercase;color:#565d64}.pure-menu-link{color:#777}.pure-menu-children{background-color:#fff}.pure-menu-heading,.pure-menu-link{padding:.5em 1em}.pure-menu-disabled{opacity:.5}.pure-menu-disabled .pure-menu-link:hover{background-color:transparent;cursor:default}.pure-menu-active>.pure-menu-link,.pure-menu-link:focus,.pure-menu-link:hover{background-color:#eee}.pure-menu-selected>.pure-menu-link,.pure-menu-selected>.pure-menu-link:visited{color:#000}.pure-table{border-collapse:collapse;border-spacing:0;empty-cells:show;border:1px solid #cbcbcb}.pure-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.pure-table td,.pure-table th{border-left:1px solid #cbcbcb;border-width:0 0 0 1px;font-size:inherit;margin:0;overflow:visible;padding:.5em 1em}.pure-table thead{background-color:#e0e0e0;color:#000;text-align:left;vertical-align:bottom}.pure-table td{background-color:transparent}.pure-table-odd td{background-color:#f2f2f2}.pure-table-striped tr:nth-child(2n-1) td{background-color:#f2f2f2}.pure-table-bordered td{border-bottom:1px solid #cbcbcb}.pure-table-bordered tbody>tr:last-child>td{border-bottom-width:0}.pure-table-horizontal td,.pure-table-horizontal th{border-width:0 0 1px 0;border-bottom:1px solid #cbcbcb}.pure-table-horizontal tbody>tr:last-child>td{border-bottom-width:0} -------------------------------------------------------------------------------- /test/get-scale-flags.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "./helpers.ts"; 2 | import scale from "../src/lib/get-scale-flag.js"; 3 | 4 | Deno.test("does nothing if inputs are -1 or 100%", () => { 5 | assertEquals(scale("100%", "100%"), null); 6 | assertEquals(scale("-1", "-1"), null); 7 | assertEquals(scale("-1", "100%"), null); 8 | assertEquals(scale("100%", "-1"), null); 9 | }); 10 | 11 | Deno.test("can handle a % and -1", () => { 12 | assertEquals(scale("55.123%", "-1"), "-vf 'scale=iw*0.55123:-1'"); 13 | }); 14 | 15 | Deno.test("can handle a -1 and %", () => { 16 | assertEquals(scale("-1", "55.123%"), "-vf 'scale=-1:ih*0.55123'"); 17 | }); 18 | 19 | Deno.test("can handle two percentages", () => { 20 | assertEquals(scale("50%", "25%"), "-vf 'scale=iw*0.5:ih*0.25'"); 21 | }); 22 | 23 | Deno.test("can handle a number and -1", () => { 24 | assertEquals(scale("320", "-1"), "-vf 'scale=320:-1'"); 25 | }); 26 | 27 | Deno.test("can handle -1 and a number", () => { 28 | assertEquals(scale("-1", "320"), "-vf 'scale=-1:320'"); 29 | }); 30 | 31 | Deno.test("can handle two numbers", () => { 32 | assertEquals(scale("666", "420"), "-vf 'scale=666:420'"); 33 | }); 34 | -------------------------------------------------------------------------------- /test/helpers.ts: -------------------------------------------------------------------------------- 1 | export function assertEquals(a: T, b: T) { 2 | if (a !== b) { 3 | throw new Error(`${JSON.stringify(a)} !== ${JSON.stringify(b)}`); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/quote-filename.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "./helpers.ts"; 2 | import quote from "../src/lib/quote-filename.js"; 3 | 4 | Deno.test("leaves a boring filename untouched", () => { 5 | assertEquals(quote("yas.txt"), "yas.txt"); 6 | }); 7 | 8 | Deno.test("escapes a filename with an apostrophe", () => { 9 | assertEquals(quote("fart's.butt"), "fart\\'s.butt"); 10 | }); 11 | 12 | Deno.test("quotes a filename with a space", () => { 13 | assertEquals(quote("memes on the web.txt"), "'memes on the web.txt'"); 14 | }); 15 | 16 | Deno.test("handles a filename with a space and a single quote", () => { 17 | assertEquals(quote("meme's on the web.txt"), "'meme'\\''s on the web.txt'"); 18 | }); 19 | --------------------------------------------------------------------------------