Work around truncated Telegram inline photos

This commit is contained in:
corrado.mulas
2026-10-04 10:32:58 +02:00
parent 0212d48555
commit d5b6ab0c24
4 changed files with 205 additions and 55 deletions

View File

@@ -39,6 +39,9 @@ const (
// maxRenderedSide guards against pathologically tall formulas.
maxRenderedSide = 4096
// inlineThumbnailSide is Telegram's conventional maximum thumbnail side.
inlineThumbnailSide = 320
)
// documentClass is the fixed preview layout: the standalone class crops
@@ -113,9 +116,12 @@ func DefaultPreamble() string {
// Result carries the compiled output. JPEG is used for URL-based Telegram
// inline photos, while PNG is used for direct chat replies.
type Result struct {
PNG []byte
JPEG []byte
PDF []byte
PNG []byte
JPEG []byte
JPEGWidth int
JPEGHeight int
Thumbnail []byte
PDF []byte
}
// LatexError wraps a LaTeX compile failure and carries the log excerpt shown
@@ -193,11 +199,12 @@ const (
FormatPNG Formats = 1 << iota
FormatJPEG
FormatPDF
FormatThumbnail
)
// Render compiles the expression and produces every format.
func (r *Renderer) Render(ctx context.Context, preamble, expression string, dpi int) (*Result, error) {
return r.RenderFormats(ctx, preamble, expression, dpi, FormatPNG|FormatJPEG|FormatPDF)
return r.RenderFormats(ctx, preamble, expression, dpi, FormatPNG|FormatJPEG|FormatPDF|FormatThumbnail)
}
// RenderFormats compiles the expression inside the given preamble and
@@ -316,11 +323,25 @@ func (r *Renderer) RenderFormats(ctx context.Context, preamble, expression strin
if err := r.renderRaster(ctx, workdir, dpi, pdfPath, jpegPath, "jpeg"); err != nil {
return nil, err
}
result.JPEGWidth, result.JPEGHeight, err = imageSize(jpegPath)
if err != nil {
return nil, fmt.Errorf("inspect rendered jpeg: %w", err)
}
result.JPEG, err = os.ReadFile(jpegPath)
if err != nil {
return nil, fmt.Errorf("read rendered jpeg: %w", err)
}
}
if formats&FormatThumbnail != 0 {
thumbnailPath := filepath.Join(workdir, "document-thumb.jpg")
if err := r.renderThumbnail(ctx, workdir, dpi, pdfPath, thumbnailPath); err != nil {
return nil, err
}
result.Thumbnail, err = os.ReadFile(thumbnailPath)
if err != nil {
return nil, fmt.Errorf("read rendered thumbnail: %w", err)
}
}
return result, nil
}
@@ -331,6 +352,12 @@ func (r *Renderer) RenderFormats(ctx context.Context, preamble, expression strin
func (r *Renderer) renderRaster(ctx context.Context, workdir string, dpi int, pdfPath, outPath, format string) error {
base := filepath.Join(workdir, "document")
args := []string{"-" + format, "-r", strconv.Itoa(dpi), "-singlefile", "-f", "1", "-l", "1"}
if format == "jpeg" {
// Baseline JPEGs avoid partial/progressive decode artifacts in some
// Telegram clients while remaining comfortably below the 5 MB inline
// photo limit for normal formula renders.
args = append(args, "-jpegopt", "quality=90,progressive=n,optimize=y")
}
if err := r.run(ctx, workdir, r.Pdftoppm, append(args, pdfPath, base)...); err != nil {
return err
}
@@ -356,6 +383,30 @@ func (r *Renderer) renderRaster(ctx context.Context, workdir string, dpi int, pd
return r.run(ctx, workdir, r.Pdftoppm, append(args, pdfPath, base)...)
}
// renderThumbnail creates the dedicated small JPEG used only by Telegram's
// inline-result picker. Reusing the full-resolution photo URL as its thumbnail
// can leave clients displaying a 320-pixel partial decode over the full photo
// canvas even though the downloaded media itself is intact.
func (r *Renderer) renderThumbnail(ctx context.Context, workdir string, dpi int, pdfPath, outPath string) error {
base := strings.TrimSuffix(outPath, filepath.Ext(outPath))
args := []string{
"-jpeg", "-jpegopt", "quality=82,progressive=n,optimize=y",
"-r", strconv.Itoa(dpi), "-singlefile", "-f", "1", "-l", "1",
"-scale-to", strconv.Itoa(inlineThumbnailSide), pdfPath, base,
}
if err := r.run(ctx, workdir, r.Pdftoppm, args...); err != nil {
return fmt.Errorf("render inline thumbnail: %w", err)
}
width, height, err := imageSize(outPath)
if err != nil {
return fmt.Errorf("inspect inline thumbnail: %w", err)
}
if width > inlineThumbnailSide || height > inlineThumbnailSide {
return fmt.Errorf("inline thumbnail is unexpectedly large: %dx%d", width, height)
}
return nil
}
func imageSize(path string) (int, int, error) {
file, err := os.Open(path)
if err != nil {