Эта страница переведена машинным способом и может содержать ошибки. Читать оригинал на английском

Документация

Изображения и ресурсы

Элемент 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))

Свойства элемента перечислены на странице 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!());
}

Поддерживаемые файлы: .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]
  • 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))
  • Задавайте удалённым изображениям размер. Пока они не загрузятся, их размер неизвестен, поэтому без ширины и высоты (или одного измерения и соотношения сторон) они схлопываются до нулевой высоты.
  • Цвет фона показывается, пока изображение загружается, либо используйте заглушку (ниже).
  • Загрузки разделяются: несколько представлений, показывающих один и тот же 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() });

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))

Классы 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))

Классы 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))
  • Оттенок (.tint_color(..) в Rust, стиль tintColor в TSX) перекрашивает непрозрачные пиксели изображения: используйте его для одноцветных значков, которые следуют теме.
  • Фильтры (.grayscale(..), .sepia(..), .blur(..); filter в TSX) применяются к изображениям на обеих платформах (Фильтры).
image(ICON).size(px(24.0)).tint_color(AdaptiveColor::rgb(0x0969da, 0x58a6ff))

#Заглушки, плавное появление и размытие

Свойство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()

#Анимированные изображения

Анимированные файлы 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))

Событие загрузки сообщает размер исходного изображения, а не тот размер, в котором оно было декодировано.

Изображение с вариантами для разной плотности пикселей загружает один из своих файлов, и событие загрузки сообщает пиксели этого файла: 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;
});
RustTSXВозвращает
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)
}

#Другие ресурсы

ФайлRust (asset!)Импорт в TSXСтраница
.ttf, .otf, .woff, .woff2FontAsset с именем семействаИмя семейства шрифтаШрифты
.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())
}

Движок читает файл из файлов приложения вне своего потока (во время 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.
  • Списки: длинные списки изображений переиспользуют свои представления.