Aller au contenu principal
~/GiwiSoft
Splash

Photobooth Desktop : une cabine photo de la webcam à l'impression

Giwi 5 min de lecture JavaScript, Réalisations

Après la version web de ma cabine photo, j’ai voulu une déclinaison desktop : une app Electron installable sur un poste d’événement, pilotable à la souris mais aussi au clavier et à la manette, avec impression directe sur une photo 4×6. Voici comment est né Photobooth Desktop.

Le principe reste le même : capture webcam, superposition de fonds PNG, enregistrement ou impression du résultat. La version desktop ajoute le plein écran kiosque, les raccourcis clavier, le support manette et l’impression.

Stack technique

Couche Technologie
Runtime Electron 43
UI Preact 10 (+ TypeScript)
Bundle esbuild
Packaging electron-builder (deb, rpm, AppImage, NSIS)
Capture getUserMedia + Canvas 2D
i18n JSON (en, fr, de, es)

Une stack volontairement minimale : pas de framework Electron lourd, pas de bundler configurable à outrance. Preact + esbuild tiennent le rendu, le reste est du Node natif.

Architecture

L’application suit l’architecture Electron classique à trois processus :

src/main/index.ts        → processus principal (IPC, fs, impression)
src/main/preload.ts → pont contextIsolation
src/mainview/ → renderer Preact (App, Viewport, SettingsOverlay…)
src/mainview/lib/ → canvas.ts, rpc.ts, constants.ts, utils.ts

Tout passe par un pont IPC 1:1 avec le renderer : chaque ipcMain.handle correspond à une fonction de lib/rpc.ts. La sécurité est standard (contextIsolation: true, nodeIntegration: false, navigation externe redirigée vers le navigateur système) :

function register<Params, Response>(channel: string, handler: (params: Params) => Response | Promise<Response>) {
ipcMain.handle(channel, (_event, params: Params) => handler(params));
}

Gestion des données : le bundle n’est pas le data store

Point important pour une app distribuée en AppImage/deb : le bundle est en lecture seule (ou écrasé à chaque rebuild). Config et fonds vivent donc dans le cache dir OS, photos dans le dossier Images :

function dataDir(): string {
if (process.platform === "darwin") return join(home, "Library", "Caches", "photobooth");
if (process.platform === "win32") return join(process.env.LOCALAPPDATA || ..., "photobooth");
return join(process.env.XDG_CACHE_HOME || join(home, ".cache"), "photobooth");
}
// Photos → ~/Pictures/photobooth (Linux/macOS), %USERPROFILE%\Pictures\photobooth (Windows)

Au premier lancement, les défauts sont seedés depuis le bundle : les PNG de backgrounds/ sont copiés vers le cache, le config.json par défaut également. L’utilisateur peut ensuite importer ses propres fonds depuis les réglages (chaque import est sanitisé, nom de fichier assaini, et dédupliqué).

Capture et composition sur Canvas

Le cœur du rendu, c’est du Canvas 2D pur. Trois fonctions font tout :

Le recadrage cover-fit de la vidéo

La webcam a un ratio quelconque ; la photo finale est fixe (1800×1200). On centre-crop la vidéo pour couvrir le cadre :

export function drawVideoCrop(c: CanvasRenderingContext2D, video: HTMLVideoElement) {
const vw = video.videoWidth, vh = video.videoHeight;
const scale = Math.max(W / vw, H / vh);
const sw = vw * scale, sh = vh * scale;
c.drawImage(video, (W - sw) / 2, (H - sh) / 2, sw, sh);
}

Le fond PNG positionnable

Un fond de cabine photo est rarement exactement au ratio de la photo. Plutôt que de l’étirer, on le cover-fit puis on l’ancre selon une position configurable (top, left bottom, 40%…) :

export function drawBgTo(c, img, cw, ch, position) {
const scale = Math.min(cw / img.naturalWidth, ch / img.naturalHeight);
const sw = img.naturalWidth * scale, sh = img.naturalHeight * scale;
const { x, y } = parsePosition(position);
c.drawImage(img, (cw - sw) * x, (ch - sh) * y, sw, sh);
}

parsePosition traduit la chaîne (top, left bottom, 40%…) en coordonnées d’ancrage normalisées 0..1. Utile pour un décor haut dont on veut montrer le sommet.

Le mode photo strip

Quatre captures sont assemblées en grille 2×2, chaque shot réduit de moitié avec un gap de 2 px :

export function createStrip(frames: ImageData[]): string {
const gap = 2, cw = W / 2, ch = H / 2;
const c = document.createElement("canvas");
c.width = W; c.height = H;
const positions = [[0, 0], [cw + gap, 0], [0, ch + gap], [cw + gap, ch + gap]];
frames.forEach((imageData, i) => {
const pos = positions[i];
if (!pos) return;
// each frame drawn halved into its quadrant
});
return c.toDataURL("image/png");
}

Enregistrement et impression

La photo est encodée en data URL PNG côté renderer puis envoyée au processus principal. Celui-ci l’écrit dans ~/Pictures/photobooth et, si demandé, la confie à lp (l’imprimante CUPS) avec les options 4×6 glossy :

register("savePhoto", async ({ image, print }: { image: string; print: boolean }) => {
const base64 = image.split(",")[1];
const buffer = Buffer.from(base64, "base64");
const filename = `photo-${Date.now()}.png`;
writeFileSync(join(PHOTOS_DIR, filename), buffer);
if (print) {
spawn("lp", ["-o", "media=4x6in", "-o", "MediaType=Glossy", filepath]);
}
return { filename };
});

Simple et efficace : pas de lib d’impression, lp fait le travail sur Linux. Sur Windows, le même chemin d’enregistrement fonctionne, l’impression passe par le dialogue système.

Mode kiosque

Pour un usage événementiel, l’app gère ses permissions webcam sans prompt (impossible dans une fenêtre kiosque) :

session.defaultSession.setPermissionRequestHandler((_wc, permission, callback) => {
callback(permission === "media" || permission === "mediaKeySystem");
});

Et un splash screen (400×280, sans cadre, toujours au premier plan) affiche la progression de chargement pendant que le renderer précharge les fonds, avant de laisser place à la fenêtre principale 1400×900.

Fonctionnalités

  • Flux webcam en direct avec superpositions PNG en temps réel
  • Positionnement des fonds : top, bottom, left, right, center, pourcentages
  • Capture 1800×1200 paysage (10×15 cm à 300 DPI)
  • Photo strip : 4 shots en grille 2×2
  • Compte à rebours personnalisable (3s, 5s, 10s)
  • Mode miroir, sélecteur de caméra
  • Raccourcis clavier configurables et support manette (boutons + axes)
  • i18n : anglais, français, allemand, espagnol
  • Watermark optionnel (barre semi-transparente en bas de photo)
  • Réglages dans l’app (plus d’édition manuelle de config.json), export/import des paramètres et fonds en un seul JSON

Packaging

Le build produit des paquets natifs pour chaque plateforme :

yarn electron:dev    # Build bundles + lance Electron (DevTools auto-open)
yarn electron:dist # Build deb, rpm, AppImage dans dist/

L’installation se fait ensuite comme n’importe quelle app : dpkg -i, rpm -i ou AppImage. Product page : giwi.github.io/photobooth-desktop.

Le projet est open source (Apache-2.0) : github.com/Giwi/photobooth-desktop. Il fait partie de la famille Photobooth. Si vous montez une cabine pour un mariage ou un événement, cette version desktop est celle qui tourne le mieux sur un poste dédié.