268 lines
8.5 KiB
C#
268 lines
8.5 KiB
C#
using Microsoft.AspNetCore.Mvc;
|
|
using PuppeteerSharp;
|
|
using HtmlToPdfService.Api.Models;
|
|
using HtmlToPdfService.Core.Services;
|
|
using HtmlToPdfService.Core.Storage;
|
|
|
|
namespace HtmlToPdfService.Api.Controllers;
|
|
|
|
/// <summary>
|
|
/// 图片转换控制器
|
|
/// </summary>
|
|
[ApiController]
|
|
[Route("api/image")]
|
|
public class ImageController : ControllerBase
|
|
{
|
|
private readonly IImageService _imageService;
|
|
private readonly IFileStorage _fileStorage;
|
|
private readonly ILogger<ImageController> _logger;
|
|
|
|
public ImageController(
|
|
IImageService imageService,
|
|
IFileStorage fileStorage,
|
|
ILogger<ImageController> logger)
|
|
{
|
|
_imageService = imageService;
|
|
_fileStorage = fileStorage;
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 从 HTML 内容生成图片
|
|
/// </summary>
|
|
[HttpPost("convert/html")]
|
|
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
|
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
|
public async Task<IActionResult> 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
|
|
});
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 从 URL 生成图片
|
|
/// </summary>
|
|
[HttpPost("convert/url")]
|
|
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
|
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
|
public async Task<IActionResult> 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
|
|
});
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 构建截图选项
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 解析截图类型
|
|
/// </summary>
|
|
private ScreenshotType ParseScreenshotType(string? format)
|
|
{
|
|
return format?.ToLowerInvariant() switch
|
|
{
|
|
"jpeg" or "jpg" => ScreenshotType.Jpeg,
|
|
"webp" => ScreenshotType.Webp,
|
|
_ => ScreenshotType.Png
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取 Content-Type
|
|
/// </summary>
|
|
private string GetContentType(string? format)
|
|
{
|
|
return format?.ToLowerInvariant() switch
|
|
{
|
|
"jpeg" or "jpg" => "image/jpeg",
|
|
"webp" => "image/webp",
|
|
_ => "image/png"
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// 解析等待条件
|
|
/// </summary>
|
|
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 }
|
|
};
|
|
}
|
|
}
|
|
|