feat: bounded huge-image decoding, webp/gif, remote rasters, Georgian/Armenian fonts

Giant rasters (500 MB aerial/GIS TIFFs) decoded whole: multiple GB of
RGBA on a wasm heap is tab death, and even desktop silently failed
past the image crate's 512 MiB default allocation cap. Decoding now
probes the header for dimensions first and runs in tiers — full decode
under a per-target budget (512 MB wasm / 4 GB native, enforced through
the decoder's allocation limits so oversize fails fast instead of
ballooning), Lanczos3 downsample to a display cap (4096² wasm /
8192² native) when the decode fits but the buffer is oversized, and a
clear localized refusal naming the dimensions and the limit when it
cannot. The probed source dimensions stay the entity's sizing truth,
so a downsampled buffer never shrinks the on-paper footprint; PDF
export self-corrects (effective DPI derives from placement × stored
pixels). Refusals surface once per image through the pick, open, and
drop/re-resolve paths.

Decode coverage gains webp and gif (pure Rust in image 0.25 — no C,
no lockfile churn), with the attach picker and drop filters matching.

Remote http(s) raster references now fetch on web: CORS-classified
failures name CORS, HTTP errors name the status, bodies over 64 MB and
8 s deadlines are bounded, and fetched bytes join the session source
registry (so ARCHIVE can package them and re-resolution reads cache).

Georgian and Armenian Noto subsets ship (generation blocks included),
so those scripts render instead of skipping; scripts still without
subsets remain silently skipped, unchanged.

(cherry picked from commit ca0d7b96)

Ported onto main (#1717 review): Armenian and Georgian take the script
indices after Lao, which main added meanwhile.
This commit is contained in:
Afonsop authored and Hakan Seven committed 2026-10-12 12:02:05 +03:00
1 parent 8a5d343927
commit 93e97bdf18
31 files changed
+840 -39

No files matched your search

+15 -1
View File
@@ -44,7 +44,11 @@ dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
# implementation for the inline (fallback/native) path and the worker path.
ocs_save = { path = "crates/ocs_save" }
flate2 = "1"
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
# Raster decode set: png/jpeg/bmp/tiff plus webp/gif (both pure-Rust in
# image 0.25, so the wasm target stays C-free). Every format named here must
# also appear in the IMAGE picker filter (io::pick_image_file) and the web
# drop filter (io::web_drop::is_image_name).
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff", "webp", "gif"] }
hayro = "0.7.1"
kurbo = "0.13"
# DWF/DWFx packages, their XPS pages and DWF/DGN underlay rasters.
@@ -144,6 +148,16 @@ web-sys = { version = "=0.3.85", features = [
"WritableStream",
"Url",
"Response",
# The wasm remote-raster fetch (image_model::fetch_remote_web) builds a
# `Request` with an `AbortController` signal for its deadline; the fetch
# rejection is classified via `DomException` (AbortError = timeout) and
# the body size is pre-checked against the Content-Length `Headers`.
"Request",
"RequestInit",
"AbortController",
"AbortSignal",
"DomException",
"Headers",
"Worker",
"WorkerOptions",
"WorkerType",
+5
View File
@@ -1909,6 +1909,10 @@ common =
.geometric = هندسي
.dimensional = بُعدي
.show-hide = إظهار/إخفاء
.remote-image-url-could-not-be-fetched-the-network-request-failed-offline-dns-or-cors = تعذر جلب الصورة البعيدة "__ocs_fmt_0__": فشل طلب الشبكة (دون اتصال، أو DNS، أو CORS).
.remote-image-url-could-not-be-fetched-the-server-returned-http-status = تعذر جلب الصورة البعيدة "__ocs_fmt_0__": أرجع الخادم HTTP __ocs_fmt_1__.
.remote-image-url-was-not-loaded-it-exceeds-the-64-mb-remote-image-limit = لم يتم تحميل الصورة البعيدة "__ocs_fmt_0__": إنها تتجاوز حد 64 MB للصور البعيدة.
.remote-image-url-could-not-be-fetched-the-request-timed-out = تعذر جلب الصورة البعيدة "__ocs_fmt_0__": انتهت مهلة الطلب.
.enter-a-valid-cell-range-such-as-a1-d10 = Enter a valid cell range such as A1:D10.
.named-ranges-require-a-spreadsheet-file = Named ranges require a spreadsheet file.
@@ -2484,6 +2488,7 @@ file =
.pick-the-file-again-to-load-its-contents = اختر الملف مرة أخرى لتحميل محتوياته في المتصفح.
.adding-folders-is-not-available-in-the-browser = إضافة المجلدات غير متاحة في المتصفح. أضف ملفات الرسومات بدلاً من ذلك.
.drawing-not-loaded-in-the-browser = رسم غير محمّل في المتصفح: __ocs_fmt_0__.
.image-over-the-decode-limit-cannot-be-attached = "__ocs_fmt_0__" بأبعاد __ocs_fmt_1__×__ocs_fmt_2__ بكسل ويتطلب فك ترميزه __ocs_fmt_3__ ميغابايت، وهو فوق حد __ocs_fmt_4__ ميغابايت للصور في هذا الإصدار — لا يمكن إرفاقه. تُرفَق الصور ضمن الحد تلقائيًا بدقة عرض مخفَّضة عند الحاجة.
selection =
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ حدد الكائنات (__ocs_arg_count__ محدد، Enter للتطبيق):
+5
View File
@@ -1896,6 +1896,10 @@ common =
.geometric = Геометрични
.dimensional = Размерни
.show-hide = Показване/скриване
.remote-image-url-could-not-be-fetched-the-network-request-failed-offline-dns-or-cors = Отдалеченото изображение "__ocs_fmt_0__" не можа да бъде извлечено: мрежовата заявка се провали (без връзка, DNS или CORS).
.remote-image-url-could-not-be-fetched-the-server-returned-http-status = Отдалеченото изображение "__ocs_fmt_0__" не можа да бъде извлечено: сървърът върна HTTP __ocs_fmt_1__.
.remote-image-url-was-not-loaded-it-exceeds-the-64-mb-remote-image-limit = Отдалеченото изображение "__ocs_fmt_0__" не беше заредено: надвишава ограничението от 64 MB за отдалечени изображения.
.remote-image-url-could-not-be-fetched-the-request-timed-out = Отдалеченото изображение "__ocs_fmt_0__" не можа да бъде извлечено: заявката надвиши времето за изчакване.
.enter-a-valid-cell-range-such-as-a1-d10 = Enter a valid cell range such as A1:D10.
.named-ranges-require-a-spreadsheet-file = Named ranges require a spreadsheet file.
@@ -2471,6 +2475,7 @@ file =
.pick-the-file-again-to-load-its-contents = Изберете файла отново, за да заредите съдържанието му в браузъра.
.adding-folders-is-not-available-in-the-browser = Добавянето на папки не е налично в браузъра. Вместо това добавете файлове с чертежи.
.drawing-not-loaded-in-the-browser = Чертежът не е зареден в браузъра: __ocs_fmt_0__.
.image-over-the-decode-limit-cannot-be-attached = "__ocs_fmt_0__" е __ocs_fmt_1__×__ocs_fmt_2__ px и при декодиране би заемала __ocs_fmt_3__ MB — над __ocs_fmt_4__ MB лимита за изображения на тази версия, затова не може да бъде прикачена. Изображенията в рамките на лимита се прикачват автоматично с намалена разделителна способност при нужда.
selection =
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Избери обекти (__ocs_arg_count__ избрани, Enter за прилагане):
+5
View File
@@ -1896,6 +1896,10 @@ common =
.geometric = Geometrické
.dimensional = Rozměrové
.show-hide = Zobrazit/skrýt
.remote-image-url-could-not-be-fetched-the-network-request-failed-offline-dns-or-cors = Vzdálený obrázek "__ocs_fmt_0__" se nepodařilo stáhnout: síťový požadavek selhal (offline, DNS nebo CORS).
.remote-image-url-could-not-be-fetched-the-server-returned-http-status = Vzdálený obrázek "__ocs_fmt_0__" se nepodařilo stáhnout: server vrátil HTTP __ocs_fmt_1__.
.remote-image-url-was-not-loaded-it-exceeds-the-64-mb-remote-image-limit = Vzdálený obrázek "__ocs_fmt_0__" nebyl načten: překračuje limit 64 MB pro vzdálené obrázky.
.remote-image-url-could-not-be-fetched-the-request-timed-out = Vzdálený obrázek "__ocs_fmt_0__" se nepodařilo stáhnout: vypršel časový limit požadavku.
.enter-a-valid-cell-range-such-as-a1-d10 = Enter a valid cell range such as A1:D10.
.named-ranges-require-a-spreadsheet-file = Named ranges require a spreadsheet file.
@@ -2471,6 +2475,7 @@ file =
.pick-the-file-again-to-load-its-contents = Vyberte soubor znovu, abyste načetli jeho obsah do prohlížeče.
.adding-folders-is-not-available-in-the-browser = Přidávání složek není v prohlížeči k dispozici. Místo toho přidejte soubory výkresů.
.drawing-not-loaded-in-the-browser = Výkres není v prohlížeči načten: __ocs_fmt_0__.
.image-over-the-decode-limit-cannot-be-attached = "__ocs_fmt_0__" má __ocs_fmt_1__×__ocs_fmt_2__ px a při dekódování by zabrala __ocs_fmt_3__ MB, přes limit __ocs_fmt_4__ MB pro obrázky v této verzi — nelze ji připojit. Obrázky v rámci limitu se podle potřeby připojují automaticky se sníženým rozlišením zobrazení.
selection =
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Vyberte objekty (vybráno __ocs_arg_count__, použít Enter):
+5
View File
@@ -1897,6 +1897,10 @@ common =
.geometric = Geometrisch
.dimensional = Bemaßung
.show-hide = Anzeigen/Ausblenden
.remote-image-url-could-not-be-fetched-the-network-request-failed-offline-dns-or-cors = Remote-Bild „__ocs_fmt_0__“ konnte nicht abgerufen werden: die Netzwerkanfrage ist fehlgeschlagen (offline, DNS oder CORS).
.remote-image-url-could-not-be-fetched-the-server-returned-http-status = Remote-Bild „__ocs_fmt_0__“ konnte nicht abgerufen werden: der Server antwortete mit HTTP __ocs_fmt_1__.
.remote-image-url-was-not-loaded-it-exceeds-the-64-mb-remote-image-limit = Remote-Bild „__ocs_fmt_0__“ wurde nicht geladen: es überschreitet das Limit von 64 MB für Remote-Bilder.
.remote-image-url-could-not-be-fetched-the-request-timed-out = Remote-Bild „__ocs_fmt_0__“ konnte nicht abgerufen werden: die Anfrage hat das Zeitlimit überschritten.
.enter-a-valid-cell-range-such-as-a1-d10 = Enter a valid cell range such as A1:D10.
.named-ranges-require-a-spreadsheet-file = Named ranges require a spreadsheet file.
@@ -2472,6 +2476,7 @@ file =
.pick-the-file-again-to-load-its-contents = Wähle die Datei erneut aus, um ihren Inhalt in den Browser zu laden.
.adding-folders-is-not-available-in-the-browser = Das Hinzufügen von Ordnern ist im Browser nicht verfügbar. Füge stattdessen Zeichnungsdateien hinzu.
.drawing-not-loaded-in-the-browser = Zeichnung ist nicht im Browser geladen: __ocs_fmt_0__.
.image-over-the-decode-limit-cannot-be-attached = "__ocs_fmt_0__" hat __ocs_fmt_1__×__ocs_fmt_2__ px und würde beim Dekodieren __ocs_fmt_3__ MB belegen, über dem __ocs_fmt_4__ MB Bildlimit dieses Builds — es kann nicht angehängt werden. Bilder innerhalb des Limits werden bei Bedarf automatisch mit reduzierter Anzeigeanflösung angehängt.
selection =
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Objekte auswählen (__ocs_arg_count__ ausgewählt, Eingabe zum Anwenden):
+5
View File
@@ -1896,6 +1896,10 @@ common =
.geometric = Γεωμετρικοί
.dimensional = Διαστατικοί
.show-hide = Εμφάνιση/Απόκρυψη
.remote-image-url-could-not-be-fetched-the-network-request-failed-offline-dns-or-cors = Η απομακρυσμένη εικόνα "__ocs_fmt_0__" δεν μπόρεσε να ανακτηθεί: το αίτημα δικτύου απέτυχε (εκτός σύνδεσης, DNS ή CORS).
.remote-image-url-could-not-be-fetched-the-server-returned-http-status = Η απομακρυσμένη εικόνα "__ocs_fmt_0__" δεν μπόρεσε να ανακτηθεί: ο διακομιστής επέστρεψε HTTP __ocs_fmt_1__.
.remote-image-url-was-not-loaded-it-exceeds-the-64-mb-remote-image-limit = Η απομακρυσμένη εικόνα "__ocs_fmt_0__" δεν φορτώθηκε: υπερβαίνει το όριο των 64 MB για απομακρυσμένες εικόνες.
.remote-image-url-could-not-be-fetched-the-request-timed-out = Η απομακρυσμένη εικόνα "__ocs_fmt_0__" δεν μπόρεσε να ανακτηθεί: το αίτημα έληξε χρονικά.
.enter-a-valid-cell-range-such-as-a1-d10 = Enter a valid cell range such as A1:D10.
.named-ranges-require-a-spreadsheet-file = Named ranges require a spreadsheet file.
@@ -2471,6 +2475,7 @@ file =
.pick-the-file-again-to-load-its-contents = Επιλέξτε ξανά το αρχείο για να φορτώσετε το περιεχόμενό του στο πρόγραμμα περιήγησης.
.adding-folders-is-not-available-in-the-browser = Η προσθήκη φακέλων δεν είναι διαθέσιμη στο πρόγραμμα περιήγησης. Προσθέστε αρχεία σχεδίων αντί για αυτό.
.drawing-not-loaded-in-the-browser = Το σχέδιο δεν έχει φορτωθεί στο πρόγραμμα περιήγησης: __ocs_fmt_0__.
.image-over-the-decode-limit-cannot-be-attached = Το "__ocs_fmt_0__" είναι __ocs_fmt_1__×__ocs_fmt_2__ px και η αποκωδικοποίησή του θα απαιτούσε __ocs_fmt_3__ MB, πάνω από το όριο __ocs_fmt_4__ MB για εικόνες σε αυτή την έκδοση — δεν μπορεί να επισυναφθεί. Οι εικόνες εντός του ορίου επισυνάπτονται κατά ανάγκη αυτόματα σε μειωμένη ανάλυση εμφάνισης.
selection =
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Επιλέξτε αντικείμενα (__ocs_arg_count__ επιλεγμένα, Enter για εφαρμογή):
+5
View File
@@ -1897,6 +1897,10 @@ common =
.geometric = Geometric
.dimensional = Dimensional
.show-hide = Show/Hide
.remote-image-url-could-not-be-fetched-the-network-request-failed-offline-dns-or-cors = Remote image "__ocs_fmt_0__" could not be fetched: the network request failed (offline, DNS, or CORS).
.remote-image-url-could-not-be-fetched-the-server-returned-http-status = Remote image "__ocs_fmt_0__" could not be fetched: the server returned HTTP __ocs_fmt_1__.
.remote-image-url-was-not-loaded-it-exceeds-the-64-mb-remote-image-limit = Remote image "__ocs_fmt_0__" was not loaded: it exceeds the 64 MB remote-image limit.
.remote-image-url-could-not-be-fetched-the-request-timed-out = Remote image "__ocs_fmt_0__" could not be fetched: the request timed out.
.enter-a-valid-cell-range-such-as-a1-d10 = Enter a valid cell range such as A1:D10.
.named-ranges-require-a-spreadsheet-file = Named ranges require a spreadsheet file.
@@ -2472,6 +2476,7 @@ file =
.pick-the-file-again-to-load-its-contents = Pick the file again to load its contents in the browser.
.adding-folders-is-not-available-in-the-browser = Adding folders is not available in the browser. Add drawing files instead.
.drawing-not-loaded-in-the-browser = Drawing not loaded in the browser: __ocs_fmt_0__.
.image-over-the-decode-limit-cannot-be-attached = "__ocs_fmt_0__" is __ocs_fmt_1__×__ocs_fmt_2__ px and would decode to __ocs_fmt_3__ MB, over the __ocs_fmt_4__ MB image limit of this build — it cannot be attached. Images within the limit are attached at a reduced display resolution when needed.
selection =
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Select objects (__ocs_arg_count__ selected, Enter to apply):
+5
View File
@@ -1897,6 +1897,10 @@ common =
.geometric = Geométricas
.dimensional = Dimensionales
.show-hide = Mostrar/Ocultar
.remote-image-url-could-not-be-fetched-the-network-request-failed-offline-dns-or-cors = La imagen remota «__ocs_fmt_0__» no se pudo descargar: la solicitud de red falló (sin conexión, DNS o CORS).
.remote-image-url-could-not-be-fetched-the-server-returned-http-status = La imagen remota «__ocs_fmt_0__» no se pudo descargar: el servidor devolvió HTTP __ocs_fmt_1__.
.remote-image-url-was-not-loaded-it-exceeds-the-64-mb-remote-image-limit = La imagen remota «__ocs_fmt_0__» no se cargó: supera el límite de 64 MB para imágenes remotas.
.remote-image-url-could-not-be-fetched-the-request-timed-out = La imagen remota «__ocs_fmt_0__» no se pudo descargar: la solicitud agotó el tiempo de espera.
.enter-a-valid-cell-range-such-as-a1-d10 = Enter a valid cell range such as A1:D10.
.named-ranges-require-a-spreadsheet-file = Named ranges require a spreadsheet file.
@@ -2472,6 +2476,7 @@ file =
.pick-the-file-again-to-load-its-contents = Vuelve a elegir el archivo para cargar su contenido en el navegador.
.adding-folders-is-not-available-in-the-browser = Añadir carpetas no está disponible en el navegador. Añade archivos de dibujos en su lugar.
.drawing-not-loaded-in-the-browser = Dibujo no cargado en el navegador: __ocs_fmt_0__.
.image-over-the-decode-limit-cannot-be-attached = "__ocs_fmt_0__" tiene __ocs_fmt_1__×__ocs_fmt_2__ px y al decodificarse ocuparía __ocs_fmt_3__ MB, por encima del límite de __ocs_fmt_4__ MB por imagen de esta versión; no se puede adjuntar. Las imágenes dentro del límite se adjuntan automáticamente con una resolución de visualización reducida cuando es necesario.
selection =
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Seleccione objetos (__ocs_arg_count__ seleccionados, Enter para aplicar):
+5
View File
@@ -1896,6 +1896,10 @@ common =
.geometric = Geometriset
.dimensional = Mitalliset
.show-hide = Näytä/Piilota
.remote-image-url-could-not-be-fetched-the-network-request-failed-offline-dns-or-cors = Etäkuvaa "__ocs_fmt_0__" ei voitu noutaa: verkkopyyntö epäonnistui (ei verkkoyhteyttä, DNS tai CORS).
.remote-image-url-could-not-be-fetched-the-server-returned-http-status = Etäkuvaa "__ocs_fmt_0__" ei voitu noutaa: palvelin palautti HTTP __ocs_fmt_1__.
.remote-image-url-was-not-loaded-it-exceeds-the-64-mb-remote-image-limit = Etäkuvaa "__ocs_fmt_0__" ei ladattu: se ylittää etäkuvien 64 MB:n rajan.
.remote-image-url-could-not-be-fetched-the-request-timed-out = Etäkuvaa "__ocs_fmt_0__" ei voitu noutaa: pyyntö aikakatkesi.
.enter-a-valid-cell-range-such-as-a1-d10 = Enter a valid cell range such as A1:D10.
.named-ranges-require-a-spreadsheet-file = Named ranges require a spreadsheet file.
@@ -2471,6 +2475,7 @@ file =
.pick-the-file-again-to-load-its-contents = Valitse tiedosto uudelleen lataaksesi sen sisällön selaimeen.
.adding-folders-is-not-available-in-the-browser = Kansioiden lisääminen ei ole käytettävissä selaimessa. Lisää sen sijaan piirustustiedostoja.
.drawing-not-loaded-in-the-browser = Piirustusta ei ole ladattu selaimeen: __ocs_fmt_0__.
.image-over-the-decode-limit-cannot-be-attached = "__ocs_fmt_0__" on __ocs_fmt_1__×__ocs_fmt_2__ px ja sen purkaminen veisi __ocs_fmt_3__ MB, yli tämän version __ocs_fmt_4__ MB:n kuvarajan — sitä ei voi liittää. Rajan sisällä olevat kuvat liitetään tarvittaessa automaattisesti pienemmällä näyttötarkkuudella.
selection =
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Valitse objektit (__ocs_arg_count__ valittu, Enter käytettäväksi):
+5
View File
@@ -1897,6 +1897,10 @@ common =
.geometric = Géométriques
.dimensional = Dimensionnelles
.show-hide = Afficher/Masquer
.remote-image-url-could-not-be-fetched-the-network-request-failed-offline-dns-or-cors = L’image distante «__ocs_fmt_0__» n’a pas pu être récupérée : la requête réseau a échoué (hors ligne, DNS ou CORS).
.remote-image-url-could-not-be-fetched-the-server-returned-http-status = L’image distante «__ocs_fmt_0__» n’a pas pu être récupérée : le serveur a renvoyé HTTP __ocs_fmt_1__.
.remote-image-url-was-not-loaded-it-exceeds-the-64-mb-remote-image-limit = L’image distante «__ocs_fmt_0__» n’a pas été chargée : elle dépasse la limite de 64 Mo pour les images distantes.
.remote-image-url-could-not-be-fetched-the-request-timed-out = L’image distante «__ocs_fmt_0__» n’a pas pu être récupérée : la requête a expiré.
.enter-a-valid-cell-range-such-as-a1-d10 = Enter a valid cell range such as A1:D10.
.named-ranges-require-a-spreadsheet-file = Named ranges require a spreadsheet file.
@@ -2472,6 +2476,7 @@ file =
.pick-the-file-again-to-load-its-contents = Choisissez de nouveau le fichier pour charger son contenu dans le navigateur.
.adding-folders-is-not-available-in-the-browser = L'ajout de dossiers n'est pas disponible dans le navigateur. Ajoutez plutôt des fichiers de dessins.
.drawing-not-loaded-in-the-browser = Dessin non chargé dans le navigateur : __ocs_fmt_0__.
.image-over-the-decode-limit-cannot-be-attached = "__ocs_fmt_0__" fait __ocs_fmt_1__×__ocs_fmt_2__ px et son décodage nécessiterait __ocs_fmt_3__ Mo, au-delà de la limite de __ocs_fmt_4__ Mo par image de cette version — elle ne peut pas être jointe. Les images dans la limite sont jointes automatiquement à une résolution d'affichage réduite si nécessaire.
selection =
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Sélectionner les objets (__ocs_arg_count__ sélectionnés, Entrez pour appliquer) :
+5
View File
@@ -1885,6 +1885,10 @@ common =
.geometric = ज्यामितीय
.dimensional = आयामी
.show-hide = दिखाएँ/छिपाएँ
.remote-image-url-could-not-be-fetched-the-network-request-failed-offline-dns-or-cors = रिमोट इमेज "__ocs_fmt_0__" लाया नहीं जा सका: नेटवर्क अनुरोध विफल हो गया (ऑफ़लाइन, DNS या CORS)।
.remote-image-url-could-not-be-fetched-the-server-returned-http-status = रिमोट इमेज "__ocs_fmt_0__" लाया नहीं जा सका: सर्वर ने HTTP __ocs_fmt_1__ लौटाया।
.remote-image-url-was-not-loaded-it-exceeds-the-64-mb-remote-image-limit = रिमोट इमेज "__ocs_fmt_0__" लोड नहीं किया गया: यह रिमोट इमेज की 64 MB सीमा से बड़ा है।
.remote-image-url-could-not-be-fetched-the-request-timed-out = रिमोट इमेज "__ocs_fmt_0__" लाया नहीं जा सका: अनुरोध का समय समाप्त हो गया।
.enter-a-valid-cell-range-such-as-a1-d10 = Enter a valid cell range such as A1:D10.
.named-ranges-require-a-spreadsheet-file = Named ranges require a spreadsheet file.
@@ -2460,6 +2464,7 @@ file =
.pick-the-file-again-to-load-its-contents = ब्राउज़र में इसकी सामग्री लोड करने के लिए फ़ाइल को फिर से चुनें।
.adding-folders-is-not-available-in-the-browser = ब्राउज़र में फ़ोल्डर जोड़ना उपलब्ध नहीं है। इसके बजाय ड्रॉइंग फ़ाइलें जोड़ें।
.drawing-not-loaded-in-the-browser = ड्रॉइंग ब्राउज़र में लोड नहीं है: __ocs_fmt_0__।
.image-over-the-decode-limit-cannot-be-attached = "__ocs_fmt_0__" __ocs_fmt_1__×__ocs_fmt_2__ px है और इसे डिकोड करने पर __ocs_fmt_3__ MB लगेगा, जो इस बिल्ड की __ocs_fmt_4__ MB छवि सीमा से अधिक है — इसे संलग्न नहीं किया जा सकता। सीमा के भीतर की छवियाँ आवश्यकता पड़ने पर स्वचालित रूप से घटी हुई डिस्प्ले रिज़ॉल्यूशन पर संलग्न की जाती हैं।
selection =
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ ऑब्जेक्ट चुनें (__ocs_arg_count__ चयनित, लागू करने के लिए Enter दबाएँ):
+5
View File
@@ -1910,6 +1910,10 @@ common =
.geometric = Geometriai
.dimensional = Méretkényszerek
.show-hide = Megjelenítés/elrejtés
.remote-image-url-could-not-be-fetched-the-network-request-failed-offline-dns-or-cors = A(z) "__ocs_fmt_0__" távoli kép nem tölthető le: a hálózati kérés sikertelen (kapcsolat nélkül, DNS vagy CORS).
.remote-image-url-could-not-be-fetched-the-server-returned-http-status = A(z) "__ocs_fmt_0__" távoli kép nem tölthető le: a kiszolgáló HTTP __ocs_fmt_1__ választ adott.
.remote-image-url-was-not-loaded-it-exceeds-the-64-mb-remote-image-limit = A(z) "__ocs_fmt_0__" távoli kép nem lett betöltve: meghaladja a távoli képek 64 MB-os korlátját.
.remote-image-url-could-not-be-fetched-the-request-timed-out = A(z) "__ocs_fmt_0__" távoli kép nem tölthető le: a kérés túllépte az időkorlátot.
.enter-a-valid-cell-range-such-as-a1-d10 = Adjon meg érvényes cellatartományt, például A1:D10.
.named-ranges-require-a-spreadsheet-file = Elnevezett tartományokhoz táblázatkezelő-fájl szükséges.
@@ -2484,6 +2488,7 @@ file =
.pick-the-file-again-to-load-its-contents = Válaszd ki újra a fájlt a tartalmának böngészőbe töltéséhez.
.adding-folders-is-not-available-in-the-browser = Mappák hozzáadása nem érhető el a böngészőben. Ehelyett rajzfájlokat adj hozzá.
.drawing-not-loaded-in-the-browser = A raj nincs betöltve a böngészőben: __ocs_fmt_0__.
.image-over-the-decode-limit-cannot-be-attached = A(z) "__ocs_fmt_0__" __ocs_fmt_1__×__ocs_fmt_2__ px felbontású, és a dekódolása __ocs_fmt_3__ MB-ot igényelne — ez meghaladja e build __ocs_fmt_4__ MB-os képhatárát, ezért nem csatolható. A határon belüli képek szükség esetén automatikusan csökkentett megjelenítési felbontással csatolódnak.
selection =
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Válasszon objektumokat (__ocs_arg_count__ kijelölve, Enter az alkalmazáshoz):
+5
View File
@@ -1896,6 +1896,10 @@ common =
.geometric = Geometrici
.dimensional = Dimensionali
.show-hide = Mostra/Nascondi
.remote-image-url-could-not-be-fetched-the-network-request-failed-offline-dns-or-cors = L'immagine remota "__ocs_fmt_0__" non è stata scaricata: la richiesta di rete non è riuscita (offline, DNS o CORS).
.remote-image-url-could-not-be-fetched-the-server-returned-http-status = L'immagine remota "__ocs_fmt_0__" non è stata scaricata: il server ha restituito HTTP __ocs_fmt_1__.
.remote-image-url-was-not-loaded-it-exceeds-the-64-mb-remote-image-limit = L'immagine remota "__ocs_fmt_0__" non è stata caricata: supera il limite di 64 MB per le immagini remote.
.remote-image-url-could-not-be-fetched-the-request-timed-out = L'immagine remota "__ocs_fmt_0__" non è stata scaricata: la richiesta è scaduta.
.enter-a-valid-cell-range-such-as-a1-d10 = Indica un intervallo valido, come A1:D10.
.named-ranges-require-a-spreadsheet-file = Intervalli denominati richiedono un foglio elettronico.
@@ -2471,6 +2475,7 @@ file =
.pick-the-file-again-to-load-its-contents = Scegli di nuovo il file per caricarne il contenuto nel browser.
.adding-folders-is-not-available-in-the-browser = L'aggiunta di cartelle non è disponibile nel browser. Aggiungi invece file di disegni.
.drawing-not-loaded-in-the-browser = Disegno non caricato nel browser: __ocs_fmt_0__.
.image-over-the-decode-limit-cannot-be-attached = "__ocs_fmt_0__" è di __ocs_fmt_1__×__ocs_fmt_2__ px e la decodifica richiederebbe __ocs_fmt_3__ MB, oltre il limite di __ocs_fmt_4__ MB per immagine di questa build: non può essere allegata. Le immagini entro il limite vengono allegate automaticamente a risoluzione di visualizzazione ridotta quando necessario.
selection =
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Seleziona oggetti (__ocs_arg_count__ selezionato, Enter da applicare):
+5
View File
@@ -1887,6 +1887,10 @@ common =
.geometric = 幾何拘束
.dimensional = 寸法拘束
.show-hide = 表示/非表示
.remote-image-url-could-not-be-fetched-the-network-request-failed-offline-dns-or-cors = リモート画像 "__ocs_fmt_0__" を取得できませんでした: ネットワークリクエストが失敗しました(オフライン、DNS、または CORS)。
.remote-image-url-could-not-be-fetched-the-server-returned-http-status = リモート画像 "__ocs_fmt_0__" を取得できませんでした: サーバーが HTTP __ocs_fmt_1__ を返しました。
.remote-image-url-was-not-loaded-it-exceeds-the-64-mb-remote-image-limit = リモート画像 "__ocs_fmt_0__" は読み込まれませんでした: リモート画像の 64 MB 上限を超えています。
.remote-image-url-could-not-be-fetched-the-request-timed-out = リモート画像 "__ocs_fmt_0__" を取得できませんでした: リクエストがタイムアウトしました。
.enter-a-valid-cell-range-such-as-a1-d10 = Enter a valid cell range such as A1:D10.
.named-ranges-require-a-spreadsheet-file = Named ranges require a spreadsheet file.
@@ -2462,6 +2466,7 @@ file =
.pick-the-file-again-to-load-its-contents = ブラウザーに内容を読み込むには、ファイルをもう一度選択してください。
.adding-folders-is-not-available-in-the-browser = ブラウザーではフォルダーを追加できません。代わりに図面ファイルを追加してください。
.drawing-not-loaded-in-the-browser = 図面がブラウザーに読み込まれていません: __ocs_fmt_0__。
.image-over-the-decode-limit-cannot-be-attached = "__ocs_fmt_0__" は __ocs_fmt_1__×__ocs_fmt_2__ px で、デコードすると __ocs_fmt_3__ MB 必要となり、このビルドの __ocs_fmt_4__ MB の画像上限を超えるため添付できません。上限内の画像は、必要に応じて自動的に低下させた表示解像度で添付されます。
selection =
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ オブジェクトを選択(__ocs_arg_count__ 個選択、Enter で適用):
+5
View File
@@ -1896,6 +1896,10 @@ common =
.geometric = 기하 구속
.dimensional = 치수 구속
.show-hide = 표시/숨기기
.remote-image-url-could-not-be-fetched-the-network-request-failed-offline-dns-or-cors = 원격 이미지 "__ocs_fmt_0__"을(를) 가져올 수 없습니다: 네트워크 요청이 실패했습니다(오프라인, DNS 또는 CORS).
.remote-image-url-could-not-be-fetched-the-server-returned-http-status = 원격 이미지 "__ocs_fmt_0__"을(를) 가져올 수 없습니다: 서버가 HTTP __ocs_fmt_1__을(를) 반환했습니다.
.remote-image-url-was-not-loaded-it-exceeds-the-64-mb-remote-image-limit = 원격 이미지 "__ocs_fmt_0__"이(가) 로드되지 않았습니다: 원격 이미지 64 MB 제한을 초과합니다.
.remote-image-url-could-not-be-fetched-the-request-timed-out = 원격 이미지 "__ocs_fmt_0__"을(를) 가져올 수 없습니다: 요청 시간이 초과되었습니다.
.enter-a-valid-cell-range-such-as-a1-d10 = Enter a valid cell range such as A1:D10.
.named-ranges-require-a-spreadsheet-file = Named ranges require a spreadsheet file.
@@ -2471,6 +2475,7 @@ file =
.pick-the-file-again-to-load-its-contents = 콘텐츠를 브라우저에 로드하려면 파일을 다시 선택하세요.
.adding-folders-is-not-available-in-the-browser = 브라우저에서는 폴더를 추가할 수 없습니다. 대신 도면 파일을 추가하세요.
.drawing-not-loaded-in-the-browser = 도면이 브라우저에 로드되지 않았습니다: __ocs_fmt_0__.
.image-over-the-decode-limit-cannot-be-attached = "__ocs_fmt_0__"은(는) __ocs_fmt_1__×__ocs_fmt_2__ px이며 디코딩 시 __ocs_fmt_3__ MB가 필요해 이 빌드의 __ocs_fmt_4__ MB 이미지 한도를 초과하므로 첨부할 수 없습니다. 한도 이내의 이미지는 필요할 때 자동으로 낮춘 표시 해상도로 첨부됩니다.
selection =
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ 객체 선택(__ocs_arg_count__ 선택, Enter 적용):
+5
View File
@@ -1897,6 +1897,10 @@ common =
.geometric = Geometrisch
.dimensional = Maatvoering
.show-hide = Tonen/Verbergen
.remote-image-url-could-not-be-fetched-the-network-request-failed-offline-dns-or-cors = Externe afbeelding "__ocs_fmt_0__" kon niet worden opgehaald: het netwerkverzoek is mislukt (offline, DNS of CORS).
.remote-image-url-could-not-be-fetched-the-server-returned-http-status = Externe afbeelding "__ocs_fmt_0__" kon niet worden opgehaald: de server gaf HTTP __ocs_fmt_1__ terug.
.remote-image-url-was-not-loaded-it-exceeds-the-64-mb-remote-image-limit = Externe afbeelding "__ocs_fmt_0__" is niet geladen: deze overschrijdt de limiet van 64 MB voor externe afbeeldingen.
.remote-image-url-could-not-be-fetched-the-request-timed-out = Externe afbeelding "__ocs_fmt_0__" kon niet worden opgehaald: het verzoek is verlopen.
.enter-a-valid-cell-range-such-as-a1-d10 = Enter a valid cell range such as A1:D10.
.named-ranges-require-a-spreadsheet-file = Named ranges require a spreadsheet file.
@@ -2472,6 +2476,7 @@ file =
.pick-the-file-again-to-load-its-contents = Kies het bestand opnieuw om de inhoud ervan in de browser te laden.
.adding-folders-is-not-available-in-the-browser = Mappen toevoegen is niet beschikbaar in de browser. Voeg in plaats daarvan tekeningbestanden toe.
.drawing-not-loaded-in-the-browser = Tekening is niet geladen in de browser: __ocs_fmt_0__.
.image-over-the-decode-limit-cannot-be-attached = "__ocs_fmt_0__" is __ocs_fmt_1__×__ocs_fmt_2__ px en zou bij het decoderen __ocs_fmt_3__ MB nodig hebben, boven de __ocs_fmt_4__ MB afbeeldingslimiet van deze build — het kan niet worden bijgevoegd. Afbeeldingen binnen de limiet worden zo nodig automatisch met een verlaagde weergaveresolutie bijgevoegd.
selection =
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Selecteer objecten (__ocs_arg_count__ geselecteerd, Voer in om toe te passen):
+5
View File
@@ -1896,6 +1896,10 @@ common =
.geometric = Geometryczne
.dimensional = Wymiarowe
.show-hide = Pokaż/Ukryj
.remote-image-url-could-not-be-fetched-the-network-request-failed-offline-dns-or-cors = Nie można pobrać zdalnego obrazu „__ocs_fmt_0__”: żądanie sieciowe nie powiodło się (brak połączenia, DNS lub CORS).
.remote-image-url-could-not-be-fetched-the-server-returned-http-status = Nie można pobrać zdalnego obrazu „__ocs_fmt_0__”: serwer zwrócił HTTP __ocs_fmt_1__.
.remote-image-url-was-not-loaded-it-exceeds-the-64-mb-remote-image-limit = Zdalny obraz „__ocs_fmt_0__” nie został wczytany: przekracza limit 64 MB dla zdalnych obrazów.
.remote-image-url-could-not-be-fetched-the-request-timed-out = Nie można pobrać zdalnego obrazu „__ocs_fmt_0__”: upłynął limit czasu żądania.
.enter-a-valid-cell-range-such-as-a1-d10 = Enter a valid cell range such as A1:D10.
.named-ranges-require-a-spreadsheet-file = Named ranges require a spreadsheet file.
@@ -2471,6 +2475,7 @@ file =
.pick-the-file-again-to-load-its-contents = Wybierz plik ponownie, aby załadować jego zawartość do przeglądarki.
.adding-folders-is-not-available-in-the-browser = Dodawanie folderów nie jest dostępne w przeglądarce. Zamiast tego dodaj pliki rysunków.
.drawing-not-loaded-in-the-browser = Rysunek nie został załadowany w przeglądarce: __ocs_fmt_0__.
.image-over-the-decode-limit-cannot-be-attached = "__ocs_fmt_0__" ma __ocs_fmt_1__×__ocs_fmt_2__ px, a jej dekodowanie wymagałoby __ocs_fmt_3__ MB — ponad __ocs_fmt_4__ MB limit obrazu w tej wersji — więc nie może być dołączona. Obrazy w ramach limitu są w razie potrzeby dołączane automatycznie w obniżonej rozdzielczości wyświetlania.
selection =
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Wybierz obiekty (wybrano __ocs_arg_count__, zastosowano Enter):
+5
View File
@@ -1897,6 +1897,10 @@ common =
.geometric = Geométricas
.dimensional = Dimensionais
.show-hide = Mostrar/Ocultar
.remote-image-url-could-not-be-fetched-the-network-request-failed-offline-dns-or-cors = A imagem remota "__ocs_fmt_0__" não pôde ser baixada: a requisição de rede falhou (offline, DNS ou CORS).
.remote-image-url-could-not-be-fetched-the-server-returned-http-status = A imagem remota "__ocs_fmt_0__" não pôde ser baixada: o servidor retornou HTTP __ocs_fmt_1__.
.remote-image-url-was-not-loaded-it-exceeds-the-64-mb-remote-image-limit = A imagem remota "__ocs_fmt_0__" não foi carregada: ela excede o limite de 64 MB para imagens remotas.
.remote-image-url-could-not-be-fetched-the-request-timed-out = A imagem remota "__ocs_fmt_0__" não pôde ser baixada: a requisição expirou.
.enter-a-valid-cell-range-such-as-a1-d10 = Enter a valid cell range such as A1:D10.
.named-ranges-require-a-spreadsheet-file = Named ranges require a spreadsheet file.
@@ -2472,6 +2476,7 @@ file =
.pick-the-file-again-to-load-its-contents = Escolha o arquivo novamente para carregar o conteúdo dele no navegador.
.adding-folders-is-not-available-in-the-browser = Adicionar pastas não está disponível no navegador. Em vez disso, adicione arquivos de desenhos.
.drawing-not-loaded-in-the-browser = Desenho não carregado no navegador: __ocs_fmt_0__.
.image-over-the-decode-limit-cannot-be-attached = "__ocs_fmt_0__" tem __ocs_fmt_1__×__ocs_fmt_2__ px e a decodificação exigiria __ocs_fmt_3__ MB, acima do limite de __ocs_fmt_4__ MB por imagem desta versão — não é possível anexá-la. Imagens dentro do limite são anexadas automaticamente com resolução de exibição reduzida quando necessário.
selection =
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Selecione objetos (__ocs_arg_count__ selecionados, Enter para aplicar):
+5
View File
@@ -1900,6 +1900,10 @@ common =
.geometric = Геометрические
.dimensional = Размерные
.show-hide = Показать/Скрыть
.remote-image-url-could-not-be-fetched-the-network-request-failed-offline-dns-or-cors = Не удалось загрузить удалённое изображение «__ocs_fmt_0__»: сетевой запрос завершился ошибкой (нет сети, DNS или CORS).
.remote-image-url-could-not-be-fetched-the-server-returned-http-status = Не удалось загрузить удалённое изображение «__ocs_fmt_0__»: сервер вернул HTTP __ocs_fmt_1__.
.remote-image-url-was-not-loaded-it-exceeds-the-64-mb-remote-image-limit = Удалённое изображение «__ocs_fmt_0__» не загружено: оно превышает лимит 64 МБ для удалённых изображений.
.remote-image-url-could-not-be-fetched-the-request-timed-out = Не удалось загрузить удалённое изображение «__ocs_fmt_0__»: время ожидания запроса истекло.
.enter-a-valid-cell-range-such-as-a1-d10 = Enter a valid cell range such as A1:D10.
.named-ranges-require-a-spreadsheet-file = Named ranges require a spreadsheet file.
@@ -2475,6 +2479,7 @@ file =
.pick-the-file-again-to-load-its-contents = Выберите файл ещё раз, чтобы загрузить его содержимое в браузер.
.adding-folders-is-not-available-in-the-browser = Добавление папок в браузере недоступно. Вместо этого добавьте файлы чертежей.
.drawing-not-loaded-in-the-browser = Чертёж не загружен в браузере: __ocs_fmt_0__.
.image-over-the-decode-limit-cannot-be-attached = "__ocs_fmt_0__" — __ocs_fmt_1__×__ocs_fmt_2__ px; для декодирования потребовалось бы __ocs_fmt_3__ МБ, что превышает ограничение __ocs_fmt_4__ МБ на изображение в этой сборке, поэтому вложить его нельзя. Изображения в пределах ограничения при необходимости вкладываются автоматически с пониженным разрешением отображения.
selection =
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Выберите объекты (__ocs_arg_count__ Выберите, Введите для применения):
+5
View File
@@ -1881,6 +1881,10 @@ common =
.geometric = Geometrik
.dimensional = Boyutsal
.show-hide = Göster/Gizle
.remote-image-url-could-not-be-fetched-the-network-request-failed-offline-dns-or-cors = "__ocs_fmt_0__" uzak görüntüsü getirilemedi: ağ isteği başarısız oldu (çevrimdışı, DNS veya CORS).
.remote-image-url-could-not-be-fetched-the-server-returned-http-status = "__ocs_fmt_0__" uzak görüntüsü getirilemedi: sunucu HTTP __ocs_fmt_1__ döndürdü.
.remote-image-url-was-not-loaded-it-exceeds-the-64-mb-remote-image-limit = "__ocs_fmt_0__" uzak görüntüsü yüklenmedi: 64 MB uzak görüntü sınırını aşıyor.
.remote-image-url-could-not-be-fetched-the-request-timed-out = "__ocs_fmt_0__" uzak görüntüsü getirilemedi: istek zaman aşımına uğradı.
.enter-a-valid-cell-range-such-as-a1-d10 = Enter a valid cell range such as A1:D10.
.named-ranges-require-a-spreadsheet-file = Named ranges require a spreadsheet file.
@@ -2455,6 +2459,7 @@ file =
.pick-the-file-again-to-load-its-contents = İçeriğini tarayıcıya yüklemek için dosyayı yeniden seçin.
.adding-folders-is-not-available-in-the-browser = Tarayıcıda klasör ekleme kullanılamıyor. Bunun yerine çizim dosyaları ekleyin.
.drawing-not-loaded-in-the-browser = Çizim tarayıcıya yüklenmedi: __ocs_fmt_0__.
.image-over-the-decode-limit-cannot-be-attached = "__ocs_fmt_0__" __ocs_fmt_1__×__ocs_fmt_2__ px boyutunda ve çözümlendiğinde __ocs_fmt_3__ MB gerektirir; bu sürümün __ocs_fmt_4__ MB görsel sınırını aştığından eklenemez. Sınır içindeki görseller gerektiğinde otomatik olarak düşürülmüş görüntü çözünürlüğüyle eklenir.
selection =
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Nesneleri seçin (__ocs_arg_count__ seçili, uygulamak için Enter):
+5
View File
@@ -1884,6 +1884,10 @@ common =
.geometric = 几何
.dimensional = 标注
.show-hide = 显示/隐藏
.remote-image-url-could-not-be-fetched-the-network-request-failed-offline-dns-or-cors = 无法获取远程图像 "__ocs_fmt_0__":网络请求失败(离线、DNS 或 CORS)。
.remote-image-url-could-not-be-fetched-the-server-returned-http-status = 无法获取远程图像 "__ocs_fmt_0__":服务器返回 HTTP __ocs_fmt_1__。
.remote-image-url-was-not-loaded-it-exceeds-the-64-mb-remote-image-limit = 远程图像 "__ocs_fmt_0__" 未加载:超过 64 MB 的远程图像限制。
.remote-image-url-could-not-be-fetched-the-request-timed-out = 无法获取远程图像 "__ocs_fmt_0__":请求超时。
.enter-a-valid-cell-range-such-as-a1-d10 = Enter a valid cell range such as A1:D10.
.named-ranges-require-a-spreadsheet-file = Named ranges require a spreadsheet file.
@@ -2459,6 +2463,7 @@ file =
.pick-the-file-again-to-load-its-contents = 请重新选择文件以将其内容加载到浏览器中。
.adding-folders-is-not-available-in-the-browser = 浏览器中无法添加文件夹。请改为添加图形文件。
.drawing-not-loaded-in-the-browser = 图形未加载到浏览器中:__ocs_fmt_0__。
.image-over-the-decode-limit-cannot-be-attached = "__ocs_fmt_0__"为 __ocs_fmt_1__×__ocs_fmt_2__ 像素,解码需要 __ocs_fmt_3__ MB,超过此版本的 __ocs_fmt_4__ MB 图像上限,因此无法附着。上限内的图像在需要时会自动以降低的显示分辨率附着。
selection =
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ 选择对象(已选择 __ocs_arg_count__ 个,按 Enter 应用):
+5
View File
@@ -1896,6 +1896,10 @@ common =
.geometric = 幾何
.dimensional = 標註
.show-hide = 顯示/隱藏
.remote-image-url-could-not-be-fetched-the-network-request-failed-offline-dns-or-cors = 無法擷取遠端影像 "__ocs_fmt_0__":網路請求失敗(離線、DNS 或 CORS)。
.remote-image-url-could-not-be-fetched-the-server-returned-http-status = 無法擷取遠端影像 "__ocs_fmt_0__":伺服器傳回 HTTP __ocs_fmt_1__。
.remote-image-url-was-not-loaded-it-exceeds-the-64-mb-remote-image-limit = 遠端影像 "__ocs_fmt_0__" 未載入:超過 64 MB 的遠端影像限制。
.remote-image-url-could-not-be-fetched-the-request-timed-out = 無法擷取遠端影像 "__ocs_fmt_0__":請求逾時。
.enter-a-valid-cell-range-such-as-a1-d10 = Enter a valid cell range such as A1:D10.
.named-ranges-require-a-spreadsheet-file = Named ranges require a spreadsheet file.
@@ -2471,6 +2475,7 @@ file =
.pick-the-file-again-to-load-its-contents = 請重新選擇檔案,將其內容載入瀏覽器。
.adding-folders-is-not-available-in-the-browser = 瀏覽器中無法新增資料夾。請改為新增圖面檔案。
.drawing-not-loaded-in-the-browser = 圖面未載入瀏覽器:__ocs_fmt_0__。
.image-over-the-decode-limit-cannot-be-attached = "__ocs_fmt_0__"為 __ocs_fmt_1__×__ocs_fmt_2__ 像素,解碼需要 __ocs_fmt_3__ MB,超過此組建的 __ocs_fmt_4__ MB 影像上限,因此無法貼附。上限內的影像會在需要時自動以降低的顯示解析度貼附。
selection =
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ 選擇物件(已選擇 __ocs_arg_count__ 個,按 Enter 套用):
+49
View File
@@ -698,11 +698,53 @@ impl OpenCADStudio {
);
registered.push(name);
}
// Remote http(s) raster refs the background fetch settled:
// successes already registered their bytes under the URL (the
// fetch task does it), failures surface on the command line —
// a network/CORS block, an HTTP status, the size cap, or the
// deadline reads differently to the user.
use crate::scene::model::image_model::{RemoteFetchError, RemoteFetchOutcome};
let mut fetched: Vec<String> = Vec::new();
while let Some(outcome) =
crate::scene::model::image_model::take_remote_fetch_outcome()
{
match outcome {
RemoteFetchOutcome::Loaded(url) => fetched.push(url),
RemoteFetchOutcome::Failed(url, reason) => {
let note = match reason {
RemoteFetchError::Network => crate::tf!(
"Remote image \"{url}\" could not be fetched: the network request failed (offline, DNS, or CORS)."
)
.into_owned(),
RemoteFetchError::Status(status) => crate::tf!(
"Remote image \"{url}\" could not be fetched: the server returned HTTP {status}."
)
.into_owned(),
RemoteFetchError::TooLarge => crate::tf!(
"Remote image \"{url}\" was not loaded: it exceeds the 64 MB remote-image limit."
)
.into_owned(),
RemoteFetchError::Timeout => crate::tf!(
"Remote image \"{url}\" could not be fetched: the request timed out."
)
.into_owned(),
};
self.command_line.push_error(&note);
}
}
}
registered.extend(fetched);
if !registered.is_empty() {
for tab in self.tabs.iter_mut() {
tab.scene.resolve_registered_image_names(&registered);
}
}
// Over-budget decode refusals from this poll's re-resolves
// reach the command line; the affected images keep their
// broken-reference placeholders.
for note in crate::scene::model::image_model::take_decode_refusals() {
self.command_line.push_error(&note);
}
Task::batch([control, dropped])
}
#[cfg(not(target_arch = "wasm32"))]
@@ -1283,6 +1325,13 @@ impl OpenCADStudio {
}
}
}
// Images in (or dropped alongside) the opened drawing that
// exceeded the decode budget refused with a note; surface
// them now that the open has settled — the entities
// themselves show the broken-reference placeholder.
for note in crate::scene::model::image_model::take_decode_refusals() {
self.command_line.push_error(&note);
}
task
}
+16 -4
View File
@@ -2012,12 +2012,16 @@ pub async fn pick_plot_style() -> Result<Option<plot_style::PlotStyleTable>, Str
/// mirroring the PDF-underlay pick. Decoding then comes from those bytes, and
/// the entity the IMAGE command commits resolves through the same registry
/// instead of a `image::open` on a path that only exists natively.
///
/// The filter names exactly the formats the `image` crate features build
/// (Cargo.toml) — png/jpeg/bmp/tiff/webp/gif — and `io::web_drop` accepts the
/// same set for drops.
pub async fn pick_image_file() -> Result<(PathBuf, u32, u32), String> {
let handle = crate::sys::file_dialog()
.set_title(crate::t!("Select Image File").as_ref())
.add_filter(
crate::t!("Images").as_ref(),
&["png", "jpg", "jpeg", "bmp", "tiff", "tif"],
&["png", "jpg", "jpeg", "bmp", "tiff", "tif", "webp", "gif"],
)
.add_filter(crate::t!("PNG").as_ref(), &["png"])
.add_filter(crate::t!("JPEG").as_ref(), &["jpg", "jpeg"])
@@ -2033,9 +2037,17 @@ pub async fn pick_image_file() -> Result<(PathBuf, u32, u32), String> {
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| reference.clone());
crate::scene::model::image_model::register_source(&reference, bytes);
let decoded = crate::scene::model::image_model::resolve_image(&reference)
.ok_or_else(|| format!("Unsupported or corrupt image \"{short}\""))?;
Ok((path, decoded.width, decoded.height))
let decoded = crate::scene::model::image_model::resolve_image(&reference).ok_or_else(|| {
// An over-budget decode refusal explains itself (dims + limit);
// anything else stays the generic unsupported/corrupt message.
crate::scene::model::image_model::take_decode_refusals()
.pop()
.unwrap_or_else(|| format!("Unsupported or corrupt image \"{short}\""))
})?;
// Entity sizing truth: the file's ORIGINAL pixel dimensions, not the
// display-capped buffer's, so a downsampled aerial still places at its
// true aspect and default size (the cap is a rendering detail).
Ok((path, decoded.source_width, decoded.source_height))
}
/// Pick an image file and prepare it for embedding (IMAGEEMBED). The bytes
+30 -4
View File
@@ -278,8 +278,8 @@ fn is_drawing_name(name: &str) -> bool {
}
/// The decodable raster set matches the IMAGE picker's filter (and the `image`
/// crate features the app builds with) — anything else would queue bytes that
/// can only ever fail to decode.
/// crate features the app builds with: png/jpeg/bmp/tiff/webp/gif) — anything
/// else would queue bytes that can only ever fail to decode.
fn is_image_name(name: &str) -> bool {
matches!(
name.rsplit('.')
@@ -287,13 +287,13 @@ fn is_image_name(name: &str) -> bool {
.unwrap_or_default()
.to_ascii_lowercase()
.as_str(),
"png" | "jpg" | "jpeg" | "bmp" | "tiff" | "tif"
"png" | "jpg" | "jpeg" | "bmp" | "tiff" | "tif" | "webp" | "gif"
)
}
#[cfg(test)]
mod tests {
use super::is_drawing_name;
use super::{is_drawing_name, is_image_name};
#[test]
fn drawing_names_use_the_worker_convention() {
@@ -306,4 +306,30 @@ mod tests {
assert!(!is_drawing_name("plan.dwg.txt"));
assert!(!is_drawing_name(""));
}
#[test]
fn image_names_match_the_picker_filter() {
for name in [
"site.png",
"photo.JPG",
"scan.Jpeg",
"tile.bmp",
"map.tiff",
"map.TIF",
"hero.webp",
"icon.WEBP",
"anim.gif",
"ANIM.GIF",
] {
assert!(is_image_name(name), "{name} should be accepted");
}
// Formats the `image` features do not decode must not queue bytes.
for name in ["clip.avif", "pix.heic", "raw.svg", "logo.ico", "noext"] {
assert!(!is_image_name(name), "{name} should be rejected");
}
// A dot-less uppercase name is its own extension, mirroring the
// drawing-name convention above.
assert!(is_image_name("WEBP"));
assert!(is_image_name("GIF"));
}
}
+5
View File
@@ -1617,6 +1617,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
"IMAGE Specify insertion point (%{name}): " => Some(("draw", "image-specify-insertion-point-name")),
"IMAGE Specify width (drag right):" => Some(("draw", "image-specify-width-drag-right")),
"IMAGE: {e}" => Some(("common", "image-e")),
"\"{name}\" is {width}×{height} px and would decode to {decoded} MB, over the {limit} MB image limit of this build — it cannot be attached. Images within the limit are attached at a reduced display resolution when needed." => Some(("file", "image-over-the-decode-limit-cannot-be-attached")),
"IMPORTOBJ: imported \"{file_stem}\" as mesh." => Some(("common", "importobj-imported-file-stem-as-mesh")),
"IMPORTOBJ: target drawing was closed." => Some(("common", "importobj-target-drawing-was-closed")),
"IMPORTOBJ: {error}" => Some(("common", "importobj-error")),
@@ -5822,6 +5823,10 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
"Autosave copy could not be read: {error}" => Some(("common", "autosave-copy-could-not-be-read")),
"An open is already in progress." => Some(("common", "an-open-is-already-in-progress")),
"Autosave copy could not be queued: too many pending opens" => Some(("common", "autosave-copy-could-not-be-queued-too-many-pending-opens")),
"Remote image \"{url}\" could not be fetched: the network request failed (offline, DNS, or CORS)." => Some(("common", "remote-image-url-could-not-be-fetched-the-network-request-failed-offline-dns-or-cors")),
"Remote image \"{url}\" could not be fetched: the server returned HTTP {status}." => Some(("common", "remote-image-url-could-not-be-fetched-the-server-returned-http-status")),
"Remote image \"{url}\" was not loaded: it exceeds the 64 MB remote-image limit." => Some(("common", "remote-image-url-was-not-loaded-it-exceeds-the-64-mb-remote-image-limit")),
"Remote image \"{url}\" could not be fetched: the request timed out." => Some(("common", "remote-image-url-could-not-be-fetched-the-request-timed-out")),
_ => None,
}
}
+589 -30
View File
@@ -4,8 +4,6 @@
// from the RasterImage entity's insertion point, u/v vectors, and pixel size.
use std::collections::HashMap;
#[cfg(not(target_arch = "wasm32"))]
use std::path::Path;
use std::sync::{Arc, Mutex, OnceLock};
/// One textured triangle vertex of an image's visible region: an RTE-split
@@ -488,6 +486,15 @@ pub struct DecodedImage {
pub pixels: Arc<Vec<u8>>,
pub width: u32,
pub height: u32,
/// The SOURCE FILE's pixel dimensions (from the pre-decode header probe),
/// which remain the entity's sizing truth even when `width`/`height`/
/// `pixels` were display-capped: the `ImageModel` world quad comes from
/// the entity's own `size` (never from these fields), and a downsampled
/// buffer keeps the aspect ratio, so the on-paper footprint never
/// shrinks — while the PDF exporter derives effective DPI from placement
/// size × stored pixels and self-corrects.
pub source_width: u32,
pub source_height: u32,
}
type ImageCacheEntry = Arc<OnceLock<Option<DecodedImage>>>;
@@ -596,11 +603,14 @@ pub fn clear_image_cache() {
}
/// Resolve an image reference to decoded pixels, memoised per path. Handles a
/// local file and — on native builds — an `http`/`https` URL. Returns `None`
/// for anything that can't be shown (missing file, offline, decode error, or a
/// URL on the web build). The result — including a `None` — is cached so the
/// raster loader and the unresolved-reference placeholder agree on one answer
/// without fetching twice.
/// local file and an `http`/`https` URL (fetched synchronously natively; on
/// the web the first resolve kicks a background fetch and returns `None` —
/// the placeholder shows until the fetched bytes land and the poll
/// re-resolves). Returns `None` for anything that can't be shown (missing
/// file, offline, decode error, or a fetch still in flight on the web build).
/// The result — including a `None` — is cached so the raster loader and the
/// unresolved-reference placeholder agree on one answer without fetching
/// twice.
pub fn resolve_image(path: &str) -> Option<DecodedImage> {
let path = path.trim();
if path.is_empty() {
@@ -623,52 +633,279 @@ pub fn resolve_image(path: &str) -> Option<DecodedImage> {
/// dropped — the only source on the web, where the reference is a name-only
/// path), then the filesystem (native), then any registered file that merely
/// shares the name (a drawing's foreign absolute path vs a dropped file).
/// `http`/`https` references stay a native-only network fetch.
/// `http`/`https` references are a network fetch — synchronous on native, a
/// one-shot background fetch on the web whose bytes register under the URL
/// (so the exact URL and any same-name path both resolve afterwards).
fn decode_reference(path: &str) -> Option<DecodedImage> {
let lower = path.to_ascii_lowercase();
if lower.starts_with("http://") || lower.starts_with("https://") {
// The web fetch registers its bytes under the URL itself; a
// re-resolve (after the poll re-populates) must read them instead of
// spawning another fetch.
#[cfg(target_arch = "wasm32")]
if let Some(bytes) = registered_bytes(path) {
return decode_or_note(decode_from_memory(&bytes), path);
}
let bytes = fetch_remote(path)?;
return decode_from_memory(&bytes);
return decode_or_note(decode_from_memory(&bytes), path);
}
if let Some(bytes) = registered_bytes(path) {
return decode_from_memory(&bytes);
return decode_or_note(decode_from_memory(&bytes), path);
}
#[cfg(not(target_arch = "wasm32"))]
if let Some(decoded) = decode_from_file(path) {
if let Some(decoded) = decode_or_note(decode_from_file(path), path) {
return Some(decoded);
}
registered_bytes_by_name(path)
.as_deref()
.and_then(|bytes| decode_from_memory(bytes))
.and_then(|bytes| decode_or_note(decode_from_memory(bytes), path))
}
/// Decode in-memory image bytes (picked file, drop, or network body) to RGBA.
fn decode_from_memory(bytes: &[u8]) -> Option<DecodedImage> {
let img = image::load_from_memory(bytes).ok()?;
rgba_image(img)
// ── Decode budgets (display-LOD bounding) ───────────────────────────────────
//
// A 500 MB aerial/GIS TIFF can decode to multiple GB of RGBA — enough to kill
// a browser tab, and a 20000×20000 raster alone is a >1 GB GPU texture (the
// image pipeline tiles whatever it receives past `max_texture_dimension_2d`,
// so the CPU-side cost is the real problem). Every decode therefore passes
// two per-target budgets, applied in `decode_with_budgets` below:
/// Ceiling on a full RGBA decode (`width × height × 4`). Decodes that would
/// exceed it run with `image::Limits` pinned to the budget, so the decoder
/// errors instead of ballooning the heap; this module turns that error into a
/// localized refusal (see `refusal_message`) — the pixels are simply not
/// available at full size. Desktops have RAM to spare and rarely hit theirs;
/// wasm lives inside a browser tab whose heap the decode must not pop.
#[cfg(not(target_arch = "wasm32"))]
const MAX_DECODE_BYTES: u64 = 4 * 1024 * 1024 * 1024;
/// wasm: 512 MB of decoded RGBA — comfortably inside a tab's heap.
#[cfg(target_arch = "wasm32")]
const MAX_DECODE_BYTES: u64 = 512 * 1024 * 1024;
/// Ceiling on the STORED pixel count — what feeds the GPU upload, the PDF
/// embed, and (on wasm) the session heap. Images over it still decode, up to
/// `MAX_DECODE_BYTES`, but are Lanczos3-downsampled to the cap before
/// entering an `ImageModel`; the original BYTES stay registered for a future
/// re-decode, and `DecodedImage::source_width/height` keep the file's true
/// dimensions as the entity's sizing truth. Native plots may want the extra
/// headroom (8192² = 67.1 Mpx); the browser keeps a stricter cap
/// (4096² = 16.8 Mpx) to bound both heap and texture upload.
#[cfg(not(target_arch = "wasm32"))]
const MAX_DISPLAY_PIXELS: u64 = 8192 * 8192;
#[cfg(target_arch = "wasm32")]
const MAX_DISPLAY_PIXELS: u64 = 4096 * 4096;
/// How a probed image size may be decoded under the budgets above.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum DecodeTier {
/// Store the decoded pixels untouched.
Full,
/// Decode fully, then downsample to the display cap before storing.
Downsample,
/// Full RGBA would exceed `MAX_DECODE_BYTES`. The decode still runs with
/// the budget enforced through `image::Limits`: it succeeds on a build
/// with headroom (native) or errors into a refusal (wasm).
OverBudget,
}
/// Budget decision for probed pixel dimensions. Pure and cfg-independent
/// when the budgets are passed explicitly, so tests can probe arbitrary
/// budgets; the decode path always passes this target's constants.
fn tier_for(
width: u32,
height: u32,
max_decode_bytes: u64,
max_display_pixels: u64,
) -> DecodeTier {
let pixels = width as u64 * height as u64;
if pixels.saturating_mul(4) > max_decode_bytes {
DecodeTier::OverBudget
} else if pixels > max_display_pixels {
DecodeTier::Downsample
} else {
DecodeTier::Full
}
}
/// Largest dimensions that keep `width × height` within `cap` while
/// preserving the aspect ratio: each side floors (so the product never
/// exceeds the cap while both sides stay above 1 px) and clamps to 1 px — a
/// degenerate strip may then stay slightly over, since it cannot shrink
/// further. Images already within the cap pass through unchanged.
fn downsample_to_cap(width: u32, height: u32, cap: u64) -> (u32, u32) {
let pixels = width as u64 * height as u64;
if width == 0 || height == 0 || pixels <= cap {
return (width, height);
}
let scale = (cap as f64 / pixels as f64).sqrt();
let shrink = |dim: u32| {
let scaled = (dim as f64 * scale).floor() as u64;
scaled.clamp(1, dim as u64) as u32
};
(shrink(width), shrink(height))
}
/// Outcome of one tiered decode attempt.
enum DecodeAttempt {
/// Pixels decoded — and display-capped when the tier called for it —
/// ready to store.
Decoded(DecodedImage),
/// The decoder's limits rejected the image: full RGBA would exceed
/// `MAX_DECODE_BYTES`. Carries the probed dimensions for the refusal
/// note; the caller decides how to surface it.
Refused { width: u32, height: u32 },
/// Unreadable, unsupported, or corrupt.
Failed,
}
/// Decode an image through the budget tiers. The header dimensions are
/// probed BEFORE any pixel decode, so the tiers act on size alone; `open` is
/// called twice — once for the probe, once for the decode — because both
/// passes consume the reader.
fn decode_with_budgets<R: std::io::BufRead + std::io::Seek>(
open: impl Fn() -> std::io::Result<image::ImageReader<R>>,
max_decode_bytes: u64,
max_display_pixels: u64,
) -> DecodeAttempt {
// Probe pass: header dimensions only, no pixel decode.
let probed = open()
.and_then(|reader| reader.with_guessed_format())
.ok()
.and_then(|reader| reader.into_dimensions().ok());
let tier = probed.map_or(DecodeTier::Full, |(width, height)| {
tier_for(width, height, max_decode_bytes, max_display_pixels)
});
let mut reader = match open().and_then(|reader| reader.with_guessed_format()) {
Ok(reader) => reader,
Err(_) => return DecodeAttempt::Failed,
};
// The decoder enforces the decode budget itself (`Limits::reserve`
// checks the output allocation up front), so a too-large image errors
// here instead of allocating first. This also RAISES the image crate's
// 512 MiB default on native, where the budget is 4 GB.
let mut limits = image::Limits::default();
limits.max_alloc = Some(max_decode_bytes);
reader.limits(limits);
let img = match reader.decode() {
Ok(img) => img,
Err(image::ImageError::Limits(_)) => {
return DecodeAttempt::Refused {
width: probed.map_or(0, |(width, _)| width),
height: probed.map_or(0, |(_, height)| height),
};
}
Err(_) => return DecodeAttempt::Failed,
};
let rgba = img.into_rgba8();
let (width, height) = rgba.dimensions();
// Display cap: an OverBudget decode that succeeded (native headroom) is
// necessarily over the cap too — the budgets' invariant guarantees it —
// so both capping tiers downsample before storing.
let (target_w, target_h) = match tier {
DecodeTier::Full => (width, height),
DecodeTier::Downsample | DecodeTier::OverBudget => {
downsample_to_cap(width, height, max_display_pixels)
}
};
let rgba = if target_w != width || target_h != height {
image::imageops::resize(&rgba, target_w, target_h, image::imageops::FilterType::Lanczos3)
} else {
rgba
};
let (stored_w, stored_h) = rgba.dimensions();
// Sizing truth: the file's probed dimensions, not the (possibly reduced)
// buffer's, so placement and aspect come from the original raster.
let (source_width, source_height) = probed.unwrap_or((stored_w, stored_h));
DecodeAttempt::Decoded(DecodedImage {
pixels: Arc::new(rgba.into_raw()),
width: stored_w,
height: stored_h,
source_width,
source_height,
})
}
/// Decode in-memory image bytes (picked file, drop, or network body) to RGBA,
/// bounded by the decode/display budgets.
fn decode_from_memory(bytes: &[u8]) -> DecodeAttempt {
decode_with_budgets(
|| Ok(image::ImageReader::new(std::io::Cursor::new(bytes))),
MAX_DECODE_BYTES,
MAX_DISPLAY_PIXELS,
)
}
#[cfg(not(target_arch = "wasm32"))]
fn decode_from_file(path: &str) -> Option<DecodedImage> {
let img = image::open(Path::new(path)).ok()?;
rgba_image(img)
fn decode_from_file(path: &str) -> DecodeAttempt {
decode_with_budgets(
|| image::ImageReader::open(path),
MAX_DECODE_BYTES,
MAX_DISPLAY_PIXELS,
)
}
fn rgba_image(img: image::DynamicImage) -> Option<DecodedImage> {
let rgba = img.into_rgba8();
let (width, height) = rgba.dimensions();
Some(DecodedImage {
pixels: Arc::new(rgba.into_raw()),
width,
height,
})
/// Localized over-budget refusal for `path`: names the image, its probed
/// dimensions, the decoded size the budget rejected, and the limit itself.
/// TIFFs rejected here have no fallback path — the `tiff` crate is only a
/// transitive dependency (via `image`'s tiff feature) and is deliberately
/// not promoted to a direct one, so the refusal IS the handling.
fn refusal_message(path: &str, width: u32, height: u32) -> String {
let name = std::path::Path::new(path)
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| path.to_string());
let decoded_mb = width as u64 * height as u64 * 4 / (1024 * 1024);
let limit_mb = MAX_DECODE_BYTES / (1024 * 1024);
crate::tf!(
"\"{name}\" is {width}×{height} px and would decode to {decoded} MB, over the {limit} MB image limit of this build — it cannot be attached. Images within the limit are attached at a reduced display resolution when needed.",
name = name,
width = width,
height = height,
decoded = decoded_mb,
limit = limit_mb
)
.into_owned()
}
/// Over-budget refusals recorded by `decode_or_note`, drained by the UI
/// (open/drop settle, image pick) to reach the command line.
fn refusal_notes() -> &'static Mutex<Vec<String>> {
static NOTES: OnceLock<Mutex<Vec<String>>> = OnceLock::new();
NOTES.get_or_init(|| Mutex::new(Vec::new()))
}
/// Take and clear every recorded decode refusal (most recent last).
pub fn take_decode_refusals() -> Vec<String> {
let mut notes = refusal_notes().lock().unwrap_or_else(|error| error.into_inner());
std::mem::take(&mut *notes)
}
/// Store one decode outcome: pixels pass through, a refusal is noted for the
/// UI, and anything else is a plain `None` (broken-reference placeholder).
fn decode_or_note(attempt: DecodeAttempt, path: &str) -> Option<DecodedImage> {
match attempt {
DecodeAttempt::Decoded(decoded) => Some(decoded),
DecodeAttempt::Refused { width, height } => {
// Several references can name the same over-budget file; keep
// one note per distinct message so the command line is not
// spammed by a drawing that inserts it repeatedly.
let message = refusal_message(path, width, height);
let mut notes = refusal_notes().lock().unwrap_or_else(|error| error.into_inner());
if notes.last() != Some(&message) {
notes.push(message);
}
None
}
DecodeAttempt::Failed => None,
}
}
/// Fetch a remote image reference. `http`/`https` only (the caller checks the
/// scheme, so `file://` and other schemes are never followed); bounded by a
/// request timeout and a response-size cap so a slow or hostile URL can't hang
/// the load or exhaust memory. Never runs on the web build — a browser has no
/// synchronous fetch and would hit CORS on a cross-origin image anyway.
/// the load or exhaust memory. Native builds fetch synchronously through
/// `ureq`; the web build defers to [`fetch_remote`] in the wasm block below.
#[cfg(not(target_arch = "wasm32"))]
fn fetch_remote(url: &str) -> Option<Vec<u8>> {
let agent = crate::network::agent(std::time::Duration::from_secs(8));
@@ -688,11 +925,210 @@ fn fetch_remote(url: &str) -> Option<Vec<u8>> {
.ok()
}
// ── Web remote raster fetch ────────────────────────────────────────────────
//
// A browser has no synchronous fetch, so the wasm `fetch_remote` can only
// START the request: the first resolve of an `http`/`https` reference spawns
// a background task, memoises the miss, and leaves the placeholder showing.
// When the task settles it registers its bytes under the URL (the same
// source registry a pick or drop uses) and queues an outcome for the 50 ms
// control poll, which re-resolves the drawing's image models — the exact
// flow a dropped raster file already takes.
#[cfg(target_arch = "wasm32")]
fn fetch_remote(_url: &str) -> Option<Vec<u8>> {
mod web_fetch {
use super::register_source;
use std::collections::{HashSet, VecDeque};
use std::sync::{Arc, Mutex, OnceLock};
/// Bodies above this are refused (with a surfaced note) before and after
/// the read, so a hostile URL can't exhaust wasm memory. The web cap is
/// roomier than the native 32 MB because there is no streaming reader
/// behind a `fetch()` body here.
const MAX_BYTES: u64 = 64 * 1024 * 1024;
/// Connect+body deadline, mirroring the native agent timeout.
const TIMEOUT_MS: i32 = 8000;
/// Outcomes the poll hasn't drained yet; a stuck app can't grow this
/// without bound. Same cap convention as the drop queues.
const OUTCOME_CAP: usize = 32;
/// Why a remote fetch failed. Kept structured so the surface (command
/// line) localizes the sentence; the URL travels alongside.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RemoteFetchError {
/// The request never produced a response: offline, DNS, refused, or
/// (most commonly on the web) a CORS block — the surfaced note says
/// so, because a CORS failure looks like an opaque network error to
/// the page.
Network,
/// The server answered with a non-OK status (404, 500, …).
Status(u16),
/// The declared or received body exceeded [`MAX_BYTES`].
TooLarge,
/// The abort deadline fired first.
Timeout,
}
/// A settled background fetch, drained by the app's control poll.
#[derive(Debug)]
pub enum RemoteFetchOutcome {
/// Bytes landed in the source registry under this URL/name — the
/// poll re-resolves image models referencing it (exact URL or a
/// same-file-name path both match).
Loaded(String),
/// The fetch failed; the caller localizes [`RemoteFetchError`].
Failed(String, RemoteFetchError),
}
fn attempted() -> &'static Mutex<HashSet<String>> {
static ATTEMPTED: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
ATTEMPTED.get_or_init(|| Mutex::new(HashSet::new()))
}
fn outcomes() -> &'static Mutex<VecDeque<RemoteFetchOutcome>> {
static OUTCOMES: OnceLock<Mutex<VecDeque<RemoteFetchOutcome>>> = OnceLock::new();
OUTCOMES.get_or_init(|| Mutex::new(VecDeque::new()))
}
/// Queue a settled outcome for the poll. Extras beyond the cap are
/// dropped with a console warning, mirroring the drop queues.
fn push_outcome(outcome: RemoteFetchOutcome) {
let mut queue = outcomes()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if queue.len() >= OUTCOME_CAP {
drop(queue);
web_sys::console::warn_1(&wasm_bindgen::JsValue::from_str(
"ocs: remote-image outcome queue full; dropping an outcome",
));
return;
}
queue.push_back(outcome);
}
/// Pop the oldest settled remote fetch the poll has not handled yet.
pub fn take_outcome() -> Option<RemoteFetchOutcome> {
outcomes()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.pop_front()
}
/// Why did the `fetch()` promise reject? An abort arrives as a
/// `DOMException` named `AbortError`; everything else (a `TypeError`,
/// in practice) is a network-level failure the page cannot distinguish
/// from a CORS block.
fn rejection_kind(error: &wasm_bindgen::JsValue) -> RemoteFetchError {
use wasm_bindgen::JsCast;
match error.dyn_ref::<web_sys::DomException>() {
Some(exception) if exception.name() == "AbortError" => RemoteFetchError::Timeout,
_ => RemoteFetchError::Network,
}
}
/// The actual request. `GET url` with an abort signal armed at
/// [`TIMEOUT_MS`]; the body is refused past [`MAX_BYTES`] using the
/// declared `Content-Length` first and the received length second.
async fn fetch_once(url: &str) -> Result<Vec<u8>, RemoteFetchError> {
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
let window = web_sys::window().ok_or(RemoteFetchError::Network)?;
let controller =
web_sys::AbortController::new().map_err(|_| RemoteFetchError::Network)?;
let init = web_sys::RequestInit::new();
init.set_method("GET");
init.set_signal(Some(&controller.signal()));
let request = web_sys::Request::new_with_str_and_init(url, &init)
.map_err(|_| RemoteFetchError::Network)?;
// Arm the deadline. The one-shot closure outlives the await on
// purpose: aborting a settled request is a no-op, and clearing the
// timer would need a handle threaded through the error paths for no
// behavioural gain.
let abort = wasm_bindgen::closure::Closure::once_into_js(move || controller.abort());
let abort = js_sys::Function::from(abort);
window
.set_timeout_with_callback_and_timeout_and_arguments(
abort.as_ref(),
TIMEOUT_MS,
&js_sys::Array::new(),
)
.map_err(|_| RemoteFetchError::Network)?;
let response = JsFuture::from(window.fetch_with_request(&request))
.await
.map_err(|error| rejection_kind(&error))?
.dyn_into::<web_sys::Response>()
.map_err(|_| RemoteFetchError::Network)?;
if !response.ok() {
return Err(RemoteFetchError::Status(response.status()));
}
// Pre-check the declared size so an oversized body is refused before
// it is buffered; the received length is checked again after the
// read (chunked replies carry no Content-Length).
let declared = response
.headers()
.get("Content-Length")
.ok()
.flatten()
.and_then(|value| value.trim().parse::<u64>().ok());
if declared.is_some_and(|declared| declared > MAX_BYTES) {
return Err(RemoteFetchError::TooLarge);
}
let buffer = JsFuture::from(
response
.array_buffer()
.map_err(|_| RemoteFetchError::Network)?,
)
.await
.map_err(|error| rejection_kind(&error))?;
let bytes = js_sys::Uint8Array::new(&buffer).to_vec();
if bytes.len() as u64 > MAX_BYTES {
return Err(RemoteFetchError::TooLarge);
}
Ok(bytes)
}
/// Start the background fetch for `url` if one hasn't been attempted this
/// session. Each URL is fetched at most once: successes register under
/// the URL (and survive `clear_image_cache` — they live in the source
/// registry), and failures stay unretried like the font store's `Failed`
/// state, so a hostile or dead URL cannot be re-hammered by every scene
/// rebuild.
pub fn spawn(url: &str) {
let url = url.trim().to_string();
{
let mut attempted = attempted()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if !attempted.insert(url.clone()) {
return;
}
}
wasm_bindgen_futures::spawn_local(async move {
match fetch_once(&url).await {
Ok(bytes) => {
register_source(&url, Arc::new(bytes));
push_outcome(RemoteFetchOutcome::Loaded(url));
}
Err(error) => push_outcome(RemoteFetchOutcome::Failed(url, error)),
}
});
}
}
/// Web: kick the one-shot background fetch and miss now — the placeholder
/// shows until the poll sees the registered bytes (see `web_fetch` above).
#[cfg(target_arch = "wasm32")]
fn fetch_remote(url: &str) -> Option<Vec<u8>> {
web_fetch::spawn(url);
None
}
#[cfg(target_arch = "wasm32")]
pub use web_fetch::{
take_outcome as take_remote_fetch_outcome, RemoteFetchError, RemoteFetchOutcome,
};
#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
use super::*;
@@ -786,4 +1222,127 @@ mod tests {
assert!(!same_file_name("", ""));
assert!(!same_file_name("C:\\dir\\", "dir"));
}
// ── Display-LOD budgets ──────────────────────────────────────────────
/// The tier decision is exact at the budget boundaries and monotone in
/// size, and u32 extremes must not overflow into a wrong tier.
#[test]
fn decode_tiers_split_at_the_budget_boundaries() {
// Well within both budgets.
assert_eq!(tier_for(64, 64, 1_000_000, 10_000), DecodeTier::Full);
// Pixels over the display cap while RGBA stays within the decode
// budget: decode, then downsample before storing.
assert_eq!(tier_for(400, 400, 1_000_000, 10_000), DecodeTier::Downsample);
// RGBA over the decode budget.
assert_eq!(tier_for(2_000, 2_000, 1_000_000, 10_000), DecodeTier::OverBudget);
// Exact boundary counts stay in the lower tier (limits are ">").
assert_eq!(tier_for(50, 200, 1_000_000, 10_000), DecodeTier::Full);
assert_eq!(tier_for(500, 500, 1_000_000, 1_000_000), DecodeTier::Full);
// Extreme dimensions: the u64 math saturates rather than wrapping.
assert_eq!(
tier_for(u32::MAX, u32::MAX, u64::MAX, u64::MAX),
DecodeTier::Full
);
assert_eq!(
tier_for(u32::MAX, u32::MAX, MAX_DECODE_BYTES, MAX_DISPLAY_PIXELS),
DecodeTier::OverBudget
);
}
/// The per-target constants keep their invariant: the decode budget can
/// always hold a display-capped image's RGBA, so an OverBudget image
/// that nevertheless decodes (native headroom) always downsamples too.
#[test]
fn budget_constants_are_consistent() {
assert!(MAX_DECODE_BYTES >= MAX_DISPLAY_PIXELS * 4);
assert!(MAX_DISPLAY_PIXELS > 0);
}
/// Downsample targets keep the aspect ratio, stay within the cap, and
/// never collapse a dimension below 1 px.
#[test]
fn downsample_targets_preserve_aspect_and_cap() {
// Within the cap (or degenerate): untouched.
assert_eq!(downsample_to_cap(100, 50, 10_000), (100, 50));
assert_eq!(downsample_to_cap(0, 50, 10_000), (0, 50));
// Over the cap: the product fits while both sides exceed 1 px, and
// the aspect survives to within rounding (unless a side is too
// small to express it — a 3 px side clamps to 1 px by design).
for (width, height) in [(4097u32, 4096u32), (20000, 15000), (12345, 6700), (3, 20000000)] {
let cap = 4096u64 * 4096;
let (w, h) = downsample_to_cap(width, height, cap);
assert!((w as u64) * h as u64 <= cap, "{width}×{height} -> {w}×{h} over cap");
assert!(w >= 1 && h >= 1);
if w >= 8 && h >= 8 {
let aspect = width as f64 / height as f64;
assert!(
(w as f64 / h as f64 - aspect).abs() / aspect < 0.05,
"{width}×{height} -> {w}×{h} lost the aspect"
);
}
}
// A degenerate strip: the 1 px side cannot shrink further, so the
// other side alone absorbs the reduction.
let (w, h) = downsample_to_cap(100_000_000, 1, 4096 * 4096);
assert_eq!(h, 1);
assert!(w < 100_000_000);
// Extreme reduction bottoms out at 1×1, never 0.
assert_eq!(downsample_to_cap(50_000, 50_000, 1), (1, 1));
}
/// A real decode keeps the metadata-vs-buffer split: the stored buffer
/// obeys the display cap while `source_*` keep the file's true pixel
/// dimensions, and a decode over the decode budget is refused with its
/// probed dimensions instead of allocating.
#[test]
fn decode_budgets_split_metadata_from_pixels() {
let bytes = png_bytes(120, 80, 0x5a);
let open = || Ok(image::ImageReader::new(std::io::Cursor::new(&bytes)));
// Full tier: buffer and metadata agree.
match decode_with_budgets(open, 1_000_000, 1_000_000) {
DecodeAttempt::Decoded(image) => {
assert_eq!((image.width, image.height), (120, 80));
assert_eq!((image.source_width, image.source_height), (120, 80));
assert_eq!(image.pixels.len(), 120 * 80 * 4);
}
_ => panic!("a small png must decode"),
}
// Display cap only: the buffer shrinks (aspect kept, ≤ cap), the
// metadata does not — the sizing truth survives the reduction.
match decode_with_budgets(open, 1_000_000, 3_000) {
DecodeAttempt::Decoded(image) => {
assert_eq!((image.source_width, image.source_height), (120, 80));
assert!((image.width as u64) * image.height as u64 <= 3_000);
assert!(image.width >= 1 && image.height >= 1);
assert_eq!(
image.pixels.len(),
image.width as usize * image.height as usize * 4
);
assert!((image.width as f64 / image.height as f64 - 1.5).abs() < 0.1);
}
_ => panic!("a display-capped png must still decode"),
}
// Decode budget below the full RGBA size: refused, carrying the
// probed dimensions — no allocation was needed to discover them.
match decode_with_budgets(open, 1_000, 1_000_000) {
DecodeAttempt::Refused { width, height } => assert_eq!((width, height), (120, 80)),
_ => panic!("an over-budget png must be refused"),
}
}
/// Refusals flow through `decode_or_note` into the note queue once per
/// distinct message, carrying the dimensions and the file name.
#[test]
fn refusals_are_noted_once_per_message() {
let _ = take_decode_refusals();
let refuse = || DecodeAttempt::Refused { width: 30000, height: 30000 };
assert!(decode_or_note(refuse(), "C:/gis/aerial.tif").is_none());
assert!(decode_or_note(refuse(), "C:/gis/aerial.tif").is_none());
let notes = take_decode_refusals();
assert_eq!(notes.len(), 1, "duplicate messages collapse: {notes:?}");
assert!(notes[0].contains("30000×30000"), "dimensions in: {}", notes[0]);
assert!(notes[0].contains("aerial.tif"), "file name in: {}", notes[0]);
assert!(take_decode_refusals().is_empty(), "the drain cleared the queue");
}
}
+21
View File
@@ -28,6 +28,8 @@ pub enum Script {
Latin,
Cyrillic,
Greek,
Armenian,
Georgian,
Arabic,
Hebrew,
Thai,
@@ -47,6 +49,8 @@ impl Script {
Script::Latin => "fonts/latin.ttf",
Script::Cyrillic => "fonts/cyrillic.ttf",
Script::Greek => "fonts/greek.ttf",
Script::Armenian => "fonts/armenian.ttf",
Script::Georgian => "fonts/georgian.ttf",
Script::Arabic => "fonts/arabic.ttf",
Script::Hebrew => "fonts/hebrew.ttf",
Script::Thai => "fonts/thai.ttf",
@@ -68,6 +72,8 @@ impl Script {
Script::Latin
| Script::Cyrillic
| Script::Greek
| Script::Armenian
| Script::Georgian
| Script::Arabic
| Script::Hebrew
| Script::Thai
@@ -89,6 +95,8 @@ impl Script {
9 => Script::Korean,
10 => Script::TraditionalChinese,
11 => Script::Lao,
12 => Script::Armenian,
13 => Script::Georgian,
_ => Script::Latin,
}
}
@@ -108,6 +116,8 @@ impl Script {
Script::Korean => 9,
Script::TraditionalChinese => 10,
Script::Lao => 11,
Script::Armenian => 12,
Script::Georgian => 13,
}
}
}
@@ -144,7 +154,9 @@ pub fn scripts_for_language_tag(language: &str) -> Vec<Script> {
"el" => Some(Script::Greek),
"he" | "yi" => Some(Script::Hebrew),
"hi" | "mr" | "ne" => Some(Script::Devanagari),
"hy" => Some(Script::Armenian),
"ja" => Some(Script::Japanese),
"ka" => Some(Script::Georgian),
"ko" => Some(Script::Korean),
"ru" | "uk" | "bg" | "sr" => Some(Script::Cyrillic),
"th" => Some(Script::Thai),
@@ -220,12 +232,18 @@ pub fn set_cjk_lang_from_codepage(code_page: &str) -> bool {
/// The script font that covers `ch`, or `None` for control / uncovered code
/// points. Ranges mirror the subset unicode ranges in `web/fonts/generate.sh`.
/// Chars outside every range (scripts with no shipped subset, e.g. most
/// South-East Asian and African scripts) return `None` and render nothing —
/// see `ttf_glyph::build_fallback`.
#[cfg(target_arch = "wasm32")]
pub fn script_of(ch: char) -> Option<Script> {
Some(match ch as u32 {
0x0000..=0x024F | 0x1E00..=0x1EFF | 0x2000..=0x206F => Script::Latin,
0x0370..=0x03FF | 0x1F00..=0x1FFF => Script::Greek,
0x0400..=0x052F | 0x2DE0..=0x2DFF | 0xA640..=0xA69F => Script::Cyrillic,
// Armenian sits directly below Hebrew in the block layout (ends at
// U+058F; Hebrew starts at U+0590), plus its presentation ligatures.
0x0530..=0x058F | 0xFB13..=0xFB17 => Script::Armenian,
0x0590..=0x05FF | 0xFB1D..=0xFB4F => Script::Hebrew,
0x0600..=0x06FF | 0x0750..=0x077F | 0x08A0..=0x08FF | 0xFB50..=0xFDFF | 0xFE70..=0xFEFF => {
Script::Arabic
@@ -233,6 +251,9 @@ pub fn script_of(ch: char) -> Option<Script> {
0x0900..=0x097F | 0xA8E0..=0xA8FF => Script::Devanagari,
0x0E00..=0x0E7F => Script::Thai,
0x0E80..=0x0EFF => Script::Lao,
// Georgian: Asomtavruli/Nuskhuri, Mtavruli capitals, and the legacy
// Supplement (khutsuri round mkhedruli).
0x10A0..=0x10FF | 0x1C90..=0x1CBF | 0x2D00..=0x2D5F => Script::Georgian,
// Hangul → always Korean; kana → always Japanese.
0x1100..=0x11FF | 0x3130..=0x318F | 0xAC00..=0xD7A3 => Script::Korean,
0x3040..=0x30FF | 0x31F0..=0x31FF => Script::Japanese,
Binary file not shown.
+10
View File
@@ -52,6 +52,16 @@ dl /tmp/NotoDeva.ttf "$RAW/NotoSansDevanagari/hinted/ttf/NotoSansDevanagari-Regu
merge /tmp/NotoDeva.ttf /tmp/NotoDevaMerged.ttf
sub /tmp/NotoDevaMerged.ttf "U+0000-024F,U+1E00-1EFF,U+2000-206F,U+0900-097F,U+A8E0-A8FF" devanagari.ttf
# Armenian + Georgian follow the same one-source pattern (both are
# non-CJK alphabets; no shaping tables needed beyond the merge).
dl /tmp/NotoArmenian.ttf "$RAW/NotoSansArmenian/hinted/ttf/NotoSansArmenian-Regular.ttf"
merge /tmp/NotoArmenian.ttf /tmp/NotoArmenianMerged.ttf
sub /tmp/NotoArmenianMerged.ttf "U+0000-024F,U+1E00-1EFF,U+2000-206F,U+0530-058F,U+FB13-FB17" armenian.ttf
dl /tmp/NotoGeorgian.ttf "$RAW/NotoSansGeorgian/hinted/ttf/NotoSansGeorgian-Regular.ttf"
merge /tmp/NotoGeorgian.ttf /tmp/NotoGeorgianMerged.ttf
sub /tmp/NotoGeorgianMerged.ttf "U+0000-024F,U+1E00-1EFF,U+2000-206F,U+10A0-10FF,U+1C90-1CBF,U+2D00-2D5F" georgian.ttf
# CJK comes from the noto-cjk repo (CFF/OTF; ttf-parser reads CFF outlines) and
# is split by language: Simplified Chinese, Traditional Chinese, Japanese and
# Korean each get their own file.
Binary file not shown.