See the Pen Resize images by size by John Doe (@04) on CodePen.
ファイルサイズを指定してリサイズできます。二分探索です。
<!-- Use preprocessors via the lang attribute! e.g. <template lang="pug"> -->
<template>
<div id="app">
<div class="demo-container">
<div class="progress-bar" :class="{ invisible: !progress }">
<div class="progress-bar-value"></div>
</div>
</div>
<h1>Resize images By Size</h1>
<input type="file" accept="image/*" @change="onchange" />
<div class="file" v-for="file in files">
<form @submit.prevent="resize(file)">
<label>
<input type="number" v-model="file.kb" :max="file.file.size / 1024" />
KB
</label>
<button>Resize</button>
</form>
<table>
<tbody>
<tr v-for="variation in file.variations">
<td>
<img :src="variation.url" />
</td>
<td class="right">{{ bytesToSize(variation.size) }}</td>
<td>
<div class="actions">
<button @click="download(variation.file.name, variation.url)">
Download
</button>
<button @click="copy(variation.blob)">Copy</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div>
<h2>List of maximum file sizes</h2>
<table class="size-table">
<tbody>
<tr>
<td>GitHub Profile Picture</td>
<td class="right">1MB</td>
</tr>
<tr>
<td>Qiita Tag Icon</td>
<td class="right">256KB</td>
</tr>
<tr>
<td>Slack Emoji</td>
<td class="right">128KB</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script>
const imgCache = {};
async function createImage(file) {
if (imgCache[file.id]) return imgCache[file.id];
const img = new Image();
img.src = URL.createObjectURL(file);
await new Promise((resolve) => img.addEventListener("load", resolve));
imgCache[file.id] = img;
return img;
}
const blobCache = {};
async function resize(file, width) {
if (blobCache[file.id + width]) return blobCache[file.id + width];
const img = await createImage(file);
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = img.naturalHeight * (width / img.naturalWidth);
const ctx = canvas.getContext("2d");
ctx.imageSmoothingEnabled = false;
ctx.drawImage(
img,
0,
0,
img.naturalWidth,
img.naturalHeight,
0,
0,
canvas.width,
canvas.height
);
const blob = await new Promise((resolve) =>
canvas.toBlob(resolve, file.type)
);
blobCache[file.id + width] = blob;
return blob;
}
async function lowerBound(array, evaluate) {
let first = 0,
last = array.length - 1,
middle;
while (first <= last) {
middle = Math.floor((first + last) / 2);
const evaluated = await evaluate(array[middle]);
if (evaluated < 0) first = middle + 1;
else last = middle - 1;
}
return array[first - 1];
}
function range(min, max, step = 1) {
return Array.from(
{ length: (max - min + step) / step },
(v, k) => min + k * step
);
}
const gcd = (a, b) => (!b ? a : gcd(b, a % b));
async function optimize(file, size) {
const img = await createImage(file);
const gcdValue = gcd(img.naturalWidth, img.naturalHeight);
const minWidth = gcdValue === 1 ? 8 : img.naturalWidth / gcdValue;
let widths = range(minWidth, img.naturalWidth, minWidth);
const width = await lowerBound(widths, async (width) => {
const blob = await resize(file, width);
return blob.size - size;
});
return resize(file, width);
}
const KB = 2 ** 10;
const MB = KB ** 2;
const GB = KB ** 3;
export default {
data() {
return {
files: [],
progress: false
};
},
computed: {
bytesToSize() {
return (bytes) => {
const sizes = ["B", "KB", "MB", "GB", "TB"];
if (bytes == 0) return "0 B";
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
if (i == 0) return bytes + " " + sizes[i];
return (bytes / Math.pow(1024, i)).toFixed(1) + " " + sizes[i];
};
}
},
methods: {
async onchange(e) {
if (!e.target.files.length) return;
this.progress = true;
for await (const file of e.target.files) {
file.id = Math.random();
const entry = {
file,
variations: [
{
file,
blob: file,
url: URL.createObjectURL(file),
size: file.size
}
]
};
this.files.push(entry);
for await (const size of [MB, 256 * KB, 128 * KB]) {
if (file.size < size) continue;
const blob = await optimize(file, size);
entry.variations.push({
file,
blob,
url: URL.createObjectURL(blob),
size
});
}
this.progress = false;
}
},
async resize(file) {
const size = 1024 * file.kb;
const blob = await optimize(file.file, size);
file.variations.push({
file: file.file,
blob,
url: URL.createObjectURL(blob),
size
});
},
async copy(blob) {
try {
console.log(blob);
await navigator.clipboard.write([
new ClipboardItem({
[blob.type]: blob
})
]);
Toastify({ text: "Copied!", duration: 10000 }).showToast();
} catch (e) {
Toastify({ text: e, duration: 10000 }).showToast();
Toastify({
text: "Right click on the image & Copy image",
duration: 10000
}).showToast();
}
},
download(filename, url) {
const a = document.createElement("a");
a.download = filename;
a.href = url;
a.click();
}
}
};
</script>
<!-- Use preprocessors via the lang attribute! e.g. <style lang="scss"> -->
<style>
#app {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.file {
border: 1px solid #eee;
border-radius: 4px;
padding: 16px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
}
img {
width: 256px;
vertical-align: middle;
}
@media screen and (max-width: 480px) {
.file {
padding: 8px;
}
img {
width: 196px;
}
}
.right {
text-align: right;
}
.size-table {
margin: 0 auto;
font-size: 24px;
}
.actions {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 16px;
}
body {
margin: 0;
}
.demo-container {
width: 100%;
margin: auto;
}
.progress-bar {
height: 8px;
background-color: rgba(5, 114, 206, 0.2);
width: 100%;
overflow: hidden;
}
.progress-bar-value {
width: 100%;
height: 100%;
background-color: rgb(5, 114, 206);
animation: indeterminateAnimation 1s infinite linear;
transform-origin: 0% 50%;
}
@keyframes indeterminateAnimation {
0% {
transform: translateX(0) scaleX(0);
}
40% {
transform: translateX(0) scaleX(0.4);
}
100% {
transform: translateX(100%) scaleX(0.5);
}
}
.invisible {
visibility: hidden;
}
</style>