Skip to content

Commit 9c4d7fb

Browse files
Merge remote-tracking branch 'origin/main' into js/skia-parity
# Conflicts: # src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs
2 parents c9a9e4d + ec167af commit 9c4d7fb

4 files changed

Lines changed: 357 additions & 40 deletions

File tree

.github/workflows/build-and-test.yml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
git config --global core.longpaths true
2828
2929
- name: Git Checkout
30-
uses: actions/checkout@v6
30+
uses: actions/checkout@v7
3131
with:
3232
fetch-depth: 0
3333
submodules: recursive
@@ -49,7 +49,7 @@ jobs:
4949
run: echo "lfs_key=$LFS_KEY" >> "$GITHUB_OUTPUT"
5050

5151
- name: Git Setup LFS Cache
52-
uses: actions/cache@v5
52+
uses: actions/cache@v6
5353
with:
5454
path: .git/lfs
5555
key: ${{ steps.expose-key.outputs.lfs_key }}
@@ -137,14 +137,14 @@ jobs:
137137
git config --global core.longpaths true
138138
139139
- name: Git Checkout
140-
uses: actions/checkout@v6
140+
uses: actions/checkout@v7
141141
with:
142142
fetch-depth: 0
143143
submodules: recursive
144144

145145
# Use the warmed key from WarmLFS. Do not recompute or recreate .lfs-assets-id here.
146146
- name: Git Setup LFS Cache
147-
uses: actions/cache@v5
147+
uses: actions/cache@v6
148148
with:
149149
path: .git/lfs
150150
key: ${{ needs.WarmLFS.outputs.lfs_key }}
@@ -157,7 +157,7 @@ jobs:
157157
uses: NuGet/setup-nuget@v4
158158

159159
- name: NuGet Setup Cache
160-
uses: actions/cache@v5
160+
uses: actions/cache@v6
161161
id: nuget-cache
162162
with:
163163
path: ~/.nuget
@@ -216,7 +216,7 @@ jobs:
216216
path: tests/Images/ActualOutput/
217217

218218
- name: Codecov Update
219-
uses: codecov/codecov-action@v6
219+
uses: codecov/codecov-action@v7
220220
if: matrix.options.codecov == true && startsWith(github.repository, 'SixLabors')
221221
with:
222222
flags: unittests
@@ -234,7 +234,7 @@ jobs:
234234
git config --global core.longpaths true
235235
236236
- name: Git Checkout
237-
uses: actions/checkout@v6
237+
uses: actions/checkout@v7
238238
with:
239239
fetch-depth: 0
240240
submodules: recursive
@@ -243,7 +243,7 @@ jobs:
243243
uses: NuGet/setup-nuget@v4
244244

245245
- name: NuGet Setup Cache
246-
uses: actions/cache@v5
246+
uses: actions/cache@v6
247247
id: nuget-cache
248248
with:
249249
path: ~/.nuget

SECURITY.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Security Policy
2+
3+
## Supported Versions
4+
5+
Six Labors provides security fixes only for the latest major version of each library.
6+
7+
Older major versions are end-of-life and do not receive security fixes.
8+
9+
Users must upgrade to the latest major version to receive security fixes.
10+
11+
| Version | Supported |
12+
| -------------------- | --------- |
13+
| Latest major version | Yes |
14+
| Older major versions | No |
15+
16+
Security fixes, if any, are provided at Six Labors' discretion.
17+
18+
This policy does not create any obligation to provide support, maintenance services, SLAs, custom fixes, hosted services, managed services, operational monitoring, professional services, consulting, or certification of customer products.
19+
20+
## Reporting a Vulnerability
21+
22+
Please report suspected security vulnerabilities using GitHub private vulnerability reporting for the relevant Six Labors repository, where available.
23+
24+
If GitHub private vulnerability reporting is not available for a repository, please report suspected security vulnerabilities by contacting Six Labors through the contact details published on the Six Labors website.
25+
26+
Do not report security vulnerabilities through public GitHub issues.
27+
28+
When reporting a vulnerability, please include as much relevant information as possible:
29+
30+
* affected package and version
31+
* target framework and runtime
32+
* operating system
33+
* input file or minimal reproduction, if safe to share
34+
* expected and actual behavior
35+
* potential security impact
36+
* whether you believe the issue is being actively exploited
37+
38+
Six Labors may review reported vulnerabilities and determine whether they are security issues affecting a supported version.
39+
40+
A report may be declined or closed without action if, in Six Labors' opinion, it:
41+
42+
* is not reproducible
43+
* does not affect a supported version
44+
* affects only an unsupported or end-of-life version
45+
* is not a security vulnerability
46+
* depends on unsafe, unsupported, or unintended use
47+
* depends on a vulnerable application, environment, dependency, configuration, or deployment outside the Six Labors library itself
48+
* lacks sufficient information for assessment
49+
* is duplicative
50+
* has already been fixed
51+
* is otherwise outside the scope of this policy
52+
53+
If a vulnerability is accepted, Six Labors may handle it through GitHub Security Advisories and, where appropriate, CVE assignment.
54+
55+
Six Labors does not guarantee any response time, fix time, release date, advisory publication date, CVE assignment, workaround, mitigation, or particular outcome for any report.
56+
57+
## Scope
58+
59+
This policy applies only to security vulnerabilities in Six Labors libraries themselves.
60+
61+
This policy does not apply to:
62+
63+
* customer applications
64+
* customer products
65+
* customer deployments
66+
* customer infrastructure
67+
* customer data
68+
* third-party services
69+
* unsupported versions
70+
* end-of-life versions
71+
* forks or modified versions
72+
* usage outside the documented or intended behavior of the relevant library
73+
74+
Organizations using Six Labors libraries are responsible for assessing, securing, testing, monitoring, updating, and maintaining their own applications, products, deployments, infrastructure, and supply chains.
75+
76+
## Cyber Resilience Act
77+
78+
Six Labors libraries are general-purpose software libraries.
79+
80+
They are not cybersecurity products, identity or access management systems, password managers, operating systems, browsers, firewalls, network management tools, SIEM tools, hypervisors, container runtimes, or other Cyber Resilience Act important or critical product classes.
81+
82+
If a Six Labors library is treated as a product with digital elements under the Cyber Resilience Act, Six Labors assesses it as an ordinary software component.
83+
84+
Organizations incorporating Six Labors libraries into products made available on the EU market are responsible for assessing and meeting their own regulatory obligations for those products, including any obligations under the Cyber Resilience Act.
85+
86+
Six Labors does not provide support, maintenance services, SLAs, managed services, hosted services, operational monitoring, custom fixes, professional services, consulting, or certification of customer products.
87+
88+
Security vulnerabilities in supported Six Labors libraries are handled through the GitHub Security Advisory process for the relevant repository, where appropriate.
89+
90+
From 11 September 2026, if Six Labors becomes aware of credible active exploitation of a vulnerability in a supported Six Labors library, or a severe security incident affecting a supported Six Labors library, Six Labors may report the matter through the applicable Cyber Resilience Act reporting mechanism where legally required.
91+
92+
## No Warranty
93+
94+
Six Labors libraries are provided in accordance with their applicable license terms.
95+
96+
Nothing in this policy creates any warranty, representation, guarantee, support obligation, maintenance obligation, service commitment, regulatory certification, or assumption of responsibility for any customer product, customer deployment, customer compliance obligation, or third-party system.

src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs

Lines changed: 71 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1141,8 +1141,24 @@ public override void DrawImage(
11411141
return;
11421142
}
11431143

1144-
Image<TPixel> convertedImage = image.CloneAs<TPixel>();
1145-
this.DrawImageCore(convertedImage, sourceRect, destinationRect, sampler, wrapX, wrapY, ownsSourceImage: true);
1144+
// Only the pixels inside the clipped source region are ever sampled by the draw operation.
1145+
// When that region covers just part of the image, crop it in the source pixel format first so
1146+
// the per-pixel format conversion runs over the required region instead of the whole image.
1147+
if (!TryGetDrawImageClip(sourceRect, destinationRect, image.Bounds, out Rectangle clippedSourceRect, out RectangleF clippedDestinationRect))
1148+
{
1149+
return;
1150+
}
1151+
1152+
if (clippedSourceRect == image.Bounds)
1153+
{
1154+
Image<TPixel> convertedImage = image.CloneAs<TPixel>();
1155+
this.DrawImageCore(convertedImage, sourceRect, destinationRect, sampler, wrapX, wrapY, ownsSourceImage: true);
1156+
return;
1157+
}
1158+
1159+
using Image croppedSource = image.Clone(ctx => ctx.Crop(clippedSourceRect));
1160+
Image<TPixel> convertedRegion = croppedSource.CloneAs<TPixel>();
1161+
this.DrawImageCore(convertedRegion, convertedRegion.Bounds, clippedDestinationRect, sampler, wrapX, wrapY, ownsSourceImage: true);
11461162
}
11471163

11481164
/// <inheritdoc cref="DrawingCanvas.DrawImage(Image, Rectangle, RectangleF, IResampler?)" />
@@ -1272,41 +1288,26 @@ private void DrawImageCore(
12721288
DrawingOptions effectiveOptions = state.Options;
12731289
DrawingOptions commandOptions = effectiveOptions;
12741290

1275-
if (sourceRect.Width <= 0 ||
1276-
sourceRect.Height <= 0 ||
1277-
destinationRect.Width <= 0 ||
1278-
destinationRect.Height <= 0)
1279-
{
1280-
return;
1281-
}
1282-
1283-
Rectangle clippedSourceRect = Rectangle.Intersect(sourceRect, image.Bounds);
1284-
if (clippedSourceRect.Width <= 0 || clippedSourceRect.Height <= 0)
1285-
{
1286-
return;
1287-
}
1288-
1289-
RectangleF clippedDestinationRect = MapSourceClipToDestination(sourceRect, destinationRect, clippedSourceRect);
1290-
if (clippedDestinationRect.Width <= 0 || clippedDestinationRect.Height <= 0)
1291+
Image<TPixel>? ownedImage = null;
1292+
try
12911293
{
1292-
return;
1293-
}
1294+
if (!TryGetDrawImageClip(sourceRect, destinationRect, image.Bounds, out Rectangle clippedSourceRect, out RectangleF clippedDestinationRect))
1295+
{
1296+
return;
1297+
}
12941298

1295-
Size scaledSize = new(
1296-
Math.Max(1, (int)MathF.Ceiling(clippedDestinationRect.Width)),
1297-
Math.Max(1, (int)MathF.Ceiling(clippedDestinationRect.Height)));
1299+
Size scaledSize = new(
1300+
Math.Max(1, (int)MathF.Ceiling(clippedDestinationRect.Width)),
1301+
Math.Max(1, (int)MathF.Ceiling(clippedDestinationRect.Height)));
12981302

1299-
bool requiresScaling =
1300-
clippedSourceRect.Width != scaledSize.Width ||
1301-
clippedSourceRect.Height != scaledSize.Height;
1303+
bool requiresScaling =
1304+
clippedSourceRect.Width != scaledSize.Width ||
1305+
clippedSourceRect.Height != scaledSize.Height;
13021306

1303-
Image<TPixel> brushImage = image;
1304-
RectangleF brushImageRegion = clippedSourceRect;
1305-
RectangleF renderDestinationRect = clippedDestinationRect;
1306-
Image<TPixel>? ownedImage = null;
1307+
Image<TPixel> brushImage = image;
1308+
RectangleF brushImageRegion = clippedSourceRect;
1309+
RectangleF renderDestinationRect = clippedDestinationRect;
13071310

1308-
try
1309-
{
13101311
// Phase 1: Prepare source pixels (crop/scale) in image-local space.
13111312
if (requiresScaling)
13121313
{
@@ -2419,6 +2420,44 @@ private static Image<TPixel> CreateTransformedDrawImage(
24192420
sampler ?? KnownResamplers.Bicubic));
24202421
}
24212422

2423+
/// <summary>
2424+
/// Computes the source and destination rectangles that a draw-image operation will actually
2425+
/// touch, clipping the requested source rectangle to the image bounds.
2426+
/// </summary>
2427+
/// <param name="sourceRect">Requested source rectangle.</param>
2428+
/// <param name="destinationRect">Requested destination rectangle.</param>
2429+
/// <param name="imageBounds">Bounds of the source image.</param>
2430+
/// <param name="clippedSourceRect">Receives the source rectangle clipped to <paramref name="imageBounds"/>.</param>
2431+
/// <param name="clippedDestinationRect">Receives the destination rectangle matching <paramref name="clippedSourceRect"/>.</param>
2432+
/// <returns><see langword="true"/> when the operation covers a non-empty region; otherwise <see langword="false"/>.</returns>
2433+
private static bool TryGetDrawImageClip(
2434+
Rectangle sourceRect,
2435+
RectangleF destinationRect,
2436+
Rectangle imageBounds,
2437+
out Rectangle clippedSourceRect,
2438+
out RectangleF clippedDestinationRect)
2439+
{
2440+
clippedSourceRect = default;
2441+
clippedDestinationRect = default;
2442+
2443+
// A zero-area source cannot be sampled and would divide by zero when mapping to the destination.
2444+
if (sourceRect.Width <= 0 || sourceRect.Height <= 0)
2445+
{
2446+
return false;
2447+
}
2448+
2449+
clippedSourceRect = Rectangle.Intersect(sourceRect, imageBounds);
2450+
if (clippedSourceRect.Width <= 0 || clippedSourceRect.Height <= 0)
2451+
{
2452+
return false;
2453+
}
2454+
2455+
clippedDestinationRect = MapSourceClipToDestination(sourceRect, destinationRect, clippedSourceRect);
2456+
2457+
// A degenerate (empty or inverted) destination maps to nothing to draw.
2458+
return clippedDestinationRect.Width > 0 && clippedDestinationRect.Height > 0;
2459+
}
2460+
24222461
/// <summary>
24232462
/// Maps a clipped source rectangle back to the corresponding destination rectangle.
24242463
/// </summary>

0 commit comments

Comments
 (0)