diff options
Diffstat (limited to 'src/Jellyfin.Drawing.Skia')
| -rw-r--r-- | src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 128 | ||||
| -rw-r--r-- | src/Jellyfin.Drawing.Skia/SkiaExtensions.cs | 58 | ||||
| -rw-r--r-- | src/Jellyfin.Drawing.Skia/SkiaHelper.cs | 12 | ||||
| -rw-r--r-- | src/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs | 8 | ||||
| -rw-r--r-- | src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs | 174 | ||||
| -rw-r--r-- | src/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs | 8 |
6 files changed, 323 insertions, 65 deletions
diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 2dac5598f..503e2f941 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; using BlurHashSharp.SkiaSharp; using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; @@ -24,6 +25,17 @@ public class SkiaEncoder : IImageEncoder private readonly ILogger<SkiaEncoder> _logger; private readonly IApplicationPaths _appPaths; private static readonly SKImageFilter _imageFilter; + private static readonly SKTypeface[] _typefaces; + + /// <summary> + /// The default sampling options, equivalent to old high quality filter settings when upscaling. + /// </summary> + public static readonly SKSamplingOptions UpscaleSamplingOptions; + + /// <summary> + /// The sampling options, used for downscaling images, equivalent to old high quality filter settings when not upscaling. + /// </summary> + public static readonly SKSamplingOptions DefaultSamplingOptions; #pragma warning disable CA1810 static SkiaEncoder() @@ -46,6 +58,26 @@ public class SkiaEncoder : IImageEncoder kernelOffset, SKShaderTileMode.Clamp, true); + + // Initialize the list of typefaces + // We have to statically build a list of typefaces because MatchCharacter only accepts a single character or code point + // But in reality a human-readable character (grapheme cluster) could be multiple code points. For example, 🚵🏻♀️ is a single emoji but 5 code points (U+1F6B5 + U+1F3FB + U+200D + U+2640 + U+FE0F) + _typefaces = + [ + SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, '鸡'), // CJK Simplified Chinese + SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, '雞'), // CJK Traditional Chinese + SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'ノ'), // CJK Japanese + SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, '각'), // CJK Korean + SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 128169), // Emojis, 128169 is the 💩emoji + SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'ז'), // Hebrew + SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'ي'), // Arabic + SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright) // Default font + ]; + + // use cubic for upscaling + UpscaleSamplingOptions = new SKSamplingOptions(SKCubicResampler.Mitchell); + // use bilinear for everything else + DefaultSamplingOptions = new SKSamplingOptions(SKFilterMode.Linear, SKMipmapMode.Linear); } /// <summary> @@ -98,6 +130,11 @@ public class SkiaEncoder : IImageEncoder => new HashSet<ImageFormat> { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png, ImageFormat.Svg }; /// <summary> + /// Gets the default typeface to use. + /// </summary> + public static SKTypeface DefaultTypeFace => _typefaces.Last(); + + /// <summary> /// Check if the native lib is available. /// </summary> /// <returns>True if the native lib is available, otherwise false.</returns> @@ -165,18 +202,47 @@ public class SkiaEncoder : IImageEncoder } } - using var codec = SKCodec.Create(path, out SKCodecResult result); + var safePath = NormalizePath(path); + if (new FileInfo(safePath).Length == 0) + { + _logger.LogDebug("Skip zero‑byte image {FilePath}", path); + return default; + } + + using var codec = SKCodec.Create(safePath, out var result); + switch (result) { case SKCodecResult.Success: + // Skia/SkiaSharp edge‑case: when the image header is parsed but the actual pixel + // decode fails (truncated JPEG/PNG, exotic ICC/EXIF, CMYK without color‑transform, etc.) + // `SKCodec.Create` returns a *non‑null* codec together with + // SKCodecResult.InternalError. The header still contains valid dimensions, + // which is all we need here – so we fall back to them instead of aborting. + // See e.g. Skia bugs #4139, #6092. + case SKCodecResult.InternalError when codec is not null: var info = codec.Info; return new ImageDimensions(info.Width, info.Height); + case SKCodecResult.Unimplemented: _logger.LogDebug("Image format not supported: {FilePath}", path); return default; + default: - _logger.LogError("Unable to determine image dimensions for {FilePath}: {SkCodecResult}", path, result); + { + var boundsInfo = SKBitmap.DecodeBounds(safePath); + + if (boundsInfo.Width > 0 && boundsInfo.Height > 0) + { + return new ImageDimensions(boundsInfo.Width, boundsInfo.Height); + } + + _logger.LogWarning( + "Unable to determine image dimensions for {FilePath}: {SkCodecResult}", + path, + result); return default; + } } } @@ -419,7 +485,7 @@ public class SkiaEncoder : IImageEncoder break; } - surface.DrawBitmap(bitmap, 0, 0); + surface.DrawBitmap(bitmap, 0, 0, DefaultSamplingOptions); return rotated; } catch (Exception e) @@ -445,18 +511,23 @@ public class SkiaEncoder : IImageEncoder { using var surface = SKSurface.Create(targetInfo); using var canvas = surface.Canvas; - using var paint = new SKPaint - { - FilterQuality = SKFilterQuality.High, - IsAntialias = isAntialias, - IsDither = isDither - }; + using var paint = new SKPaint(); + paint.IsAntialias = isAntialias; + paint.IsDither = isDither; + + // Historically, kHigh implied cubic filtering, but only when upsampling. + // If specified kHigh, and were down-sampling, Skia used to switch back to kMedium (bilinear filtering plus mipmaps). + // With current skia API, passing Mitchell cubic when down-sampling will cause serious quality degradation. + var samplingOptions = source.Width > targetInfo.Width || source.Height > targetInfo.Height + ? DefaultSamplingOptions + : UpscaleSamplingOptions; paint.ImageFilter = _imageFilter; canvas.DrawBitmap( source, SKRect.Create(0, 0, source.Width, source.Height), SKRect.Create(0, 0, targetInfo.Width, targetInfo.Height), + samplingOptions, paint); return surface.Snapshot(); @@ -535,20 +606,13 @@ public class SkiaEncoder : IImageEncoder canvas.Clear(SKColor.Parse(options.BackgroundColor)); } + using var paint = new SKPaint(); // Add blur if option is present - if (blur > 0) - { - // create image from resized bitmap to apply blur - using var paint = new SKPaint(); - using var filter = SKImageFilter.CreateBlur(blur, blur); - paint.ImageFilter = filter; - canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), paint); - } - else - { - // draw resized bitmap onto canvas - canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height)); - } + using var filter = blur > 0 ? SKImageFilter.CreateBlur(blur, blur) : null; + paint.ImageFilter = filter; + + // create image from resized bitmap to apply blur + canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), DefaultSamplingOptions, paint); // If foreground layer present then draw if (hasForegroundColor) @@ -674,7 +738,7 @@ public class SkiaEncoder : IImageEncoder throw new InvalidOperationException("Image height does not match first image height."); } - canvas.DrawBitmap(img, x * imgWidth, y * imgHeight.Value); + canvas.DrawBitmap(img, x * imgWidth, y * imgHeight.Value, DefaultSamplingOptions); } } @@ -705,4 +769,22 @@ public class SkiaEncoder : IImageEncoder _logger.LogError(ex, "Error drawing indicator overlay"); } } + + /// <summary> + /// Return the typeface that contains the glyph for the given character. + /// </summary> + /// <param name="c">The text character.</param> + /// <returns>The typeface contains the character.</returns> + public static SKTypeface? GetFontForCharacter(string c) + { + foreach (var typeface in _typefaces) + { + if (typeface.ContainsGlyphs(c)) + { + return typeface; + } + } + + return null; + } } diff --git a/src/Jellyfin.Drawing.Skia/SkiaExtensions.cs b/src/Jellyfin.Drawing.Skia/SkiaExtensions.cs new file mode 100644 index 000000000..f7d6842ff --- /dev/null +++ b/src/Jellyfin.Drawing.Skia/SkiaExtensions.cs @@ -0,0 +1,58 @@ +using SkiaSharp; + +namespace Jellyfin.Drawing.Skia; + +/// <summary> +/// The SkiaSharp extensions. +/// </summary> +public static class SkiaExtensions +{ + /// <summary> + /// Draws an SKBitmap on the canvas with specified SkSamplingOptions. + /// </summary> + /// <param name="canvas">The SKCanvas to draw on.</param> + /// <param name="bitmap">The SKBitmap to draw.</param> + /// <param name="dest">The destination SKRect.</param> + /// <param name="options">The SKSamplingOptions to use for rendering.</param> + /// <param name="paint">Optional SKPaint to apply additional effects or styles.</param> + public static void DrawBitmap(this SKCanvas canvas, SKBitmap bitmap, SKRect dest, SKSamplingOptions options, SKPaint? paint = null) + { + using var image = SKImage.FromBitmap(bitmap); + canvas.DrawImage(image, dest, options, paint); + } + + /// <summary> + /// Draws an SKBitmap on the canvas at the specified coordinates with the given SkSamplingOptions. + /// </summary> + /// <param name="canvas">The SKCanvas to draw on.</param> + /// <param name="bitmap">The SKBitmap to draw.</param> + /// <param name="x">The x-coordinate where the bitmap will be drawn.</param> + /// <param name="y">The y-coordinate where the bitmap will be drawn.</param> + /// <param name="options">The SKSamplingOptions to use for rendering.</param> + /// <param name="paint">Optional SKPaint to apply additional effects or styles.</param> + public static void DrawBitmap(this SKCanvas canvas, SKBitmap bitmap, float x, float y, SKSamplingOptions options, SKPaint? paint = null) + { + using var image = SKImage.FromBitmap(bitmap); + canvas.DrawImage(image, x, y, options, paint); + } + + /// <summary> + /// Draws an SKBitmap on the canvas using a specified source rectangle, destination rectangle, + /// and optional paint, with the given SkSamplingOptions. + /// </summary> + /// <param name="canvas">The SKCanvas to draw on.</param> + /// <param name="bitmap">The SKBitmap to draw.</param> + /// <param name="source"> + /// The source SKRect defining the portion of the bitmap to draw. + /// </param> + /// <param name="dest"> + /// The destination SKRect defining the area on the canvas where the bitmap will be drawn. + /// </param> + /// <param name="options">The SKSamplingOptions to use for rendering.</param> + /// <param name="paint">Optional SKPaint to apply additional effects or styles.</param> + public static void DrawBitmap(this SKCanvas canvas, SKBitmap bitmap, SKRect source, SKRect dest, SKSamplingOptions options, SKPaint? paint = null) + { + using var image = SKImage.FromBitmap(bitmap); + canvas.DrawImage(image, source, dest, options, paint); + } +} diff --git a/src/Jellyfin.Drawing.Skia/SkiaHelper.cs b/src/Jellyfin.Drawing.Skia/SkiaHelper.cs index bd1b2b0da..87446236c 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaHelper.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaHelper.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.IO; using SkiaSharp; namespace Jellyfin.Drawing.Skia; @@ -27,12 +28,17 @@ public static class SkiaHelper currentIndex = 0; } - SKBitmap? bitmap = skiaEncoder.Decode(paths[currentIndex], false, null, out _); - + var imagePath = paths[currentIndex]; imagesTested[currentIndex] = 0; - currentIndex++; + if (!Path.Exists(imagePath)) + { + continue; + } + + SKBitmap? bitmap = skiaEncoder.Decode(imagePath, false, null, out _); + if (bitmap is not null) { newIndex = currentIndex; diff --git a/src/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs b/src/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs index 03733d4f8..554707a3f 100644 --- a/src/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs +++ b/src/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs @@ -101,10 +101,12 @@ public class SplashscreenBuilder { var imageWidth = Math.Abs(posterHeight * currentImage.Width / currentImage.Height); using var resizedBitmap = new SKBitmap(imageWidth, posterHeight); - currentImage.ScalePixels(resizedBitmap, SKFilterQuality.High); - + var samplingOptions = currentImage.Width > imageWidth || currentImage.Height > posterHeight + ? SkiaEncoder.DefaultSamplingOptions + : SkiaEncoder.UpscaleSamplingOptions; + currentImage.ScalePixels(resizedBitmap, samplingOptions); // draw on canvas - canvas.DrawBitmap(resizedBitmap, currentWidthPos, currentHeight); + canvas.DrawBitmap(resizedBitmap, currentWidthPos, currentHeight, samplingOptions); // resize to the same aspect as the original currentWidthPos += imageWidth + Spacing; diff --git a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs index 4aff26c16..64c33d5c2 100644 --- a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs +++ b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Text.RegularExpressions; using SkiaSharp; @@ -23,9 +24,6 @@ public partial class StripCollageBuilder _skiaEncoder = skiaEncoder; } - [GeneratedRegex(@"[^\p{IsCJKUnifiedIdeographs}\p{IsCJKUnifiedIdeographsExtensionA}\p{IsKatakana}\p{IsHiragana}\p{IsHangulSyllables}\p{IsHangulJamo}]")] - private static partial Regex NonCjkPatternRegex(); - [GeneratedRegex(@"\p{IsArabic}|\p{IsArmenian}|\p{IsHebrew}|\p{IsSyriac}|\p{IsThaana}")] private static partial Regex IsRtlTextRegex(); @@ -111,43 +109,33 @@ public partial class StripCollageBuilder // resize to the same aspect as the original var backdropHeight = Math.Abs(width * backdrop.Height / backdrop.Width); - using var residedBackdrop = SkiaEncoder.ResizeImage(backdrop, new SKImageInfo(width, backdropHeight, backdrop.ColorType, backdrop.AlphaType, backdrop.ColorSpace)); + using var resizedBackdrop = SkiaEncoder.ResizeImage(backdrop, new SKImageInfo(width, backdropHeight, backdrop.ColorType, backdrop.AlphaType, backdrop.ColorSpace)); + using var paint = new SKPaint(); // draw the backdrop - canvas.DrawImage(residedBackdrop, 0, 0); + canvas.DrawImage(resizedBackdrop, 0, 0, SkiaEncoder.DefaultSamplingOptions, paint); // draw shadow rectangle - using var paintColor = new SKPaint - { - Color = SKColors.Black.WithAlpha(0x78), - Style = SKPaintStyle.Fill - }; + using var paintColor = new SKPaint(); + paintColor.Color = SKColors.Black.WithAlpha(0x78); + paintColor.Style = SKPaintStyle.Fill; canvas.DrawRect(0, 0, width, height, paintColor); - var typeFace = SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright); - - // use the system fallback to find a typeface for the given CJK character - var filteredName = NonCjkPatternRegex().Replace(libraryName ?? string.Empty, string.Empty); - if (!string.IsNullOrEmpty(filteredName)) - { - typeFace = SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, filteredName[0]); - } + var typeFace = SkiaEncoder.DefaultTypeFace; // draw library name - using var textPaint = new SKPaint - { - Color = SKColors.White, - Style = SKPaintStyle.Fill, - TextSize = 112, - TextAlign = SKTextAlign.Center, - Typeface = typeFace, - IsAntialias = true - }; + using var textFont = new SKFont(); + textFont.Size = 112; + textFont.Typeface = typeFace; + using var textPaint = new SKPaint(); + textPaint.Color = SKColors.White; + textPaint.Style = SKPaintStyle.Fill; + textPaint.IsAntialias = true; // scale down text to 90% of the width if text is larger than 95% of the width - var textWidth = textPaint.MeasureText(libraryName); + var textWidth = textFont.MeasureText(libraryName); if (textWidth > width * 0.95) { - textPaint.TextSize = 0.9f * width * textPaint.TextSize / textWidth; + textFont.Size = 0.9f * width * textFont.Size / textWidth; } if (string.IsNullOrWhiteSpace(libraryName)) @@ -155,13 +143,22 @@ public partial class StripCollageBuilder return bitmap; } + var realWidth = DrawText(null, 0, (height / 2f) + (textFont.Metrics.XHeight / 2), libraryName, textPaint, textFont); + if (realWidth > width * 0.95) + { + textFont.Size = 0.9f * width * textFont.Size / realWidth; + realWidth = DrawText(null, 0, (height / 2f) + (textFont.Metrics.XHeight / 2), libraryName, textPaint, textFont); + } + + var padding = (width - realWidth) / 2; + if (IsRtlTextRegex().IsMatch(libraryName)) { - canvas.DrawShapedText(libraryName, width / 2f, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), textPaint); + DrawText(canvas, width - padding, (height / 2f) + (textFont.Metrics.XHeight / 2), libraryName, textPaint, textFont, true); } else { - canvas.DrawText(libraryName, width / 2f, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), textPaint); + DrawText(canvas, padding, (height / 2f) + (textFont.Metrics.XHeight / 2), libraryName, textPaint, textFont); } return bitmap; @@ -187,17 +184,128 @@ public partial class StripCollageBuilder continue; } - // Scale image. The FromBitmap creates a copy + // Scale image var imageInfo = new SKImageInfo(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace); using var resizeImage = SkiaEncoder.ResizeImage(currentBitmap, imageInfo); + using var paint = new SKPaint(); // draw this image into the strip at the next position var xPos = x * cellWidth; var yPos = y * cellHeight; - canvas.DrawImage(resizeImage, xPos, yPos); + canvas.DrawImage(resizeImage, xPos, yPos, SkiaEncoder.DefaultSamplingOptions, paint); } } return bitmap; } + + /// <summary> + /// Draw shaped text with given SKPaint. + /// </summary> + /// <param name="canvas">If not null, draw text to this canvas, otherwise only measure the text width.</param> + /// <param name="x">x position of the canvas to draw text.</param> + /// <param name="y">y position of the canvas to draw text.</param> + /// <param name="text">The text to draw.</param> + /// <param name="textPaint">The SKPaint to style the text.</param> + /// <param name="textFont">The SKFont to style the text.</param> + /// <param name="alignment">The alignment of the text. Default aligns to left.</param> + /// <returns>The width of the text.</returns> + private static float MeasureAndDrawText(SKCanvas? canvas, float x, float y, string text, SKPaint textPaint, SKFont textFont, SKTextAlign alignment = SKTextAlign.Left) + { + var width = textFont.MeasureText(text); + canvas?.DrawShapedText(text, x, y, alignment, textFont, textPaint); + return width; + } + + /// <summary> + /// Draw shaped text with given SKPaint, search defined type faces to render as many texts as possible. + /// </summary> + /// <param name="canvas">If not null, draw text to this canvas, otherwise only measure the text width.</param> + /// <param name="x">x position of the canvas to draw text.</param> + /// <param name="y">y position of the canvas to draw text.</param> + /// <param name="text">The text to draw.</param> + /// <param name="textPaint">The SKPaint to style the text.</param> + /// <param name="textFont">The SKFont to style the text.</param> + /// <param name="isRtl">If true, render from right to left.</param> + /// <returns>The width of the text.</returns> + private static float DrawText(SKCanvas? canvas, float x, float y, string text, SKPaint textPaint, SKFont textFont, bool isRtl = false) + { + float width = 0; + var alignment = isRtl ? SKTextAlign.Right : SKTextAlign.Left; + + if (textFont.ContainsGlyphs(text)) + { + // Current font can render all characters in text + return MeasureAndDrawText(canvas, x, y, text, textPaint, textFont, alignment); + } + + // Iterate over all text elements using TextElementEnumerator + // We cannot use foreach here because a human-readable character (grapheme cluster) can be multiple code points + // We cannot render character by character because glyphs do not always have same width + // And the result will look very unnatural due to the width difference and missing natural spacing + var start = 0; + var enumerator = StringInfo.GetTextElementEnumerator(text); + while (enumerator.MoveNext()) + { + bool notAtEnd; + var textElement = enumerator.GetTextElement(); + if (textFont.ContainsGlyphs(textElement)) + { + continue; + } + + // If we get here, we have a text element which cannot be rendered with current font + // Draw previous characters which can be rendered with current font + if (start != enumerator.ElementIndex) + { + var regularText = text.Substring(start, enumerator.ElementIndex - start); + width += MeasureAndDrawText(canvas, MoveX(x, width), y, regularText, textPaint, textFont, alignment); + start = enumerator.ElementIndex; + } + + // Search for next point where current font can render the character there + while ((notAtEnd = enumerator.MoveNext()) && !textFont.ContainsGlyphs(enumerator.GetTextElement())) + { + // Do nothing, just move enumerator to the point where current font can render the character + } + + // Now we have a substring that should pick another font + // The enumerator may or may not be already at the end of the string + var subtext = notAtEnd + ? text.Substring(start, enumerator.ElementIndex - start) + : text[start..]; + + var fallback = SkiaEncoder.GetFontForCharacter(textElement); + + if (fallback is not null) + { + using var fallbackTextFont = new SKFont(); + fallbackTextFont.Size = textFont.Size; + fallbackTextFont.Typeface = fallback; + using var fallbackTextPaint = new SKPaint(); + fallbackTextPaint.Color = textPaint.Color; + fallbackTextPaint.Style = textPaint.Style; + fallbackTextPaint.IsAntialias = textPaint.IsAntialias; + + // Do the search recursively to select all possible fonts + width += DrawText(canvas, MoveX(x, width), y, subtext, fallbackTextPaint, fallbackTextFont, isRtl); + } + else + { + // Used up all fonts and no fonts can be found, just use current font + width += MeasureAndDrawText(canvas, MoveX(x, width), y, text[start..], textPaint, textFont, alignment); + } + + start = notAtEnd ? enumerator.ElementIndex : text.Length; + } + + // Render the remaining text that current fonts can render + if (start < text.Length) + { + width += MeasureAndDrawText(canvas, MoveX(x, width), y, text[start..], textPaint, textFont, alignment); + } + + return width; + float MoveX(float currentX, float dWidth) => isRtl ? currentX - dWidth : currentX + dWidth; + } } diff --git a/src/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs b/src/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs index 456b84b8c..46c48357e 100644 --- a/src/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs +++ b/src/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs @@ -34,10 +34,12 @@ public static class UnplayedCountIndicator Style = SKPaintStyle.Fill }; + using var font = new SKFont(); + canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint); paint.Color = new SKColor(255, 255, 255, 255); - paint.TextSize = 24; + font.Size = 24; paint.IsAntialias = true; var y = OffsetFromTopRightCorner + 9; @@ -55,9 +57,9 @@ public static class UnplayedCountIndicator { x -= 15; y -= 2; - paint.TextSize = 18; + font.Size = 18; } - canvas.DrawText(text, x, y, paint); + canvas.DrawText(text, x, y, font, paint); } } |
