diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index f9b1d30..5c561f8 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -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 diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json new file mode 100644 index 0000000..32ffe06 --- /dev/null +++ b/.devcontainer/devcontainer-lock.json @@ -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" + } + } +} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d41169f..de08d3b 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -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": {} + } } diff --git a/.devcontainer/install-extensions.sh b/.devcontainer/install-extensions.sh new file mode 100644 index 0000000..6177e6e --- /dev/null +++ b/.devcontainer/install-extensions.sh @@ -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""" + + + + {escape(pkg["displayName"])} + {escape(pkg["description"])} + {escape(",".join(pkg.get("categories", [])))} + {escape(",".join(pkg.get("categories", [])))} + Public + + + + + + + + + + + + + +""" + +# 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( + ['', + '', + *[f' ' for ext, ctype in sorted(defaults.items())], + '', '']) + +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 diff --git a/.devcontainer/overleaf-keymap/package.json b/.devcontainer/overleaf-keymap/package.json new file mode 100644 index 0000000..a356c1b --- /dev/null +++ b/.devcontainer/overleaf-keymap/package.json @@ -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" + } + ] + } +} diff --git a/.devcontainer/paste-figure/extension.js b/.devcontainer/paste-figure/extension.js new file mode 100644 index 0000000..46d8983 --- /dev/null +++ b/.devcontainer/paste-figure/extension.js @@ -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() {} } diff --git a/.devcontainer/paste-figure/package.json b/.devcontainer/paste-figure/package.json new file mode 100644 index 0000000..cca7b89 --- /dev/null +++ b/.devcontainer/paste-figure/package.json @@ -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`." + } + } + } + } +} diff --git a/Slides/README.md b/Slides/README.md new file mode 100644 index 0000000..5357a07 --- /dev/null +++ b/Slides/README.md @@ -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. diff --git a/Slides/img/partners/README.md b/Slides/img/partners/README.md new file mode 100644 index 0000000..e4a49b7 --- /dev/null +++ b/Slides/img/partners/README.md @@ -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. diff --git a/Slides/img/partners/academia-sinica.png b/Slides/img/partners/academia-sinica.png new file mode 100644 index 0000000..ba5f5b9 Binary files /dev/null and b/Slides/img/partners/academia-sinica.png differ diff --git a/Slides/img/partners/aid.png b/Slides/img/partners/aid.png new file mode 100644 index 0000000..c0c5209 Binary files /dev/null and b/Slides/img/partners/aid.png differ diff --git a/Slides/img/partners/ensta.png b/Slides/img/partners/ensta.png new file mode 100644 index 0000000..9eef4aa Binary files /dev/null and b/Slides/img/partners/ensta.png differ diff --git a/Slides/img/partners/ifremer.png b/Slides/img/partners/ifremer.png new file mode 100644 index 0000000..bbd12d9 Binary files /dev/null and b/Slides/img/partners/ifremer.png differ diff --git a/Slides/img/partners/labsticc.png b/Slides/img/partners/labsticc.png new file mode 100644 index 0000000..ca8cbd6 Binary files /dev/null and b/Slides/img/partners/labsticc.png differ diff --git a/Slides/img/partners/lip6.png b/Slides/img/partners/lip6.png new file mode 100644 index 0000000..057f886 Binary files /dev/null and b/Slides/img/partners/lip6.png differ diff --git a/Slides/img/partners/sorbonne.png b/Slides/img/partners/sorbonne.png new file mode 100644 index 0000000..d519c78 Binary files /dev/null and b/Slides/img/partners/sorbonne.png differ diff --git a/Slides/img/partners/taipei.png b/Slides/img/partners/taipei.png new file mode 100644 index 0000000..7ea3c1a Binary files /dev/null and b/Slides/img/partners/taipei.png differ diff --git a/Slides/img/partners/ubo.png b/Slides/img/partners/ubo.png new file mode 100644 index 0000000..8db58dc Binary files /dev/null and b/Slides/img/partners/ubo.png differ diff --git a/Slides/slides-partners.sty b/Slides/slides-partners.sty new file mode 100644 index 0000000..86bead4 --- /dev/null +++ b/Slides/slides-partners.sty @@ -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 diff --git a/Slides/slides.cls b/Slides/slides.cls index 0d7c9f7..7bc329a 100644 --- a/Slides/slides.cls +++ b/Slides/slides.cls @@ -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} + \ifx\beamer@shortauthor\@empty\else\usebeamertemplate*{author}\fi + \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 diff --git a/Slides/slides.tex b/Slides/slides.tex index ff49d4b..a3c092a 100644 --- a/Slides/slides.tex +++ b/Slides/slides.tex @@ -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} - \end{tabular} - \end{flushleft} - \end{table} + \begin{tabular}{@{}l@{}} + Vincent Lannurien~\inst{1} \\ + Laurent Lemarchand~\inst{1} \\ + Abdel Malek Toumi~\inst{2} \\ + Jalil Boukhobza~\inst{2} + \end{tabular} } { 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 } }