Add automatic slide partner logos and refresh LaTeX devcontainer

This commit is contained in:
khannurien
2026-10-11 11:58:39 +00:00
parent 949dfbe414
commit f68b9b647d
21 changed files with 980 additions and 98 deletions

View File

@@ -1,54 +1,102 @@
FROM mcr.microsoft.com/devcontainers/base:ubuntu
# syntax=docker/dockerfile:1
FROM mcr.microsoft.com/devcontainers/base:ubuntu-24.04
# Install dependencies for TeX Live installation and usage
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
LABEL org.opencontainers.image.title="LaTeX Dev Container" \
org.opencontainers.image.description="Full TeX Live + LaTeX Workshop dev container" \
org.opencontainers.image.source="https://github.com/khannurien/latex-devcontainer" \
org.opencontainers.image.licenses="MIT"
ARG USERNAME=vscode
ARG TEXLIVE_YEAR=2026
# Point this at a frozen snapshot (e.g. the TUG historic tlnet-final tree for a
# given year) once TEXLIVE_YEAR is no longer the current release.
ARG TEXLIVE_REPO="https://mirror.ctan.org/systems/texlive/tlnet"
# Docs are what `texdoc` reads; sources are almost never needed.
ARG INSTALL_DOCFILES=1
ARG INSTALL_SRCFILES=0
# Runtime dependencies. Note: biber, latexmk, latexindent and chktex all ship
# with scheme-full, so they are deliberately NOT installed from apt -- the apt
# versions routinely disagree with the TeX Live ones (biblatex/biber in
# particular). What apt provides here is what TeX Live cannot: perl itself, the
# perl modules latexindent needs, ghostscript for pdfcrop/ps2pdf/epstopdf, and
# fontconfig so xelatex/lualatex can see system fonts.
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
perl \
biber \
wget \
ca-certificates \
fontconfig \
ghostscript \
make \
git \
curl \
python3 \
python3-pygments \
libyaml-tiny-perl \
libfile-homedir-perl \
libunicode-linebreak-perl \
liblog-log4perl-perl \
liblog-dispatch-perl \
&& rm -rf /var/lib/apt/lists/*
# Set non-root user provided by Microsoft base image
ARG USERNAME=vscode
# Switch to user home directory
USER $USERNAME
WORKDIR /home/$USERNAME
# Set TeX Live paths and update environment
ENV TEXLIVE_INSTALL_PREFIX="/usr/local/texlive"
ENV TEXLIVE_INSTALL_TEXDIR="${TEXLIVE_INSTALL_PREFIX}/2025"
ENV PATH="${TEXLIVE_INSTALL_TEXDIR}/bin/x86_64-linux:${PATH}"
ENV TEXLIVE_INSTALL_TEXDIR="${TEXLIVE_INSTALL_PREFIX}/${TEXLIVE_YEAR}"
# Download and install TeX Live 2025 as root
RUN wget -q https://mirror.ctan.org/systems/texlive/tlnet/install-tl-unx.tar.gz && \
tar -xzf install-tl-unx.tar.gz && \
rm install-tl-unx.tar.gz && \
cd install-tl-* && \
echo "selected_scheme scheme-full" > texlive.profile && \
echo "TEXDIR ${TEXLIVE_INSTALL_TEXDIR}" >> texlive.profile && \
echo "TEXMFCONFIG /home/vscode/.texlive2025/texmf-config" >> texlive.profile && \
echo "TEXMFVAR /home/vscode/.texlive2025/texmf-var" >> texlive.profile && \
echo "instopt_adjustpath 1" >> texlive.profile && \
echo "tlpdbopt_autobackup 0" >> texlive.profile && \
sudo perl ./install-tl --profile=texlive.profile --no-interaction && \
cd .. && rm -rf install-tl-*
# Install TeX Live as root. instopt_adjustpath symlinks the binaries into
# /usr/local/bin, which keeps the image architecture-agnostic.
RUN set -eux; \
cd /tmp; \
# mirror.ctan.org redirects to a *different* random mirror on every request, so
# resolve it once and take the tarball, its checksum and the packages all from
# that same host. Retry because individual mirrors have broken TLS or lag sync.
for attempt in 1 2 3 4 5; do \
rm -f install-tl-unx.tar.gz install-tl-unx.tar.gz.sha512; \
resolved="$(curl -fsSL --retry 3 -o install-tl-unx.tar.gz.sha512 \
-w '%{url_effective}' "${TEXLIVE_REPO}/install-tl-unx.tar.gz.sha512")" || continue; \
base="${resolved%/*}"; \
curl -fsSL --retry 3 -o install-tl-unx.tar.gz "${base}/install-tl-unx.tar.gz" || continue; \
if sha512sum -c install-tl-unx.tar.gz.sha512; then TL_REPO="${base}"; break; fi; \
echo "attempt ${attempt}: checksum mismatch from ${base}, retrying"; \
done; \
test -n "${TL_REPO:-}"; \
echo "using mirror: ${TL_REPO}"; \
tar -xzf install-tl-unx.tar.gz; \
cd install-tl-*/; \
{ \
echo "selected_scheme scheme-full"; \
echo "TEXDIR ${TEXLIVE_INSTALL_TEXDIR}"; \
echo "TEXMFCONFIG /home/${USERNAME}/.texlive${TEXLIVE_YEAR}/texmf-config"; \
echo "TEXMFVAR /home/${USERNAME}/.texlive${TEXLIVE_YEAR}/texmf-var"; \
echo "TEXMFHOME /home/${USERNAME}/texmf"; \
echo "instopt_adjustpath 1"; \
echo "tlpdbopt_autobackup 0"; \
echo "tlpdbopt_install_docfiles ${INSTALL_DOCFILES}"; \
echo "tlpdbopt_install_srcfiles ${INSTALL_SRCFILES}"; \
} > texlive.profile; \
perl ./install-tl --profile=texlive.profile --no-interaction --repository "${TL_REPO}"; \
cd /tmp; rm -rf install-tl-*; \
ln -s "$(ls -1 "${TEXLIVE_INSTALL_TEXDIR}/bin" | head -n1)" "${TEXLIVE_INSTALL_TEXDIR}/bin/default"; \
mkdir -p "/home/${USERNAME}/.texlive${TEXLIVE_YEAR}/texmf-config" \
"/home/${USERNAME}/.texlive${TEXLIVE_YEAR}/texmf-var" \
"/home/${USERNAME}/texmf"; \
chown -R "${USERNAME}:${USERNAME}" "/home/${USERNAME}/.texlive${TEXLIVE_YEAR}" "/home/${USERNAME}/texmf"
# User-writeable directories for TeX Live and LuaLaTeX
ENV TEXMFCONFIG="/home/vscode/.texlive2025/texmf-config"
ENV TEXMFVAR="/home/vscode/.texlive2025/texmf-var"
ENV LUAOTFLOAD_HOME="/home/vscode/.texlive2025/texmf-var/luatex-cache"
ENV LUAOTFLOAD_DB="/home/vscode/.texlive2025/texmf-var/luatex-cache/database.sqlite"
# Stable, architecture-independent path to the engines.
ENV PATH="${TEXLIVE_INSTALL_TEXDIR}/bin/default:${PATH}"
ENV TEXMFCONFIG="/home/${USERNAME}/.texlive${TEXLIVE_YEAR}/texmf-config"
ENV TEXMFVAR="/home/${USERNAME}/.texlive${TEXLIVE_YEAR}/texmf-var"
ENV TEXMFHOME="/home/${USERNAME}/texmf"
# Install latexmk
RUN sudo tlmgr option autobackup 0 && \
sudo tlmgr install latexmk && \
sudo tlmgr path add
USER $USERNAME
# Warm the luaotfload font database so the first lualatex run isn't a minutes-long
# font scan, and fail the build early if the toolchain is broken.
RUN set -eux; \
tlmgr --version; \
lualatex --version >/dev/null; \
biber --version >/dev/null; \
latexmk --version >/dev/null; \
latexindent --version >/dev/null; \
luaotfload-tool --update --quiet
# Set the working directory back to a neutral location
WORKDIR /workspace

View File

@@ -0,0 +1,19 @@
{
"features": {
"ghcr.io/khannurien/devcontainer-features/claude-code:1": {
"version": "1.2.0",
"resolved": "ghcr.io/khannurien/devcontainer-features/claude-code@sha256:17eb69bfd15ff5206c0bff2945935a4349f7423cabee461a0ba2681d52205db9",
"integrity": "sha256:17eb69bfd15ff5206c0bff2945935a4349f7423cabee461a0ba2681d52205db9"
},
"ghcr.io/khannurien/devcontainer-features/opencode:1": {
"version": "1.1.0",
"resolved": "ghcr.io/khannurien/devcontainer-features/opencode@sha256:8df3f8c98a104ce7e089ba3e8df39e0d3890753c8dc92e03deb54527d30d3574",
"integrity": "sha256:8df3f8c98a104ce7e089ba3e8df39e0d3890753c8dc92e03deb54527d30d3574"
},
"ghcr.io/khannurien/devcontainer-features/rtk:1": {
"version": "1.1.0",
"resolved": "ghcr.io/khannurien/devcontainer-features/rtk@sha256:4a60b3a1d1b64904de3d856c61cdf7e5943e3c74d114609787dc88d9db7c788f",
"integrity": "sha256:4a60b3a1d1b64904de3d856c61cdf7e5943e3c74d114609787dc88d9db7c788f"
}
}
}

View File

@@ -1,12 +1,20 @@
{
"name": "LaTeX Dev Container",
"image": "ghcr.io/khannurien/latex-devcontainer:2025.12",
"image": "ghcr.io/khannurien/latex-devcontainer:2026.08",
// "build": {
// "dockerfile": "Dockerfile",
// "context": ".."
// },
"remoteUser": "vscode",
"updateRemoteUserUID": true,
// Local extensions, packaged and installed by the script below:
// .devcontainer/overleaf-keymap -- Overleaf's keyboard shortcuts
// .devcontainer/paste-figure -- Ctrl+V an image -> figure environment
"postCreateCommand": "bash .devcontainer/install-extensions.sh",
"customizations": {
"vscode": {
"settings": {
// VSCode text editor settings
"editor.padding.top": 20,
"editor.padding.bottom": 20,
"editor.fontSize": 16,
"editor.tabSize": 2,
"editor.lineHeight": 1.8,
@@ -25,7 +33,18 @@
"latex-workshop.intellisense.package.enabled": true,
"latex-workshop.intellisense.unimathsymbols.enabled": true,
"latex-workshop.intellisense.citation.backend": "biblatex",
// Linting and formatting (chktex and latexindent ship with TeX Live)
"latex-workshop.chktex.enabled": true,
"latex-workshop.chktex.run": "onSave",
"latex-workshop.formatting.latex": "latexindent",
"[latex]": {
"editor.formatOnSave": true
},
"latex-workshop.latex.outDir": "build",
"latex-workshop.latex.clean.subfolder.enabled": true,
"search.exclude": {
"**/build": true
},
"latex-workshop.latex.autoBuild.run": "onFileChange",
"latex-workshop.latex.autoBuild.interval": 3000,
"latex-workshop.synctex.afterBuild.enabled": true,
@@ -107,22 +126,16 @@
}
],
"latex-workshop.latex.recipe.default": "lastUsed",
// VSCode workbench settings
"workbench.startupEditor": "none",
"workbench.sideBar.location": "left",
"workbench.activityBar.location": "top",
"workbench.panel.opensMaximized": "always"
// Overleaf-style image paste, from .devcontainer/paste-figure. Ctrl+V (or a
// drag & drop) of an image into a .tex file writes it here and inserts a
// figure environment around it; edit "pasteFigure.template" to change the
// block that gets inserted.
"pasteFigure.directory": "img"
},
"extensions": [
"james-yu.latex-workshop",
"wayou.vscode-todo-highlight",
"Braver.vscode-solarized",
"hediet.vscode-drawio",
"mathematic.vscode-pdf"
"jgclark.vscode-todo-highlight"
]
}
},
"remoteUser": "vscode",
"updateRemoteUserUID": true,
"features": {}
}
}

View File

@@ -0,0 +1,103 @@
#!/usr/bin/env bash
# Install this project's local VS Code extensions into the container's VS Code server.
#
# devcontainer.json has no place for keyboard shortcuts or editor behaviour beyond
# settings: customizations.vscode only understands "settings" and "extensions", and
# keybindings.json is resolved by the VS Code client, not by the server -- so
# dropping a file into ~/.vscode-server/data/User/ does nothing. What *does* travel
# with the container is an extension. So each directory listed below is packaged as
# a .vsix here and installed; edit those directories to change behaviour, this
# script only ships them.
set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
extensions=(
overleaf-keymap # Overleaf's keyboard shortcuts
paste-figure # Ctrl+V an image -> saved to img/ and wrapped in a figure
)
# The Dev Containers extension installs customizations.vscode.extensions around
# the same time postCreateCommand fires, and both writes touch extensions.json.
# Wait (bounded) for its marker so we don't interleave with it.
marker="$HOME/.vscode-server/data/Machine/.installExtensionsMarker"
for _ in $(seq 1 60); do
[ -e "$marker" ] && break
sleep 1
done
# The remote CLI isn't on PATH during postCreateCommand; find the server binary.
code_server="$(ls -1t "$HOME"/.vscode-server/bin/*/bin/code-server 2>/dev/null | head -n1 || true)"
if [ -z "$code_server" ]; then
echo "install-extensions: no VS Code server found, skipping local extensions." >&2
exit 0
fi
build="$(mktemp -d)"
trap 'rm -rf "$build"' EXIT
for name in "${extensions[@]}"; do
src="${here}/${name}"
vsix="${build}/${name}.vsix"
python3 - "$src" "$vsix" <<'PY'
import json, mimetypes, os, sys, zipfile
from xml.sax.saxutils import escape
src, out = sys.argv[1], sys.argv[2]
pkg = json.load(open(f"{src}/package.json"))
ident, version, publisher = pkg["name"], pkg["version"], pkg["publisher"]
kinds = ",".join(pkg.get("extensionKind", ["workspace"]))
manifest = f"""<?xml version="1.0" encoding="utf-8"?>
<PackageManifest Version="2.0.0" xmlns="http://schemas.microsoft.com/developer/vsx-schema/2011">
<Metadata>
<Identity Language="en-US" Id="{ident}" Version="{version}" Publisher="{publisher}" />
<DisplayName>{escape(pkg["displayName"])}</DisplayName>
<Description xml:space="preserve">{escape(pkg["description"])}</Description>
<Tags>{escape(",".join(pkg.get("categories", [])))}</Tags>
<Categories>{escape(",".join(pkg.get("categories", [])))}</Categories>
<GalleryFlags>Public</GalleryFlags>
<Properties>
<Property Id="Microsoft.VisualStudio.Code.Engine" Value="{pkg["engines"]["vscode"]}" />
<Property Id="Microsoft.VisualStudio.Code.ExtensionKind" Value="{kinds}" />
</Properties>
</Metadata>
<Installation>
<InstallationTarget Id="Microsoft.VisualStudio.Code" />
</Installation>
<Dependencies />
<Assets>
<Asset Type="Microsoft.VisualStudio.Code.Manifest" Path="extension/package.json" Addressable="true" />
</Assets>
</PackageManifest>
"""
# Every file of the extension goes in, so extensions can ship code, not just a manifest.
files = []
for root, _, names in os.walk(src):
for name in sorted(names):
path = os.path.join(root, name)
files.append((path, os.path.relpath(path, src).replace(os.sep, "/")))
defaults = {"json": "application/json", "js": "application/javascript",
"vsixmanifest": "text/xml", "xml": "text/xml"}
for path, _ in files:
ext = os.path.splitext(path)[1].lstrip(".").lower()
if ext and ext not in defaults:
defaults[ext] = mimetypes.guess_type(path)[0] or "application/octet-stream"
content_types = "\n".join(
['<?xml version="1.0" encoding="utf-8"?>',
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">',
*[f' <Default Extension="{ext}" ContentType="{ctype}" />' for ext, ctype in sorted(defaults.items())],
'</Types>', ''])
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as z:
z.writestr("extension.vsixmanifest", manifest)
z.writestr("[Content_Types].xml", content_types)
for path, rel in files:
z.write(path, f"extension/{rel}")
PY
"$code_server" --install-extension "$vsix" --force
done

View File

@@ -0,0 +1,144 @@
{
"name": "overleaf-keymap",
"displayName": "Overleaf Keymap",
"description": "Makes VS Code's keyboard shortcuts behave like Overleaf's.",
"publisher": "local",
"version": "1.0.0",
"license": "MIT",
"engines": {
"vscode": "^1.75.0"
},
"categories": [
"Keymaps"
],
"extensionKind": [
"workspace"
],
"contributes": {
"keybindings": [
{
"//": "Overleaf: Ctrl+B -> \\textbf{...}. Only inside LaTeX files, so Ctrl+B still toggles the sidebar everywhere else.",
"key": "ctrl+b",
"mac": "cmd+b",
"command": "latex-workshop.shortcut.textbf",
"when": "editorTextFocus && !editorReadonly && editorLangId =~ /^latex$|^latex-expl3$|^doctex$|^bibtex$|^rsweave$/"
},
{
"//": "Overleaf: Ctrl+I -> \\textit{...}",
"key": "ctrl+i",
"mac": "cmd+i",
"command": "latex-workshop.shortcut.textit",
"when": "editorTextFocus && !editorReadonly && editorLangId =~ /^latex$|^latex-expl3$|^doctex$|^bibtex$|^rsweave$/"
},
{
"//": "Overleaf: Ctrl+Enter -> compile. (Ctrl+S also compiles here, via latex-workshop.latex.autoBuild.run = onFileChange.)",
"key": "ctrl+enter",
"mac": "cmd+enter",
"command": "latex-workshop.build",
"when": "editorTextFocus && !virtualWorkspace && editorLangId =~ /^latex$|^latex-expl3$|^doctex$|^bibtex$|^rsweave$/"
},
{
"//": "Overleaf: Ctrl+D -> delete current line (VS Code default: add selection to next find match).",
"key": "ctrl+d",
"mac": "cmd+d",
"command": "editor.action.deleteLines",
"when": "textInputFocus && !editorReadonly"
},
{
"//": "Overleaf: Ctrl+Shift+D -> duplicate selection (VS Code default: show Run and Debug).",
"key": "ctrl+shift+d",
"mac": "cmd+shift+d",
"command": "editor.action.duplicateSelection",
"when": "textInputFocus && !editorReadonly"
},
{
"//": "Overleaf: Ctrl+U -> uppercase the selection (VS Code default: soft undo of cursor motion).",
"key": "ctrl+u",
"mac": "ctrl+u",
"command": "editor.action.transformToUppercase",
"when": "editorTextFocus && !editorReadonly"
},
{
"//": "Overleaf: Ctrl+Shift+U -> lowercase the selection (VS Code default: focus the Output panel).",
"key": "ctrl+shift+u",
"mac": "ctrl+shift+u",
"command": "editor.action.transformToLowercase",
"when": "editorTextFocus && !editorReadonly"
},
{
"//": "Overleaf: Ctrl+Shift+L -> go to line number (VS Code default: select all occurrences of selection).",
"key": "ctrl+shift+l",
"mac": "cmd+shift+l",
"command": "workbench.action.gotoLine",
"when": "editorTextFocus"
},
{
"//": "Overleaf: Ctrl+G -> find next (VS Code default: go to line).",
"key": "ctrl+g",
"mac": "cmd+g",
"command": "editor.action.nextMatchFindAction",
"when": "editorFocus"
},
{
"//": "Overleaf: Ctrl+Shift+G -> find previous (VS Code default: show Source Control).",
"key": "ctrl+shift+g",
"mac": "cmd+shift+g",
"command": "editor.action.previousMatchFindAction",
"when": "editorFocus"
},
{
"//": "Overleaf: F2 -> toggle fold (VS Code default: rename symbol, which LaTeX has no use for).",
"key": "f2",
"mac": "f2",
"command": "editor.toggleFold",
"when": "editorTextFocus && editorLangId =~ /^latex$|^latex-expl3$|^doctex$|^bibtex$|^rsweave$/"
},
{
"//": "Overleaf: Alt+Shift+0 -> unfold all",
"key": "alt+shift+0",
"mac": "cmd+alt+0",
"command": "editor.unfoldAll",
"when": "editorTextFocus"
},
{
"//": "Overleaf: Alt+Shift+1 -> fold all",
"key": "alt+shift+1",
"mac": "cmd+alt+1",
"command": "editor.foldAll",
"when": "editorTextFocus"
},
{
"//": "Overleaf: Alt+Left/Right -> start/end of line (VS Code default: navigate back/forward, still on Ctrl+Alt+- and Ctrl+Shift+-).",
"key": "alt+left",
"mac": "ctrl+a",
"command": "cursorHome",
"when": "editorTextFocus"
},
{
"key": "alt+right",
"mac": "ctrl+e",
"command": "cursorEnd",
"when": "editorTextFocus"
},
{
"key": "alt+shift+left",
"mac": "ctrl+shift+a",
"command": "cursorHomeSelect",
"when": "editorTextFocus"
},
{
"key": "alt+shift+right",
"mac": "ctrl+shift+e",
"command": "cursorEndSelect",
"when": "editorTextFocus"
},
{
"//": "Overleaf: Alt+Space -> open autocomplete (Ctrl+Space is already the VS Code default).",
"key": "alt+space",
"mac": "alt+space",
"command": "editor.action.triggerSuggest",
"when": "editorTextFocus && !editorReadonly && !suggestWidgetVisible"
}
]
}
}

View File

@@ -0,0 +1,196 @@
// Overleaf-style image paste for LaTeX: Ctrl+V an image into a .tex file and it
// is written into the project and replaced by a \begin{figure} block.
//
// This has to be a paste *provider* rather than one of the usual "paste image"
// extensions (mushan.vscode-paste-image and friends): those shell out to
// xclip/pbpaste/powershell on whichever machine runs the extension host, which
// for a dev container is the container -- where there is no clipboard. VS Code
// serialises the clipboard payload from the client to the provider instead, so
// DataTransferItem.asFile() gives us the bytes the user copied on their desktop.
const vscode = require('vscode')
const path = require('path')
const LANGUAGES = ['latex', 'latex-expl3', 'rsweave', 'doctex']
// What \includegraphics can actually consume, for payloads that carry a filename.
const IMAGE_EXTENSIONS = new Set([
'.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.tif', '.tiff', '.svg', '.pdf', '.eps'
])
// Clipboard bitmaps arrive with a mime type and a placeholder name, so the
// extension has to come from the mime instead.
const IMAGE_MIMES = new Map([
['image/png', '.png'],
['image/jpeg', '.jpg'],
['image/jpg', '.jpg'],
['image/gif', '.gif'],
['image/webp', '.webp'],
['image/bmp', '.bmp'],
['image/tiff', '.tif'],
['image/svg+xml', '.svg'],
['application/pdf', '.pdf']
])
// 'files' matches any DataTransferItem backed by a file (drag & drop, or a copy
// from the OS file manager); the explicit mimes cover clipboard bitmaps.
const MIME_HINTS = ['files', ...IMAGE_MIMES.keys()]
// Names the OS invents for a clipboard bitmap; not worth keeping.
const GENERIC_NAMES = new Set(['', 'image', 'images', 'clipboard', 'unnamed', 'untitled', 'download'])
function settings(document) {
return vscode.workspace.getConfiguration('pasteFigure', document)
}
/** First image-ish file in the payload, or undefined -- in which case we let VS Code paste normally. */
function findImage(dataTransfer) {
const candidates = []
dataTransfer.forEach((item, mime) => {
const file = item.asFile()
if (!file) {
return
}
const named = path.extname(file.name || '').toLowerCase()
if (IMAGE_EXTENSIONS.has(named)) {
candidates.push({ file, ext: named })
return
}
const byMime = IMAGE_MIMES.get(mime.toLowerCase().split(';')[0].trim())
if (byMime) {
candidates.push({ file, ext: byMime })
}
})
return candidates[0]
}
function timestamp() {
const d = new Date()
const pad = n => String(n).padStart(2, '0')
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`
}
/** A filename LaTeX will not choke on: no spaces, no accents, no braces. */
function slugify(raw) {
return raw
.normalize('NFKD')
.replace(/[\u0300-\u036f]/g, '') // drop the accents NFKD just split off
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
}
function defaultName(file) {
const slug = slugify(path.basename(file.name || '', path.extname(file.name || '')))
return GENERIC_NAMES.has(slug) ? `pasted-${timestamp()}` : slug
}
/**
* Ask for the image name, pre-filled with the generated one so Enter keeps it.
* Whatever is typed goes through the same slugify; an extension typed out of habit
* is dropped since the real one comes from the payload. undefined means Escape.
*/
async function askName(file, ext, directory, token) {
const fallback = defaultName(file)
const resolve = input => {
const typed = input.trim()
const stem = path.extname(typed).toLowerCase() === ext ? typed.slice(0, -ext.length) : typed
return slugify(stem) || fallback
}
const shown = vscode.workspace.asRelativePath(directory, false)
const input = await vscode.window.showInputBox({
title: 'Pasted image name',
prompt: 'Enter keeps the suggested name, Escape cancels the paste.',
value: fallback,
validateInput: input => ({
message: `Saved as ${shown}/${resolve(input)}${ext}`,
severity: vscode.InputBoxValidationSeverity.Info
})
}, token)
return input === undefined ? undefined : resolve(input)
}
/**
* Where images live and what \includegraphics paths are relative to. Both are the
* workspace folder when there is one -- latexmk runs from there, and the existing
* slides use workspace-relative paths like img/logos.png.
*/
function locate(document) {
const folder = vscode.workspace.getWorkspaceFolder(document.uri)
const root = folder ? folder.uri : vscode.Uri.joinPath(document.uri, '..')
const relative = settings(document).get('directory') || 'img'
return { root, directory: vscode.Uri.joinPath(root, ...relative.split('/').filter(Boolean)) }
}
async function freeUri(directory, base, ext) {
for (let i = 0; i < 1000; i++) {
const uri = vscode.Uri.joinPath(directory, i === 0 ? `${base}${ext}` : `${base}-${i}${ext}`)
try {
await vscode.workspace.fs.stat(uri)
} catch {
return uri
}
}
throw new Error(`too many files named ${base}${ext} in ${directory.fsPath}`)
}
/** Save the image next to the document and return the figure snippet to insert. */
async function insertFigure(document, dataTransfer, token) {
const image = findImage(dataTransfer)
if (!image) {
return undefined
}
const data = await image.file.data()
if (token.isCancellationRequested || data.length === 0) {
return undefined
}
const { root, directory } = locate(document)
const name = await askName(image.file, image.ext, directory, token)
if (name === undefined || token.isCancellationRequested) {
return undefined
}
await vscode.workspace.fs.createDirectory(directory)
const target = await freeUri(directory, name, image.ext)
await vscode.workspace.fs.writeFile(target, data)
const relative = path.relative(root.fsPath, target.fsPath).split(path.sep).join('/')
const template = settings(document).get('template')
const body = Array.isArray(template) ? template.join('\n') : String(template)
// $ and \ are snippet syntax; the path is slugified but the directory may not be.
return new vscode.SnippetString(body.replace(/\$IMAGE/g, relative.replace(/[$\\}]/g, '\\$&')))
}
/** Failures must not swallow the paste: report, then fall back to a normal paste. */
async function trySnippet(document, dataTransfer, token) {
try {
return await insertFigure(document, dataTransfer, token)
} catch (err) {
vscode.window.showErrorMessage(`Could not insert the pasted image: ${err instanceof Error ? err.message : String(err)}`)
return undefined
}
}
function activate(context) {
const selector = LANGUAGES.map(language => ({ language, scheme: 'file' }))
const kind = vscode.DocumentPasteEditKind?.Empty?.append('latex', 'figure')
context.subscriptions.push(vscode.languages.registerDocumentPasteEditProvider(selector, {
async provideDocumentPasteEdits(document, _ranges, dataTransfer, _context, token) {
const snippet = await trySnippet(document, dataTransfer, token)
return snippet ? [new vscode.DocumentPasteEdit(snippet, 'Insert figure', kind)] : undefined
}
}, { providedPasteEditKinds: kind ? [kind] : [], pasteMimeTypes: MIME_HINTS }))
// Same handling for images dragged into the editor.
context.subscriptions.push(vscode.languages.registerDocumentDropEditProvider(selector, {
async provideDocumentDropEdits(document, _position, dataTransfer, token) {
const snippet = await trySnippet(document, dataTransfer, token)
return snippet ? new vscode.DocumentDropEdit(snippet, 'Insert figure', kind) : undefined
}
}, { providedDropEditKinds: kind ? [kind] : [], dropMimeTypes: MIME_HINTS }))
}
module.exports = { activate, deactivate() {} }

View File

@@ -0,0 +1,59 @@
{
"name": "latex-paste-figure",
"displayName": "LaTeX Paste Figure",
"description": "Overleaf-style Ctrl+V: pasting an image into a .tex file saves it and inserts a figure environment.",
"publisher": "local",
"version": "1.1.0",
"license": "MIT",
"engines": {
"vscode": "^1.97.0"
},
"categories": [
"Other"
],
"//": "Must run in the container: the provider writes the image into the workspace.",
"extensionKind": [
"workspace"
],
"capabilities": {
"untrustedWorkspaces": {
"supported": true
}
},
"main": "./extension.js",
"activationEvents": [
"onLanguage:latex",
"onLanguage:latex-expl3",
"onLanguage:rsweave",
"onLanguage:doctex"
],
"contributes": {
"configuration": {
"title": "LaTeX Paste Figure",
"properties": {
"pasteFigure.directory": {
"type": "string",
"default": "img",
"scope": "resource",
"markdownDescription": "Folder the pasted image is written to, relative to the workspace root. Created on demand."
},
"pasteFigure.template": {
"type": "array",
"items": {
"type": "string"
},
"scope": "resource",
"default": [
"\\begin{figure}",
"\t\\centering",
"\t\\includegraphics[width=${1:0.8\\linewidth}]{$IMAGE}",
"\t\\caption{${2:Enter Caption}}",
"\t\\label{fig:${3:enter-label}}",
"\\end{figure}"
],
"markdownDescription": "Lines inserted at the cursor, in [snippet syntax](https://code.visualstudio.com/docs/editor/userdefinedsnippets#_snippet-syntax). `$IMAGE` is replaced by the path of the saved image; `${1:...}` are the tab stops you cycle through with `Tab`."
}
}
}
}
}

77
Slides/README.md Normal file
View File

@@ -0,0 +1,77 @@
# Slides
Compile `slides.tex` with LuaLaTeX (and Biber for references).
## Partner and funding logos
Enable partners in the preamble, in the desired display order:
```latex
\partner{labsticc}\partner{ubo}\partner{aid}\partner{ifremer}
```
Logos for the following partners are included in `img/partners/`:
| Identifier | Partner |
| --- | --- |
| `labsticc` | Lab-STICC |
| `ubo` | Université de Bretagne Occidentale |
| `ensta` | ENSTA, Institut Polytechnique de Paris |
| `aid` | Agence de l'innovation de défense |
| `ifremer` | Ifremer |
| `lip6` | Laboratoire d'informatique de Sorbonne Université |
| `sorbonne` | Sorbonne Université |
| `academia-sinica` | Academia Sinica |
| `taipei` | University of Taipei |
For example, enable the additional four with:
```latex
\partner{lip6}\partner{sorbonne}\partner{academia-sinica}\partner{taipei}
```
Enable ENSTA with `\partner{ensta}`.
Asset sources are recorded in [`img/partners/README.md`](img/partners/README.md).
Add other individual logos in `img/partners/`, named after their identifier:
```text
img/partners/labsticc.png
img/partners/ubo.png
img/partners/aid.png
img/partners/ifremer.png
```
PNG, PDF, JPG and JPEG are supported, searched in that order. All nine bundled
assets are transparent PNGs. Prefer high-resolution transparent PNGs or vector
PDFs, tightly cropped to the artwork;
embedded white space counts towards a logo's dimensions. SVGs should first be
exported to PDF. The existing `img/logos.png` is a combined UBO/Lab-STICC image,
not two independently selectable logos.
For other file locations or identifiers, register a path (with or without its
extension) and then enable the partner:
```latex
\declarepartner{my-funder}{img/custom-funder.pdf}
\partner{my-funder}
```
The title page places the logos at the bottom right, inside the theme's margins.
Column count, logo sizes, cell sizes, spacing and row wrapping are computed from
the number of available logos, their aspect ratios, the column width and the
slide height. Logos have equal bounding-box area for comparable visual weight:
wide wordmarks are shorter than square seals, rather than dominating the block.
Aspect ratios are preserved; logos are centred within evenly spaced grid cells.
The final row is right-aligned and stays within a bounded title-page area.
Repeated selections are ignored. Missing files produce warnings and are skipped,
without inserting placeholder logos. Partners do not appear on ordinary slides.
With no partners selected, the existing `\titlegraphic{...}` is still used.
When partners are selected, they replace the legacy title graphic.
### Additional information
No additional logo files are needed for the nine included partners. If a funder
requires a specific logo variant, minimum size, ordering or acknowledgement text,
provide those project-specific requirements. No manual layout dimensions are needed.

View File

@@ -0,0 +1,30 @@
# Partner logo sources
Downloaded from official institutional websites on 2026-10-11. Logos remain the
property of their respective institutions; availability here does not grant
unrestricted use or imply endorsement. Follow the applicable branding rules.
| File | Official asset source |
| --- | --- |
| `labsticc.png` | [IMT Atlantique / Lab-STICC](https://www.imt-atlantique.fr/sites/default/files/users/user1867/logo-lab-sticc.png) |
| `ubo.png` | Official inline SVG labelled `Logo UBO` on [UBO's branding page](https://www.univ-brest.fr/fr/page/charte-graphique-ubo-et-logos-disposition-des-partenaires), rendered to a transparent 3,161 × 1,299 PNG at 600 DPI |
| `ensta.png` | Official blue vertical ENSTA / IP Paris transparent PNG (`Ensta-logotype-05-bleu-rvb.png`) from the [ENSTA RGB logo archive](https://www.ensta.fr/sites/default/files/content/article/pdf/LogoENSTA_RVB.zip), linked from [ENSTA's branding page](https://www.ensta.fr/logo-et-charte-graphique); cropped to 3,638 × 2,286 pixels |
| `aid.png` | [Ministère des Armées / AID](https://www.defense.gouv.fr/sites/default/files/aid/logo.png), displayed on [AID's RAPID page](https://www.defense.gouv.fr/aid/deposez-votre-projet/rapid-regime-dappui-linnovation-duale) |
| `ifremer.png` | Inline `logo-ifremer` SVG from [Ifremer](https://www.ifremer.fr/fr); rendered using the site's brand colour `#0032c6` |
| `lip6.png` | Inline `LIP6` SVG from [LIP6](https://www.lip6.fr/); rendered using `--LIP6_logo_color: #25314F` from the site's `css/common.css` |
| `sorbonne.png` | [Sorbonne Université SVG](https://www.sorbonne-universite.fr/sites/default/files/media/2019-10/logo_4.svg), rendered to PNG |
| `academia-sinica.png` | [Academia Sinica](https://www.sinica.edu.tw/images/AS_logo_2.png), referenced by its [official website](https://www.sinica.edu.tw/en) |
| `taipei.png` | [University of Taipei masthead](https://www.utaipei.edu.tw/var/file/0/1000/img/1583/366369593.jpg), linked from its [official website](https://www.utaipei.edu.tw/) |
All nine files are tightly cropped RGBA PNGs with actual transparent pixels,
not just an alpha channel containing an opaque white background. Existing alpha
was preserved. For opaque sources, only white background connected to the outer
edge was removed; white artwork inside the Academia Sinica seal was retained,
and antialiased contour pixels were unmatted to avoid white fringes.
The SVG-derived assets were rendered at high resolution (at least 1,900 pixels
wide). UBO was re-rendered from vector paths, not upscaled from the old 489-pixel
PNG; its SVG viewport was extended to preserve the descender in “Bretagne”.
LIP6's white circular website backdrop was omitted, keeping the plain
logotype; glyph paths, proportions and lettering were not changed. Compilation
needs no SVG converter, network access or additional shell commands.

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

BIN
Slides/img/partners/aid.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 KiB

BIN
Slides/img/partners/ubo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

202
Slides/slides-partners.sty Normal file
View File

@@ -0,0 +1,202 @@
\NeedsTeXFormat{LaTeX2e}
\ProvidesPackage{slides-partners}[2026/10/11 Automatic title-page partner logos]
\RequirePackage{graphicx}
\RequirePackage{expl3}
\RequirePackage{xparse}
\ExplSyntaxOn
\seq_new:N \g__slides_partners_seq
\prop_new:N \g__slides_partner_files_prop
\seq_new:N \l__slides_partner_files_seq
\seq_new:N \l__slides_partner_ratios_seq
\box_new:N \l__slides_partner_box
\tl_new:N \l__slides_partner_file_tl
\tl_new:N \l__slides_partner_resolved_tl
\int_new:N \l__slides_partner_count_int
\int_new:N \l__slides_partner_columns_int
\int_new:N \l__slides_partner_rows_int
\fp_new:N \l__slides_partner_minratio_fp
\fp_new:N \l__slides_partner_maxratio_fp
\fp_new:N \l__slides_partner_candidate_fp
\fp_new:N \l__slides_partner_scale_fp
\dim_new:N \l__slides_partner_width_dim
\dim_new:N \l__slides_partner_maxheight_dim
\dim_new:N \l__slides_partner_xgap_dim
\dim_new:N \l__slides_partner_ygap_dim
\dim_new:N \l__slides_partner_cellwidth_dim
\dim_new:N \l__slides_partner_cellheight_dim
% Selection is ordered and idempotent. File lookup is delayed until rendering,
% so a declaration can appear before or after its corresponding selection.
\NewDocumentCommand{\partner}{m}
{
\seq_if_in:NnF \g__slides_partners_seq {#1}
{ \seq_gput_right:Nn \g__slides_partners_seq {#1} }
}
\NewDocumentCommand{\declarepartner}{mm}
{ \prop_gput:Nnn \g__slides_partner_files_prop {#1} {#2} }
\cs_new_protected:Npn \__slides_partners_collect:
{
\seq_clear:N \l__slides_partner_files_seq
\seq_clear:N \l__slides_partner_ratios_seq
\seq_map_inline:Nn \g__slides_partners_seq
{
\prop_get:NnNF \g__slides_partner_files_prop {##1}
\l__slides_partner_file_tl
{ \tl_set:Nn \l__slides_partner_file_tl {img/partners/##1} }
\tl_clear:N \l__slides_partner_resolved_tl
\file_if_exist:VTF \l__slides_partner_file_tl
{ \tl_set_eq:NN \l__slides_partner_resolved_tl \l__slides_partner_file_tl }
{
\clist_map_inline:nn {png,pdf,jpg,jpeg}
{
\file_if_exist:nT {\l__slides_partner_file_tl .####1}
{
\tl_set:Nx \l__slides_partner_resolved_tl
{\l__slides_partner_file_tl .####1}
\clist_map_break:
}
}
}
\tl_if_empty:NTF \l__slides_partner_resolved_tl
{
\PackageWarning{slides-partners}
{Logo~for~partner~`##1'~not~found~at~
`\tl_use:N \l__slides_partner_file_tl'.~Skipping~it}
}
{
\seq_put_right:NV \l__slides_partner_files_seq
\l__slides_partner_resolved_tl
\hbox_set:Nn \l__slides_partner_box
{ \includegraphics{\tl_use:N \l__slides_partner_resolved_tl} }
\seq_put_right:Nx \l__slides_partner_ratios_seq
{
\fp_eval:n
{ \dim_to_fp:n {\box_wd:N \l__slides_partner_box} /
\dim_to_fp:n {\box_ht:N \l__slides_partner_box +
\box_dp:N \l__slides_partner_box} }
}
}
}
}
% Equal-area logos in equal-width cells give seals and wide wordmarks comparable
% visual weight. Try every column count, maximizing the common area scale while
% fitting the tallest and widest logos into the available title-page rectangle.
\cs_new_protected:Npn \__slides_partners_layout:
{
\int_set:Nn \l__slides_partner_count_int
{ \seq_count:N \l__slides_partner_files_seq }
\dim_set:Nn \l__slides_partner_width_dim {\linewidth}
\dim_set:Nn \l__slides_partner_maxheight_dim {.24\paperheight}
\dim_set:Nn \l__slides_partner_xgap_dim
{ \fp_to_dim:n {min(.04 * \dim_to_fp:n {\linewidth},
.25 * \dim_to_fp:n {\linewidth} / max(1,\l__slides_partner_count_int-1))} }
\dim_set:Nn \l__slides_partner_ygap_dim
{ \fp_to_dim:n {min(.015 * \dim_to_fp:n {\paperheight},
.25 * \dim_to_fp:n {\l__slides_partner_maxheight_dim} /
max(1,\l__slides_partner_count_int-1))} }
\fp_set:Nn \l__slides_partner_minratio_fp {inf}
\fp_zero:N \l__slides_partner_maxratio_fp
\seq_map_inline:Nn \l__slides_partner_ratios_seq
{
\fp_set:Nn \l__slides_partner_minratio_fp
{min(\l__slides_partner_minratio_fp,##1)}
\fp_set:Nn \l__slides_partner_maxratio_fp
{max(\l__slides_partner_maxratio_fp,##1)}
}
\fp_zero:N \l__slides_partner_scale_fp
\int_set:Nn \l__slides_partner_columns_int {1}
\int_step_inline:nnnn {\l__slides_partner_count_int} {-1} {1}
{
\int_set:Nn \l__slides_partner_rows_int
{ \fp_eval:n {ceil(\l__slides_partner_count_int / ##1)} }
\fp_set:Nn \l__slides_partner_candidate_fp
{ min(.14 * \dim_to_fp:n {\paperheight},
(\dim_to_fp:n {\l__slides_partner_maxheight_dim} -
(\l__slides_partner_rows_int-1) *
\dim_to_fp:n {\l__slides_partner_ygap_dim}) /
\l__slides_partner_rows_int * sqrt(\l__slides_partner_minratio_fp),
(\dim_to_fp:n {\l__slides_partner_width_dim} -
(##1-1) * \dim_to_fp:n {\l__slides_partner_xgap_dim}) /
##1 / sqrt(\l__slides_partner_maxratio_fp)) }
% Break equal-size ties by minimizing unused cells in the final row.
\fp_compare:nT
{ \l__slides_partner_candidate_fp > \l__slides_partner_scale_fp + .0001 ||
(abs(\l__slides_partner_candidate_fp - \l__slides_partner_scale_fp) <= .0001 &&
##1 * \l__slides_partner_rows_int <
\l__slides_partner_columns_int *
ceil(\l__slides_partner_count_int / \l__slides_partner_columns_int)) }
{
\fp_set_eq:NN \l__slides_partner_scale_fp \l__slides_partner_candidate_fp
\int_set:Nn \l__slides_partner_columns_int {##1}
}
}
% Size cells to the widest rendered logo, not the entire available column.
% The enclosing row still right-aligns the compact grid in the title footer.
\dim_set:Nn \l__slides_partner_cellwidth_dim
{ \fp_to_dim:n {\l__slides_partner_scale_fp * sqrt(\l__slides_partner_maxratio_fp)} }
\dim_set:Nn \l__slides_partner_cellheight_dim
{ \fp_to_dim:n {\l__slides_partner_scale_fp / sqrt(\l__slides_partner_minratio_fp)} }
}
\cs_new_protected:Npn \__slides_partners_typeset:
{
\vbox
{
\offinterlineskip
\int_step_inline:nn {\l__slides_partner_count_int}
{
\int_compare:nNnT
{\int_mod:nn {##1-1} {\l__slides_partner_columns_int}} = {0}
{
\int_compare:nNnT {##1} > {1}
{ \vskip \l__slides_partner_ygap_dim }
\hbox_to_wd:nn {\l__slides_partner_width_dim}
{
\hfil
\int_step_inline:nnn {##1}
{\int_min:nn {##1+\l__slides_partner_columns_int-1}
{\l__slides_partner_count_int}}
{
\int_compare:nNnT {####1} > {##1}
{ \hskip \l__slides_partner_xgap_dim }
\vbox_to_ht:nn {\l__slides_partner_cellheight_dim}
{
\vfil
\hbox_to_wd:nn {\l__slides_partner_cellwidth_dim}
{
\hfil
\includegraphics[height=\fp_to_dim:n
{\l__slides_partner_scale_fp / sqrt(
\seq_item:Nn \l__slides_partner_ratios_seq {####1})}]
{\seq_item:Nn \l__slides_partner_files_seq {####1}}
\hfil
}
\vfil
}
}
}
}
}
}
}
\NewDocumentCommand{\insertpartnerlogos}{}
{
\group_begin:
\seq_if_empty:NTF \g__slides_partners_seq
{ \inserttitlegraphic } % Backwards compatibility with existing slides.
{
\__slides_partners_collect:
\seq_if_empty:NF \l__slides_partner_files_seq
{
\__slides_partners_layout:
\__slides_partners_typeset:
}
}
\group_end:
}
\ExplSyntaxOff
\endinput

View File

@@ -170,6 +170,7 @@
\RequirePackage{amsmath,amssymb,amsfonts}
% resizebox
\RequirePackage{graphicx}
\RequirePackage{slides-partners}
\RequirePackage{subcaption}
% tables
\RequirePackage{tabularx}
@@ -199,36 +200,24 @@
% https://ctan.math.illinois.edu/macros/latex/contrib/beamer-contrib/themes/moloch/moloch.pdf
\makeatletter
\setbeamertemplate{title page}{
\begin{minipage}[b][\paperheight]{\textwidth}
% \vfill%
% Use the theme's margins rather than a full-paper-height box, which placed
% the footer almost on the page edge. Stretch only the space above the footer.
\begin{minipage}[t][\dimexpr\paperheight-2\beamer@leftmargin\relax][s]{\textwidth}
\vspace*{.04\paperheight}
\ifx\inserttitle\@empty\else\usebeamertemplate*{title}\fi
\ifx\insertsubtitle\@empty\else\usebeamertemplate*{subtitle}\fi
\vspace{.2cm}
\ifx\insertdate\@empty\else\usebeamertemplate*{date}\fi
\usebeamertemplate*{title separator}
\vspace{-.2cm}
\begin{columns}[]
\column{0.4\linewidth}
\ifx\beamer@shortauthor\@empty\else\usebeamertemplate*{author}\fi
\column{0.4\linewidth}
% \begin{table}[]
% \begin{flushleft}
% \small
% \begin{tabular}{ll}
% Rapporteur & Blabla \\
% Rapporteur & Blabla
% \end{tabular}
% \end{flushleft}
% \end{table}
\end{columns}
\begin{columns}[b]
\column{0.4\linewidth}
\vfill
\begin{columns}[b,onlytextwidth]
\column{0.52\linewidth}
\ifx\insertinstitute\@empty\else\usebeamertemplate*{institute}\fi
\column{0.5\linewidth}
\vspace{-2cm}
\ifx\inserttitlegraphic\@empty\else\inserttitlegraphic\fi
\column{0.44\linewidth}
\insertpartnerlogos
\end{columns}
\vspace*{.2cm}
\vspace*{.02\paperheight}
\end{minipage}
}
\makeatother

View File

@@ -4,20 +4,28 @@
\title{Présentation}
\subtitle{À propos de...}
\date{\today}
\titlegraphic{\flushright\includegraphics[height=1.3cm]{img/logos.png}\vspace{.5cm}}
% Enable partners in display order; logo layout and dimensions are automatic.
\partner{aid}
\partner{labsticc}
\partner{ubo}
\partner{ensta}
% \partner{ifremer}
% \partner{lip6}
% \partner{sorbonne}
% \partner{academia-sinica}
% \partner{taipei}
% See README.md for supported formats and custom logo paths.
% Optional legacy title graphic (used only when no partners are enabled):
% \titlegraphic{\flushright\includegraphics[height=1.3cm]{img/logos.png}\vspace{.5cm}}
\author{
\texorpdfstring{
\begin{table}[]
\begin{flushleft}
\begin{tabular}{ll}
& Vincent Lannurien~\inst{1} \\
& Laurent Lemarchand~\inst{1} \\
& Abdel Malek Toumi~\inst{2} \\
& Jalil Boukhobza~\inst{2}
\begin{tabular}{@{}l@{}}
Vincent Lannurien~\inst{1} \\
Laurent Lemarchand~\inst{1} \\
Abdel Malek Toumi~\inst{2} \\
Jalil Boukhobza~\inst{2}
\end{tabular}
\end{flushleft}
\end{table}
}
{
Vincent Lannurien \and
@@ -29,19 +37,13 @@
\institute{
\texorpdfstring{
\begin{table}[]
\centering
\begin{flushleft}
\begin{tabular}{L}
\inst{1}~Lab-STICC, CNRS UMR 6285, Université de Bretagne Occidentale, Brest
\inst{2}~Lab-STICC, CNRS UMR 6285, ENSTA, Institut Polytechnique de Paris, Brest \\
\end{tabular}
\end{flushleft}
\end{table}
\raggedright
\inst{1}~Lab-STICC, CNRS, UMR 6285, Université de Bretagne Occidentale, Brest\par\medskip
\inst{2}~Lab-STICC, CNRS, UMR 6285, ENSTA, Institut Polytechnique de Paris, Brest
}
{
Lab-STICC, CNRS UMR 6285, ENSTA, Institut Polytechnique de Paris, Brest \and
Lab-STICC, CNRS UMR 6285, Université de Bretagne Occidentale, Brest
Lab-STICC, CNRS, UMR 6285, ENSTA, Institut Polytechnique de Paris, Brest \and
Lab-STICC, CNRS, UMR 6285, Université de Bretagne Occidentale, Brest
}
}