LoginSignup
0
0

ファイルサイズで画像をリサイズする

Last updated at Posted at 2023-09-16

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>
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0