using Microsoft.AspNetCore.Mvc; using PuppeteerSharp; using HtmlToPdfService.Api.Models; using HtmlToPdfService.Core.Services; using HtmlToPdfService.Core.Storage; namespace HtmlToPdfService.Api.Controllers; /// /// 图片转换控制器 /// [ApiController] [Route("api/image")] public class ImageController : ControllerBase { private readonly IImageService _imageService; private readonly IFileStorage _fileStorage; private readonly ILogger _logger; public ImageController( IImageService imageService, IFileStorage fileStorage, ILogger logger) { _imageService = imageService; _fileStorage = fileStorage; _logger = logger; } /// /// 从 HTML 内容生成图片 /// [HttpPost("convert/html")] [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] public async Task ConvertHtml( [FromBody] ConvertHtmlToImageRequest request, CancellationToken cancellationToken) { if (!ModelState.IsValid) { return BadRequest(ModelState); } try { _logger.LogInformation("收到 HTML 转图片请求"); // 构建截图选项 var screenshotOptions = BuildScreenshotOptions(request.Options); // 执行转换 var result = await _imageService.ConvertHtmlToImageAsync( request.Html, screenshotOptions, request.Options?.Width, request.Options?.Height, request.DelayAfterLoad, request.Callback?.Url, request.Callback?.Headers, request.Callback?.IncludePdfData, request.SaveLocal, cancellationToken); // 返回图片文件 var contentType = GetContentType(request.Options?.Format); var fileName = $"screenshot.{request.Options?.Format ?? "png"}"; return File(result.PdfData!, contentType, fileName); } catch (ArgumentException ex) { _logger.LogWarning(ex, "请求参数无效"); return BadRequest(new ProblemDetails { Status = StatusCodes.Status400BadRequest, Title = "请求参数无效", Detail = ex.Message }); } catch (TimeoutException ex) { _logger.LogError(ex, "转换超时"); return StatusCode(StatusCodes.Status503ServiceUnavailable, new ProblemDetails { Status = StatusCodes.Status503ServiceUnavailable, Title = "服务繁忙", Detail = ex.Message }); } catch (Exception ex) { _logger.LogError(ex, "HTML 转图片失败"); return StatusCode(StatusCodes.Status500InternalServerError, new ProblemDetails { Status = StatusCodes.Status500InternalServerError, Title = "转换失败", Detail = ex.Message }); } } /// /// 从 URL 生成图片 /// [HttpPost("convert/url")] [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] public async Task ConvertUrl( [FromBody] ConvertUrlToImageRequest request, CancellationToken cancellationToken) { if (!ModelState.IsValid) { return BadRequest(ModelState); } try { _logger.LogInformation("收到 URL 转图片请求: {Url}", request.Url); // 构建截图选项 var screenshotOptions = BuildScreenshotOptions(request.Options); // 解析等待条件 WaitUntilNavigation[]? waitUntil = null; if (!string.IsNullOrEmpty(request.WaitUntil)) { waitUntil = ParseWaitUntil(request.WaitUntil); } // 执行转换 var result = await _imageService.ConvertUrlToImageAsync( request.Url, screenshotOptions, request.Options?.Width, request.Options?.Height, waitUntil, request.Timeout, request.DelayAfterLoad, request.Callback?.Url, request.Callback?.Headers, request.Callback?.IncludePdfData, request.SaveLocal, cancellationToken); // 返回图片文件 var contentType = GetContentType(request.Options?.Format); var fileName = $"screenshot.{request.Options?.Format ?? "png"}"; return File(result.PdfData!, contentType, fileName); } catch (ArgumentException ex) { _logger.LogWarning(ex, "请求参数无效"); return BadRequest(new ProblemDetails { Status = StatusCodes.Status400BadRequest, Title = "请求参数无效", Detail = ex.Message }); } catch (TimeoutException ex) { _logger.LogError(ex, "转换超时"); return StatusCode(StatusCodes.Status503ServiceUnavailable, new ProblemDetails { Status = StatusCodes.Status503ServiceUnavailable, Title = "服务繁忙", Detail = ex.Message }); } catch (Exception ex) { _logger.LogError(ex, "URL 转图片失败"); return StatusCode(StatusCodes.Status500InternalServerError, new ProblemDetails { Status = StatusCodes.Status500InternalServerError, Title = "转换失败", Detail = ex.Message }); } } /// /// 构建截图选项 /// private ScreenshotOptions BuildScreenshotOptions(ImageOptionsDto? options) { if (options == null) { return new ScreenshotOptions { FullPage = true }; } var screenshotOptions = new ScreenshotOptions { FullPage = options.FullPage, Type = ParseScreenshotType(options.Format), OmitBackground = options.OmitBackground ?? false }; // JPEG 和 WebP 支持质量设置 if (options.Quality.HasValue && (screenshotOptions.Type == ScreenshotType.Jpeg || screenshotOptions.Type == ScreenshotType.Webp)) { screenshotOptions.Quality = options.Quality.Value; } // 如果指定了截图区域 if (options.Clip != null) { screenshotOptions.Clip = new PuppeteerSharp.Media.Clip { X = options.Clip.X, Y = options.Clip.Y, Width = options.Clip.Width, Height = options.Clip.Height }; } return screenshotOptions; } /// /// 解析截图类型 /// private ScreenshotType ParseScreenshotType(string? format) { return format?.ToLowerInvariant() switch { "jpeg" or "jpg" => ScreenshotType.Jpeg, "webp" => ScreenshotType.Webp, _ => ScreenshotType.Png }; } /// /// 获取 Content-Type /// private string GetContentType(string? format) { return format?.ToLowerInvariant() switch { "jpeg" or "jpg" => "image/jpeg", "webp" => "image/webp", _ => "image/png" }; } /// /// 解析等待条件 /// private WaitUntilNavigation[] ParseWaitUntil(string waitUntil) { return waitUntil.ToLowerInvariant() switch { "load" => new[] { WaitUntilNavigation.Load }, "domcontentloaded" => new[] { WaitUntilNavigation.DOMContentLoaded }, "networkidle0" => new[] { WaitUntilNavigation.Networkidle0 }, "networkidle2" => new[] { WaitUntilNavigation.Networkidle2 }, _ => new[] { WaitUntilNavigation.Networkidle2 } }; } }