diff --git a/Cargo.toml b/Cargo.toml index 293562e29..6ef6bf45c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -179,6 +179,8 @@ web-sys = { version = "=0.3.85", features = [ "DataTransfer", "FileList", "Event", + "ReadableStreamDefaultReader", + "ReadableStream", ] } iced = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39", features = ["webgl", "fira-sans"] } diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index 01badc366..1718cd00b 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -2412,6 +2412,7 @@ command = file = .already-loaded-different-contents-kept = "__ocs_fmt_0__" محمّل بالفعل بمحتوى مختلف؛ تم الاحتفاظ بالملف الموجود. + .image-refusal-unknown-size = تعذّر قياس أبعاد "__ocs_fmt_0__" من ترويسته وسيتجاوز فك ترميزه حد الصور __ocs_fmt_1__ ميغابايت في هذا الإصدار — لا يمكن إرفاقه. .parse-ms-purge-ms-caches-ms-xref-ms-total-ms = التحليل __ocs_fmt_0__ ملث · التنظيف __ocs_fmt_1__ ملث · الذاكرة المؤقتة __ocs_fmt_2__ ملث · المرجع الخارجي __ocs_fmt_3__ ملث · الإجمالي __ocs_fmt_4__ ملث .archive-save-the-drawing-first-it-has-no-file = ARCHIVE: احفظ الرسم أولًا (ليس له مسار ملف بعد). .autosaved-1-drawing = حُفظ رسم واحد تلقائيًا diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index ddf3325a6..0ca293595 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -2399,6 +2399,7 @@ command = file = .already-loaded-different-contents-kept = "__ocs_fmt_0__" вече е зареден с различно съдържание; съществуващият файл е запазен. + .image-refusal-unknown-size = Размерът на „__ocs_fmt_0__“ не можа да се измери от заглавната му част, а декодирането му би надвишило __ocs_fmt_1__ MB лимита на тази сборка — не може да се прикачи. .parse-ms-purge-ms-caches-ms-xref-ms-total-ms = анализ __ocs_fmt_0__ms · изчистване __ocs_fmt_1__ms · кешове __ocs_fmt_2__ms · xref __ocs_fmt_3__ms · общо __ocs_fmt_4__ms .archive-save-the-drawing-first-it-has-no-file = ARCHIVE: първо запази чертежа (все още няма път до файл). .autosaved-1-drawing = Автоматично запазен 1 чертеж diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index b4946dbcc..f2936b718 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -2399,6 +2399,7 @@ command = file = .already-loaded-different-contents-kept = "__ocs_fmt_0__" je již načten s jiným obsahem; existující soubor byl zachován. + .image-refusal-unknown-size = „__ocs_fmt_0__“ nešlo změřit ze záhlaví a jeho dekódování by překročilo limit __ocs_fmt_1__ MB tohoto sestavení — nelze připojit. .parse-ms-purge-ms-caches-ms-xref-ms-total-ms = analyzovat __ocs_fmt_0__ms · vyčistit __ocs_fmt_1__ms · mezipaměti __ocs_fmt_2__ms · xref __ocs_fmt_3__ms · celkem __ocs_fmt_4__ms .archive-save-the-drawing-first-it-has-no-file = ARCHIVE: nejprve uložte výkres (ještě nemá žádnou cestu k souboru). .autosaved-1-drawing = Automaticky uložen 1 výkres diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index f5f8e181c..4b24920f4 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -2400,6 +2400,7 @@ command = file = .already-loaded-different-contents-kept = "__ocs_fmt_0__" ist bereits mit anderem Inhalt geladen; die vorhandene Datei wurde behalten. + .image-refusal-unknown-size = „__ocs_fmt_0__“ konnte anhand des Headers nicht vermessen werden, und das Dekodieren überschreitet das __ocs_fmt_1__-MB-Limit dieses Builds — kann nicht angehängt werden. .parse-ms-purge-ms-caches-ms-xref-ms-total-ms = analysiere __ocs_fmt_0__ms · Säubere __ocs_fmt_1__ms · Puffer __ocs_fmt_2__ms · xref __ocs_fmt_3__ms · gesamt __ocs_fmt_4__ms .archive-save-the-drawing-first-it-has-no-file = ARCHIVE: Speichern Sie die Zeichnung zuerst (sie hat noch keinen Dateipfad). .autosaved-1-drawing = 1 Zeichnung automatisch gespeichert diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index 8feb3116f..fa9049697 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -2399,6 +2399,7 @@ command = file = .already-loaded-different-contents-kept = Το "__ocs_fmt_0__" είναι ήδη φορτωμένο με διαφορετικό περιεχόμενο· διατηρήθηκε το υπάρχον αρχείο. + .image-refusal-unknown-size = Το "__ocs_fmt_0__" δεν μπόρεσε να μετρηθεί από την κεφαλίδα του και η αποκωδικοποίησή του θα ξεπερνούσε το όριο __ocs_fmt_1__ MB αυτής της build — δεν μπορεί να επισυναφθεί. .parse-ms-purge-ms-caches-ms-xref-ms-total-ms = ανάλυση __ocs_fmt_0__ms · εκκαθάριση __ocs_fmt_1__ms · κρυφές μνήμες __ocs_fmt_2__ms · xref __ocs_fmt_3__ms · σύνολο __ocs_fmt_4__ms .archive-save-the-drawing-first-it-has-no-file = ARCHIVE: αποθηκεύστε πρώτα το σχέδιο (δεν έχει ακόμη διαδρομή αρχείου). .autosaved-1-drawing = Αυτόματη αποθήκευση 1 σχεδίου diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index 268de84ce..21ee79120 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -1888,6 +1888,7 @@ common = .enter-a-github-url-or-repository-in-owner-repo-format = Enter a GitHub URL or repository in owner/repo format. .dwgunits-convert-drawing-to = DWGUNITS Convert drawing to: .opened-plot-preview = Opened plot preview. + .plot-preview-task = Plot preview .auto-constrain = Auto Constrain .show-all = Show All @@ -2400,6 +2401,7 @@ command = file = .already-loaded-different-contents-kept = "__ocs_fmt_0__" is already loaded with different contents; the existing file was kept. + .image-refusal-unknown-size = "__ocs_fmt_0__" could not be sized from its header and decoding it exceeded the __ocs_fmt_1__ MB image limit of this build — it cannot be attached. .parse-ms-purge-ms-caches-ms-xref-ms-total-ms = parse __ocs_fmt_0__ms · purge __ocs_fmt_1__ms · caches __ocs_fmt_2__ms · xref __ocs_fmt_3__ms · total __ocs_fmt_4__ms .archive-save-the-drawing-first-it-has-no-file = ARCHIVE: save the drawing first (it has no file path yet). .autosaved-1-drawing = Autosaved 1 drawing @@ -2441,6 +2443,8 @@ file = .saveall-saved-saved-drawing-s = SAVEALL: saved __ocs_fmt_0__ drawing(s)__ocs_fmt_1__. .saveall-already-has-a-save-running = SAVEALL: __ocs_fmt_0__ already has a save running .saveall-failed-e = SAVEALL: __ocs_fmt_0__ failed: __ocs_fmt_1__ + .extraction-max-sources-web = At most __ocs_fmt_0__ drawings can be added in the browser. + .web-already-open-shared-copy = "__ocs_fmt_0__" is already open in another tab. On the web both tabs share one stored copy — the last save overwrites the other. .load-linetype-file = Load linetype file .script-path-to-the-scr-file = SCRIPT path to the .scr file: .script-cannot-read-p-e = SCRIPT: cannot read __ocs_fmt_0__: __ocs_fmt_1__ diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index ddeacdd3b..725c64892 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -2400,6 +2400,7 @@ command = file = .already-loaded-different-contents-kept = "__ocs_fmt_0__" ya está cargado con contenido diferente; se conservó el archivo existente. + .image-refusal-unknown-size = "__ocs_fmt_0__" no pudo medirse por su cabecera y decodificarlo excedería el límite de __ocs_fmt_1__ MB de esta build — no se puede adjuntar. .parse-ms-purge-ms-caches-ms-xref-ms-total-ms = análisis __ocs_fmt_0__ ms · limpieza __ocs_fmt_1__ ms · cachés __ocs_fmt_2__ ms · ref. ext. __ocs_fmt_3__ ms · total __ocs_fmt_4__ ms .archive-save-the-drawing-first-it-has-no-file = ARCHIVE: guarde primero el dibujo (todavía no tiene una ruta de archivo). .autosaved-1-drawing = Se guardó automáticamente 1 dibujo diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index 948d33bf9..ec69cc927 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -2399,6 +2399,7 @@ command = file = .already-loaded-different-contents-kept = "__ocs_fmt_0__" on jo ladattu eri sisällöllä; olemassa oleva tiedosto säilytettiin. + .image-refusal-unknown-size = Kohteen "__ocs_fmt_0__" kokoa ei voitu mitata otsakkeesta ja sen purkaminen ylittäisi tämän käännöksen __ocs_fmt_1__ MB kuvarajan — sitä ei voi liittää. .parse-ms-purge-ms-caches-ms-xref-ms-total-ms = jäsentää __ocs_fmt_0__ms · tyhjentää __ocs_fmt_1__ms · välimuistit __ocs_fmt_2__ms · xref __ocs_fmt_3__ms · yhteensä __ocs_fmt_4__ms .archive-save-the-drawing-first-it-has-no-file = ARCHIVE: tallenna piirustus ensin (sillä ei ole vielä tiedostopolkua). .autosaved-1-drawing = Automaattisesti tallennettu 1 piirros diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index f17e69955..b627ebae9 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -2400,6 +2400,7 @@ command = file = .already-loaded-different-contents-kept = "__ocs_fmt_0__" est déjà chargé avec un contenu différent ; le fichier existant a été conservé. + .image-refusal-unknown-size = "__ocs_fmt_0__" n’a pas pu être mesuré par son en-tête et son décodage dépasserait la limite de __ocs_fmt_1__ Mo de cette version — impossible de l’attacher. .parse-ms-purge-ms-caches-ms-xref-ms-total-ms = Analyse : __ocs_fmt_0__ ms, purge : __ocs_fmt_1__ ms, caches : __ocs_fmt_2__ ms, références externes : __ocs_fmt_3__ ms, total : __ocs_fmt_4__ ms .archive-save-the-drawing-first-it-has-no-file = ARCHIVE : enregistrer le dessin en premier (il n'a pas encore de chemin de fichier). .autosaved-1-drawing = 1 dessin enregistré automatiquement diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index aed7d6fa5..bf8d96f8d 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -2388,6 +2388,7 @@ command = file = .already-loaded-different-contents-kept = "__ocs_fmt_0__" अलग सामग्री के साथ पहले से लोड है; मौजूदा फ़ाइल रखी गई। + .image-refusal-unknown-size = "__ocs_fmt_0__" को उसके हेडर से मापा नहीं जा सका और उसे डिकोड करना इस बिल्ड की __ocs_fmt_1__ MB छवि सीमा पार कर देगा — जोड़ा नहीं जा सकता। .parse-ms-purge-ms-caches-ms-xref-ms-total-ms = पार्स __ocs_fmt_0__ms · purge __ocs_fmt_1__Ms · कैश __ocs_fmt_2__ms · xref __ocs_fmt_3__ms · कुल __ocs_fmt_4__ms .archive-save-the-drawing-first-it-has-no-file = ARCHIVE: पहले ड्रॉइंग सहेजें (अभी फ़ाइल पथ निर्धारित नहीं है)। .autosaved-1-drawing = 1 ड्रॉइंग स्वतः सहेजी गई diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index 888f38580..8c8ccff2b 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -2412,6 +2412,7 @@ command = file = .already-loaded-different-contents-kept = A(z) "__ocs_fmt_0__" már betöltve eltérő tartalommal; a meglévő fájl megőrizve. + .image-refusal-unknown-size = A(z) "__ocs_fmt_0__" nem mérhető a fejlécéből, és dekódolása meghaladná ezen build __ocs_fmt_1__ MB-os képhatárát — nem csatolható. .parse-ms-purge-ms-caches-ms-xref-ms-total-ms = elemzése __ocs_fmt_0__ms · __ocs_fmt_1__ms törlése · gyorsítótárak __ocs_fmt_2__ms · xref __ocs_fmt_3__ms · összesen __ocs_fmt_4__ms .archive-save-the-drawing-first-it-has-no-file = ARCHIVE: először mentse a rajzot (még nincs fájl elérési útja). .autosaved-1-drawing = Automatikusan mentett 1 rajzot diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index ec070c9ad..046d92cf8 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -2399,6 +2399,7 @@ command = file = .already-loaded-different-contents-kept = "__ocs_fmt_0__" è già caricato con contenuti diversi; il file esistente è stato mantenuto. + .image-refusal-unknown-size = "__ocs_fmt_0__" non è stato misurabile dall’intestazione e decodificarlo supererebbe il limite di __ocs_fmt_1__ MB di questa build — impossibile allegarlo. .parse-ms-purge-ms-caches-ms-xref-ms-total-ms = analizza __ocs_fmt_0__ms · elimina __ocs_fmt_1__ms · memorizza nella cache __ocs_fmt_2__ms · xref __ocs_fmt_3__ms · totale __ocs_fmt_4__ms .archive-save-the-drawing-first-it-has-no-file = ARCHIVE: salva prima il disegno (non ha ancora il percorso del file). .autosaved-1-drawing = 1 disegno salvato automaticamente diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index f9b1bb731..219f59a04 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -2390,6 +2390,7 @@ command = file = .already-loaded-different-contents-kept = "__ocs_fmt_0__" は既に別の内容で読み込まれています。既存のファイルを保持しました。 + .image-refusal-unknown-size = "__ocs_fmt_0__" のサイズをヘッダーから測定できず、デコードするとこのビルドの __ocs_fmt_1__ MB 画像制限を超えます — 添付できません。 .parse-ms-purge-ms-caches-ms-xref-ms-total-ms = 解析 __ocs_fmt_0__ms · パージ __ocs_fmt_1__ms · キャッシュ __ocs_fmt_2__ms · 外部参照 __ocs_fmt_3__ms · 合計 __ocs_fmt_4__ms .archive-save-the-drawing-first-it-has-no-file = ARCHIVE: 先に図面を保存してください(まだファイルパスがありません)。 .autosaved-1-drawing = 1 件の図面を自動保存しました diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index 561407809..168d6956d 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -2399,6 +2399,7 @@ command = file = .already-loaded-different-contents-kept = "__ocs_fmt_0__"이(가) 이미 다른 내용으로 로드되었습니다. 기존 파일을 유지했습니다. + .image-refusal-unknown-size = "__ocs_fmt_0__"의 크기를 헤더에서 측정할 수 없고 디코딩 시 이 빌드의 __ocs_fmt_1__ MB 이미지 한도를 초과합니다 — 첨부할 수 없습니다. .parse-ms-purge-ms-caches-ms-xref-ms-total-ms = __ocs_fmt_0__ms 구문 분석 · __ocs_fmt_1__ms 제거 · 캐시 __ocs_fmt_2__ms · xref __ocs_fmt_3__ms · 총 __ocs_fmt_4__ms .archive-save-the-drawing-first-it-has-no-file = ARCHIVE: 먼저 도면을 저장합니다(아직 파일 경로가 없음). .autosaved-1-drawing = 그림 1개 자동 저장됨 diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index f1087f1be..7b3c61ceb 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -2400,6 +2400,7 @@ command = file = .already-loaded-different-contents-kept = "__ocs_fmt_0__" is al geladen met andere inhoud; het bestaande bestand is bewaard. + .image-refusal-unknown-size = "__ocs_fmt_0__" kon niet via de header gemeten worden en decoderen overschrijdt de __ocs_fmt_1__ MB-limiet van deze build — kan niet worden toegevoegd. .parse-ms-purge-ms-caches-ms-xref-ms-total-ms = ontleed __ocs_fmt_0__ms · zuivering __ocs_fmt_1__ms · caches __ocs_fmt_2__ms · xref __ocs_fmt_3__ms · totaal __ocs_fmt_4__ms .archive-save-the-drawing-first-it-has-no-file = ARCHIVE: sla de tekening eerst op (er is nog geen bestandspad). .autosaved-1-drawing = Automatisch opslaan van 1 tekening diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index 234047dad..aa99d53a4 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -2399,6 +2399,7 @@ command = file = .already-loaded-different-contents-kept = "__ocs_fmt_0__" jest już wczytany z inną zawartością; zachowano istniejący plik. + .image-refusal-unknown-size = „__ocs_fmt_0__” nie dał się zmierzyć z nagłówka, a jego dekodowanie przekroczy limit __ocs_fmt_1__ MB tej kompilacji — nie można go dołączyć. .parse-ms-purge-ms-caches-ms-xref-ms-total-ms = analizuj __ocs_fmt_0__ms · usuwaj __ocs_fmt_1__ms · pamięci podręczne __ocs_fmt_2__ms · xref __ocs_fmt_3__ms · łącznie __ocs_fmt_4__ms .archive-save-the-drawing-first-it-has-no-file = ARCHIVE: najpierw zapisz rysunek (nie ma on jeszcze ścieżki do pliku). .autosaved-1-drawing = Automatycznie zapisano 1 rysunek diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index baab11479..af4be1784 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -2400,6 +2400,7 @@ command = file = .already-loaded-different-contents-kept = "__ocs_fmt_0__" já está carregado com conteúdo diferente; o ficheiro existente foi mantido. + .image-refusal-unknown-size = "__ocs_fmt_0__" não pôde ser medido pelo cabeçalho e a sua descodificação excedeu o limite de __ocs_fmt_1__ MB desta build — não pode ser anexado. .parse-ms-purge-ms-caches-ms-xref-ms-total-ms = análise __ocs_fmt_0__ms · limpeza __ocs_fmt_1__ms · caches __ocs_fmt_2__ms · ref. externa __ocs_fmt_3__ms · total __ocs_fmt_4__ms .archive-save-the-drawing-first-it-has-no-file = ARCHIVE: salve o desenho primeiro (ele ainda não tem um caminho de arquivo). .autosaved-1-drawing = 1 desenho salvo automaticamente diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index 8b2c04346..5baa9bd16 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -2403,6 +2403,7 @@ command = file = .already-loaded-different-contents-kept = "__ocs_fmt_0__" уже загружен с другим содержимым; существующий файл сохранён. + .image-refusal-unknown-size = «__ocs_fmt_0__» не удалось измерить по заголовку, а его декодирование превысит лимит __ocs_fmt_1__ МБ этой сборки — вложить нельзя. .parse-ms-purge-ms-caches-ms-xref-ms-total-ms = parse __ocs_fmt_0__ms · чистка __ocs_fmt_1__ms · кэши __ocs_fmt_2__ms · xref __ocs_fmt_3__ms · общая __ocs_fmt_4__ms .archive-save-the-drawing-first-it-has-no-file = ARCHIVE: сначала сохраните чертёж (путь к файлу ещё не задан). .autosaved-1-drawing = Автоматически сохранён 1 чертёж diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index ae0b0c3c9..61006001f 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -2384,6 +2384,7 @@ command = file = .already-loaded-different-contents-kept = "__ocs_fmt_0__" zaten farklı içerikle yüklü; mevcut dosya tutuldu. + .image-refusal-unknown-size = "__ocs_fmt_0__" başlığından ölçülemedi ve kodu çözülürse bu sürümün __ocs_fmt_1__ MB görüntü limiti aşılır — eklenemez. .parse-ms-purge-ms-caches-ms-xref-ms-total-ms = ayrıştırma __ocs_fmt_0__ ms · temizleme __ocs_fmt_1__ ms · önbellekler __ocs_fmt_2__ ms · xref __ocs_fmt_3__ ms · toplam __ocs_fmt_4__ ms .archive-save-the-drawing-first-it-has-no-file = ARCHIVE: önce çizimi kaydedin; çizimin henüz bir dosya yolu yok. .autosaved-1-drawing = 1 çizim otomatik kaydedildi diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index 9b68e5fe7..13d29f252 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -2387,6 +2387,7 @@ command = file = .already-loaded-different-contents-kept = "__ocs_fmt_0__"已以不同内容加载;保留了现有文件。 + .image-refusal-unknown-size = 无法从文件头测得“__ocs_fmt_0__”的尺寸,解码将超过本版本 __ocs_fmt_1__ MB 的图像上限——无法附着。 .parse-ms-purge-ms-caches-ms-xref-ms-total-ms = 解析 __ocs_fmt_0__ 毫秒 · 清除 __ocs_fmt_1__ 毫秒 · 缓存 __ocs_fmt_2__ 毫秒 · xref __ocs_fmt_3__ 毫秒 · 总计 __ocs_fmt_4__ 毫秒 .archive-save-the-drawing-first-it-has-no-file = ARCHIVE:请先保存图形(尚未设置文件路径)。 .autosaved-1-drawing = 自动保存了 1 个绘图 diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index 2754c36a9..0000b1422 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -2399,6 +2399,7 @@ command = file = .already-loaded-different-contents-kept = "__ocs_fmt_0__"已以不同內容載入;保留了現有檔案。 + .image-refusal-unknown-size = 無法從檔頭測得「__ocs_fmt_0__」的尺寸,解碼將超過本版本 __ocs_fmt_1__ MB 的影像上限——無法附加。 .parse-ms-purge-ms-caches-ms-xref-ms-total-ms = 解析 __ocs_fmt_0__ 毫秒 · 清除 __ocs_fmt_1__ 毫秒 · 快取 __ocs_fmt_2__ 毫秒 · xref __ocs_fmt_3__ 毫秒 · 總計 __ocs_fmt_4__ 毫秒 .archive-save-the-drawing-first-it-has-no-file = ARCHIVE:請先儲存圖形(尚未設定檔案路徑)。 .autosaved-1-drawing = 自動儲存了 1 個繪圖 diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 3813603c3..03330fd51 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -2484,8 +2484,8 @@ impl OpenCADStudio { self.tabs[i].dirty = false; } } - Err(error) => self.command_line.push_error( - crate::tf!("SAVEALL: {} failed: {error}", entry.path.display()).as_ref(), + Err(e) => self.command_line.push_error( + crate::tf!("SAVEALL: {} failed: {e}", entry.path.display()).as_ref(), ), } } diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index d277ae755..ea13822f6 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -734,6 +734,14 @@ impl OpenCADStudio { } } registered.extend(fetched); + if crate::scene::model::image_model::take_overflowed() { + // A settled fetch's rebuild trigger was dropped at the + // outcome cap; do one unconditional catch-up pass over + // every registered name. + registered.extend( + crate::scene::model::image_model::registered_image_names(), + ); + } if !registered.is_empty() { for tab in self.tabs.iter_mut() { tab.scene.resolve_registered_image_names(®istered); diff --git a/src/io/mod.rs b/src/io/mod.rs index d4a836592..15f71ccd2 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -2045,7 +2045,7 @@ pub async fn pick_image_file() -> Result<(PathBuf, u32, u32), String> { // references, which must survive for their own drains. crate::scene::model::image_model::decode_refusals() .iter() - .find(|note| note.contains(short.as_str())) + .find(|note| note.contains(&format!("\"{}\"", short))) .cloned() .unwrap_or_else(|| format!("Unsupported or corrupt image \"{short}\"")) })?; diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index fc2630f20..a95b414ca 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -2280,6 +2280,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")), "\"{name}\" is already loaded with different contents; the existing file was kept." => Some(("file", "already-loaded-different-contents-kept")), + "\"{name}\" could not be sized from its header and decoding it exceeded the {limit} MB image limit of this build — it cannot be attached." => Some(("file", "image-refusal-unknown-size")), "Opening Patreon page..." => Some(("view", "opening-patreon-page")), "Opening feedback page..." => Some(("view", "opening-feedback-page")), "Opening file" => Some(("ui", "opening-file")), @@ -2701,6 +2702,8 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "SAVEALL: saved {saved} drawing(s){}." => Some(("file", "saveall-saved-saved-drawing-s")), "SAVEALL: {} already has a save running" => Some(("file", "saveall-already-has-a-save-running")), "SAVEALL: {} failed: {e}" => Some(("file", "saveall-failed-e")), + "At most {MAX_EXTRACTION_SOURCES} drawings can be added in the browser." => Some(("file", "extraction-max-sources-web")), + "\"{}\" is already open in another tab. On the web both tabs share one stored copy — the last save overwrites the other." => Some(("file", "web-already-open-shared-copy")), "SAVETIME autosave interval in minutes (0 = off):" => Some(("styles", "savetime-autosave-interval-in-minutes-0-off")), "SAVETIME = {}" => Some(("styles", "savetime")), "SCALE Specify base point [%{count} objects]:" => Some(("modify", "scale-specify-base-point-count-objects")), @@ -5281,6 +5284,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Enter a GitHub URL or repository in owner/repo format." => Some(("common", "enter-a-github-url-or-repository-in-owner-repo-format")), "DWGUNITS Convert drawing to:" => Some(("common", "dwgunits-convert-drawing-to")), "Opened plot preview." => Some(("common", "opened-plot-preview")), + "Plot preview" => Some(("common", "plot-preview-task")), "%{cmd} Select objects or [Settings] (%{count} selected, Enter to apply):" => Some(("common", "select-objects-or-settings-selected-enter-to-apply")), "%{cmd} Select objects or [Settings]:" => Some(("common", "select-objects-or-settings")), "+ Add parameter" => Some(("common", "add-parameter")), diff --git a/src/scene/model/image_model.rs b/src/scene/model/image_model.rs index 7d186e45f..6f555ede8 100644 --- a/src/scene/model/image_model.rs +++ b/src/scene/model/image_model.rs @@ -183,6 +183,19 @@ impl ImageModel { pub fn from_raster_image(img: &codec::entities::RasterImage) -> Option { let w = img.size.x; let h = img.size.y; + // A degenerate or non-finite size/vectors (hostile entity data) + // produce NaN placement math that aborts whole PDF exports; treat + // it as an unresolvable reference instead. + if !(w.is_finite() && h.is_finite() && w > 0.0 && h > 0.0) { + return None; + } + let vector_ok = |v: &codec::Vector3| { + let (x, y, z) = (v.x, v.y, v.z); + x.is_finite() && y.is_finite() && z.is_finite() && (x != 0.0 || y != 0.0 || z != 0.0) + }; + if !vector_ok(&img.u_vector) || !vector_ok(&img.v_vector) { + return None; + } // Model-space geometry is drawn in (WCS - world_offset) so large UTM- // scale coordinates stay within f32 precision; offset the image too. // Corners come from a large insertion point plus small u/v spans. @@ -504,6 +517,14 @@ fn image_cache() -> &'static Mutex> { CACHE.get_or_init(|| Mutex::new(HashMap::new())) } +/// Insertion order of the source cache's keys, so by-name resolution has a +/// deterministic winner (the MOST RECENT registration) instead of HashMap's +/// unspecified iteration order. +fn source_order() -> &'static Mutex> { + static ORDER: OnceLock>> = OnceLock::new(); + ORDER.get_or_init(|| Mutex::new(Vec::new())) +} + fn source_cache() -> &'static Mutex>>> { static SOURCES: OnceLock>>>> = OnceLock::new(); SOURCES.get_or_init(|| Mutex::new(HashMap::new())) @@ -536,10 +557,18 @@ pub(crate) fn same_file_name(a: &str, b: &str) -> bool { /// finally supplies a missing reference) re-decodes instead of replaying a /// stale success or a cached failure. pub fn register_source(path: &str, bytes: Arc>) { - source_cache() - .lock() - .unwrap_or_else(|error| error.into_inner()) - .insert(path.to_string(), bytes); + { + let mut sources = source_cache() + .lock() + .unwrap_or_else(|error| error.into_inner()); + if !sources.contains_key(path) { + source_order() + .lock() + .unwrap_or_else(|error| error.into_inner()) + .push(path.to_string()); + } + sources.insert(path.to_string(), bytes); + } if let Ok(mut cache) = image_cache().lock() { cache.retain(|cached, _| !same_file_name(cached, path)); } @@ -574,10 +603,17 @@ fn registered_bytes_by_name(path: &str) -> Option>> { let cache = source_cache() .lock() .unwrap_or_else(|error| error.into_inner()); - cache + // Deterministic winner: walk the insertion order newest-first so the + // most recent drop/pick wins, instead of HashMap iteration order. + let order = source_order() + .lock() + .unwrap_or_else(|error| error.into_inner()); + order .iter() - .find(|(key, _)| same_file_name(key, path)) - .map(|(_, bytes)| Arc::clone(bytes)) + .rev() + .filter(|key| cache.contains_key(*key)) + .find(|key| same_file_name(key, path)) + .and_then(|key| cache.get(key).cloned()) } /// Original image file bytes registered for a reference, resolved the way @@ -701,12 +737,51 @@ enum DecodeTier { 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). + /// The decode's peak memory would exceed `MAX_DECODE_BYTES`. The probe + /// bills the peak of the decoder's NATIVE bytes per pixel and the final + /// RGBA buffer (4/px): both transiently coexist mid-decode on 16-bit + /// formats, and the decoder's own limits reserve at native bpp — so an + /// image is refused up front when either exceeds the budget, without + /// decoding. OverBudget, } +/// The decoder's own memory accounting for a format: how many bytes per +/// pixel the DECODE transiently needs (native layout, plus the RGBA output +/// where they coexist) and any codec-specific scratch multiplier on top. +struct NativeFootprint { + /// Peak bytes per pixel during decode (native bpp + RGBA where both + /// live simultaneously; plain 8-bit formats overlap them in place). + peak_bpp: u64, + /// Extra codec scratch allowance the decoder stacks on the reserve + /// (image's TIFF codec needs ~3x in the worst strip/tile layout). + codec_factor: u64, +} + +fn native_footprint(format: Option, color: Option) -> NativeFootprint { + let native_bpp = color.map_or(4u64, |color| match color { + image::ColorType::L8 => 1, + image::ColorType::La8 => 2, + image::ColorType::Rgb8 => 3, + image::ColorType::Rgba8 => 4, + image::ColorType::L16 => 2, + image::ColorType::La16 => 4, + image::ColorType::Rgb16 => 6, + image::ColorType::Rgba16 => 8, + image::ColorType::Rgb32F => 12, + image::ColorType::Rgba32F => 16, + _ => 4, + }); + // A 16-bit frame and its RGBA conversion can coexist; an 8-bit one is + // converted with at most a small row buffer in flight. + let peak_bpp = if native_bpp > 4 { native_bpp + 4 } else { 4 }; + let codec_factor = match format { + Some(image::ImageFormat::Tiff) => 3, + _ => 1, + }; + NativeFootprint { peak_bpp, codec_factor } +} + /// 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. @@ -715,9 +790,27 @@ fn tier_for( height: u32, max_decode_bytes: u64, max_display_pixels: u64, +) -> DecodeTier { + tier_for_native(width, height, 4, 1, max_decode_bytes, max_display_pixels) +} + +/// [`tier_for`] with the probed format's native footprint: the caller +/// supplies the probe's color type and format so 16-bit images are billed +/// at what their decode actually allocates. +fn tier_for_native( + width: u32, + height: u32, + peak_bpp: u64, + codec_factor: u64, + 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 { + if pixels + .saturating_mul(peak_bpp) + .saturating_mul(codec_factor) + > max_decode_bytes + { DecodeTier::OverBudget } else if pixels > max_display_pixels { DecodeTier::Downsample @@ -766,24 +859,45 @@ fn decode_with_budgets( max_decode_bytes: u64, max_display_pixels: u64, ) -> DecodeAttempt { - // Probe pass: header dimensions only, no pixel decode. + // Probe pass: header dimensions AND color type, no pixel decode. The + // probe reader carries the full budget too so constructing a decoder on + // a huge-but-in-budget header cannot itself fail on image's default + // 512 MiB probe limits. 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) + .and_then(|mut reader| { + let format = reader.format(); + let mut probe_limits = image::Limits::default(); + probe_limits.max_alloc = Some(max_decode_bytes); + reader.limits(probe_limits); + use image::ImageDecoder as _; + let decoder = reader.into_decoder().ok()?; + let (width, height) = decoder.dimensions(); + let color = decoder.color_type(); + Some((width, height, format, color)) + }); + let tier = probed.map_or(DecodeTier::Full, |(width, height, format, color)| { + let footprint = native_footprint(format, Some(color)); + tier_for_native( + width, + height, + footprint.peak_bpp, + footprint.codec_factor, + max_decode_bytes, + max_display_pixels, + ) }); - // Trust the probe, not the decoder limits: `Limits::reserve` checks the - // decoder's NATIVE bytes-per-pixel, so a grayscale/RGB JPEG/PNG/BMP over - // the RGBA budget decodes fine and then `into_rgba8()` balloons to up to - // 4x it — exactly the tab death this exists to prevent. The probed - // dimensions already prove it cannot fit; refuse without decoding. + // Trust the probe, not the decoder limits: the tier above bills the + // decode's real peak (native bpp, codec scratch), so an OverBudget + // image provably cannot fit; refuse without decoding. This covers the + // grayscale/RGB over-RGBA-budget balloon AND the 16-bit formats whose + // native layout is double the RGBA size. if tier == DecodeTier::OverBudget { return DecodeAttempt::Refused { - width: probed.map_or(0, |(width, _)| width), - height: probed.map_or(0, |(_, height)| height), + width: probed.map_or(0, |(width, _, _, _)| width), + height: probed.map_or(0, |(_, height, _, _)| height), }; } @@ -792,24 +906,32 @@ fn decode_with_budgets( Err(_) => return DecodeAttempt::Failed, }; // For formats that FIT the budget, the limits still guard runaway - // allocations mid-decode. TIFF additionally stacks its own intermediate - // scratch buffers on top of the reserve (empirically ~3x the frame size - // in image 0.25), so refusing an in-budget TIFF at face value would - // produce a note claiming e.g. "400 MB, over the 512 MB limit". - let tiff = matches!(reader.format(), Some(image::ImageFormat::Tiff)); + // allocations mid-decode. GIF is special: the probe reads the LOGICAL + // SCREEN, but a crafted frame may declare itself arbitrarily larger + // and the gif crate does not enforce frame-vs-screen consistency — so + // the frame buffer is capped to the probed size plus slack, refusing + // frame bombs into the existing Refused path. TIFF stacks its own + // intermediate scratch on top of the reserve (~3x worst case in image + // 0.25); the tier already folded that factor in, and the allowance + // matches it so an in-tier TIFF never codec-refuses. let mut limits = image::Limits::default(); - limits.max_alloc = Some(if tiff { - max_decode_bytes.saturating_mul(3) - } else { - max_decode_bytes + limits.max_alloc = Some(match reader.format() { + Some(image::ImageFormat::Gif) => probed + .map(|(width, height, _, _)| { + (width as u64).saturating_mul(height as u64).saturating_mul(4) + 64 * 1024 * 1024 + }) + .unwrap_or(max_decode_bytes) + .min(max_decode_bytes), + Some(image::ImageFormat::Tiff) => max_decode_bytes.saturating_mul(3), + _ => 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), + width: probed.map_or(0, |(width, _, _, _)| width), + height: probed.map_or(0, |(_, height, _, _)| height), }; } Err(_) => return DecodeAttempt::Failed, @@ -834,7 +956,7 @@ fn decode_with_budgets( 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)); + let (source_width, source_height) = probed.map_or((stored_w, stored_h), |(w, h, _, _)| (w, h)); DecodeAttempt::Decoded(DecodedImage { pixels: Arc::new(rgba.into_raw()), width: stored_w, @@ -873,6 +995,16 @@ fn refusal_message(path: &str, width: u32, height: u32) -> String { .file_name() .map(|name| name.to_string_lossy().into_owned()) .unwrap_or_else(|| path.to_string()); + if width == 0 || height == 0 { + // The probe could not determine a size (exotic header) — do not + // print a self-contradictory "0x0 px" figure. + return crate::tf!( + "\"{name}\" could not be sized from its header and decoding it exceeded the {limit} MB image limit of this build — it cannot be attached.", + name = name, + limit = MAX_DECODE_BYTES / (1024 * 1024) + ) + .into_owned(); + } let decoded_mb = (width as u64).saturating_mul(height as u64).saturating_mul(4) / (1024 * 1024); let limit_mb = MAX_DECODE_BYTES / (1024 * 1024); crate::tf!( @@ -1029,11 +1161,32 @@ mod web_fetch { web_sys::console::warn_1(&wasm_bindgen::JsValue::from_str( "ocs: remote-image outcome queue full; dropping an outcome", )); + // The dropped outcome may be a Loaded whose rebuild trigger is + // lost; remember that so the next poll does one unconditional + // image re-resolution instead of waiting for another event. + *overflowed() + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) = true; return; } queue.push_back(outcome); } + /// Whether an outcome was dropped at the cap and a catch-up rebuild is + /// owed. [`take_overflowed`] consumes it. + fn overflowed() -> &'static Mutex { + static FLAG: OnceLock> = OnceLock::new(); + FLAG.get_or_init(|| Mutex::new(false)) + } + + /// Consume the dropped-outcome flag (see [`overflowed`]). + pub fn take_overflowed() -> bool { + let mut flag = overflowed() + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + std::mem::replace(&mut *flag, false) + } + /// Pop the oldest settled remote fetch the poll has not handled yet. pub fn take_outcome() -> Option { outcomes() @@ -1054,6 +1207,58 @@ mod web_fetch { } } + /// Read a response body incrementally, refusing past [`MAX_BYTES`] the + /// moment the cap is crossed (not after buffering the whole body). + async fn stream_body_capped( + window: &web_sys::Window, + response: &web_sys::Response, + ) -> Result, RemoteFetchError> { + use wasm_bindgen::JsCast; + use wasm_bindgen_futures::JsFuture; + + let Some(stream) = response.body() else { + // No stream API (or already consumed): fall back to buffering, + // with the received-length check still applying after. + 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); + } + return Ok(bytes); + }; + let reader = stream.get_reader().dyn_into::() + .map_err(|_| RemoteFetchError::Network)?; + let mut bytes: Vec = Vec::new(); + loop { + let chunk = JsFuture::from(reader.read()) + .await + .map_err(|error| rejection_kind(&error))?; + let done = js_sys::Reflect::get(&chunk, &wasm_bindgen::JsValue::from_str("done")) + .ok() + .and_then(|value| value.as_bool()) + .unwrap_or(true); + if done { + break; + } + let value = js_sys::Reflect::get(&chunk, &wasm_bindgen::JsValue::from_str("value")) + .map_err(|_| RemoteFetchError::Network)?; + let view = js_sys::Uint8Array::new(&value); + if bytes.len() as u64 + view.length() as u64 > MAX_BYTES { + // Release the reader lock so the underlying connection can + // be torn down, then refuse. + let _ = JsFuture::from(reader.cancel()).await; + return Err(RemoteFetchError::TooLarge); + } + bytes.extend_from_slice(&view.to_vec()); + } + let _ = window; // reader holds the stream; window kept for symmetry + Ok(bytes) + } + /// 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. @@ -1092,8 +1297,9 @@ mod web_fetch { 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). + // it is buffered; then STREAM the body, aborting the moment the + // received bytes pass MAX_BYTES — array_buffer() would materialize + // a hostile chunked reply in full before any check could fire. let declared = response .headers() .get("Content-Length") @@ -1103,18 +1309,7 @@ mod web_fetch { 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) + stream_body_capped(&window, &response).await } /// Start the background fetch for `url` if one hasn't been attempted this @@ -1155,7 +1350,8 @@ fn fetch_remote(url: &str) -> Option> { #[cfg(target_arch = "wasm32")] pub use web_fetch::{ - take_outcome as take_remote_fetch_outcome, RemoteFetchError, RemoteFetchOutcome, + take_outcome as take_remote_fetch_outcome, take_overflowed, + RemoteFetchError, RemoteFetchOutcome, }; #[cfg(all(test, not(target_arch = "wasm32")))]