├── .gitignore ├── robots.txt ├── static ├── fonts │ ├── birme.woff │ ├── logo.woff │ └── sans-serif.woff └── images │ ├── favicon.ico │ ├── favicon.png │ ├── watermark.webp │ └── stripes-light.png ├── Dockerfile ├── comparison-images ├── high │ ├── beer.jpg │ ├── cat-gang.jpg │ ├── forest-path.jpg │ ├── doggy-christmas.jpg │ └── london-traffic-light.jpg └── medium │ ├── beer.jpg │ ├── cat-gang.jpg │ ├── forest-path.jpg │ ├── doggy-christmas.jpg │ └── london-traffic-light.jpg ├── docker-compose.yml ├── css └── meddon.css ├── LICENSE ├── README.md ├── js ├── hermite.js └── main.js └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | comparison-images/src -------------------------------------------------------------------------------- /robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / -------------------------------------------------------------------------------- /static/fonts/birme.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livelifebythecode/birme-sd-variant/HEAD/static/fonts/birme.woff -------------------------------------------------------------------------------- /static/fonts/logo.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livelifebythecode/birme-sd-variant/HEAD/static/fonts/logo.woff -------------------------------------------------------------------------------- /static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livelifebythecode/birme-sd-variant/HEAD/static/images/favicon.ico -------------------------------------------------------------------------------- /static/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livelifebythecode/birme-sd-variant/HEAD/static/images/favicon.png -------------------------------------------------------------------------------- /static/fonts/sans-serif.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livelifebythecode/birme-sd-variant/HEAD/static/fonts/sans-serif.woff -------------------------------------------------------------------------------- /static/images/watermark.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livelifebythecode/birme-sd-variant/HEAD/static/images/watermark.webp -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.24.0-alpine 2 | 3 | COPY --chmod=0777 . /usr/share/nginx/html/ 4 | 5 | CMD ["nginx", "-g", "daemon off;"] -------------------------------------------------------------------------------- /comparison-images/high/beer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livelifebythecode/birme-sd-variant/HEAD/comparison-images/high/beer.jpg -------------------------------------------------------------------------------- /static/images/stripes-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livelifebythecode/birme-sd-variant/HEAD/static/images/stripes-light.png -------------------------------------------------------------------------------- /comparison-images/medium/beer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livelifebythecode/birme-sd-variant/HEAD/comparison-images/medium/beer.jpg -------------------------------------------------------------------------------- /comparison-images/high/cat-gang.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livelifebythecode/birme-sd-variant/HEAD/comparison-images/high/cat-gang.jpg -------------------------------------------------------------------------------- /comparison-images/high/forest-path.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livelifebythecode/birme-sd-variant/HEAD/comparison-images/high/forest-path.jpg -------------------------------------------------------------------------------- /comparison-images/medium/cat-gang.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livelifebythecode/birme-sd-variant/HEAD/comparison-images/medium/cat-gang.jpg -------------------------------------------------------------------------------- /comparison-images/medium/forest-path.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livelifebythecode/birme-sd-variant/HEAD/comparison-images/medium/forest-path.jpg -------------------------------------------------------------------------------- /comparison-images/high/doggy-christmas.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livelifebythecode/birme-sd-variant/HEAD/comparison-images/high/doggy-christmas.jpg -------------------------------------------------------------------------------- /comparison-images/medium/doggy-christmas.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livelifebythecode/birme-sd-variant/HEAD/comparison-images/medium/doggy-christmas.jpg -------------------------------------------------------------------------------- /comparison-images/high/london-traffic-light.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livelifebythecode/birme-sd-variant/HEAD/comparison-images/high/london-traffic-light.jpg -------------------------------------------------------------------------------- /comparison-images/medium/london-traffic-light.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livelifebythecode/birme-sd-variant/HEAD/comparison-images/medium/london-traffic-light.jpg -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | birme: 5 | container_name: birme 6 | build: . 7 | ports: 8 | - 8080:80 9 | # restart policy 10 | restart: unless-stopped -------------------------------------------------------------------------------- /css/meddon.css: -------------------------------------------------------------------------------- 1 | /* Designed by Vernon Adams */ 2 | /* https://fonts.google.com/specimen/Meddon/about */ 3 | @font-face { 4 | font-family: 'Meddon'; 5 | font-style: normal; 6 | font-weight: 400; 7 | font-display: swap; 8 | src: url(https://fonts.gstatic.com/s/meddon/v20/kmK8ZqA2EgDNeHTpgx1A.woff2) format('woff2'); 9 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 10 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 4 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 5 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 6 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 7 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 8 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 9 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # -> [Demo Site](https://storage.googleapis.com/birme-sd-variant/index.html?target_width=512&target_height=512) <- 2 | 3 | # Birme Variant for Stable Diffusion 4 | When training Stable Diffusion (or other generative image models) we need high quality and cropped training images at 512x512. Birme is the best tool for doing this quickly, and with the help of [smartcrop.js](https://github.com/jwagner/smartcrop.js/) it's truly a powerful tool for batch cropping images. 5 | 6 | ## Local Install 7 | Clone the repository and open index.html in your favorite browser (excluding Firefox). Feel free to bookmark! 8 | ```bash 9 | git clone https://github.com/livelifebythecode/birme-sd-variant.git 10 | cd birme-sd-variant 11 | python -m webbrowser index.html # or simply open the index.html file 12 | ``` 13 | 14 | ## Run with Docker-Compose 15 | ```bash 16 | git clone https://github.com/livelifebythecode/birme-sd-variant.git 17 | cd birme-sd-variant 18 | docker-compose up -d 19 | # Open browser to => http://:8080 20 | ``` 21 | 22 | ## Problem 23 | Birme restricts the users ability to choose what smoothing is applied which can result in a lower quality cropped image. 24 | 25 | In the Birme code, notice the line `con.imageSmoothingQuality = "medium";` hardcodes the smoothing quality when we crop the image. 26 | ```js 27 | process_image(img, file) { 28 | ... 29 | let canvas = document.createElement("canvas"); 30 | canvas.width = tw; 31 | canvas.height = th; 32 | let con = canvas.getContext("2d"); 33 | con.imageSmoothingEnabled = true; 34 | con.imageSmoothingQuality = "medium"; 35 | ... 36 | } 37 | ``` 38 | (sourced on 10-14-22: [line #627](https://www.birme.net/static/js/scripts-323dd.js?953e6bb6)) 39 | 40 | ## Solution 41 | Select the desired smoothing quality in the "Image Format / Quality" settings 42 | ![Image of the Quality Preset dropdown box in the "Image Format / Quality settings](https://i.imgur.com/j2Uh1KJ.png) 43 | 44 | ## Results 45 | TODO: Show comparison of 'Medium', 'High', and 'Hermite' quality presets 46 | High works better on landscape/subjects typically, where as Medium is better at smoothing close up text. 47 | 48 | ## Limitations 49 | - 🦊 FIREFOX NOT SUPPORTED - [supported browsers](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/imageSmoothingQuality#browser_compatibility) 50 | 51 | ## Authors 52 | - [Birme Author, support them](https://www.birme.net/) 53 | - Small feature written by me 54 | 55 | ## Extra 56 | The Hermite quality option uses the [Hermite resize library](https://github.com/viliusle/Hermite-resize) so you can experiment with what gives you the best quality image for your source images. 57 | -------------------------------------------------------------------------------- /js/hermite.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Hermite resize - fast image resize/resample using Hermite filter. 3 | * https://github.com/viliusle/Hermite-resize 4 | */ 5 | function Hermite_class() { 6 | var cores; 7 | var workers_archive = []; 8 | var workerBlobURL; 9 | 10 | /** 11 | * contructor 12 | */ 13 | this.init = function () { 14 | cores = navigator.hardwareConcurrency || 4; 15 | }(); 16 | 17 | /** 18 | * Returns CPU cores count 19 | * 20 | * @returns {int} 21 | */ 22 | this.getCores = function () { 23 | return cores; 24 | }; 25 | 26 | /** 27 | * Hermite resize. Detect cpu count and use best option for user. 28 | * 29 | * @param {HtmlElement} canvas 30 | * @param {int} width 31 | * @param {int} height 32 | * @param {boolean} resize_canvas if true, canvas will be resized. Optional. 33 | * @param {boolean} on_finish finish handler. Optional. 34 | */ 35 | this.resample_auto = function (canvas, width, height, resize_canvas, on_finish) { 36 | var cores = this.getCores(); 37 | 38 | if (!!window.Worker && cores > 1) { 39 | //workers supported and we have at least 2 cpu cores - using multithreading 40 | this.resample(canvas, width, height, resize_canvas, on_finish); 41 | } 42 | else { 43 | //1 cpu version 44 | this.resample_single(canvas, width, height, true); 45 | if (on_finish != undefined) { 46 | on_finish(); 47 | } 48 | } 49 | }; 50 | 51 | /** 52 | * Hermite resize. Resize actual image. 53 | * 54 | * @param {string} image_id 55 | * @param {int} width 56 | * @param {int} height optional. 57 | * @param {int} percentages optional. 58 | * @param {string} multi_core optional. 59 | */ 60 | this.resize_image = function (image_id, width, height, percentages, multi_core) { 61 | var img = document.getElementById(image_id); 62 | 63 | //create temp canvas 64 | var temp_canvas = document.createElement("canvas"); 65 | temp_canvas.width = img.width; 66 | temp_canvas.height = img.height; 67 | var temp_ctx = temp_canvas.getContext("2d"); 68 | 69 | //draw image 70 | temp_ctx.drawImage(img, 0, 0); 71 | 72 | //prepare size 73 | if (width == undefined && height == undefined && percentages != undefined) { 74 | width = img.width / 100 * percentages; 75 | height = img.height / 100 * percentages; 76 | } 77 | if (height == undefined) { 78 | var ratio = img.width / width; 79 | height = img.height / ratio; 80 | } 81 | width = Math.round(width); 82 | height = Math.round(height); 83 | 84 | var on_finish = function () { 85 | var dataURL = temp_canvas.toDataURL(); 86 | img.width = width; 87 | img.height = height; 88 | img.src = dataURL; 89 | 90 | dataURL = null; 91 | temp_canvas = null; 92 | }; 93 | 94 | //resize 95 | if (multi_core == undefined || multi_core == true) { 96 | this.resample(temp_canvas, width, height, true, on_finish); 97 | } 98 | else { 99 | this.resample_single(temp_canvas, width, height, true); 100 | on_finish(); 101 | } 102 | }; 103 | 104 | /** 105 | * Hermite resize, multicore version - fast image resize/resample using Hermite filter. 106 | * 107 | * @param {HtmlElement} canvas 108 | * @param {int} width 109 | * @param {int} height 110 | * @param {boolean} resize_canvas if true, canvas will be resized. Optional. 111 | * @param {boolean} on_finish finish handler. Optional. 112 | */ 113 | this.resample = function (canvas, width, height, resize_canvas, on_finish) { 114 | var width_source = canvas.width; 115 | var height_source = canvas.height; 116 | width = Math.round(width); 117 | height = Math.round(height); 118 | var ratio_h = height_source / height; 119 | 120 | //stop old workers 121 | if (workers_archive.length > 0) { 122 | for (var c = 0; c < cores; c++) { 123 | if (workers_archive[c] != undefined) { 124 | workers_archive[c].terminate(); 125 | delete workers_archive[c]; 126 | } 127 | } 128 | } 129 | workers_archive = new Array(cores); 130 | var ctx = canvas.getContext("2d"); 131 | 132 | //prepare source and target data for workers 133 | var data_part = []; 134 | var block_height = Math.ceil(height_source / cores / 2) * 2; 135 | var end_y = -1; 136 | for (var c = 0; c < cores; c++) { 137 | //source 138 | var offset_y = end_y + 1; 139 | if (offset_y >= height_source) { 140 | //size too small, nothing left for this core 141 | continue; 142 | } 143 | 144 | end_y = offset_y + block_height - 1; 145 | end_y = Math.min(end_y, height_source - 1); 146 | 147 | var current_block_height = block_height; 148 | current_block_height = Math.min(block_height, height_source - offset_y); 149 | 150 | //console.log('source split: ', '#'+c, offset_y, end_y, 'height: '+current_block_height); 151 | 152 | data_part[c] = {}; 153 | data_part[c].source = ctx.getImageData(0, offset_y, width_source, block_height); 154 | data_part[c].target = true; 155 | data_part[c].start_y = Math.ceil(offset_y / ratio_h); 156 | data_part[c].height = current_block_height; 157 | } 158 | 159 | //clear and resize canvas 160 | if (resize_canvas === true) { 161 | canvas.width = width; 162 | canvas.height = height; 163 | } 164 | else { 165 | ctx.clearRect(0, 0, width_source, height_source); 166 | } 167 | 168 | //start 169 | var workers_in_use = 0; 170 | for (var c = 0; c < cores; c++) { 171 | if (data_part[c] == undefined) { 172 | //no job for this worker 173 | continue; 174 | } 175 | 176 | workers_in_use++; 177 | var my_worker = new Worker(workerBlobURL); 178 | workers_archive[c] = my_worker; 179 | 180 | my_worker.onmessage = function (event) { 181 | workers_in_use--; 182 | var core = event.data.core; 183 | workers_archive[core].terminate(); 184 | delete workers_archive[core]; 185 | 186 | //draw 187 | var height_part = Math.ceil(data_part[core].height / ratio_h); 188 | data_part[core].target = ctx.createImageData(width, height_part); 189 | data_part[core].target.data.set(event.data.target); 190 | ctx.putImageData(data_part[core].target, 0, data_part[core].start_y); 191 | 192 | if (workers_in_use <= 0) { 193 | //finish 194 | if (on_finish != undefined) { 195 | on_finish(); 196 | } 197 | } 198 | }; 199 | var objData = { 200 | width_source: width_source, 201 | height_source: data_part[c].height, 202 | width: width, 203 | height: Math.ceil(data_part[c].height / ratio_h), 204 | core: c, 205 | source: data_part[c].source.data.buffer, 206 | }; 207 | my_worker.postMessage(objData, [objData.source]); 208 | } 209 | }; 210 | 211 | // Build a worker from an anonymous function body - purpose is to avoid separate file 212 | workerBlobURL = window.URL.createObjectURL(new Blob(['(', 213 | function () { 214 | //begin worker 215 | onmessage = function (event) { 216 | var core = event.data.core; 217 | var width_source = event.data.width_source; 218 | var height_source = event.data.height_source; 219 | var width = event.data.width; 220 | var height = event.data.height; 221 | 222 | var ratio_w = width_source / width; 223 | var ratio_h = height_source / height; 224 | var ratio_w_half = Math.ceil(ratio_w / 2); 225 | var ratio_h_half = Math.ceil(ratio_h / 2); 226 | 227 | var source = new Uint8ClampedArray(event.data.source); 228 | var source_h = source.length / width_source / 4; 229 | var target_size = width * height * 4; 230 | var target_memory = new ArrayBuffer(target_size); 231 | var target = new Uint8ClampedArray(target_memory, 0, target_size); 232 | //calculate 233 | for (var j = 0; j < height; j++) { 234 | for (var i = 0; i < width; i++) { 235 | var x2 = (i + j * width) * 4; 236 | var weight = 0; 237 | var weights = 0; 238 | var weights_alpha = 0; 239 | var gx_r = 0; 240 | var gx_g = 0; 241 | var gx_b = 0; 242 | var gx_a = 0; 243 | var center_y = j * ratio_h; 244 | 245 | var xx_start = Math.floor(i * ratio_w); 246 | var xx_stop = Math.ceil((i + 1) * ratio_w); 247 | var yy_start = Math.floor(j * ratio_h); 248 | var yy_stop = Math.ceil((j + 1) * ratio_h); 249 | 250 | xx_stop = Math.min(xx_stop, width_source); 251 | yy_stop = Math.min(yy_stop, height_source); 252 | 253 | for (var yy = yy_start; yy < yy_stop; yy++) { 254 | var dy = Math.abs(center_y - yy) / ratio_h_half; 255 | var center_x = i * ratio_w; 256 | var w0 = dy * dy; //pre-calc part of w 257 | for (var xx = xx_start; xx < xx_stop; xx++) { 258 | var dx = Math.abs(center_x - xx) / ratio_w_half; 259 | var w = Math.sqrt(w0 + dx * dx); 260 | if (w >= 1) { 261 | //pixel too far 262 | continue; 263 | } 264 | //hermite filter 265 | weight = 2 * w * w * w - 3 * w * w + 1; 266 | //calc source pixel location 267 | var pos_x = 4 * (xx + yy * width_source); 268 | //alpha 269 | gx_a += weight * source[pos_x + 3]; 270 | weights_alpha += weight; 271 | //colors 272 | if (source[pos_x + 3] < 255) 273 | weight = weight * source[pos_x + 3] / 250; 274 | gx_r += weight * source[pos_x]; 275 | gx_g += weight * source[pos_x + 1]; 276 | gx_b += weight * source[pos_x + 2]; 277 | weights += weight; 278 | } 279 | } 280 | target[x2] = gx_r / weights; 281 | target[x2 + 1] = gx_g / weights; 282 | target[x2 + 2] = gx_b / weights; 283 | target[x2 + 3] = gx_a / weights_alpha; 284 | } 285 | } 286 | 287 | //return 288 | var objData = { 289 | core: core, 290 | target: target, 291 | }; 292 | postMessage(objData, [target.buffer]); 293 | }; 294 | //end worker 295 | }.toString(), 296 | ')()'], {type: 'application/javascript'})); 297 | 298 | /** 299 | * Hermite resize - fast image resize/resample using Hermite filter. 1 cpu version! 300 | * 301 | * @param {HtmlElement} canvas 302 | * @param {int} width 303 | * @param {int} height 304 | * @param {boolean} resize_canvas if true, canvas will be resized. Optional. 305 | */ 306 | this.resample_single = function (canvas, width, height, resize_canvas) { 307 | var width_source = canvas.width; 308 | var height_source = canvas.height; 309 | width = Math.round(width); 310 | height = Math.round(height); 311 | 312 | var ratio_w = width_source / width; 313 | var ratio_h = height_source / height; 314 | var ratio_w_half = Math.ceil(ratio_w / 2); 315 | var ratio_h_half = Math.ceil(ratio_h / 2); 316 | 317 | var ctx = canvas.getContext("2d"); 318 | var img = ctx.getImageData(0, 0, width_source, height_source); 319 | var img2 = ctx.createImageData(width, height); 320 | var data = img.data; 321 | var data2 = img2.data; 322 | 323 | for (var j = 0; j < height; j++) { 324 | for (var i = 0; i < width; i++) { 325 | var x2 = (i + j * width) * 4; 326 | var weight = 0; 327 | var weights = 0; 328 | var weights_alpha = 0; 329 | var gx_r = 0; 330 | var gx_g = 0; 331 | var gx_b = 0; 332 | var gx_a = 0; 333 | var center_y = j * ratio_h; 334 | 335 | var xx_start = Math.floor(i * ratio_w); 336 | var xx_stop = Math.ceil((i + 1) * ratio_w); 337 | var yy_start = Math.floor(j * ratio_h); 338 | var yy_stop = Math.ceil((j + 1) * ratio_h); 339 | xx_stop = Math.min(xx_stop, width_source); 340 | yy_stop = Math.min(yy_stop, height_source); 341 | 342 | for (var yy = yy_start; yy < yy_stop; yy++) { 343 | var dy = Math.abs(center_y - yy) / ratio_h_half; 344 | var center_x = i * ratio_w; 345 | var w0 = dy * dy; //pre-calc part of w 346 | for (var xx = xx_start; xx < xx_stop; xx++) { 347 | var dx = Math.abs(center_x - xx) / ratio_w_half; 348 | var w = Math.sqrt(w0 + dx * dx); 349 | if (w >= 1) { 350 | //pixel too far 351 | continue; 352 | } 353 | //hermite filter 354 | weight = 2 * w * w * w - 3 * w * w + 1; 355 | var pos_x = 4 * (xx + yy * width_source); 356 | //alpha 357 | gx_a += weight * data[pos_x + 3]; 358 | weights_alpha += weight; 359 | //colors 360 | if (data[pos_x + 3] < 255) 361 | weight = weight * data[pos_x + 3] / 250; 362 | gx_r += weight * data[pos_x]; 363 | gx_g += weight * data[pos_x + 1]; 364 | gx_b += weight * data[pos_x + 2]; 365 | weights += weight; 366 | } 367 | } 368 | data2[x2] = gx_r / weights; 369 | data2[x2 + 1] = gx_g / weights; 370 | data2[x2 + 2] = gx_b / weights; 371 | data2[x2 + 3] = gx_a / weights_alpha; 372 | } 373 | } 374 | //clear and resize canvas 375 | if (resize_canvas === true) { 376 | canvas.width = width; 377 | canvas.height = height; 378 | } 379 | else { 380 | ctx.clearRect(0, 0, width_source, height_source); 381 | } 382 | 383 | //draw 384 | ctx.putImageData(img2, 0, 0); 385 | }; 386 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | BIRME - Variant For SD 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 32 | 33 |
34 |
35 |
36 |
37 |
38 |

Bulk Image Resizing Made Easy 2.0

39 |

BIRME - Bulk Image Resizing Made Easy 2.0

40 |

BIRME is a flexible and easy to use bulk image resizer. It can resize your images to any specific dimension and crop them proportionately if necessary. It's an online tool and you don't 41 | need to download or install on your computer. BIRME is absolutely free to use.

42 |
43 |

Drop your images here

44 | 45 |
- OR -
46 |
47 | 48 | 49 |
50 | 51 |

(Looking for the real BIRME version 2?)

52 |

(Looking for version 1?)

53 |
54 |
55 |
56 | 57 | 58 |
59 |
60 |
61 |
62 | 63 | 64 | 65 |
66 |
67 |
68 | 69 | px 70 |
71 | 72 |
73 |
74 |
75 | 76 | px 77 |
78 | 79 |
80 |
81 |
82 | 83 | : 84 | 85 |
86 |
87 |
88 | 89 | 90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
Set image focal point
105 |
106 | 107 |
108 |
109 | 110 |
111 |
112 | 113 | 114 |
115 | 116 |
117 |
118 |
119 | Color: 120 | 121 |
122 |
123 | Thickness: 124 | 125 |
126 |
127 |
128 |
129 |
130 | 131 |
132 |
133 |
134 | 135 | 142 |
143 |
144 |
145 |
146 | 147 | 148 |
149 |
150 | 151 |
152 |
153 | 154 | 155 |
156 |
Quality %
157 |
158 | 159 |
160 |
161 | 162 | 163 |
164 |
Quality %
165 |
166 | Need Help? 167 |
168 |
169 | 170 | 171 |
172 | 173 | 174 |
175 | File name pattern e.g. image-xxx
or ORIGINAL-NAME_400x400 176 | 177 |
178 |
179 | File name starting number 180 | 181 |
182 |
183 |
184 |
185 | 188 |
189 |
190 | 191 |
192 |
193 |
194 | Remove All 195 | Reset Settings 196 |
197 |
198 |
199 |
200 |

About

201 |

I made this simple copy of BIRME to add more image quality options for Stable Diffusion users who crop training images!

202 |

NOTE: FIREFOX NOT SUPPORTED (supported browsers)

203 |

Support the author of BIRME

204 |
205 |
206 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | -------------------------------------------------------------------------------- /js/main.js: -------------------------------------------------------------------------------- 1 | class BFile { 2 | constructor(file, n, url) { 3 | const dot_index = file.name.lastIndexOf("."); 4 | this.file = file; 5 | this.n = n; 6 | this.size = file.size; 7 | this.base_name = file.name.substr(0, dot_index); 8 | this.extension = file.name.substr(dot_index + 1); 9 | this.fx = this.fy = 0.5; 10 | this.is_custom_focal = false; 11 | this.url = url; 12 | } 13 | 14 | get output_format() { 15 | if (config.image_format == "jpeg") { 16 | return { ext: "jpg", format: "image/jpeg" }; 17 | } 18 | if (config.image_format == "webp") { 19 | return { ext: "webp", format: "image/webp" }; 20 | } 21 | // Preserve format 22 | switch (this.extension.toLowerCase()) { 23 | case "png": 24 | return { ext: "png", format: "image/png" }; 25 | case "jpg": 26 | case "jpeg": 27 | return { ext: this.extension, format: "image/jpeg" }; 28 | case "webp": 29 | return { ext: "webp", format: "image/webp" }; 30 | } 31 | } 32 | 33 | auto_focal(callback) { 34 | smartcrop 35 | .crop(this.image, { 36 | width: Math.min(this.width, this.height), 37 | height: Math.min(this.width, this.height), 38 | }) 39 | .then(result => { 40 | this.fx = result.topCrop.x / this.width; 41 | this.fy = result.topCrop.y / this.height; 42 | callback(this.image); 43 | }); 44 | } 45 | read(callback) { 46 | loadImage(this.path, image => { 47 | this.image = image; 48 | this.width = image.width; 49 | this.height = image.height; 50 | this.auto_focal(callback); 51 | }); 52 | } 53 | 54 | get path() { 55 | return this.url ? this.url : this.file; 56 | } 57 | 58 | get is_jpeg() { 59 | return ["jpg", "jpeg"].includes(this.extension.toLowerCase()); 60 | } 61 | 62 | get truncated_filename() { 63 | let filename = this.base_name; 64 | if (this.base_name.length > 20) { 65 | filename = this.base_name.substr(0, 15) + ".." + this.base_name.substr(this.base_name.length - 5); 66 | } 67 | return filename + "." + this.extension; 68 | } 69 | 70 | get is_supported() { 71 | return ["jpg", "jpeg", "png", "webp"].indexOf(this.extension.toLowerCase()) > -1; 72 | } 73 | 74 | get focal_x() { 75 | if (this.is_custom_focal || config.auto_focal) { 76 | return this.fx; 77 | } else { 78 | return parseFloat(config.focal_x); 79 | } 80 | } 81 | 82 | get focal_y() { 83 | if (this.is_custom_focal || config.auto_focal) { 84 | return this.fy; 85 | } else { 86 | return parseFloat(config.focal_y); 87 | } 88 | } 89 | } 90 | 91 | const default_parameters = { 92 | target_width: 1200, 93 | target_height: 1200, 94 | no_resize: false, 95 | auto_width: false, 96 | auto_height: false, 97 | focal_x: 0.5, 98 | focal_y: 0.5, 99 | auto_focal: true, 100 | image_format: "preserve", 101 | quality_jpeg: 92, 102 | quality_webp: 50, 103 | rename: "", 104 | rename_start: 0, 105 | border_width: 0, 106 | border_color: "#000", 107 | wm_text: "", 108 | wm_font: "sans-serif", 109 | wm_size: 18, 110 | wm_position: "bottom-right", 111 | wm_margin: 20, 112 | quality_preset: "high" 113 | }; 114 | 115 | class BConfig { 116 | constructor() { 117 | this.load(); 118 | } 119 | 120 | load(reset = false) { 121 | let query_params = {}; 122 | let url = document.location.href; 123 | let parts = url.substr(url.lastIndexOf("?") + 1).split("&"); 124 | 125 | for (let p of parts) { 126 | if (p.indexOf("=") == -1) { 127 | continue; 128 | } 129 | let _tempt = p.split("="); 130 | query_params[_tempt[0]] = this.clean_value(_tempt[1]); 131 | } 132 | if (!reset) { 133 | let old_url = localStorage.getItem("url"); 134 | 135 | if ($.isEmptyObject(query_params) && old_url) { 136 | history.replaceState(null, null, old_url); 137 | this.load(reset); 138 | return; 139 | } 140 | } 141 | 142 | for (let k in default_parameters) { 143 | let v = default_parameters[k]; 144 | if (query_params.hasOwnProperty(k) && !reset) { 145 | v = query_params[k]; 146 | } 147 | this[k] = v; 148 | 149 | if (k == "image_format") { 150 | k = "image_format_" + v; 151 | } 152 | 153 | let ele = $("#" + k); 154 | if (!ele.length) { 155 | continue; 156 | } 157 | switch (ele.attr("type").toLowerCase()) { 158 | case "checkbox": 159 | case "radio": 160 | ele.prop("checked", v); 161 | break; 162 | case "number": 163 | ele.val(parseInt(v)); 164 | break; 165 | default: 166 | ele.val(v); 167 | } 168 | } 169 | 170 | this.update_focal(); 171 | 172 | this.toggle_auto_wh("auto_width", "auto_height"); 173 | this.toggle_auto_wh("auto_height", "auto_width"); 174 | this.calculate_ratio(); 175 | this.toggle_no_resize(); 176 | this.toggle_auto_focal(); 177 | this.update_watermark_preview(); 178 | 179 | if (reset) { 180 | birme.preview_visible(true); 181 | this.update_url(); 182 | } 183 | } 184 | 185 | update(ele) { 186 | let name = ele.type == "radio" ? ele.name : ele.id; 187 | let value; 188 | if (ele.type == "checkbox") { 189 | value = $(ele).prop("checked"); 190 | } else { 191 | value = ele.value; 192 | } 193 | 194 | if (name == "quality_preset_select") { 195 | value = ele.options[ele.selectedIndex].value; 196 | } 197 | 198 | this[name] = value; 199 | if (name == "auto_width") { 200 | this.toggle_auto_wh("auto_width", "auto_height"); 201 | } else if (name == "auto_height") { 202 | this.toggle_auto_wh("auto_height", "auto_width"); 203 | } else if (name == "no_resize") { 204 | this.toggle_no_resize(); 205 | } 206 | 207 | if (["target_width", "target_height", "auto_width", "auto_height"].includes(name)) { 208 | if (name == "target_width") { 209 | this.last_edit = "width"; 210 | } else if (name == "target_height") { 211 | this.last_edit = "height"; 212 | } 213 | this.calculate_ratio(); 214 | } 215 | 216 | if (name.indexOf("wm_") == 0) { 217 | this.update_watermark_preview(); 218 | } 219 | 220 | this.toggle_auto_focal(); 221 | birme.preview_visible(true); 222 | this.update_url(); 223 | } 224 | 225 | toggle_auto_wh(key, other_key) { 226 | let input = $("#target" + key.substr(4)); 227 | 228 | if (this[key]) { 229 | input.attr("disabled", "disabled"); 230 | } else { 231 | input.removeAttr("disabled"); 232 | } 233 | if (this[key] && this[other_key]) { 234 | $("#" + other_key).trigger("click"); 235 | } 236 | 237 | if (this.auto_width || this.auto_height) { 238 | $("body").addClass("auto-size"); 239 | } else { 240 | $("body").removeClass("auto-size"); 241 | } 242 | } 243 | 244 | toggle_auto_focal() { 245 | if (this.auto_width || this.auto_height || this.no_resize) { 246 | $(".crop-auto, .crop-align").addClass("d-none"); 247 | } else { 248 | $(".crop-auto").removeClass("d-none"); 249 | if (this.auto_focal) { 250 | $(".crop-align").addClass("d-none"); 251 | } else { 252 | $(".crop-align").removeClass("d-none"); 253 | } 254 | } 255 | } 256 | 257 | toggle_no_resize() { 258 | $(".no-resize") 259 | .siblings() 260 | .each((index, s) => { 261 | if (this.no_resize) { 262 | s.classList.add("d-none"); 263 | } else { 264 | s.classList.remove("d-none"); 265 | } 266 | }); 267 | } 268 | 269 | toggle_convert_to_jpeg() { 270 | for (let f of birme.files) { 271 | if (f.extension == "png") { 272 | $(".convert-to-jpeg").removeClass("d-none"); 273 | break; 274 | } else { 275 | $(".convert-to-jpeg").addClass("d-none"); 276 | } 277 | } 278 | } 279 | 280 | update_focal() { 281 | let n = this.focal_x / 0.5 + (this.focal_y / 0.5) * 3 + 1; 282 | const indicator = $(".anchor-points div:last-child"); 283 | const anchor = $(`.anchor-points div:nth-child(${n})`); 284 | indicator.css({ 285 | top: anchor.css("top"), 286 | left: anchor.css("left"), 287 | }); 288 | } 289 | 290 | set_focal(ele) { 291 | ele = $(ele); 292 | const n = parseInt(ele.attr("data-n")); 293 | if (n == 9) { 294 | return; 295 | } 296 | this.focal_x = (n % 3) * 0.5; 297 | this.focal_y = Math.floor(n / 3) * 0.5; 298 | const indicator = $(".anchor-points div:last-child"); 299 | indicator.css({ top: ele.css("top"), left: ele.css("left") }); 300 | birme.preview_visible(true); 301 | this.update_url(); 302 | } 303 | 304 | toggle_panel(ele) { 305 | $(".panel.show .options-holder").slideUp(300); 306 | $(".panel.show").removeClass("show"); 307 | 308 | $(ele).parent().addClass("show"); 309 | $(ele).next().slideDown(300); 310 | } 311 | 312 | clean_value(v) { 313 | v = decodeURIComponent(v); 314 | if (v.toLowerCase() == "true" || v.toLowerCase() == "false") { 315 | return v == "true"; 316 | } else { 317 | return v; 318 | } 319 | } 320 | 321 | update_url() { 322 | let params = []; 323 | for (let k in default_parameters) { 324 | let v = this[k]; 325 | if (v != default_parameters[k]) { 326 | params.push(k + "=" + encodeURIComponent(v)); 327 | } 328 | } 329 | history.replaceState(null, null, "?" + params.join("&")); 330 | localStorage.setItem("url", document.location.search); 331 | } 332 | 333 | update_ratio() { 334 | let r = parseFloat($("#ratio_w").val()) / parseFloat($("#ratio_h").val()); 335 | if (this.last_edit == "height") { 336 | this.target_width = Math.floor(this.target_height * r); 337 | $("#target_width").val(this.target_width); 338 | } else { 339 | this.target_height = Math.floor(this.target_width / r); 340 | $("#target_height").val(this.target_height); 341 | } 342 | this.toggle_auto_focal(); 343 | birme.preview_visible(true); 344 | this.update_url(); 345 | } 346 | 347 | calculate_ratio() { 348 | if (this.auto_width || this.auto_height) { 349 | $(".ratio").addClass("d-none"); 350 | return; 351 | } else { 352 | $(".ratio").removeClass("d-none"); 353 | } 354 | let w = this.target_width; 355 | let h = this.target_height; 356 | 357 | for (let i = 2; i <= Math.min(w, h); i++) { 358 | if (w % i == 0 && h % i == 0) { 359 | w /= i; 360 | h /= i; 361 | i = 1; 362 | } 363 | } 364 | $("#ratio_w").val(w); 365 | $("#ratio_h").val(h); 366 | } 367 | 368 | map_font(f) { 369 | return { cursive: "Meddon", serif: "Times New Roman", "sans-serif": "Helvetica, Arial" }[f]; 370 | } 371 | update_watermark_preview() { 372 | document.querySelector(".wm-preview .text").style.fontFamily = this.map_font(this.wm_font); 373 | document.querySelector(".wm-preview .text").style.fontSize = this.wm_size + "px"; 374 | if (this.wm_text) { 375 | localStorage.removeItem("wm_image"); 376 | document.querySelector(".wm-preview .text").innerHTML = this.wm_text ? this.wm_text : "©2022 All Rights Reserved"; 377 | document.querySelector(".wm-preview .image").innerHTML = ""; 378 | } else { 379 | document.querySelector(".wm-preview .text").innerHTML = ""; 380 | let image = localStorage.getItem("wm_image"); 381 | if (image) { 382 | document.querySelector(".wm-preview .image").innerHTML = ``; 383 | this.wm_image = document.querySelector(".wm-preview .image img"); 384 | setTimeout(() => { 385 | this.wm_image_height = document.querySelector(".wm-preview .image").offsetHeight; 386 | this.wm_image_width = document.querySelector(".wm-preview .image").offsetWidth; 387 | }, 100); 388 | } 389 | } 390 | } 391 | upload_wm(e) { 392 | let _files = e["dataTransfer"] ? e.dataTransfer.files : e.target.files; 393 | loadImage(_files[0], image => { 394 | let canv = document.createElement("canvas"); 395 | let con = canv.getContext("2d"); 396 | canv.width = image.width; 397 | canv.height = image.height; 398 | con.drawImage(image, 0, 0); 399 | localStorage.setItem("wm_image", canv.toDataURL()); 400 | this.wm_text = ""; 401 | document.querySelector("#wm_text").value = ""; 402 | this.update_watermark_preview(); 403 | }); 404 | e.currentTarget.value = ""; 405 | } 406 | } 407 | 408 | class Birme { 409 | constructor() { 410 | this.files = []; 411 | this.files_to_add = []; 412 | this.output_zip = false; 413 | this.zip = new JSZip(); 414 | this.file_counter = 0; 415 | this.selected_holder = null; 416 | this.mask_pattern = new Image(); 417 | this.mask_pattern.src = "static/images/stripes-light.png"; 418 | this.masonry = new Masonry(".tiles-holder", { 419 | transitionDuration: 0, 420 | }); 421 | 422 | let drop_area = document.querySelector("body"); 423 | drop_area.addEventListener("drop", e => { 424 | e.stopPropagation(); 425 | e.preventDefault(); 426 | this.add_all(e); 427 | }); 428 | drop_area.addEventListener("dragover", e => { 429 | e.stopPropagation(); 430 | e.preventDefault(); 431 | }); 432 | drop_area.addEventListener("dragenter", e => { 433 | e.stopPropagation(); 434 | e.preventDefault(); 435 | }); 436 | document.querySelector(".tiles-holder").addEventListener("scroll", _ => this.preview_visible(false)); 437 | window.addEventListener("resize", _ => this.preview_visible(true)); 438 | window.addEventListener("mouseup", _ => $(document).off("mousemove")); 439 | } 440 | 441 | add_all(e) { 442 | this.files_to_add = []; 443 | let _files = e["dataTransfer"] ? e.dataTransfer.files : e.target.files; 444 | for (let i = 0; i < _files.length; i++) { 445 | let f = new BFile(_files[i], this.files.length); 446 | if (f.is_supported) { 447 | this.files.push(f); 448 | this.files_to_add.push(f); 449 | } 450 | } 451 | config.toggle_convert_to_jpeg(); 452 | $("body").addClass("not-empty"); 453 | this.add_one(); 454 | } 455 | 456 | add_one() { 457 | let f = this.files_to_add.shift(); 458 | if (!f) { 459 | setTimeout(() => { 460 | this.preview_visible(false); 461 | }, 500); 462 | return; 463 | } 464 | let ele = ` 465 |
466 |
467 |
x
468 | 469 |
470 |

${f.truncated_filename}

471 |
`; 472 | $(".tiles-holder").append(ele); 473 | let dom_ele = document.querySelector(".tile:last-child"); 474 | this.masonry.appended(dom_ele); 475 | let holder = $(dom_ele.querySelector(".image-holder")); 476 | f.read(img => { 477 | holder.append(img); 478 | this.add_one(); 479 | }); 480 | holder.data("file", f); 481 | holder.on("mousedown", this._image_mousedown); 482 | holder.children(".btn-delete").on("click", this.remove_one); 483 | this.masonry.layout(); 484 | } 485 | 486 | remove_one(event) { 487 | let holder = $(event.target).closest(".image-holder"); 488 | for (let i = 0; i < birme.files.length; i++) { 489 | if (birme.files[i] == holder.data("file")) { 490 | birme.files.splice(i, 1); 491 | break; 492 | } 493 | } 494 | birme.masonry.remove(holder.parent().get(0)); 495 | $(holder).parent().detach(); 496 | if (birme.files.length == 0) { 497 | $("body").removeClass("not-empty"); 498 | } else { 499 | birme.masonry.layout(); 500 | } 501 | } 502 | 503 | preview_visible(force_update = false) { 504 | this.masonry.layout(); 505 | let tiles = document.querySelectorAll(".tile"); 506 | let holder = document.querySelector(".tiles-holder"); 507 | for (let i = 0; i < tiles.length; i++) { 508 | if (tiles[i].offsetTop + tiles[i].offsetHeight - holder.scrollTop > 0 && tiles[i].offsetTop - holder.scrollTop < holder.offsetHeight) { 509 | this.preview_one(tiles[i], this.files[i], force_update); 510 | } 511 | } 512 | } 513 | 514 | preview_one(holder, file, force_update) { 515 | const mask = holder.querySelector(".image-mask"); 516 | if (!force_update && mask.getAttribute("width") > 0) { 517 | return; 518 | } 519 | var img = holder.querySelector("img"); 520 | const tw = config.target_width; 521 | const th = config.target_height; 522 | const fx = file.focal_x; 523 | const fy = file.focal_y; 524 | const w = img.offsetWidth; 525 | const h = img.offsetHeight; 526 | let nw = w; 527 | let nh = h; 528 | if (!(config.auto_width || config.auto_height || config.no_resize)) { 529 | nw = tw * Math.min(w / tw, h / th); 530 | nh = th * Math.min(w / tw, h / th); 531 | } 532 | 533 | mask.width = w; 534 | mask.height = h; 535 | 536 | const ctx = mask.getContext("2d"); 537 | ctx.fillStyle = ctx.createPattern(this.mask_pattern, "repeat"); 538 | ctx.fillRect(0, 0, w, h); 539 | ctx.clearRect((w - nw) * fx, (h - nh) * fy, nw, nh); 540 | if (config.border_width > 0) { 541 | let border_width = Math.max(2, Math.round((config.border_width * w) / tw)); 542 | ctx.strokeStyle = config.border_color; 543 | ctx.lineWidth = border_width; 544 | ctx.strokeRect((w - nw) * fx + border_width / 2, (h - nh) * fy + border_width / 2, nw - border_width, nh - border_width); 545 | } 546 | } 547 | 548 | save_all(output_zip) { 549 | this.show_modal("loading"); 550 | this.output_zip = output_zip; 551 | // if (this.files.length == 1) { 552 | // this.output_zip = false; 553 | // } 554 | this.zip = new JSZip(); 555 | this.files_to_save = this.files.slice(0); 556 | this.save_one(); 557 | } 558 | 559 | save_zip(b, filename) { 560 | this.zip.file(filename, b, { 561 | base64: true, 562 | }); 563 | if (this.files_to_save.length == 0) { 564 | let w = config.auto_width ? "auto" : config.target_width; 565 | let h = config.auto_height ? "auto" : config.target_height; 566 | this.zip 567 | .generateAsync({ 568 | type: "blob", 569 | }) 570 | .then(content => { 571 | saveAs(content, `birme-${w}x${h}.zip`); 572 | this.hide_modal(); 573 | }); 574 | } else { 575 | this.save_one(); 576 | } 577 | } 578 | 579 | save_one() { 580 | if (this.files_to_save.length == 0) { 581 | this.hide_modal(); 582 | return; 583 | } 584 | let f = this.files_to_save.shift(); 585 | loadImage(f.path, img => this.process_image(img, f), { orientation: 1 }); 586 | } 587 | 588 | process_image(img, file) { 589 | let tw = config.target_width; 590 | let th = config.target_height; 591 | 592 | const fx = file.focal_x; 593 | const fy = file.focal_y; 594 | 595 | const iw = img.width; 596 | const ih = img.height; 597 | 598 | if (config.no_resize) { 599 | tw = file.width; 600 | th = file.height; 601 | } else if (config.auto_width) { 602 | tw = (img.width * th) / ih; 603 | } else if (config.auto_height) { 604 | th = (img.height * tw) / iw; 605 | } 606 | 607 | let scale = Math.min(iw / tw, ih / th); 608 | let srcw = tw * scale; 609 | let srch = th * scale; 610 | 611 | let smoothingEnabled = true; 612 | let smoothingQuality = "high"; 613 | 614 | switch (config.quality_preset) { 615 | case "low": 616 | case "medium": 617 | case "high": 618 | smoothingEnabled = true; 619 | smoothingQuality = config.quality_preset; 620 | break; 621 | case "disabled": 622 | smoothingEnabled = false; 623 | break; 624 | case "hermite": 625 | smoothingEnabled = false; 626 | break; 627 | default: 628 | console.error(`FATAL ERROR: Unexpected quality_preset=${config.quality_preset}, try re-selecting your chosen downscale quality preset in settings!`); 629 | throw Error(`FATAL ERROR: Unexpected quality_preset=${config.quality_preset} :(`); 630 | } 631 | 632 | // TODO: SUPPORT JPEG/WEBP QUALITY AND BORDER WIDTH SETTING 633 | if (config.quality_preset == "hermite") { 634 | let canvasHermite = document.createElement("canvas"); 635 | let ctxHermite = canvasHermite.getContext("2d"); 636 | 637 | //prepare canvas 638 | canvasHermite.width = srcw; 639 | canvasHermite.height = srch; 640 | 641 | //crop image based on focal selection (at full resolution) 642 | ctxHermite.drawImage(img, (iw - srcw) * fx, (ih - srch) * fy, srcw, srch, 0, 0, srcw, srch); 643 | 644 | // Use Hermite library for image downscaling 645 | var HERMITE = new Hermite_class(); 646 | HERMITE.resample_single(canvasHermite, tw, th, true); 647 | 648 | const new_filename = file.base_name + "." + file.output_format.ext; 649 | if (this.output_zip) { 650 | canvasHermite.toBlob(b => this.save_zip(b, new_filename), file.output_format.format); 651 | } else { 652 | canvasHermite.toBlob( 653 | b => { 654 | saveAs(b, new_filename); 655 | this.save_one(); 656 | }, 657 | file.output_format.format 658 | ); 659 | } 660 | return; 661 | } 662 | 663 | let canvas = document.createElement("canvas"); 664 | canvas.width = tw; 665 | canvas.height = th; 666 | let con = canvas.getContext("2d"); 667 | 668 | // UTILIZE SEMI-SMART IMAGE DOWNSAMPLING 669 | con.imageSmoothingEnabled = smoothingEnabled; 670 | con.imageSmoothingQuality = smoothingQuality; 671 | 672 | let output = file.output_format; 673 | // Draw a white background for transparent images 674 | if (output.format == "image/jpeg" && !file.is_jpeg) { 675 | con.fillStyle = "white"; 676 | con.fillRect(0, 0, tw, th); 677 | } 678 | /******************************************* 679 | * Border 680 | ******************************************/ 681 | let hw = 0; 682 | if (config.border_width > 0) { 683 | con.lineWidth = config.border_width; 684 | con.strokeStyle = config.border_color; 685 | hw = config.border_width / 2; 686 | con.strokeRect(hw, hw, tw - hw * 2, th - hw * 2); 687 | } 688 | /******************************************* 689 | * Image after the border 690 | ******************************************/ 691 | con.drawImage(img, (iw - srcw) * fx, (ih - srch) * fy, srcw, srch, 692 | hw, hw, tw - hw * 2, th - hw * 2); 693 | if (config.wm_text) { // ENGAGE WATERMARKING TEXT 694 | con.font = config.wm_size + "px " + config.map_font(config.wm_font); 695 | con.textBaseline = "top"; 696 | con.textAlign = "right"; 697 | con.fillStyle = "rgba(255,255,255,0.8)"; 698 | con.shadowOffsetY = 2; 699 | con.shadowBlur = 5; 700 | con.shadowColor = "rgba(0,0,0,0.8)"; 701 | con.fillText(config.wm_text, tw - 10, th - config.wm_size - 10); 702 | } else if (config.wm_image) { // ENGAGE WATERMARKING IMAGE 703 | con.drawImage(config.wm_image, tw - config.wm_image_width - 10, th - 10 - config.wm_image_height); 704 | } 705 | let new_filename; 706 | if (config.rename) { 707 | if (config.rename.indexOf('ORIGINAL-NAME') > -1) { 708 | new_filename = config.rename.replace('ORIGINAL-NAME', file.base_name); 709 | if (new_filename.toLowerCase().indexOf('.' + output.ext) == -1) new_filename += '.' + output.ext; 710 | } else { 711 | let filename = config.rename.toLowerCase(); 712 | var pattern = new RegExp("x{2,}"); 713 | var result = pattern.exec(filename); 714 | if (!result) { 715 | pattern = new RegExp("x+"); 716 | result = pattern.exec(filename); 717 | } 718 | if (!result) { 719 | alert('Sorry the filename pattern cannot be recognized.\nPlease try something like "image-xxx".'); 720 | return; 721 | } 722 | let front = filename.substr(0, result.index); 723 | let end = filename.substr(result.index + result[0].length); 724 | let index = config.rename_start + ""; 725 | config.rename_start++; 726 | new_filename = front + index.padStart(result[0].length, "0") + end; 727 | new_filename = new_filename.replace(/(\.jpe?g)|(\.png)/i, ""); 728 | new_filename += "." + output.ext; 729 | config.update_url(); 730 | $("#rename_start").val(config.rename_start); 731 | } 732 | } else { 733 | new_filename = file.base_name + "." + output.ext; 734 | } 735 | 736 | let quality = 92; 737 | if (output.format == "image/jpeg") { 738 | quality = config.quality_jpeg / 100; 739 | } else if (output.format == "image/webp") { 740 | quality = config.quality_webp / 100; 741 | } else if (output.format == "image/png") { 742 | quality = -1; 743 | } 744 | if (this.output_zip) { 745 | if (quality > 0) { 746 | canvas.toBlob(b => this.save_zip(b, new_filename), output.format, quality); 747 | } else { 748 | canvas.toBlob(b => this.save_zip(b, new_filename), output.format); 749 | } 750 | } else { 751 | if (quality > 0) { 752 | canvas.toBlob( 753 | b => { 754 | saveAs(b, new_filename); 755 | this.save_one(); 756 | }, 757 | output.format, 758 | quality 759 | ); 760 | } else { 761 | canvas.toBlob( 762 | b => { 763 | saveAs(b, new_filename); 764 | this.save_one(); 765 | }, 766 | output.format 767 | ); 768 | } 769 | } 770 | } 771 | 772 | show_section(section, jump = false) { 773 | let ty = $(".section-" + section).offset().top - $("nav").height() - 13; 774 | if (jump) { 775 | $("html,body").scrollTop(ty); 776 | } else { 777 | $("html,body").animate({ 778 | scrollTop: ty, 779 | }); 780 | } 781 | } 782 | show_modal(name) { 783 | $(".modal").addClass("show-" + name); 784 | } 785 | hide_modal() { 786 | $(".modal").removeClass("show-loading"); 787 | $(".modal").removeClass("show-wm"); 788 | } 789 | 790 | _image_mousedown(event) { 791 | if (config.auto_width || config.auto_height) return; 792 | let holder = $(event.originalEvent.target); 793 | if (!holder.hasClass("image-holder")) { 794 | holder = holder.closest(".image-holder"); 795 | } 796 | let file = holder.data("file"); 797 | holder.data("x", event.clientX); 798 | holder.data("y", event.clientY); 799 | 800 | holder.data("fx", file.focal_x); 801 | holder.data("fy", file.focal_y); 802 | birme.selected_holder = holder; 803 | $(document).off("mousemove"); 804 | $(document).on("mousemove", birme._image_mousemove); 805 | } 806 | 807 | _image_mousemove(event) { 808 | let holder = birme.selected_holder; 809 | let file = holder.data("file"); 810 | 811 | let x = event.clientX; 812 | let y = event.clientY; 813 | let ox = holder.data("x"); 814 | let oy = holder.data("y"); 815 | 816 | let fx = holder.data("fx"); 817 | let fy = holder.data("fy"); 818 | 819 | let new_fx = fx + ((x - ox) / holder.width()) * 2; 820 | let new_fy = fy + ((y - oy) / holder.height()) * 2; 821 | 822 | new_fx = Math.max(0, Math.min(1, new_fx)); 823 | new_fy = Math.max(0, Math.min(1, new_fy)); 824 | 825 | file.fx = new_fx; 826 | file.fy = new_fy; 827 | file.is_custom_focal = true; 828 | 829 | if (new_fx != fx || new_fy != fy) { 830 | birme.preview_one(holder.get(0), file, true); 831 | } 832 | } 833 | 834 | _get_holder_index(holder) { 835 | let holders = $(".image-holder"); 836 | for (let i = 0; i < holders.length; i++) { 837 | if (holders[i] == holder) { 838 | return i; 839 | } 840 | } 841 | return -1; 842 | } 843 | 844 | _add_test_image(n, ext) { 845 | var f = new BFile(new File([""], `test-image-${n}.${ext}`), this.files.length, `http://${document.location.host}/static/images/test/${n}.${ext}`); 846 | this.files.push(f); 847 | this.files_to_add.push(f); 848 | this.add_one(); 849 | } 850 | } 851 | 852 | let birme = new Birme(); 853 | let config = new BConfig(); 854 | 855 | // Testing code 856 | if (document.location.href.indexOf("8080") > -1) { 857 | for (var i = 0; i < 1; i++) { 858 | birme._add_test_image(i + 1, "jpg"); 859 | // birme._add_test_image(i, "webp"); 860 | } 861 | // birme._add_test_image("2", "png"); 862 | $("body").addClass("not-empty"); 863 | // birme.show_modal("wm"); 864 | } 865 | --------------------------------------------------------------------------------