mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-15 03:07:56 +01:00
Add a new image provider for `MarkdownTextBlock` that allows loading
images from additional sources:
- **file scheme**
- Enables loading images using the `file:` scheme.
- Intentionally restricts file URIs to absolute paths to ensure correct
resolution when passed through the CmdPal extension/host boundary. (In
most cases, 3rd-party extensions will provide the paths, but the CmdPal
host performs the actual loading and would otherwise resolve paths
relative to itself.)
- **data scheme**
- Enables loading images from URIs with the `data:` scheme (both Base64
and URL-encoded forms).
- Note: the Markdown control itself cannot handle large input and may
hang before the code introduced in this PR is invoked.
- **ms-appx scheme**
- This scheme is now supported for loading images.
- However, since the Command Palette host performs the loading,
`ms-appx:` resolution applies to the host and not the extensions, which
limits its usefulness.
- **ms-appdata scheme**
- This scheme is now supported for loading images.
- Similar to `ms-appx:`, resolution applies to the host, not the
extensions, limiting its usefulness.
---
Additionally, this PR introduces the concept of **_image source
hints_**, implemented as query string parameters piggy-backed on the
original URI.
These hints allow users to influence the behavior of images within
Markdown content.
- `--x-cmdpal-fit`
- `none`: no automatic scaling, provides image as is (default)
- `fit`: scale to fit the available space
- `--x-cmdpal-upscale`
- `true`: allow upscaling
- `false`: downscale only (default)
- `--x-cmdpal-width`: desired width in pixels
- `--x-cmdpal-height`: desired height in pixels
- `--x-cmdpal-maxwidth`: max width in pixels
- `--x-cmdpal-maxheight`: max height in pixels
---
Since `MarkdownTextBlock` requires conforming to the `IImageProvider`
interface—which accepts only a raw URI and must return an `Image`
control—this PR also introduces a new class `RtbInlineImageFactory`.
The factory hooks into the root text block upon loading and listens for
events related to **layout** and **DPI changes**, ensuring that images
adapt correctly to the control’s environment.
```csharp
public interface IImageProvider
{
Task<Image> GetImage(string url);
bool ShouldUseThisProvider(string url);
}
```
---
Pictures? Videos!
Loading images from new schemes:
https://github.com/user-attachments/assets/e0f4308d-30b2-4c81-86db-353048c708c1
New image source scaling options:
https://github.com/user-attachments/assets/ec5b007d-3140-4f0a-b163-7b278233ad40
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
- [x] Closes: #41752
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx
<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
---------
Co-authored-by: Michael Jolley <mike@baldbeardedbuilder.com>
74 lines
2.6 KiB
C#
74 lines
2.6 KiB
C#
// Copyright (c) Microsoft Corporation
|
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
|
// See the LICENSE file in the project root for more information.
|
|
|
|
using System.Runtime.InteropServices.WindowsRuntime;
|
|
using Windows.Storage.Streams;
|
|
|
|
namespace Microsoft.CmdPal.UI.Helpers.MarkdownImageProviders;
|
|
|
|
/// <summary>
|
|
/// Implementation of IImageProvider to handle http/https images, but adds
|
|
/// a new functionality to handle image scaling.
|
|
/// </summary>
|
|
internal sealed partial class HttpImageSourceProvider : IImageSourceProvider
|
|
{
|
|
private readonly HttpClient _http;
|
|
|
|
public HttpImageSourceProvider(HttpClient? httpClient = null)
|
|
=> _http = httpClient ?? SharedHttpClient.Instance;
|
|
|
|
public bool ShouldUseThisProvider(string url)
|
|
=> Uri.TryCreate(url, UriKind.Absolute, out var uri)
|
|
&& (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps);
|
|
|
|
public async Task<ImageSourceInfo> GetImageSource(string url)
|
|
{
|
|
if (!ShouldUseThisProvider(url))
|
|
{
|
|
throw new ArgumentException("URL must be absolute http/https.", nameof(url));
|
|
}
|
|
|
|
using var req = new HttpRequestMessage(HttpMethod.Get, url);
|
|
|
|
req.Headers.TryAddWithoutValidation("Accept", "image/*,text/xml;q=0.9,application/xml;q=0.9,*/*;q=0.8");
|
|
|
|
using var resp = await _http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
|
|
resp.EnsureSuccessStatusCode();
|
|
|
|
var contentType = resp.Content.Headers.ContentType?.MediaType;
|
|
|
|
using var mem = new InMemoryRandomAccessStream();
|
|
await CopyToRandomAccessStreamAsync(resp, mem);
|
|
|
|
var hints = ImageHints.ParseHintsFromUri(new Uri(url));
|
|
var imageSource = await ImageSourceFactory.CreateAsync(
|
|
new ImageSourceFactory.ImagePayload(mem, contentType, new Uri(url)),
|
|
new ImageSourceFactory.ImageDecodeOptions { SniffContent = true });
|
|
|
|
return new ImageSourceInfo(imageSource, hints);
|
|
}
|
|
|
|
private static async Task CopyToRandomAccessStreamAsync(HttpResponseMessage resp, InMemoryRandomAccessStream mem)
|
|
{
|
|
var data = await resp.Content.ReadAsByteArrayAsync();
|
|
await mem.WriteAsync(data.AsBuffer());
|
|
mem.Seek(0);
|
|
}
|
|
|
|
private static class SharedHttpClient
|
|
{
|
|
public static readonly HttpClient Instance = Create();
|
|
|
|
private static HttpClient Create()
|
|
{
|
|
var c = new HttpClient
|
|
{
|
|
Timeout = TimeSpan.FromSeconds(30),
|
|
};
|
|
c.DefaultRequestHeaders.UserAgent.ParseAdd("CommandPalette/1.0");
|
|
return c;
|
|
}
|
|
}
|
|
}
|