Документация
Изображения и ресурсы
Элемент image (image(..) в Rust, <Image> в TSX) показывает растровое изображение или SVG-рисунок: файл, поставляемый вместе с приложением, или изображение, загруженное по URL. Изображения читаются и декодируются вне главного потока, уменьшаются до размера, в котором их показывает представление, и кэшируются; главный поток только отображает уже декодированный результат.
const LOGO: ImageAsset = asset!("logo@2x.png");
image(LOGO)
image("https://picsum.photos/seed/nsh/600/400").w_full().h(px(200.0)).rounded(px(12.0))import { Image } from "nsh";
import logo from "./assets/logo@2x.png";
<Image src={logo} />
<Image src="https://picsum.photos/seed/nsh/600/400" style={{ width: "100%", height: 200, borderRadius: 12 }} />Свойства элемента перечислены на странице image.
#Изображения в приложении
Rust-крейт хранит свои файлы в каталоге assets/: asset!("logo@2x.png") (путь относительно assets/) является ImageAsset, { url, size, scales }, читаемым при компиляции крейта, а registry.assets(assets!()) в функции #[native_module] крейта поставляет вместе с приложением все файлы каталога. В TSX файл изображения импортируется как модуль: импорт даёт ImageAsset, { url, size, scales? }, а сборщик поставляет файл вместе с приложением:
const LOGO: ImageAsset = asset!("logo@2x.png");
// LOGO.url: "asset:/@my-app/logo@2x.png", LOGO.size: 64 × 64 points
#[native_module("app/native")]
fn native(registry: &mut Registry) {
registry.assets(assets!());
}import logo from "./assets/logo@2x.png";
logo; // { url: "asset:/assets/logo@2x.png", size: { width: 64, height: 64 } }Поддерживаемые файлы: .png, .jpg, .jpeg, .gif, .webp, .heic и .svg (SVG-изображения).
Размеры указываются в пунктах. Суффикс @2x или @3x в имени файла означает, что в файле 2 или 3 пикселя на пункт, поэтому logo@2x.png размером 128×128 пикселей — это ресурс 64×64 пункта. Без суффикса один пиксель равен одному пункту. Файл, имя которого содержит суффикс, показывается как есть; файл без суффикса показывает свои варианты для разной плотности пикселей, если они есть.
Естественный размер. Изображение из ресурсов размещается в своём естественном размере, если стиль оставляет ширину или высоту незаданной, поэтому для image(LOGO) и <Image src={logo} /> стиль не нужен. Чтобы задать другой размер, укажите одно измерение и соотношение сторон либо оба измерения.
В Rust отсутствующий или повреждённый файл приводит к ошибке сборки, а изменённый файл вызывает пересборку крейта. В TSX ресурсы разрешаются относительно импортирующего модуля, а их url отсчитывается от корня приложения (каталога точки входа). nsh build копирует их в out/assets/, nsh package помещает их в приложение, а во время разработки их отдаёт сервер разработки.
#Плотность пикселей
Назовите файл без суффикса и положите рядом его версии с большей плотностью, и приложение загрузит ту, что подходит экрану:
assets/
├── logo.png 64×64 pixels
├── logo@2x.png 128×128
└── logo@3x.png 192×192
const LOGO: ImageAsset = asset!("logo.png");
// LOGO.size: 64 × 64 points, LOGO.scales: [1.0, 2.0, 3.0]import logo from "./assets/logo.png";
logo; // { url: "asset:/assets/logo.png", size: { width: 64, height: 64 }, scales: [1, 2, 3] }asset!и сборщик ищут соседние файлы@1.5x,@2x,@3xи@4xдля изображения, названного без суффикса.scalesперечисляет их, причём 1 соответствует файлу с основным именем; у изображения без соседних файлов масштабов нет (в TSX оно импортируется как{ url, size }). Сборщик поставляет найденные соседние файлы, аassets!()поставляет весь каталог.- Изображение загружает наименьший масштаб, не меньший коэффициента пикселей экрана, а если такого нет — наибольший, как выбирает React Native:
logo.pngна экране 1×,logo@2x.pngна экране 2× иlogo@3x.pngна телефоне 3× (и при 2.625). В вебе загружается только этот файл. - Когда коэффициент пикселей меняется (окно перенесено на другой экран, масштабирование в браузере), изображения, для которых выбор меняется, загружаются заново, с их событиями загрузки. Их размер остаётся размером файла с основным именем, в пунктах.
- Вариант, размер которого в пикселях не равен размеру файла с основным именем, умноженному на масштаб, даёт предупреждение сборки в TSX и приводит к ошибке сборки в Rust.
- Файл с суффиксом плотности (
./logo@3x.png,asset!("logo@3x.png")) используется сам по себе. - SVG-изображение отрисовывается для экрана при любом коэффициенте, поэтому вариантов у него нет.
Достаточно и одного файла: экспортируйте его в максимальной нужной плотности (@3x для максимальной чёткости на любом телефоне) и назовите с суффиксом; при декодировании изображения уменьшаются до размера экрана. Варианты позволяют каждому экрану загружать, декодировать и хранить тот файл, который ему подходит.
#SVG-изображения
Импортируйте файл .svg как любое другое изображение. Его размер в пунктах задаётся атрибутами width и height (или одним из них, а другой вычисляется из соотношения сторон viewBox), иначе — viewBox: CSS-пиксель SVG равен пункту, а суффиксы @2x оставляют SVG-файлы без изменений.
import logo from "./assets/logo.svg"; // <svg width="120" height="40" …>
import star from "./assets/star.svg"; // <svg viewBox="0 0 24 24" …>
logo; // { url: "asset:/assets/logo.svg", size: { width: 120, height: 40 } }
<Image src={logo} />
<Image src={star} style={{ width: 36, height: 36, tintColor: "#0969da" }} />
SVG чёткий в том размере, в котором показывается. Браузер рисует его как векторную графику; iOS, macOS и Android рисуют его в размере, в котором его показывает представление, в пикселях, когда изображение загружается (как и при уменьшении растровых изображений; представление, которое потом увеличивается, масштабирует этот рисунок до следующей загрузки изображения); Windows, Linux и GPUI рисуют его один раз с четырьмя пикселями на пункт (до 2048 пикселей по стороне) и масштабируют для каждого представления. Всё остальное работает как для растровых изображений: objectFit, tintColor (через непрозрачные фигуры SVG, поэтому одноцветные значки следуют теме), фильтры, blurRadius, заглушки, события загрузки, Image.getSize и Image.prefetch. onLoad и Image.getSize сообщают размер SVG в целых пунктах; изображение размещается в точном размере. Удалённые SVG-файлы загружаются так же, через те же загрузки и проверки capability; NSH распознаёт SVG по содержимому (в том числе сжатому gzip, .svgz), независимо от того, чем заканчивается его URL.
Что рисуют нативные платформы. iOS, macOS, Android, Windows, Linux и GPUI рисуют SVG одним и тем же средством отрисовки (resvg), поэтому рисунок выглядит одинаково везде: фигуры и контуры, заливки и обводки, градиенты и узоры, контуры обрезки и маски, прозрачность, фильтры и преобразования. Текст (<text>), анимация (SMIL и CSS), встроенные изображения (<image>) и HTML (<foreignObject>) отображаются в браузере; нативные платформы рисуют остальное содержимое SVG статично, а nsh build называет файлы, которые их используют. Чтобы получить одинаковый результат везде, экспортируйте текст в контуры.
Самодостаточная картинка. На нативных платформах SVG рисует только то, что в нём содержится: файлы, URL, изображения data: и другие документы, на которые он ссылается, пропускаются. Нативные платформы рисуют SVG, в том числе удалённые, в пределах, которые удерживают память под контролем: документ до 16 МиБ (сжатый — после распаковки), рисунок до 8192 пикселей по стороне и до 4096 × 4096 пикселей в целом (64 МиБ; более крупный рисуется меньшего размера), а слои (группы с прозрачностью, масками, контурами обрезки и фильтрами, которые средство отрисовки рисует отдельно) — до 256 МиБ одновременно. SVG, превышающий предел документа или слоёв, не загружается и выдаёт onError.
Сообщаемые размеры в вебе. Браузер сам измеряет SVG, импортированные или удалённые: onLoad и Image.getSize сообщают width и height SVG, если они есть, а иначе размер, который даёт ему браузер; браузеры обычно выводят его из значения по умолчанию 300 × 150 и соотношения сторон viewBox. Импортированные SVG размещаются в размере, заданном сборщиком, на всех платформах. Задайте SVG атрибуты width и height, чтобы сообщаемый размер был одинаковым везде.
#Удалённые изображения
Передайте элементу URL: строку в Rust (замыкание, если он меняется) или строку с URL в TSX:
image(move || user.with(|u| u.avatar_url.clone()))
.size(px(48.0))
.rounded(px(24.0))
.bg(rgb(0xeaeef2))<Image src={user().avatarUrl} style={{ width: 48, height: 48, borderRadius: 24, backgroundColor: "#eaeef2" }} />- Задавайте удалённым изображениям размер. Пока они не загрузятся, их размер неизвестен, поэтому без ширины и высоты (или одного измерения и соотношения сторон) они схлопываются до нулевой высоты.
- Цвет фона показывается, пока изображение загружается, либо используйте заглушку (ниже).
- Загрузки разделяются: несколько представлений, показывающих один и тот же URL, загружают его один раз, в том числе когда декодируют его в разных размерах. Загрузка, которую больше никто не ждёт, отменяется.
- Декодированные изображения кэшируются в памяти (кэш очищается при нехватке памяти) с ключом из URL, размера декодирования и радиуса размытия.
- Если приложение объявляет capabilities, хост изображения должен быть разрешён в
network; отклонённое изображение завершается ошибкой с событием error.
#Хранение на диске
Загруженные изображения также хранятся на диске в том виде, в каком их отправил сервер, поэтому приложение, открытое повторно (переписка, полная аватаров и фотографий, или лента), показывает ранее показанные изображения без повторной загрузки, в том числе без сети. Приложению не нужны настройки: каждое изображение с URL http:// или https:// проходит через кэш на iOS, Android, macOS, Windows, Linux и GPUI, а в браузере то же самое делает собственный HTTP-кэш браузера.
Кэш следует HTTP-заголовкам кэширования сервера:
- Изображение сохраняется, если ответ не содержит
Cache-Control: no-store. - Оно используется без запроса, пока свежее: в течение
max-age, иначе доExpires, иначе в течение одной десятой времени, прошедшего сLast-Modified(не более недели). - После этого оно проверяется на сервере (
If-None-Matchс егоETag,If-Modified-Sinceс егоLast-Modified): неизменённое изображение (304) используется снова без загрузки, изменённое заменяется. - Когда запрос не удался (нет сети) или на сервере произошла ошибка (
5xx), показывается сохранённая копия, если только ответ не содержалmust-revalidateилиno-cache.
У каждого приложения свой кэш объёмом 256 МиБ; при превышении в первую очередь удаляются наименее давно использованные изображения. Несколько представлений, поверхностей и процессов одного приложения безопасно используют его совместно. Встраивающие приложения выбирают, где он находится и какого он размера, или отключают его:
// macOS, Windows, Linux and GPUI: the AppView's options (or AppView::set_cache).
let mut cache = CacheOptions::at(CacheLocation::Dir(caches.join("nsh"))); // each app in <dir>/<app id>
cache.images = CachePolicy::MaxSize(ByteSize::mib(64)); // CachePolicy::Off: none
let view = AppView::new(mtm, AppOptions { cache, ..AppOptions::default() });// iOS: NshAppView, before loading.
app.cacheOptions = NshCacheOptions(location: .directory(caches.appendingPathComponent("nsh")),
images: .maxSize(megabytes: 64)) // .off: none// Android: NshView, before loading.
nsh.setCacheOptions(NshCacheOptions.builder()
.location(new File(getCacheDir(), "nsh"))
.imagesMaxMegabytes(64) // imagesOff(): none
.build());CacheLocation::Off (.off, noDiskCache()) ничего не хранит на диске.
По умолчанию кэш находится в системном каталоге кэша: Library/Caches/nsh на iOS, nsh в каталоге кэша на Android, ~/Library/Caches на macOS, %LOCALAPPDATA% на Windows и $XDG_CACHE_HOME (~/.cache) на Linux, причём каждое приложение — в своём <app id>/images. Система может очищать эти каталоги, когда ей нужно место.
#Режимы изменения размера
Стиль objectFit, то есть CSS-свойство object-fit (.object_fit(..) в Rust), определяет, как изображение заполняет рамку другой формы:
| Значение | Результат |
|---|---|
"cover" (ResizeMode::Cover, по умолчанию) | Заполняет рамку, сохраняя соотношение сторон и обрезая выступающее |
"contain" (ResizeMode::Contain) | Вписывается в рамку, сохраняя соотношение сторон и оставляя пустые полосы |
"fill" (ResizeMode::Stretch) | Заполняет рамку точно, искажая изображение |
"none" (ResizeMode::Center) | Естественный размер, размещается в objectPosition (по умолчанию по центру) |
"scale-down" (ResizeMode::ScaleDown) | Естественный размер, уменьшается, чтобы поместиться, если он больше |
image(photo).object_fit(ResizeMode::Contain).size(px(140.0)).bg(rgb(0xeaeef2))<Image src={photo} style={{ width: 140, height: 140, objectFit: "contain", backgroundColor: "#eaeef2" }} />Классы Tailwind object-cover, object-contain, object-fill, object-none и object-scale-down задают тот же стиль.
#Положение изображения
Положение изображения заданного размера внутри его рамки определяется стилем objectPosition (.object_position(..) и .object_top(), … в Rust), то есть CSS-свойством object-position: по умолчанию это центр. Изображение в режиме cover обрезается вокруг этой точки, а в режиме contain перемещается внутри пустых полос. Он принимает ключевые слова ("top", "left bottom"), проценты и длины ("25% 75%", "10px 0") и края со смещениями ("right 10px bottom 20%"). Так высокая фотография в пузыре чата показывает свою верхнюю часть:
image(photo).object_fit(ResizeMode::Cover).object_top().w(px(220.0)).h(px(160.0)).rounded(px(16.0))<Image src={photo} style={{ width: 220, height: 160, borderRadius: 16, objectPosition: "top" }} />Классы Tailwind — стандартные для Tailwind: object-top, object-bottom, object-left, object-right, object-center, object-top-left, object-top-right, object-bottom-left, object-bottom-right и произвольные значения (object-[25%_75%]).
#Стилизация изображений
Изображения — это представления: они принимают радиусы углов (в том числе для каждого угла отдельно), границы, тени, прозрачность и преобразования. Углы обрезают изображение.
image(avatar).size(px(90.0)).rounded(px(45.0)).border(px(3.0)).border_color(rgb(0x8250df))<Image src={avatar} style={{ width: 90, height: 90, borderRadius: 45, borderWidth: 3, borderColor: "#8250df" }} />- Оттенок (
.tint_color(..)в Rust, стильtintColorв TSX) перекрашивает непрозрачные пиксели изображения: используйте его для одноцветных значков, которые следуют теме. - Фильтры (
.grayscale(..),.sepia(..),.blur(..);filterв TSX) применяются к изображениям на обеих платформах (Фильтры).
image(ICON).size(px(24.0)).tint_color(AdaptiveColor::rgb(0x0969da, 0x58a6ff))<Image src={icon} style={{ width: 24, height: 24, tintColor: active() ? "#0969da" : "#6e7781" }} />#Заглушки, плавное появление и размытие
| Свойство | Rust | Описание |
|---|---|---|
placeholder | .placeholder(..) | Изображение, которое показывается, пока не загрузится src: URL или ресурс; лучше всего локальный ресурс |
fadeDuration | .fade_duration(ms) | Количество миллисекунд, за которое загруженное изображение плавно появляется. По умолчанию 0 на iOS, 300 на Android |
blurRadius | .blur_radius(px(..)) | Размывает изображение с этим радиусом в пунктах; применяется один раз при декодировании |
image(move || format!("https://picsum.photos/seed/{}/300/300", id.get()))
.placeholder(PLACEHOLDER)
.fade_duration(250.0)
.size(px(100.0))
.rounded(px(12.0))
image(cover).blur_radius(px(16.0)).absolute().inset_0()<Image
src={`https://picsum.photos/seed/${id()}/300/300`}
placeholder={placeholder}
fadeDuration={250}
style={{ width: 100, height: 100, borderRadius: 12 }}
/>
<Image src={cover} blurRadius={16} style={{ position: "absolute", top: 0, right: 0, bottom: 0, left: 0 }} />#Анимированные изображения
Анимированные файлы GIF, WebP и APNG воспроизводятся автоматически, как ресурсы или по URL. Они принимают те же стили, включая objectFit; тонированное анимированное изображение на iOS показывает свой первый кадр.
#События загрузки
| Событие | Rust | Данные | Когда |
|---|---|---|---|
onLoadStart | .on_load_start(..) | Загрузка начинается | |
onProgress | .on_progress(..) | { loaded, total } в байтах (total равно 0, если неизвестно) | Во время загрузки, не чаще чем раз в 100 мс |
onLoad | .on_load(..) | { size } | Изображение готово; размер источника в пикселях (для SVG — в пунктах, в вебе так, как его измеряет браузер) |
onError | .on_error(..) | { message } (&str в Rust) | Загрузка или декодирование не удались |
onLoadEnd | .on_load_end(..) | После onLoad или onError |
let status = signal(SharedString::from("loading…"));
image(url)
.on_load(move |size: &Size<DevicePixels>| status.set(format!("{}×{}", size.width.0, size.height.0).into()))
.on_error(move |message: &str| status.set(message.to_owned().into()))
.w(px(90.0))
.h(px(60.0))const status = signal("loading…");
<Image
src={url}
onLoad={({ size }) => status.set(`${size.width}×${size.height}`)}
onError={(e) => status.set(e.message)}
style={{ width: 90, height: 60 }}
/>Событие загрузки сообщает размер исходного изображения, а не тот размер, в котором оно было декодировано.
Изображение с вариантами для разной плотности пикселей загружает один из своих файлов, и событие загрузки сообщает пиксели этого файла: 192×192 для logo@3x.png на экране 3×, тогда как Image.getSize(logo) разрешается в 64×64 импортированного файла. Когда коэффициент пикселей меняется и изображение загружает другой вариант, эта загрузка тоже сообщает свои события: onLoadStart, затем onLoad с размером нового файла (или onError), затем onLoadEnd.
#Размер изображения и предзагрузка
Изображения, которых нет на экране, можно измерить и загрузить заранее: image_size(..) и prefetch_image(..) в Rust, Image.getSize и Image.prefetch (на том же Image, который отображает элемент) в TSX:
spawn(async move {
// Ok(ImageSize { width, height }), in pixels.
let size = image_size("https://example.com/photo.jpg").await;
// Ok(()) once the bytes are in memory.
let loaded = prefetch_image("https://example.com/next.jpg").await;
});import { Image } from "nsh";
const size = await Image.getSize("https://example.com/photo.jpg"); // { width, height } in pixels
await Image.prefetch("https://example.com/next.jpg"); // once the bytes are in memory| Rust | TSX | Возвращает |
|---|---|---|
image_size(src) | Image.getSize(src) | Размер изображения в пикселях (для SVG в пунктах, в вебе так, как его измеряет браузер); ошибку, если изображение не удаётся загрузить |
prefetch_image(src) | Image.prefetch(src) | Ничего, когда байты изображения загружены; ошибку, если изображение не удаётся загрузить |
В TSX src — это строка с URL или импортированное изображение. Для импортированного изображения с вариантами обе функции загружают вариант, который представления загружают на этом экране, а getSize возвращает размер импортированного файла. Обе сохраняют скачанные байты для представлений, которые покажут это изображение следующими, поэтому изображение, предзагруженное до показа, появляется без скачивания.
Типичный случай использования — задание размера удалённого изображения по его соотношению сторон:
fn remote_image(url: &'static str) -> impl IntoElement {
let ratio = signal(1.0_f32);
spawn(async move {
if let Ok(size) = image_size(url).await {
ratio.set(size.width.0 as f32 / size.height.0 as f32);
}
});
image(url).w_full().aspect_ratio(ratio)
}function RemoteImage(props: { url: string }) {
const ratio = signal(1);
Image.getSize(props.url).then((s) => ratio.set(s.width / s.height));
return <Image src={props.url} style={{ width: "100%", aspectRatio: ratio() }} />;
}#Другие ресурсы
| Файл | Rust (asset!) | Импорт в TSX | Страница |
|---|---|---|---|
.ttf, .otf, .woff, .woff2 | FontAsset с именем семейства | Имя семейства шрифта | Шрифты |
.json и другие файлы | FileAsset с её URI | .json: разобранное значение, встроенное в скрипт | Структура проекта |
В обоих случаях шрифты регистрируются до первого кадра.
#Чтение файла во время выполнения
fetch читает файл, поставляемый вместе с приложением, по его URL asset:/ — одинаково в TSX и в Rust, на любой платформе. У импортированного изображения есть свой URL (url), а у файлов Rust-крейта он берётся из asset! (FileAsset::uri) и assets!():
const GREETING: FileAsset = asset!("locales/en.txt");
#[component]
fn Greeting() -> impl IntoElement {
let greeting = signal(String::new());
spawn(async move {
match fetch(GREETING.uri).await {
Ok(response) => greeting.set(response.text()),
Err(error) => greeting.set(error),
}
});
text(move || greeting.get())
}import flag from "./images/flag.png";
const bytes = await (await fetch(flag.url)).bytes();Движок читает файл из файлов приложения вне своего потока (во время nsh run — с dev-сервера), и никаких разрешений не требуется: это собственные файлы приложения. Веб-приложение скачивает то, что страница не загрузила заранее, оттуда, откуда отдаёт свои ресурсы. Ответ — 200 с байтами файла, его Content-Length и Content-Type, определяемым по расширению (image/png, application/json, …), а его url — это URL asset:/, как бы файл ни был получен; файл, который приложение не поставляет, отклоняется как сетевая ошибка с указанием его имени. URL указывает файл по его пути внутри файлов приложения, а запрос читает его методом GET (или HEAD): URL, путь которого выходит за их пределы (.., абсолютный путь), и запрос с другим методом отклоняются так же, до того как что-либо будет прочитано или скачано.
#Собственный URL модуля
В TSX import.meta.url — это URL модуля, в котором он написан, поэтому new URL("./data.json", import.meta.url) указывает на файл рядом с ним. У модулей локального или упакованного десктопного приложения URL file: в каталоге приложения, которые fs/promises читает при наличии разрешения ${pluginDir}; у модулей веб-приложения это URL страницы; у модулей упакованного мобильного приложения — URL asset:/, как у импортированных изображений. Чтобы поставлять изображение с приложением, импортируйте его: URL, созданный таким способом, указывает на файл, но не упаковывает его.
Растровые изображения также рисуются на канвасе с помощью draw_image / drawImage (Canvas), а градиенты заменяют многие декоративные изображения (Стилизация).
#См. также
- Элемент image: все свойства, включая
getSizeиprefetch. - Списки: длинные списки изображений переиспользуют свои представления.