Skip to content

Instantly share code, notes, and snippets.

@motsu0
Last active February 19, 2023 10:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save motsu0/b1486bd23782908c8b5a3bb21d517b5c to your computer and use it in GitHub Desktop.
Save motsu0/b1486bd23782908c8b5a3bb21d517b5c to your computer and use it in GitHub Desktop.
.block{
margin: 8px 0 32px 0;
}
.bt-control{
display: block;
margin: 12px 0;
cursor: pointer;
}
.message{
margin: 12px 0;
color: #e00000;
}
/* */
#input-file{
display: none;
}
#bt-file-input{
display: block;
width: 80%;
height: 60px;
font-size: 1.2rem;
cursor: pointer;
}
#bt-file-input.s-dragover{
background-color: #ddd;
}
/* */
.bt-del{
display: block;
margin: 0 auto;
border: none;
background-color: #e00000;
color: #fff;
cursor: pointer;
}
.bt-del:hover{
background-color: #d00000;
}
/* */
.settings__row{
padding: 8px;
border: 1px dashed #555;
}
.settings__row:nth-of-type(n+2){
border-top: none;
}
.input-number{
width: 100px;
height: 20px;
margin: 4px;
}
.settings__radio-group{
color: #999;
}
.settings__radio-group:nth-of-type(n+2){
margin-top: 4px;
}
.radio{
vertical-align: middle;
}
.radio:checked+.label-radio{
color: #222;
}
.radio:checked~.radio-text{
color: #222;
}
.radio,.label-radio{
cursor: pointer;
}
.label-radio[for="radio-jpg"]{
margin-right: 20px;
}
#input-quality{
width: 40px;
height: 20px;
margin-left: 4px;
}
#input-name-plus{
width: 100px;
height: 20px;
}
#bt-submit{
padding: 8px 16px;
margin-top: 24px;
cursor: pointer;
}
/* */
.area-table{
overflow-x: auto;
}
.table-file{
border-collapse: collapse;
}
.table-file__thead{
background-color: #C8E6C9;
text-align: center;
}
.table-file__td{
box-sizing: border-box;
border: 1px solid #777;
padding: 2px 8px;
word-break: keep-all;
white-space: nowrap;
}
.table-file__td--right{
text-align: right;
}
/* */
.s-hide{
display: none;
}
.t-red{
color: #e00000;
}
<h3>入力</h3>
<div class="block">
<input type="file" id="input-file" multiple>
<button id="bt-file-input">画像を読み込む</button>
<p>
※あまりファイル数が多すぎると固まるので注意。<br>
最大20枚程度を推奨。
</p>
</div>
<h3>ファイルリスト</h3>
<div class="block">
<div class="control">
<button id="bt-clear" class="bt-control">入力ファイルを全て消去</button>
</div>
<div id="message-input" class="message s-hide">いくつかの画像の読込に失敗しました。</div>
<div class="area-table">
<table class="table-file">
<thead class="table-file__thead">
<tr>
<td class="table-file__td">削除</td>
<td class="table-file__td">ファイル名</td>
<td class="table-file__td">容量</td>
</tr>
</thead>
<tbody id="tbody-file-input"></tbody>
</table>
</div>
</div>
<h4>出力設定</h4>
<div class="block">
<div class="settings">
<div class="settings__row">
最大横幅(width) :<input type="number" id="input-width" class="input-number" value="">px<br>
最大縦幅(height):<input type="number" id="input-height" class="input-number" value="">px
</div>
<div class="settings__row">
<div class="settings__radio-group">
<input type="radio" name="extension" value="jpg" id="radio-jpg" class="radio" checked>
<label for="radio-jpg" class="label-radio">.jpg</label>
<span class="radio-text">画質(0-100):<input type="number" id="input-quality" placeholder="80" value="80" min="0" max="100"></span>
</div>
<div class="settings__radio-group">
<input type="radio" name="extension" value="png" id="radio-png" class="radio">
<label for="radio-png" class="label-radio">.png</label>
</div>
</div>
<div class="settings__row">
出力ファイル名 = 入力ファイル名 + <input type="text" id="input-name-plus" value="-resized" placeholder="-resized"> + 拡張子
</div>
</div>
<button id="bt-submit">変換する</button>
</div>
<h3>出力</h3>
<div class="block">
<div class="control">
<button id="bt-dl-all" class="bt-control">全て保存</button>
<button id="bt-dl-zip" class="bt-control">zipにまとめて保存</button>
</div>
<div id="message-output" class="message s-hide"></div>
<div class="area-table">
<table class="table-file">
<thead class="table-file__thead">
<tr>
<td class="table-file__td"></td>
<td class="table-file__td">ファイル名</td>
<td class="table-file__td">元容量</td>
<td class="table-file__td">新容量</td>
<td class="table-file__td">圧縮率</td>
<td class="table-file__td">元解像度</td>
<td class="table-file__td">新解像度</td>
</tr>
</thead>
<tbody id="tbody-file-output"></tbody>
</table>
</div>
</div>
const nowloading = new nowLoading();
nowloading.start();
const el_input_file = document.getElementById('input-file');
const bt_file_input = document.getElementById('bt-file-input');
const bt_clear = document.getElementById('bt-clear');
const el_message_input = document.getElementById('message-input');
const els_tr_file_input = document.getElementsByClassName('tr-file-input');
const el_tbody_file_input = document.getElementById('tbody-file-input');
const el_input_width = document.getElementById('input-width');
const el_input_height = document.getElementById('input-height');
const el_radio_jpg = document.getElementById('radio-jpg');
const el_input_quality = document.getElementById('input-quality');
const el_input_name_plus = document.getElementById('input-name-plus');
const bt_submit = document.getElementById('bt-submit');
const bt_dl_all = document.getElementById('bt-dl-all');
const bt_dl_zip = document.getElementById('bt-dl-zip');
const el_message_output = document.getElementById('message-output');
const el_tbody_file_output = document.getElementById('tbody-file-output');
const els_a_download = document.getElementsByClassName('a-download');
let array_URL = [];
let list_files_input = [];
let list_files_output = [];
init();
nowloading.stop();
function init(){
bt_file_input.addEventListener('click',()=>{
el_input_file.click();
});
//ボタンへのdragenter
bt_file_input.addEventListener('dragenter',()=>{
bt_file_input.classList.add('s-dragover');
});
//ボタンへのdragover
bt_file_input.addEventListener('dragover',e=>{
e.preventDefault();
});
//ボタンからのdragleave
bt_file_input.addEventListener('dragleave',()=>{
bt_file_input.classList.remove('s-dragover');
});
//ボタンへのdrop => ファイル処理
bt_file_input.addEventListener('drop',e=>{
e.preventDefault();
bt_file_input.classList.remove('s-dragover');
const files = e.dataTransfer.files;
nowloading.start();
setTimeout(()=>{
fileCheck(files);
},1);
});
//ボタンクリック => ファイル処理
el_input_file.addEventListener('change',e=>{
const files = e.target.files;
nowloading.start();
setTimeout(()=>{
fileCheck(files);
},1);
});
//
bt_clear.addEventListener('click',fileClear);
bt_submit.addEventListener('click',()=>{
nowloading.start();
setTimeout(()=>{
fileChange();
},1);
});
bt_dl_all.addEventListener('click',downloadAll);
bt_dl_zip.addEventListener('click',downloadZip);
}
function fileCheck(files){
if(files.length===0){
nowloading.stop();
return
}
const files_img = [...files].filter(file=>file.type.includes('image'));
el_input_file.value = '';
if(files_img.length===0){
nowloading.stop();
return
}
fileLoad(files_img);
}
function fileLoad(files){
const array_promise = [];
let is_img_onerror = false;
el_message_input.classList.add('s-hide');
files.forEach(file=>{
//img処理
const promise = new Promise((resolve,reject)=>{
const img = new Image();
img.onload = ()=>{
resolve({
file: file,
element: img
});
};
img.onerror = ()=>{
is_img_onerror = true;
resolve();
}
const reader = new FileReader();
reader.onload = ev=>{
img.src = ev.target.result;
}
reader.readAsDataURL(file);
});
array_promise.push(promise);
});
Promise.all(array_promise)
.then(values=>{
if(is_img_onerror){
console.error('some imgs load failure');
el_message_input.classList.remove('s-hide');
}else{
console.log('all imgs loaded');
}
values.forEach(obj_val=>{
if(obj_val===undefined) return;
const file = obj_val.file;
list_files_input.push({
name: file.name,
size_old: file.size,
element: obj_val.element
});
//dom処理
const el_tr = document.createElement('tr');
el_tr.classList.add('tr-file-input');
//td 削除
const el_td_del = document.createElement('td');
el_td_del.classList.add('table-file__td');
const bt_del = document.createElement('button');
bt_del.classList.add('bt-del');
bt_del.textContent = '✕';
bt_del.addEventListener('click',()=>{
fileDel(el_tr);
});
el_td_del.appendChild(bt_del);
el_tr.appendChild(el_td_del);
//td ファイル名
const el_td_name = document.createElement('td');
el_td_name.classList.add('table-file__td');
el_td_name.classList.add('table-file__td--name');
el_td_name.textContent = file.name;
el_tr.appendChild(el_td_name);
//td 容量
const el_td_size_old = document.createElement('td');
el_td_size_old.classList.add('table-file__td');
el_td_size_old.classList.add('table-file__td--right');
const str_size = sizeToStr(file.size);
el_td_size_old.textContent = str_size;
el_tr.appendChild(el_td_size_old);
//tr 追加
el_tbody_file_input.appendChild(el_tr);
});
nowloading.stop();
});
}
function fileClear(){
el_tbody_file_input.textContent = '';
list_files_input = [];
el_message_input.classList.add('s-hide');
}
function fileDel(el_tr){
const index = [...els_tr_file_input].indexOf(el_tr);
list_files_input = list_files_input.filter((v,i)=>i!==index);
el_tr.remove();
el_message_output.classList.add('s-hide');
}
function fileChange(){
el_message_output.classList.add('s-hide');
if(list_files_input.length===0){
nowloading.stop();
return;
}
el_tbody_file_output.textContent = '';
list_files_output = [];
array_URL.forEach(url=>{
window.URL.revokeObjectURL(url);
});
array_URL = [];
//共通値取得
const width_max = getWH(Number(el_input_width.value));
const height_max = getWH(Number(el_input_height.value));
const extension = (()=>{
if(el_radio_jpg.checked){
return '.jpg';
}else{
return '.png';
}
})();
const quality = (()=>{
let temp = Number(el_input_quality.value);
if(temp<0){
temp = 0;
el_input_quality.value = 0;
}
if(temp>100){
temp = 100;
el_input_quality.value = 100;
}
return temp/100;
})();
const type_img = (()=>{
if(el_radio_jpg.checked){
return 'image/jpeg';
}else{
return 'image/png';
}
})();
const array_promise = [];
list_files_input.forEach(file=>{
//縮小割合算出
const img = file.element;
const width_old = img.naturalWidth;
const height_old = img.naturalHeight;
const rate_w = calcRate(width_old, width_max);
const rate_h = calcRate(height_old, height_max);
const rate = Math.min(rate_w,rate_h);
const width_new = (()=>{
const temp = Math.floor(width_old*rate);
if(temp===0) return 1;
return temp;
})();
const height_new = (()=>{
const temp = Math.floor(height_old*rate);
if(temp===0) return 1;
return temp;
})();
//canvas初期化 => draw
const canvas = document.createElement('canvas');
canvas.width = width_new;
canvas.height = height_new;
const ctx = canvas.getContext('2d');
ctx.drawImage(img,0,0,width_new,height_new);
//dom処理
const el_tr = document.createElement('tr');
//td 保存
const el_td_download = document.createElement('td');
el_td_download.classList.add('table-file__td');
const el_a = document.createElement('a');
const match_name = file.name.match(/(.*)\..*/);
const name_new = match_name[1] + el_input_name_plus.value + extension;
file.name_new = name_new;
el_a.classList.add('a-download');
el_a.download = name_new;
el_td_download.appendChild(el_a);
el_tr.appendChild(el_td_download);
//td ファイル名
const el_td_name = document.createElement('td');
el_td_name.classList.add('table-file__td');
el_td_name.textContent = name_new;
el_tr.appendChild(el_td_name);
//td 元容量
const el_td_size_old = document.createElement('td');
el_td_size_old.classList.add('table-file__td');
el_td_size_old.classList.add('table-file__td--right');
const str_size_old = sizeToStr(file.size_old);
el_td_size_old.textContent = str_size_old;
el_tr.appendChild(el_td_size_old);
//td 新容量
const el_td_size_new = document.createElement('td');
el_td_size_new.classList.add('table-file__td');
el_td_size_new.classList.add('table-file__td--right');
el_tr.appendChild(el_td_size_new);
//td 圧縮率
const el_td_compress = document.createElement('td');
el_td_compress.classList.add('table-file__td');
el_td_compress.classList.add('table-file__td--right');
el_tr.appendChild(el_td_compress);
//td 元解像度
const el_td_wh_old = document.createElement('td');
el_td_wh_old.classList.add('table-file__td');
el_td_wh_old.classList.add('table-file__td--right');
el_td_wh_old.textContent = width_old+'×'+height_old;
el_tr.appendChild(el_td_wh_old);
//td 新解像度
const el_td_wh_new = document.createElement('td');
el_td_wh_new.classList.add('table-file__td');
el_td_wh_new.classList.add('table-file__td--right');
el_td_wh_new.textContent = width_new+'×'+height_new;
el_tr.appendChild(el_td_wh_new);
//
el_tbody_file_output.appendChild(el_tr);
//canvas => a 処理
const promise = new Promise(resolve=>{
if(el_radio_jpg.checked){
canvas.toBlob(blob=>{
fileChangeSub(file, blob, el_a, el_td_size_new, el_td_compress);
resolve();
},'image/jpeg',quality);
}else{
canvas.toBlob(blob=>{
fileChangeSub(file, blob, el_a, el_td_size_new, el_td_compress);
resolve();
},'image/png');
}
});
array_promise.push(promise);
});
Promise.all(array_promise)
.then(()=>{
console.log('all imgs changed');
nowloading.stop();
});
function fileChangeSub(file, blob, el_a, el_td_size_new, el_td_compress){
//dlリンク処理
const url_img = window.URL.createObjectURL(blob);
el_a.href = url_img;
el_a.textContent = '保存';
array_URL.push(url_img);
//zip用処理
list_files_output.push({
name: file.name_new,
blob: blob
});
//dom処理
el_td_size_new.textContent = sizeToStr(blob.size);
el_td_compress.textContent = calcCompress(file.size_old, blob.size);
}
}
function downloadAll(){
nowloading.start();
[...els_a_download].forEach(el_a=>{
el_a.click();
});
nowloading.stop();
}
function downloadZip(){
if(list_files_output.length===0) return;
nowloading.start();
const zip = new JSZip();
list_files_output.forEach(file=>{
zip.file(file.name, file.blob);
});
zip.generateAsync({type: 'blob'})
.then(blob=>{
const url = window.URL.createObjectURL(blob);
const el_a = document.createElement('a');
el_a.download = 'resized.zip';
el_a.href = url;
el_a.click();
window.URL.revokeObjectURL(url);
nowloading.stop();
});
}
function sizeToStr(size){
if(size<1024) return size+'B';
if(size<1024*1024){
const size_kb = (size / 1024).toFixed(1);
return size_kb+'KB';
}
const size_mb = (size / (1024*1024)).toFixed(1);
return size_mb+'MB';
}
function getWH(value){
if(!Number.isInteger(value)) return undefined;
if(value<1) return undefined;
return value;
}
function calcRate(val_old, val_new){
if(val_new===undefined) return 1;
if(val_new/val_old>1) return 1;
return val_new / val_old;
}
function calcCompress(val_old, val_new){
const val = Math.round(((val_old-val_new)/val_old)*1000)/10;
if(val%1===0) return val + '.0%';
return val + '%';
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment