diff --git a/Cargo.toml b/Cargo.toml index c59429762..0be33e3a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,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" # PDF writer shared by desktop export and the web export/preview/print path. printpdf = { version = "0.9.1", default-features = false, features = ["images"] } @@ -124,6 +128,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", diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index 44447193d..18a5f6678 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -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__": انتهت مهلة الطلب. errors = .save-warning = تحذير الحفظ @@ -2004,6 +2008,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 للتطبيق): diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index 20deec855..7b7d6cd99 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -1874,6 +1874,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__" не можа да бъде извлечено: заявката надвиши времето за изчакване. errors = .save-warning = Предупреждение при запазване @@ -1991,6 +1995,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 за прилагане): diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index 24fe5dbab..6d5578481 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -1874,6 +1874,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. errors = .save-warning = Uložit varování @@ -1991,6 +1995,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): diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index 974a876b9..24c0dd284 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -1875,6 +1875,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. errors = .save-warning = Warnhinweise speichern @@ -1992,6 +1996,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): diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index 446a67c01..be93ddb51 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -1874,6 +1874,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__" δεν μπόρεσε να ανακτηθεί: το αίτημα έληξε χρονικά. errors = .save-warning = Προειδοποίηση αποθήκευσης @@ -1991,6 +1995,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 για εφαρμογή): diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index 838c2714c..c059b53be 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -1875,6 +1875,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. errors = .save-warning = Save Warning @@ -1992,6 +1996,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): diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index 8103840dc..6683f4624 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -1875,6 +1875,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. errors = .save-warning = Advertencia de guardado @@ -1992,6 +1996,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): diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index c91c23785..637b4b57f 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -1874,6 +1874,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. errors = .save-warning = Tallenna varoitus @@ -1991,6 +1995,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): diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index becf3e8d9..3d98356fd 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -1875,6 +1875,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é. errors = .save-warning = Enregistrer l' avertissement @@ -1992,6 +1996,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) : diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index 11b9dbc7a..b2330a422 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -1863,6 +1863,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__" लाया नहीं जा सका: अनुरोध का समय समाप्त हो गया। errors = .save-warning = चेतावनी सहेजें @@ -1980,6 +1984,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 दबाएँ): diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index 713915321..d76a45ce0 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -1874,6 +1874,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. errors = .save-warning = Figyelmeztetés mentése @@ -1991,6 +1995,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__ Objektumok kiválasztása (__ocs_arg_count__ kiválasztva, Enter alkalmazni): diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index ebbfdedde..949e8bd11 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -1874,6 +1874,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. errors = .save-warning = Salva avviso @@ -1991,6 +1995,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): diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index 51d99e85b..b27286783 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -1865,6 +1865,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__" を取得できませんでした: リクエストがタイムアウトしました。 errors = .save-warning = 保存の警告 @@ -1982,6 +1986,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 で適用): diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index 8efcb0c7e..ad35812ac 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -1874,6 +1874,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__"을(를) 가져올 수 없습니다: 요청 시간이 초과되었습니다. errors = .save-warning = 경고 저장 @@ -1991,6 +1995,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 적용): diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index 8825a4a3e..6d0f052e2 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -1875,6 +1875,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. errors = .save-warning = Waarschuwing opslaan @@ -1992,6 +1996,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): diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index a2543d47d..95a2b39b7 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -1874,6 +1874,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. errors = .save-warning = Zapisz ostrzeżenie @@ -1991,6 +1995,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): diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index 714d2cb27..bd070f9c1 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -1875,6 +1875,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. errors = .save-warning = Aviso de salvamento @@ -1992,6 +1996,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): diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index ebd46c550..e4368d063 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -1878,6 +1878,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__»: время ожидания запроса истекло. errors = .save-warning = Спасти предостережение @@ -1995,6 +1999,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__ Выберите, Введите для применения): diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index 039b9744a..ab9e3dba0 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -1859,6 +1859,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ı. errors = .save-warning = Kaydetme Uyarısı @@ -1975,6 +1979,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): diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index ccbae3d93..b7e204b1a 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -1862,6 +1862,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__":请求超时。 errors = .save-warning = 保存警告 @@ -1979,6 +1983,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 应用): diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index 64446ac4b..7b173cf8c 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -1874,6 +1874,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__":請求逾時。 errors = .save-warning = 儲存警告 @@ -1991,6 +1995,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 套用): diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 319d66d4a..fc116e3da 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -412,11 +412,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 = 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(¬e); + } + } + } + registered.extend(fetched); if !registered.is_empty() { for tab in self.tabs.iter_mut() { tab.scene.resolve_registered_image_names(®istered); } } + // 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(¬e); + } Task::batch([control, dropped]) } #[cfg(not(target_arch = "wasm32"))] @@ -993,6 +1035,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(¬e); + } task } diff --git a/src/io/mod.rs b/src/io/mod.rs index 7add2af46..c5d0ce33f 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -1872,12 +1872,16 @@ mod plot_style_registration_tests { /// 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"]) @@ -1893,9 +1897,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)) } /// Save `doc` to `path` with the given DXF version, overriding `doc.version`. diff --git a/src/io/web_drop.rs b/src/io/web_drop.rs index 1f6ac571b..fde207aba 100644 --- a/src/io/web_drop.rs +++ b/src/io/web_drop.rs @@ -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")); + } } diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index 6672a517e..2620bb16c 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -1242,6 +1242,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")), @@ -1885,7 +1886,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Opening OCS Discussions for help and questions..." => Some(("view", "opening-ocs-discussions-for-help-and-questions")), "Opening OCS Web..." => Some(("view", "opening-ocs-web")), "Opening the command reference..." => Some(("view", "opening-the-command-reference")), - "\"{name}\" is already loaded with different contents; the existing file was kept." => Some(("file", "already-loaded-different-contents-kept")), + "\"{name}\" is already loaded with different contents; the existing file was kept." => Some(("view", "already-loaded-different-contents-kept")), "Opening Patreon page..." => Some(("view", "opening-patreon-page")), "Opening feedback page..." => Some(("view", "opening-feedback-page")), "Opening file" => Some(("ui", "opening-file")), @@ -4620,6 +4621,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, } } diff --git a/src/scene/model/image_model.rs b/src/scene/model/image_model.rs index b751e5a1f..54f8eb752 100644 --- a/src/scene/model/image_model.rs +++ b/src/scene/model/image_model.rs @@ -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 @@ -380,6 +378,15 @@ pub struct DecodedImage { pub pixels: Arc>, 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>>; @@ -488,11 +495,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 { let path = path.trim(); if path.is_empty() { @@ -515,52 +525,279 @@ pub fn resolve_image(path: &str) -> Option { /// 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 { 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 { - 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( + open: impl Fn() -> std::io::Result>, + 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 { - 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 { - 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> { + static NOTES: OnceLock>> = 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 { + 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 { + 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> { let agent = crate::network::agent(std::time::Duration::from_secs(8)); @@ -580,11 +817,210 @@ fn fetch_remote(url: &str) -> Option> { .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> { +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> { + static ATTEMPTED: OnceLock>> = OnceLock::new(); + ATTEMPTED.get_or_init(|| Mutex::new(HashSet::new())) + } + + fn outcomes() -> &'static Mutex> { + static OUTCOMES: OnceLock>> = 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 { + 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::() { + 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, 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::() + .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::().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> { + 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::*; @@ -678,4 +1114,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"); + } } diff --git a/src/scene/text/web_font.rs b/src/scene/text/web_font.rs index 73e752133..203dfb20f 100644 --- a/src/scene/text/web_font.rs +++ b/src/scene/text/web_font.rs @@ -28,6 +28,8 @@ pub enum Script { Latin, Cyrillic, Greek, + Armenian, + Georgian, Arabic, Hebrew, Thai, @@ -46,6 +48,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", @@ -66,6 +70,8 @@ impl Script { Script::Latin | Script::Cyrillic | Script::Greek + | Script::Armenian + | Script::Georgian | Script::Arabic | Script::Hebrew | Script::Thai @@ -85,6 +91,8 @@ impl Script { 8 => Script::Japanese, 9 => Script::Korean, 10 => Script::TraditionalChinese, + 11 => Script::Armenian, + 12 => Script::Georgian, _ => Script::Latin, } } @@ -103,6 +111,8 @@ impl Script { Script::Japanese => 8, Script::Korean => 9, Script::TraditionalChinese => 10, + Script::Armenian => 11, + Script::Georgian => 12, } } } @@ -139,7 +149,9 @@ pub fn scripts_for_language_tag(language: &str) -> Vec