Fermer

décembre 3, 2023

Conversion d’image de navigateur à l’aide de FFmpeg.wasm

Conversion d’image de navigateur à l’aide de FFmpeg.wasm


Ce guide montre comment optimiser les images pour le Web en effectuant un transcodage d’images directement dans le navigateur à l’aide de la bibliothèque FFmpeg.wasm.

L’optimisation de l’image est importante et la sélection du format d’image approprié détermine de manière significative la taille des fichiers image. Chaque format d’image fournit son algorithme de compression unique, ses vitesses d’encodage et de décodage et ses exigences d’utilisation spécifiques. Par conséquent, la sélection du bon format d’image est essentielle pour des applications Web performantes.

Dans cet article, nous nous concentrerons sur certains des formats d’images raster les plus utilisés sur le Web aujourd’hui, notamment JPEG, PNG, GIF, WebP et AVIF.

Ici, nous verrons comment convertir des images d’un format à un autre en utilisant FFmpeg.wasm-un port WebAssembly de FFmpeg. Cet outil nous permet de gérer la manipulation des médias directement dans nos navigateurs.

Configuration du projet

Créer un Suivant.js application en utilisant la commande suivante :

npx create-next-app ffmpeg-convert-image

Ensuite, nous devons installer notre dépendance principale, ffmpeg.wasm. Ce package comporte deux sous-packages : @ffmpeg/core, qui est le port principal d’assemblage Web du module FFmpeg, et @ffmpeg/ffmpeg, qui est la bibliothèque qui sera utilisée directement dans notre application pour interagir avec le premier. Pour l’instant, nous installerons uniquement @ffmpeg/ffmpeg ; par la suite, nous inclurons @ffmpeg/core en utilisant un lien CDN.

npm i @ffmpeg/ffmpeg

Pour manipuler les ressources multimédias, le module @ffmpeg/core utilise des threads WebAssembly, et pour prendre en charge le multithreading, ces threads nécessitent un stockage partagé, ils utilisent donc l’API SharedArrayBuffer du navigateur.

Par défaut, cette API n’est pas disponible pour les pages Web en raison de problèmes de sécurité. Pour nous assurer que tout fonctionne correctement, nous devons indiquer explicitement au navigateur que notre page Web nécessite l’accès à cette API. Nous pouvons y parvenir en définissant le populaire En-têtes COOP (cross-origin-opener-policy) et COEP (cross-origin-embedder-policy) sur notre document principal.

Pour définir ces en-têtes de réponse, nous aurons besoin d’un serveur. Le framework Next.js nous offre plusieurs façons d’effectuer des tâches liées au serveur au sein de notre application. Mettez à jour votre pages/index.js fichier avec les éléments suivants :

function App() {
  return null;
}
export default App;

export async function getServerSideProps(context) {
  
  context.res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
  context.res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");

  return {
    props: {},
  };
}

Dans le code ci-dessus, nous avons défini et exporté un getServerSideProps fonction, qui s’exécutera côté serveur chaque fois que nous demanderons cette page. Cette fonction reçoit un objet contextuel qui contient le request et response objets. Nous utilisons le setHeader méthode sur le response s’oppose à l’inclusion des en-têtes COOP et COEP.

Pour les styles pertinents pour notre application, créez un fichier appelé App.css dans ton styles répertoire et ajoutez-y les styles suivants :

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
    Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
}
input[type="file"] {
  display: none;
}
select {
  padding: 15px;
  font-size: 1.4rem;
  background-color: transparent;
  color: #f1f1f1;
}
.form_g {
  display: grid;
  background-color: #0c050b;
}
.form_g label {
  padding: 10px;
  background-color: #0a57fe;
  color: #f2f2f2;
  font-size: 1.2rem;
}
.file_picker {
  border: 3px dashed #666;
  cursor: pointer;
  display: block;
  display: grid;
  place-items: center;
  font-weight: 700;
  height: 100%;
  padding: 12rem 0;
}
#file_picker_small {
  padding: 3rem 2rem;
}
.deck {
  max-width: 1200px;
  margin: auto;
  display: grid;
  grid-template-columns: repeat(2, minmax(300px, 1fr));
  align-items: start;
  margin-top: 1.4rem;
  gap: 4rem;
}
.deck > * {
  border-radius: 5px;
  align-items: start;
}
.deck > button {
  align-self: center;
}
.grid_txt_2 {
  display: grid;
  gap: 1rem;
}
.flexi {
  display: flex;
  gap: 10px;
  text-transform: uppercase;
  font-size: 25px;
  align-items: center;
  justify-content: center;
}

button {
  border: none;
  cursor: pointer;
}
button:disabled {
  background-color: #f4f4f4;
  color: rgb(155, 150, 150);
}
.btn {
  padding: 1rem 2rem;
  border-radius: 100px;
  font-size: 1.2rem;
}
.btn_g {
  background-color: #24d34e;
  color: #f2f2f2;
}
.col-r {
  color: red;
  font-weight: bolder;
  font-size: 40px;
}
.col-g {
  color: #24d34e;
  font-weight: bolder;
  font-size: 40px;
}
.btn_b {
  background-color: #0a57fe;
  color: #f3f3f3;
}
.bord_g_2 {
  border: 2px solid rgb(230, 220, 220);
}
.p_2 {
  padding: 2rem;
}
.u-center {
  text-align: center;
}

Décomposer l’application

Notre application peut être décomposée en les composants suivants :

Composants d'application – InputImage Preview, OutputImageFormSelector, File Picker, OutputImage

Nous expliquerons brièvement chaque composant, créerons les fichiers qui hébergeront leur logique spécifique et mettrons à jour leur contenu si nécessaire. A la racine de votre projet, créez un dossier nommé components.

Le composant de sélection de fichiers

Composant de sélection de fichiers - état 1, choisir le fichier ;  état 2 choisir une autre image

Ce composant permettra à l’utilisateur de sélectionner et d’afficher des fichiers image depuis son ordinateur. Créez un fichier nommé FilePicker.js dans ton components dossier et ajoutez-y ce qui suit :

function FilePicker({ handleChange, availableImage }) {
  return (
    <label
      htmlFor="x"
      id={`${availableImage ? "file_picker_small" : ""}`}
      className={`file_picker `}
    >
      <span>
        {availableImage ? "Select another image" : "Click to select an image"}
      </span>
      <input onChange={handleChange} type="file" id="x" accept="image/*" />
    </label>
  );
}
export default FilePicker;

Le FilePicker Le composant accepte deux accessoires : une fonction appelée handleChangequi est lié aux champs de saisie onChange événement, et un booléen appelé availableImageutilisé pour styliser le sélecteur de fichiers.

Le composant InputImagePreviewer

Composant d'aperçu d'image - image, taille : 0,29 Mo, format : JPEG

Créez un fichier nommé InputImagePreviewer.js dans ton components dossier et ajoutez-y ce qui suit :

export default function InputImagePreviewer({ previewURL, inputImage }) {
  return (
    <div className="bord_g_2 p_2 u-center">
      <img src={previewURL} width="450" alt="preview image" />
      <p className="flexi u-center">
        SIZE:
        <b>{(inputImage.size / 1024 / 1024).toFixed(2) + "MB"}</b>
      </p>
      <p className="flexi u-center">
        FORMAT:
        <b>{/\.(\w+)$/gi.exec(inputImage.name)[1]} </b>
      </p>
    </div>
  );
}

Ce composant accepte deux accessoires : inputImagele blob d’image sélectionné dans le sélecteur de fichiers, et previewURL, une URL de données représentant ce blob. Il affiche l’image avec quelques informations, comme la taille de l’image en Mo et son format. Nous affichons le format en extrayant l’extension du fichier du name propriété sur le fichier blob.

Le composant OutputImageFormatSelector

Ce composant sera une simple liste déroulante de sélection permettant aux utilisateurs de sélectionner le format souhaité dans lequel ils souhaitent que leur image d’entrée soit encodée.

Composant de sélection de format - convertir en : jpeg

Créez un fichier nommé OutputImageFormatSelector.js dans ton components dossier et ajoutez-y ce qui suit :

export default function OutputImageFormatSelector({
  disabled,
  format,
  handleFormatChange,
}) {
  return (
    <div className="form_g">
      <label> Convert to:</label>
      <select disabled={disabled} value={format} onChange={handleFormatChange}>
        <option value=""> --select output format</option>
        <option value="jpeg">JPEG</option>
        <option value="png">PNG</option>
        <option value="gif">GIF</option>
        <option value="webp">WEBP</option>
      </select>
    </div>
  );
}

Ce composant accepte trois accessoires : disabled est un booléen utilisé pour activer ou désactiver la sélection d’un format d’image. Ceci est important pour empêcher l’utilisateur de choisir un format différent pendant le processus de conversion d’image. Le format prop est le format sélectionné, et handleFormatChange est une fonction liée au select dérouler onChange événement.

Comme vu ci-dessus, le select Le champ affiche une liste de formats d’image. Vous avez peut-être remarqué que le format AVIF n’est pas inclus. La raison en est que même si la bibliothèque officielle FFmpeg inclut prise en charge du format AVIF le 13 mars 2022la dernière version de @ffmpeg/core, que nous utilisons, n’est pas encore configurée pour prendre en charge AVIF.

Le composant OutputImage

Composant d'image de sortie - image, taille : 1,94 Mo, 674 %, bouton de téléchargement

Créez un fichier nommé OutputImage.js dans ton components dossier et ajoutez-y ce qui suit :

const OutPutImage = ({
  handleDownload,
  transCodedImageData,
  format,
  loading,
}) => {
  if (loading)
    return (
      <article>
        <p>
          Converting to <b> {format}</b> please wait....
        </p>
      </article>
    );
  const { newImageSize, originalImageSize, newImageSrc } = transCodedImageData;
  if (!loading && !originalImageSize)
    return (
      <article>
        <h2> Select an image file from your computer</h2>
      </article>
    );
  const newImageIsBigger = originalImageSize < newImageSize;
  const toPercentage = (val) => Math.floor(val * 100) + "%";

  return newImageSrc ? (
    <article>
      <section className="grid_txt_2">
        <div className="bord_g_2 p_2 u-center">
          <img src={newImageSrc} width="450" />
          <p className="flexi u-center">
            <span>
              {" "}
              SIZE: {(newImageSize / 1024 / 1024).toFixed(2) + "MB"}{" "}
            </span>
          </p>
          <p>
            <span className={newImageIsBigger ? "col-r" : "col-g"}>
              {toPercentage(newImageSize / originalImageSize)}
            </span>
          </p>
        </div>
        <button onClick={handleDownload} className="btn btn_g">
          {" "}
          Download
        </button>
      </section>
    </article>
  ) : null;
};
export default OutPutImage;

Ce composant fait plusieurs choses. Premièrement, il utilise le loading prop pour réagir aux états de chargement pendant le processus de transcodage d’image. Ensuite, à partir du transcodedImageData prop, il extrait les propriétés nécessaires (newImageSize, originalImageSize et newImageSrc) pour restituer l’image transcodée à l’écran.

Il restitue également des informations sur l’image, telles que sa taille en Mo et une valeur en pourcentage comparant la taille de l’image transcodée et la taille du fichier d’origine. Si la taille du fichier d’origine est de 10 Mo et la taille du fichier transcodé de 5 Mo, le rendu est de 5 Mo et 50 %.

Le composant restitue également un button avec son événement click lié au handleDownload prop pour télécharger l’image sur l’appareil de l’utilisateur.

Notre application aura besoin de quelques fonctions d’assistance. Créons un fichier nommé helpers.js à la racine de notre application et ajoutez-y ce qui suit :

const readFileAsBase64 = async (file) => {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => {
      resolve(reader.result);
    };
    reader.onerror = reject;
    reader.readAsDataURL(file);
  });
};
const download = (url) => {
  const link = document.createElement("a");
  link.href = url;
  link.setAttribute("download", "");
  link.click();
};
export { readFileAsBase64, download };

Ce fichier crée et exporte deux fonctions : readFileAsBase64 attend un fichier blob en entrée et utilise l’API FileReader pour le convertir en URL de données. Si l’opération réussit, elle est résolue avec l’URL des données ; sinon, il renvoie une erreur.

Le download La fonction attend une URL de données en tant que paramètre, puis tente de télécharger le fichier pointé par l’URL sur l’appareil de l’utilisateur. Il crée par programme une balise d’ancrage et y ajoute deux attributs.

Le href l’attribut pointe vers l’URI de données du fichier, tandis que l’attribut download L’attribut rend le fichier téléchargeable. Enfin, la balise d’ancrage est cliquée par programme pour déclencher un téléchargement sur l’appareil de l’utilisateur.

Rendu des composants

Maintenant que nous avons compris les principaux composants de notre application, utilisons-les dans notre pages/index.js dossier pour compléter notre candidature. Ajoutez ce qui suit à votre pages/index.js déposer:

import { useState } from "react";
import * as helpers from "../helpers";
import FilePicker from "../components/FilePicker";
import OutputImage from "../components/OutPutImage";
import InputImagePreviewer from "../components/InputImagePreviewer";
import OutputImageFormatSelector from "../components/OutputImageFormatSelector";
import { createFFmpeg, fetchFile } from "@ffmpeg/ffmpeg";

const FF = createFFmpeg({
  
  corePath: "https://unpkg.com/@ffmpeg/core@0.10.0/dist/ffmpeg-core.js",
});

(async function () {
  await FF.load();
})();

function App() {
  const [inputImage, setInputImage] = useState(null);
  const [transcodedImageData, setTranscodedImageData] = useState({});
  const [format, setFormat] = useState("");
  const [URL, setURL] = useState(null);
  const [loading, setLoading] = useState(false);

  const handleChange = async (e) => {
    let file = e.target.files[0];
    setInputImage(file);
    setURL(await helpers.readFileAsBase64(file));
  };

  const handleFormatChange = ({ target: { value } }) => {
    if (value === format) return;
    setFormat(value);
    transcodeImage(inputImage, value);
  };

  const transcodeImage = async (inputImage, format) => {
    setLoading(true);
    if (!FF.isLoaded()) await FF.load();
    FF.FS("writeFile", inputImage.name, await fetchFile(inputImage));
    try {
      await FF.run("-i", inputImage.name, `img.${format}`);
      const data = FF.FS("readFile", `img.${format}`);
      let blob = new Blob([data.buffer], { type: `image/${format}` });
      let dataURI = await helpers.readFileAsBase64(blob);
      setTranscodedImageData({
        newImageSize: blob.size,
        originalImageSize: inputImage.size,
        newImageSrc: dataURI,
      });
      FF.FS("unlink", `img.${format}`);
    } catch (error) {
      console.log({ message: error });
    } finally {
      setLoading(false);
    }
  };

  return (
    <main className="App">
      <section className="deck">
        <article className="grid_txt_2 ">
          {URL && (
            <>
              <InputImagePreviewer inputImage={inputImage} previewURL={URL} />
              <OutputImageFormatSelector
                disabled={loading}
                format={format}
                handleFormatChange={handleFormatChange}
              />
            </>
          )}
          <FilePicker
            handleChange={handleChange}
            availableImage={!!inputImage}
          />
        </article>
        <OutputImage
          handleDownload={() =>
            helpers.download(transcodedImageData.newImageSrc)
          }
          transCodedImageData={transcodedImageData}
          format={format}
          loading={loading}
        />
      </section>
    </main>
  );
}

export default App;

export async function getServerSideProps(context) {
  
  context.res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
  context.res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
  return {
    props: {},
  };
}

Dans le code ci-dessus, nous commençons par importer les composants dont nous avons besoin. Comme vous pouvez le voir, nous importons également deux fonctions : createFfmpeg et fetchFile depuis le module @ffmpeg/ffmpeg.

En utilisant le createFFmpeg fonction, nous créons une instance FFmpeg en passant un paramètre d’objet qui inclut l’option core path où nous spécifions une URL vers le module @ffmpeg/core.

Ensuite, nous créons une fonction asynchrone auto-invoquée qui appelle le load méthode sur notre instance FFmpeg. En faisant cela, nous téléchargeons le script @ffmpeg/core.

Ensuite, notre App Le composant commence par créer plusieurs variables d’état et quelques fonctions. Pour comprendre la place de chaque variable et fonction dans ce composant, nous expliquerons comment l’utilisateur interagit avec l’interface utilisateur rendue et les appels de fonction sous-jacents impliqués à chaque point.

Premièrement, lorsque l’utilisateur sélectionne un fichier sur son ordinateur à partir du composant FilePicker, cela déclenche le handleChange fonction où le fichier et son URL correspondante sont stockés dans l’état en utilisant le setInputImage et setURL les fonctions. Ensuite, l’application effectue un nouveau rendu avec l’image sélectionnée.

Ensuite, l’utilisateur sélectionne le format souhaité dans la liste déroulante de sélection, ce qui déclenche le handleFormatChange fonction. Cette fonction extrait la valeur prop de l’objet événement et vérifie d’abord si le format d’image sélectionné est le même que celui précédemment sélectionné. S’ils sont identiques, cela revient simplement. Cependant, si les formats diffèrent, il stocke le nouveau format dans le format variable d’état, puis invoque la transcodeImage fonction. Cette fonction reçoit deux paramètres : le blob de l’image d’entrée et le format souhaité pour transcoder l’image.

Le transcodeImage la fonction commence en basculant le loading état (à ce stade, l’application est restituée et le OutputImage le composant affiche le texte de chargement) et garantit que l’instance FFmpeg a été chargée avec succès ; sinon, il invoque son load méthode à nouveau.

Pour que FFmpeg.wasm manipule n’importe quel fichier, nous devons le stocker sur son disque dur (il s’agit d’un système de fichiers de stockage en mémoire utilisé par FFmpeg.wasm basé sur le module MEMFS qui stocke les fichiers sous forme de tableaux typés, en particulier Unit8Arrays en mémoire).

Donc pour cela, nous stockons le fichier image d’entrée dans son système de fichiers en utilisant le FS méthode où nous spécifions trois arguments. La première-"writeFile"– est l’opération que nous voulons effectuer ; dans notre cas, c’est une écriture. Le second est le nom de fichier que nous définissons à l’aide du fichier blob. name propriété. Et le troisième est le contenu du fichier, qui est le blob, et il est transmis au fetchFile fonction pour convertir notre blob au format attendu.

Ensuite, nous écrivons une commande CLI qui effectue la conversion du fichier image à l’aide du run méthode sur notre instance FFmpeg. Cette commande ressemble à ceci :

await FF.run("-i", inputImage.name, `img.${format}`);

Dans le code ci-dessus, "-i" spécifie le paramètre du fichier d’entrée. Ensuite, nous avons spécifié le fichier image que nous avons écrit dans le système de fichiers en mémoire à l’aide de notre inputImage.name propriété. Enfin, nous avons créé une chaîne basée sur le format d’image sélectionné, img.${format}, représentant le fichier de sortie. En fonction du format de ce fichier, le module @ffmpeg/core sélectionne les codecs appropriés dans sa liste de codecs pris en charge, transcode l’image et la stocke dans son système de fichiers avec le nom de fichier que nous avons spécifié.

Nous avons fait deux choses pour restituer notre fichier image transcodé à l’écran. Nous lisons son contenu depuis le système de fichiers du module FFmpeg.wasm vers une variable que nous avons appelée data. Nous avons ensuite converti son contenu en blob puis en URL de données.

L’URL des données est suffisante pour restituer l’image transformée à l’écran, mais rappelez-vous notre Outputimage Le composant nécessite plus que cela. Il restitue également la taille du contenu de l’URL de données et la manière dont il se compare à l’image d’origine. Comme étape finale, nous créons et stockons un objet représentant ce qui serait envoyé au OutputImage composant comme accessoires.

Cet objet est stocké dans le transcodedImagedata état variable. Via un appel à son setTranscodedDatanous définissons et passons cet objet qui porte ces noms utilisés comme clés d’objet : newImageSrc, newImageSize et originaImageSizereprésentant l’URL de l’image transformée, sa taille et la taille de l’image d’entrée d’origine.

À ce stade, l’application effectue un nouveau rendu et le OutputImage Le composant restitue l’image transcodée, sa taille et sa comparaison avec l’image d’origine.

Pour voir l’application en cours d’exécution, exécutez cette commande dans votre terminal :

npm run dev

GIF montrant la conversion d'image

Trouvez le terminer le projet sur GitHub.

Conclusion

Ce guide montre comment exploiter différents formats d’image pour améliorer les performances de nos applications, étant donné qu’il n’existe pas de format d’image unique. Il met l’accent sur l’utilité des formats d’image contemporains tels que WebP et AVIF pour l’optimisation des images Web. De plus, il nous montre comment effectuer un transcodage d’images directement dans notre navigateur à l’aide de la bibliothèque FFmpeg.wasm.




Source link

décembre 3, 2023