Ce guide montre comment ajouter réal-time LaTeX math rendering à une application ASP.NET en utilisant Aspose.Tex pour .NET. Vous construirez une petite API web qui accepte l’entrée LaTEX, le rend à PNG, retourne les bytes d’image avec le type de contenu correct, et cache les résultats sur le disque.
Ce que vous construirez
Une application ASP.NET Core avec :
Endpoint
POST /api/latex/png
qui accepte LaTeX et retourne une image PNGUne page HTML simple où les utilisateurs font une équation et voient une prévision en direct
Le caching disque clé par un hash de contenu et DPI
Validation d’entrée de base et des directories de travail sandboxed
Vous pouvez copier le code et le faire comme il est.
Principaux
Windows, Linux ou macOS avec .NET 6 ou ultérieur
Visual Studio 2022 ou VS Code* avec extension C#
Le paquet NuGet Aspose.TeX
dotnet add package Aspose.TeX
Aspose.TeX exposé TeXOptions
, TeXConfig.ObjectLaTeX
, PngSaveOptions
, ImageDevice
, TeXJob
, InputFileSystemDirectory
, et OutputFileSystemDirectory
Vous les utiliserez pour rendre LaTeX à PNG.
Le projet Layout
Créez un ASP.NET Core Web API, puis ajoutez un service de poids léger plus une page HTML minimale.
AsposeTexDemo/
Program.cs
Services/
LatexRenderer.cs
wwwroot/
index.html
Service : LaTeX à PNG Render
Ce service écrira le LaTeX à une .tex
fichier, exécute le travail Aspose.TeX, et retourne les bytes PNG. Il implémentera également un simple cache sur disque.
// File: Services/LatexRenderer.cs
using System.Security.Cryptography;
using System.Text;
using Aspose.TeX;
namespace AsposeTexDemo.Services;
public sealed class LatexRenderer
{
private readonly string _cacheRoot;
private readonly ILogger<LatexRenderer> _log;
public LatexRenderer(IWebHostEnvironment env, ILogger<LatexRenderer> log)
{
_cacheRoot = Path.Combine(env.ContentRootPath, "tex-cache");
Directory.CreateDirectory(_cacheRoot);
_log = log;
}
// Public entry point. Renders LaTeX to PNG and returns bytes.
public async Task<byte[]> RenderPngAsync(string latexBody, int dpi = 200, CancellationToken ct = default)
{
// Validate and normalize input
var normalized = NormalizeLatex(latexBody);
ValidateLatex(normalized);
// Cache key depends on content and dpi
var key = Hash($"{normalized}\n{dpi}");
var cacheDir = Path.Combine(_cacheRoot, key);
var cachePng = Path.Combine(cacheDir, "out.png");
if (File.Exists(cachePng))
{
_log.LogDebug("Cache hit: {Key}", key);
return await File.ReadAllBytesAsync(cachePng, ct);
}
Directory.CreateDirectory(cacheDir);
// Prepare a minimal document that wraps the math
var texDoc = BuildStandaloneDocument(normalized);
// Write the .tex source into an isolated working folder
var workDir = Path.Combine(cacheDir, "work");
Directory.CreateDirectory(workDir);
var texPath = Path.Combine(workDir, "doc.tex");
await File.WriteAllTextAsync(texPath, texDoc, Encoding.UTF8, ct);
// Configure Aspose.TeX conversion options
var options = TeXOptions.ConsoleAppOptions(TeXConfig.ObjectLaTeX);
options.InputWorkingDirectory = new InputFileSystemDirectory(workDir);
options.OutputWorkingDirectory = new OutputFileSystemDirectory(workDir);
var png = new PngSaveOptions
{
// If you want higher fidelity on HiDPI displays, raise this number
Resolution = dpi,
// When false, the ImageDevice buffers PNG bytes in memory so you can capture them without file I/O
// You can also leave the default (true) and read the file from disk. Both modes are shown below.
DeviceWritesImages = false
};
options.SaveOptions = png;
// Run the job; capture PNG bytes from the device
var device = new ImageDevice();
new TeXJob(texPath, device, options).Run();
if (device.Result == null || device.Result.Length == 0)
throw new InvalidOperationException("No PNG output generated by TeX engine.");
var pngBytes = device.Result[0];
// Persist into cache for the next request
await File.WriteAllBytesAsync(cachePng, pngBytes, ct);
// Clean up working files except cachePng if you want to keep cache slim
TryDeleteDirectory(workDir);
return pngBytes;
}
private static void TryDeleteDirectory(string dir)
{
try { if (Directory.Exists(dir)) Directory.Delete(dir, true); }
catch { /* swallow to avoid noisy logs in high traffic */ }
}
// Minimal, safe preamble for math using Object LaTeX
private static string BuildStandaloneDocument(string latexBody)
{
// With standalone class, the output image is tightly cropped around content
return
$@"\documentclass{{standalone}}
\usepackage{{amsmath}}
\usepackage{{amssymb}}
\begin{{document}}
{latexBody}
\end{{document}}";
}
// Allow plain math snippets like x^2 + y^2 = z^2 and also wrapped forms like \[ ... \]
private static string NormalizeLatex(string input)
{
input = input.Trim();
// If user did not wrap math, wrap in display math to get proper spacing
if (!(input.StartsWith(@"\[") && input.EndsWith(@"\]"))
&& !(input.StartsWith(@"$$") && input.EndsWith(@"$$")))
{
return $"\\[{input}\\]";
}
return input;
}
// Very conservative validation to avoid file inclusion or shell escapes
private static void ValidateLatex(string input)
{
// Disallow commands that can touch the filesystem or process environment
string[] blocked = {
@"\write18", @"\input", @"\include", @"\openout", @"\write", @"\read",
@"\usepackage", // preamble is fixed in BuildStandaloneDocument; avoid arbitrary packages
@"\loop", @"\csname", @"\newread", @"\newwrite"
};
foreach (var b in blocked)
{
if (input.Contains(b, StringComparison.OrdinalIgnoreCase))
throw new ArgumentException($"The LaTeX contains a forbidden command: {b}");
}
if (input.Length > 4000)
throw new ArgumentException("Equation too long. Please keep input under 4000 characters.");
}
private static string Hash(string s)
{
using var sha = SHA256.Create();
var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(s));
return Convert.ToHexString(bytes).ToLowerInvariant();
}
}
API Web : Point de fin ASP.NET
Ceci définit un contrat JSON, enregistre le rendu, et expose une POST
C’est un endpoint qui retourne le PNG.
// File: Program.cs
using System.Text.Json.Serialization;
using AsposeTexDemo.Services;
var builder = WebApplication.CreateBuilder(args);
// Optional: serve a simple static page for testing
builder.Services.AddDirectoryBrowser();
// Add the renderer
builder.Services.AddSingleton<LatexRenderer>();
// Configure Kestrel limits for small payloads
builder.WebHost.ConfigureKestrel(opt =>
{
opt.Limits.MaxRequestBodySize = 256 * 1024; // 256 KB per request is plenty for math
});
var app = builder.Build();
// Serve wwwroot for quick manual testing
app.UseDefaultFiles();
app.UseStaticFiles();
// DTOs
public record LatexRequest(
[property: JsonPropertyName("latex")] string Latex,
[property: JsonPropertyName("dpi")] int? Dpi
);
app.MapPost("/api/latex/png", async (LatexRequest req, LatexRenderer renderer, HttpContext ctx, CancellationToken ct) =>
{
if (string.IsNullOrWhiteSpace(req.Latex))
return Results.BadRequest(new { error = "Missing 'latex'." });
int dpi = req.Dpi is > 0 and <= 600 ? req.Dpi.Value : 200;
try
{
var bytes = await renderer.RenderPngAsync(req.Latex, dpi, ct);
ctx.Response.Headers.CacheControl = "public, max-age=31536000, immutable";
return Results.File(bytes, "image/png");
}
catch (ArgumentException ex)
{
return Results.BadRequest(new { error = ex.Message });
}
catch (Exception ex)
{
// Hide engine details from clients; log the exception server-side if needed
return Results.StatusCode(500);
}
});
// Health check
app.MapGet("/health", () => Results.Ok(new { ok = true }));
app.Run();
Page de test simple
Mettez cela dans wwwroot/index.html
Essayer l’API dans un navigateur sans aucun cadre front-end.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Aspose.TeX LaTeX Demo</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<style>
body{font-family:system-ui,Segoe UI,Roboto,Arial,sans-serif;margin:2rem;line-height:1.4}
textarea{width:100%;height:8rem}
.row{display:flex;gap:1rem;align-items:flex-start;margin-top:1rem;flex-wrap:wrap}
.card{border:1px solid #ddd;border-radius:8px;padding:1rem;flex:1 1 320px}
img{max-width:100%;height:auto;border:1px solid #eee;border-radius:4px;background:#fff}
label{font-weight:600}
input[type=number]{width:6rem}
.muted{color:#666;font-size:.9rem}
.error{color:#b00020}
</style>
</head>
<body>
<h1>Real-time LaTeX to PNG with Aspose.TeX</h1>
<p class="muted">Type LaTeX math and click Render. The server returns a PNG image rendered by Aspose.TeX.</p>
<div class="card">
<label for="latex">LaTeX</label><br />
<textarea id="latex">x^2 + y^2 = z^2</textarea><br />
<label for="dpi">DPI</label>
<input id="dpi" type="number" min="72" max="600" value="200" />
<button id="btn">Render</button>
<div id="msg" class="error"></div>
</div>
<div class="row">
<div class="card">
<h3>Preview</h3>
<img id="preview" alt="Rendered equation will appear here" />
</div>
<div class="card">
<h3>cURL</h3>
<pre id="curl" class="muted"></pre>
</div>
</div>
<script>
const btn = document.getElementById('btn');
const latex = document.getElementById('latex');
const dpi = document.getElementById('dpi');
const img = document.getElementById('preview');
const msg = document.getElementById('msg');
const curl = document.getElementById('curl');
function updateCurl() {
const payload = JSON.stringify({ latex: latex.value, dpi: Number(dpi.value) }, null, 0);
curl.textContent =
`curl -s -X POST http://localhost:5000/api/latex/png \
-H "Content-Type: application/json" \
-d '${payload}' --output out.png`;
}
updateCurl();
[latex, dpi].forEach(el => el.addEventListener('input', updateCurl));
btn.addEventListener('click', async () => {
msg.textContent = '';
img.src = '';
try {
const payload = { latex: latex.value, dpi: Number(dpi.value) };
const res = await fetch('/api/latex/png', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
msg.textContent = err.error || `Error ${res.status}`;
return;
}
const blob = await res.blob();
img.src = URL.createObjectURL(blob);
} catch (e) {
msg.textContent = 'Request failed.';
}
});
</script>
</body>
</html>
Exécutez le projet
dotnet run
Open http://localhost:5000
ou http://localhost:5173
En fonction de votre profil de lancement.Type une équation et cliquez sur Render.Les mises à jour prévisibles avec la sortie PNG du côté du serveur.
Les notes de configuration et de déploiement
- **Aspose.TeX licence*Si vous avez un fichier de licence, définissez-le pendant le démarrage pour supprimer les limites d’évaluation.
// in Program.cs, before first use
// new Aspose.TeX.License().SetLicense("Aspose.Total.lic");
- Localisation du cache*Le Render enregistre les fichiers cache
tex-cache/
Dans la racine du contenu. sur Linux ou les déploiements conteneurs, vous pouvez monter cette voie sur un volume persistant.
- Localisation du cache*Le Render enregistre les fichiers cache
- Limites de taille de requête *Le tableau d’exemple demande la taille de 256 KB. Augmentez si vous soutenez des entrées plus grandes.
- Accès à l’origine*Si vous utilisez l’API d’une origine différente du site, activez CORS en conséquence.
Sécurité Checklist
- Le service rejette des commandes potentiellement dangereuses comme
\input
,\include
, et\write18
Gardez la liste de permission étroite et maintenez le préambule fixéBuildStandaloneDocument
. - Limitez la longueur d’entrée pour bloquer les charges pathologiques.
- Render dans un catalogue de travail unique par requête et supprimer le registre après succès. l’échantillon ne conserve que le PNG caché.
- Considérons la limitation des taux au niveau du proxy ou du portail d’API inversé pour les sites publics.
Conseils de performance
- Utilisez cache disque pour éviter de recomputer les mêmes équations.L’échantillon a hashes LaTeX plus DPI pour dédoubler les résultats.
- Gardez DPI entre 150 et 300 pour la plupart des besoins d’UI. Une augmentation de DPI plus élevée rend le temps et la taille de l’image.
- Réchauffez l’application en rendant une formule courante au start-up si vous voulez que la première demande d’utilisateur soit instantanée.
- Si vous avez besoin d’une sortie vectorielle pour le contenu zoomable, cliquez sur
SvgSaveOptions
etSvgDevice
, puis embrassez le SVG. Le reste du pipeline est le même.
Troubleshooting
- Blanque de sortie ou erreursVérifiez les logs du serveur.Si LaTeX utilise des paquets en dehors de la préambule fixe, supprimez-les.
\usepackage
Input utilisateur par design. - Clipping ou gros margesLe
standalone
la classe de document envoie généralement les marges étroitement. si vous voyez encore de l’espace supplémentaire,\[
et\]
pour afficher les mathématiques ou les supprimer pour la taille inline. - Texte écrite *Augmentation
PngSaveOptions.Resolution
A 200 à 300 DPI, la plupart des cas d’UI semblent crisp.
- Texte écrite *Augmentation
API de référence rapide utilisé dans le code
TeXOptions.ConsoleAppOptions(TeXConfig.ObjectLaTeX)
: crée des options pour le moteur Object LaTeXPngSaveOptions
: contrôler la production PNG avecResolution
etDeviceWritesImages
ImageDevice
: les buffers PNG résultent dans la mémoire lorsqueDeviceWritesImages = false
TeXJob(texPath, device, options).Run()
Composer le.tex
Fichiers dans le dispositifInputFileSystemDirectory
etOutputFileSystemDirectory
: définir les directrices de travail pour l’entrée et la sortie
Avec ces blocs de construction, votre application ASP.NET peut rendre LaTeX sur la demande, les résultats de cache et servir les équations PNG crisp de manière fiable.