SkiaEncoder.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Globalization;
  4. using System.IO;
  5. using MediaBrowser.Common.Configuration;
  6. using MediaBrowser.Controller.Drawing;
  7. using MediaBrowser.Controller.Extensions;
  8. using MediaBrowser.Model.Drawing;
  9. using MediaBrowser.Model.Globalization;
  10. using Microsoft.Extensions.Logging;
  11. using SkiaSharp;
  12. using static Jellyfin.Drawing.Skia.SkiaHelper;
  13. namespace Jellyfin.Drawing.Skia
  14. {
  15. public class SkiaEncoder : IImageEncoder
  16. {
  17. private readonly ILogger _logger;
  18. private readonly IApplicationPaths _appPaths;
  19. private readonly ILocalizationManager _localizationManager;
  20. private static readonly HashSet<string> _transparentImageTypes
  21. = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" };
  22. public SkiaEncoder(
  23. ILogger<SkiaEncoder> logger,
  24. IApplicationPaths appPaths,
  25. ILocalizationManager localizationManager)
  26. {
  27. _logger = logger;
  28. _appPaths = appPaths;
  29. _localizationManager = localizationManager;
  30. }
  31. public string Name => "Skia";
  32. public bool SupportsImageCollageCreation => true;
  33. public bool SupportsImageEncoding => true;
  34. public IReadOnlyCollection<string> SupportedInputFormats =>
  35. new HashSet<string>(StringComparer.OrdinalIgnoreCase)
  36. {
  37. "jpeg",
  38. "jpg",
  39. "png",
  40. "dng",
  41. "webp",
  42. "gif",
  43. "bmp",
  44. "ico",
  45. "astc",
  46. "ktx",
  47. "pkm",
  48. "wbmp",
  49. // TODO
  50. // Are all of these supported? https://github.com/google/skia/blob/master/infra/bots/recipes/test.py#L454
  51. // working on windows at least
  52. "cr2",
  53. "nef",
  54. "arw"
  55. };
  56. public IReadOnlyCollection<ImageFormat> SupportedOutputFormats
  57. => new HashSet<ImageFormat>() { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png };
  58. /// <summary>
  59. /// Test to determine if the native lib is available
  60. /// </summary>
  61. public static void TestSkia()
  62. {
  63. // test an operation that requires the native library
  64. SKPMColor.PreMultiply(SKColors.Black);
  65. }
  66. private static bool IsTransparent(SKColor color)
  67. => (color.Red == 255 && color.Green == 255 && color.Blue == 255) || color.Alpha == 0;
  68. public static SKEncodedImageFormat GetImageFormat(ImageFormat selectedFormat)
  69. {
  70. switch (selectedFormat)
  71. {
  72. case ImageFormat.Bmp:
  73. return SKEncodedImageFormat.Bmp;
  74. case ImageFormat.Jpg:
  75. return SKEncodedImageFormat.Jpeg;
  76. case ImageFormat.Gif:
  77. return SKEncodedImageFormat.Gif;
  78. case ImageFormat.Webp:
  79. return SKEncodedImageFormat.Webp;
  80. default:
  81. return SKEncodedImageFormat.Png;
  82. }
  83. }
  84. private static bool IsTransparentRow(SKBitmap bmp, int row)
  85. {
  86. for (var i = 0; i < bmp.Width; ++i)
  87. {
  88. if (!IsTransparent(bmp.GetPixel(i, row)))
  89. {
  90. return false;
  91. }
  92. }
  93. return true;
  94. }
  95. private static bool IsTransparentColumn(SKBitmap bmp, int col)
  96. {
  97. for (var i = 0; i < bmp.Height; ++i)
  98. {
  99. if (!IsTransparent(bmp.GetPixel(col, i)))
  100. {
  101. return false;
  102. }
  103. }
  104. return true;
  105. }
  106. private SKBitmap CropWhiteSpace(SKBitmap bitmap)
  107. {
  108. var topmost = 0;
  109. for (int row = 0; row < bitmap.Height; ++row)
  110. {
  111. if (IsTransparentRow(bitmap, row))
  112. {
  113. topmost = row + 1;
  114. }
  115. else
  116. {
  117. break;
  118. }
  119. }
  120. int bottommost = bitmap.Height;
  121. for (int row = bitmap.Height - 1; row >= 0; --row)
  122. {
  123. if (IsTransparentRow(bitmap, row))
  124. {
  125. bottommost = row;
  126. }
  127. else
  128. {
  129. break;
  130. }
  131. }
  132. int leftmost = 0, rightmost = bitmap.Width;
  133. for (int col = 0; col < bitmap.Width; ++col)
  134. {
  135. if (IsTransparentColumn(bitmap, col))
  136. {
  137. leftmost = col + 1;
  138. }
  139. else
  140. {
  141. break;
  142. }
  143. }
  144. for (int col = bitmap.Width - 1; col >= 0; --col)
  145. {
  146. if (IsTransparentColumn(bitmap, col))
  147. {
  148. rightmost = col;
  149. }
  150. else
  151. {
  152. break;
  153. }
  154. }
  155. var newRect = SKRectI.Create(leftmost, topmost, rightmost - leftmost, bottommost - topmost);
  156. using (var image = SKImage.FromBitmap(bitmap))
  157. using (var subset = image.Subset(newRect))
  158. {
  159. return SKBitmap.FromImage(subset);
  160. }
  161. }
  162. /// <inheritdoc />
  163. public ImageDimensions GetImageSize(string path)
  164. {
  165. if (path == null)
  166. {
  167. throw new ArgumentNullException(nameof(path));
  168. }
  169. if (!File.Exists(path))
  170. {
  171. throw new FileNotFoundException("File not found", path);
  172. }
  173. using (var codec = SKCodec.Create(path, out SKCodecResult result))
  174. {
  175. EnsureSuccess(result);
  176. var info = codec.Info;
  177. return new ImageDimensions(info.Width, info.Height);
  178. }
  179. }
  180. private static bool HasDiacritics(string text)
  181. => !string.Equals(text, text.RemoveDiacritics(), StringComparison.Ordinal);
  182. private bool RequiresSpecialCharacterHack(string path)
  183. {
  184. if (_localizationManager.HasUnicodeCategory(path, UnicodeCategory.OtherLetter))
  185. {
  186. return true;
  187. }
  188. if (HasDiacritics(path))
  189. {
  190. return true;
  191. }
  192. return false;
  193. }
  194. private string NormalizePath(string path)
  195. {
  196. if (!RequiresSpecialCharacterHack(path))
  197. {
  198. return path;
  199. }
  200. var tempPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + Path.GetExtension(path) ?? string.Empty);
  201. Directory.CreateDirectory(Path.GetDirectoryName(tempPath));
  202. File.Copy(path, tempPath, true);
  203. return tempPath;
  204. }
  205. private static SKEncodedOrigin GetSKEncodedOrigin(ImageOrientation? orientation)
  206. {
  207. if (!orientation.HasValue)
  208. {
  209. return SKEncodedOrigin.TopLeft;
  210. }
  211. switch (orientation.Value)
  212. {
  213. case ImageOrientation.TopRight:
  214. return SKEncodedOrigin.TopRight;
  215. case ImageOrientation.RightTop:
  216. return SKEncodedOrigin.RightTop;
  217. case ImageOrientation.RightBottom:
  218. return SKEncodedOrigin.RightBottom;
  219. case ImageOrientation.LeftTop:
  220. return SKEncodedOrigin.LeftTop;
  221. case ImageOrientation.LeftBottom:
  222. return SKEncodedOrigin.LeftBottom;
  223. case ImageOrientation.BottomRight:
  224. return SKEncodedOrigin.BottomRight;
  225. case ImageOrientation.BottomLeft:
  226. return SKEncodedOrigin.BottomLeft;
  227. default:
  228. return SKEncodedOrigin.TopLeft;
  229. }
  230. }
  231. internal SKBitmap Decode(string path, bool forceCleanBitmap, ImageOrientation? orientation, out SKEncodedOrigin origin)
  232. {
  233. if (!File.Exists(path))
  234. {
  235. throw new FileNotFoundException("File not found", path);
  236. }
  237. var requiresTransparencyHack = _transparentImageTypes.Contains(Path.GetExtension(path));
  238. if (requiresTransparencyHack || forceCleanBitmap)
  239. {
  240. using (var stream = new SKFileStream(NormalizePath(path)))
  241. using (var codec = SKCodec.Create(stream))
  242. {
  243. if (codec == null)
  244. {
  245. origin = GetSKEncodedOrigin(orientation);
  246. return null;
  247. }
  248. // create the bitmap
  249. var bitmap = new SKBitmap(codec.Info.Width, codec.Info.Height, !requiresTransparencyHack);
  250. // decode
  251. _ = codec.GetPixels(bitmap.Info, bitmap.GetPixels());
  252. origin = codec.EncodedOrigin;
  253. return bitmap;
  254. }
  255. }
  256. var resultBitmap = SKBitmap.Decode(NormalizePath(path));
  257. if (resultBitmap == null)
  258. {
  259. return Decode(path, true, orientation, out origin);
  260. }
  261. // If we have to resize these they often end up distorted
  262. if (resultBitmap.ColorType == SKColorType.Gray8)
  263. {
  264. using (resultBitmap)
  265. {
  266. return Decode(path, true, orientation, out origin);
  267. }
  268. }
  269. origin = SKEncodedOrigin.TopLeft;
  270. return resultBitmap;
  271. }
  272. private SKBitmap GetBitmap(string path, bool cropWhitespace, bool forceAnalyzeBitmap, ImageOrientation? orientation, out SKEncodedOrigin origin)
  273. {
  274. if (cropWhitespace)
  275. {
  276. using (var bitmap = Decode(path, forceAnalyzeBitmap, orientation, out origin))
  277. {
  278. return CropWhiteSpace(bitmap);
  279. }
  280. }
  281. return Decode(path, forceAnalyzeBitmap, orientation, out origin);
  282. }
  283. private SKBitmap GetBitmap(string path, bool cropWhitespace, bool autoOrient, ImageOrientation? orientation)
  284. {
  285. SKEncodedOrigin origin;
  286. if (autoOrient)
  287. {
  288. var bitmap = GetBitmap(path, cropWhitespace, true, orientation, out origin);
  289. if (bitmap != null && origin != SKEncodedOrigin.TopLeft)
  290. {
  291. using (bitmap)
  292. {
  293. return OrientImage(bitmap, origin);
  294. }
  295. }
  296. return bitmap;
  297. }
  298. return GetBitmap(path, cropWhitespace, false, orientation, out origin);
  299. }
  300. private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin)
  301. {
  302. //var transformations = {
  303. // 2: { rotate: 0, flip: true},
  304. // 3: { rotate: 180, flip: false},
  305. // 4: { rotate: 180, flip: true},
  306. // 5: { rotate: 90, flip: true},
  307. // 6: { rotate: 90, flip: false},
  308. // 7: { rotate: 270, flip: true},
  309. // 8: { rotate: 270, flip: false},
  310. //}
  311. switch (origin)
  312. {
  313. case SKEncodedOrigin.TopRight:
  314. {
  315. var rotated = new SKBitmap(bitmap.Width, bitmap.Height);
  316. using (var surface = new SKCanvas(rotated))
  317. {
  318. surface.Translate(rotated.Width, 0);
  319. surface.Scale(-1, 1);
  320. surface.DrawBitmap(bitmap, 0, 0);
  321. }
  322. return rotated;
  323. }
  324. case SKEncodedOrigin.BottomRight:
  325. {
  326. var rotated = new SKBitmap(bitmap.Width, bitmap.Height);
  327. using (var surface = new SKCanvas(rotated))
  328. {
  329. float px = (float)bitmap.Width / 2;
  330. float py = (float)bitmap.Height / 2;
  331. surface.RotateDegrees(180, px, py);
  332. surface.DrawBitmap(bitmap, 0, 0);
  333. }
  334. return rotated;
  335. }
  336. case SKEncodedOrigin.BottomLeft:
  337. {
  338. var rotated = new SKBitmap(bitmap.Width, bitmap.Height);
  339. using (var surface = new SKCanvas(rotated))
  340. {
  341. float px = (float)bitmap.Width / 2;
  342. float py = (float)bitmap.Height / 2;
  343. surface.Translate(rotated.Width, 0);
  344. surface.Scale(-1, 1);
  345. surface.RotateDegrees(180, px, py);
  346. surface.DrawBitmap(bitmap, 0, 0);
  347. }
  348. return rotated;
  349. }
  350. case SKEncodedOrigin.LeftTop:
  351. {
  352. // TODO: Remove dual canvases, had trouble with flipping
  353. using (var rotated = new SKBitmap(bitmap.Height, bitmap.Width))
  354. {
  355. using (var surface = new SKCanvas(rotated))
  356. {
  357. surface.Translate(rotated.Width, 0);
  358. surface.RotateDegrees(90);
  359. surface.DrawBitmap(bitmap, 0, 0);
  360. }
  361. var flippedBitmap = new SKBitmap(rotated.Width, rotated.Height);
  362. using (var flippedCanvas = new SKCanvas(flippedBitmap))
  363. {
  364. flippedCanvas.Translate(flippedBitmap.Width, 0);
  365. flippedCanvas.Scale(-1, 1);
  366. flippedCanvas.DrawBitmap(rotated, 0, 0);
  367. }
  368. return flippedBitmap;
  369. }
  370. }
  371. case SKEncodedOrigin.RightTop:
  372. {
  373. var rotated = new SKBitmap(bitmap.Height, bitmap.Width);
  374. using (var surface = new SKCanvas(rotated))
  375. {
  376. surface.Translate(rotated.Width, 0);
  377. surface.RotateDegrees(90);
  378. surface.DrawBitmap(bitmap, 0, 0);
  379. }
  380. return rotated;
  381. }
  382. case SKEncodedOrigin.RightBottom:
  383. {
  384. // TODO: Remove dual canvases, had trouble with flipping
  385. using (var rotated = new SKBitmap(bitmap.Height, bitmap.Width))
  386. {
  387. using (var surface = new SKCanvas(rotated))
  388. {
  389. surface.Translate(0, rotated.Height);
  390. surface.RotateDegrees(270);
  391. surface.DrawBitmap(bitmap, 0, 0);
  392. }
  393. var flippedBitmap = new SKBitmap(rotated.Width, rotated.Height);
  394. using (var flippedCanvas = new SKCanvas(flippedBitmap))
  395. {
  396. flippedCanvas.Translate(flippedBitmap.Width, 0);
  397. flippedCanvas.Scale(-1, 1);
  398. flippedCanvas.DrawBitmap(rotated, 0, 0);
  399. }
  400. return flippedBitmap;
  401. }
  402. }
  403. case SKEncodedOrigin.LeftBottom:
  404. {
  405. var rotated = new SKBitmap(bitmap.Height, bitmap.Width);
  406. using (var surface = new SKCanvas(rotated))
  407. {
  408. surface.Translate(0, rotated.Height);
  409. surface.RotateDegrees(270);
  410. surface.DrawBitmap(bitmap, 0, 0);
  411. }
  412. return rotated;
  413. }
  414. default: return bitmap;
  415. }
  416. }
  417. public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat)
  418. {
  419. if (string.IsNullOrWhiteSpace(inputPath))
  420. {
  421. throw new ArgumentNullException(nameof(inputPath));
  422. }
  423. if (string.IsNullOrWhiteSpace(inputPath))
  424. {
  425. throw new ArgumentNullException(nameof(outputPath));
  426. }
  427. var skiaOutputFormat = GetImageFormat(selectedOutputFormat);
  428. var hasBackgroundColor = !string.IsNullOrWhiteSpace(options.BackgroundColor);
  429. var hasForegroundColor = !string.IsNullOrWhiteSpace(options.ForegroundLayer);
  430. var blur = options.Blur ?? 0;
  431. var hasIndicator = options.AddPlayedIndicator || options.UnplayedCount.HasValue || !options.PercentPlayed.Equals(0);
  432. using (var bitmap = GetBitmap(inputPath, options.CropWhiteSpace, autoOrient, orientation))
  433. {
  434. if (bitmap == null)
  435. {
  436. throw new ArgumentOutOfRangeException(string.Format("Skia unable to read image {0}", inputPath));
  437. }
  438. var originalImageSize = new ImageDimensions(bitmap.Width, bitmap.Height);
  439. if (!options.CropWhiteSpace
  440. && options.HasDefaultOptions(inputPath, originalImageSize)
  441. && !autoOrient)
  442. {
  443. // Just spit out the original file if all the options are default
  444. return inputPath;
  445. }
  446. var newImageSize = ImageHelper.GetNewImageSize(options, originalImageSize);
  447. var width = newImageSize.Width;
  448. var height = newImageSize.Height;
  449. using (var resizedBitmap = new SKBitmap(width, height, bitmap.ColorType, bitmap.AlphaType))
  450. {
  451. // scale image
  452. bitmap.ScalePixels(resizedBitmap, SKFilterQuality.High);
  453. // If all we're doing is resizing then we can stop now
  454. if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator)
  455. {
  456. Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
  457. using (var outputStream = new SKFileWStream(outputPath))
  458. using (var pixmap = new SKPixmap(new SKImageInfo(width, height), resizedBitmap.GetPixels()))
  459. {
  460. pixmap.Encode(outputStream, skiaOutputFormat, quality);
  461. return outputPath;
  462. }
  463. }
  464. // create bitmap to use for canvas drawing used to draw into bitmap
  465. using (var saveBitmap = new SKBitmap(width, height))//, bitmap.ColorType, bitmap.AlphaType))
  466. using (var canvas = new SKCanvas(saveBitmap))
  467. {
  468. // set background color if present
  469. if (hasBackgroundColor)
  470. {
  471. canvas.Clear(SKColor.Parse(options.BackgroundColor));
  472. }
  473. // Add blur if option is present
  474. if (blur > 0)
  475. {
  476. // create image from resized bitmap to apply blur
  477. using (var paint = new SKPaint())
  478. using (var filter = SKImageFilter.CreateBlur(blur, blur))
  479. {
  480. paint.ImageFilter = filter;
  481. canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), paint);
  482. }
  483. }
  484. else
  485. {
  486. // draw resized bitmap onto canvas
  487. canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height));
  488. }
  489. // If foreground layer present then draw
  490. if (hasForegroundColor)
  491. {
  492. if (!double.TryParse(options.ForegroundLayer, out double opacity))
  493. {
  494. opacity = .4;
  495. }
  496. canvas.DrawColor(new SKColor(0, 0, 0, (byte)((1 - opacity) * 0xFF)), SKBlendMode.SrcOver);
  497. }
  498. if (hasIndicator)
  499. {
  500. DrawIndicator(canvas, width, height, options);
  501. }
  502. Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
  503. using (var outputStream = new SKFileWStream(outputPath))
  504. {
  505. using (var pixmap = new SKPixmap(new SKImageInfo(width, height), saveBitmap.GetPixels()))
  506. {
  507. pixmap.Encode(outputStream, skiaOutputFormat, quality);
  508. }
  509. }
  510. }
  511. }
  512. }
  513. return outputPath;
  514. }
  515. public void CreateImageCollage(ImageCollageOptions options)
  516. {
  517. double ratio = (double)options.Width / options.Height;
  518. if (ratio >= 1.4)
  519. {
  520. new StripCollageBuilder(this).BuildThumbCollage(options.InputPaths, options.OutputPath, options.Width, options.Height);
  521. }
  522. else if (ratio >= .9)
  523. {
  524. new StripCollageBuilder(this).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height);
  525. }
  526. else
  527. {
  528. // TODO: Create Poster collage capability
  529. new StripCollageBuilder(this).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height);
  530. }
  531. }
  532. private void DrawIndicator(SKCanvas canvas, int imageWidth, int imageHeight, ImageProcessingOptions options)
  533. {
  534. try
  535. {
  536. var currentImageSize = new ImageDimensions(imageWidth, imageHeight);
  537. if (options.AddPlayedIndicator)
  538. {
  539. PlayedIndicatorDrawer.DrawPlayedIndicator(canvas, currentImageSize);
  540. }
  541. else if (options.UnplayedCount.HasValue)
  542. {
  543. UnplayedCountIndicator.DrawUnplayedCountIndicator(canvas, currentImageSize, options.UnplayedCount.Value);
  544. }
  545. if (options.PercentPlayed > 0)
  546. {
  547. PercentPlayedDrawer.Process(canvas, currentImageSize, options.PercentPlayed);
  548. }
  549. }
  550. catch (Exception ex)
  551. {
  552. _logger.LogError(ex, "Error drawing indicator overlay");
  553. }
  554. }
  555. }
  556. }