From 92edc6e4643a095da03a56602c89c0825b87b7dd Mon Sep 17 00:00:00 2001 From: cre8ive-marketing <168519979+cre8ive-marketing@users.noreply.github.com> Date: Fri, 23 Oct 2026 17:06:58 +0530 Subject: [PATCH 1/4] CAD: choose the AutoCAD version to write DWG and DXF in (#489) A preset that writes DWG or DXF has an AutoCAD version: 2018 (default), 2013, 2010, 2007, 2004, 2000 or the source's own (older than 2000 becomes 2000), and DWG -> DWG / DXF -> DXF can change the version. - ACadSharp 3.8 can't write a 2007 DWG: chosen, it is 2004 (AutoCAD 2007 opens it); kept from a 2007 source, 2010. - Before 2007 text is in $DWGCODEPAGE. CadCodePages picks the code page that holds most of the text (the drawing's own on a tie; Korean for Hangul, Japanese for Kana, Big5 then GBK for Chinese, East Asian pages only for East Asian text) and writes anything it can't hold as \U+XXXX. Korean goes out as ANSI_949 like AutoCAD writes it (ACadSharp needs KCS5601 to encode it, so the DXF header is renamed after writing). - Objects the version doesn't have are left out with a note: tables and arc dimensions before 2004, multileaders and PDF underlays before 2007, meshes before 2010. - SVG -> DXF honours the version too. --- docs/ENGINES.md | 15 +- src/Filee.App/Assets/i18n/ar.json | 5 + src/Filee.App/Assets/i18n/en.json | 5 + src/Filee.App/Assets/i18n/es.json | 5 + src/Filee.App/Assets/i18n/fr.json | 5 + src/Filee.App/Assets/i18n/hi.json | 5 + src/Filee.App/Assets/i18n/ja.json | 5 + src/Filee.App/Assets/i18n/ko.json | 5 + src/Filee.App/Assets/i18n/ne.json | 5 + src/Filee.App/Assets/i18n/pl.json | 5 + src/Filee.App/Assets/i18n/pt-BR.json | 5 + src/Filee.App/Assets/i18n/ru.json | 5 + src/Filee.App/Assets/i18n/th.json | 5 + src/Filee.App/Assets/i18n/zh-CN.json | 5 + src/Filee.App/Assets/i18n/zh-TW.json | 5 + .../ViewModels/PresetEditorViewModel.cs | 16 +- src/Filee.App/Views/PresetEditorView.axaml | 14 ++ src/Filee.Core/Presets/Preset.cs | 30 ++++ src/Filee.Engines/Cad/CadCodePages.cs | 125 ++++++++++++++ src/Filee.Engines/Cad/CadConverter.cs | 6 +- src/Filee.Engines/Cad/CadFile.cs | 94 ++++++++++- src/Filee.Engines/Vector/SvgDxfWriter.cs | 6 +- src/Filee.Engines/Vector/VectorConverter.cs | 2 +- tests/Filee.App.Tests/RenderTests.cs | 22 +++ tests/Filee.Engines.Tests/CadVersionTests.cs | 154 ++++++++++++++++++ 25 files changed, 538 insertions(+), 16 deletions(-) create mode 100644 src/Filee.Engines/Cad/CadCodePages.cs create mode 100644 tests/Filee.Engines.Tests/CadVersionTests.cs diff --git a/docs/ENGINES.md b/docs/ENGINES.md index 616f82221..9ec93e8c3 100644 --- a/docs/ENGINES.md +++ b/docs/ENGINES.md @@ -291,7 +291,7 @@ route they have no SVG target (PDF → SVG, for example, needs LibreOffice). The ## SVG → DXF / DWG -`Vector/SvgDxfWriter.cs` writes the drawing's outlines with ACadSharp (DXF, AutoCAD 2018 like the CAD engine; DWG is +`Vector/SvgDxfWriter.cs` writes the drawing's outlines with ACadSharp (DXF in the preset's version like the CAD engine; DWG is DXF → DWG by the CAD engine). The SVG is loaded with Svg.Skia as for PDF output, so groups, transforms, ``, nested ``, CSS and text are resolved the same way. @@ -848,8 +848,17 @@ and writes the files; Filee's own renderer draws them with SkiaSharp (one drawin 2018, ASCII and binary. DWG R13 and older fail with "version not supported". Korean AutoCAD writes pre-2007 DXF with the code page name `ANSI_949`, which ACadSharp 3.8 does not map; Filee rewrites that header in memory so the text is decoded as CP949 (`CadFile.OpenDxf`, covered by a test that fails once ACadSharp handles it). -- **Writing** (DWG ↔ DXF): always **AutoCAD 2018 (AC1032)** — the current DWG format (AutoCAD 2018 and later, DWG - TrueView, BricsCAD, ODA tools); 2018 DXF stores text as UTF-8. ACadSharp's DWG writer is younger than its reader +- **Writing** (DWG ↔ DXF, and DWG → DWG / DXF → DXF in another version): **AutoCAD 2018 (AC1032)** by default — the + current DWG format (AutoCAD 2018 and later, DWG TrueView, BricsCAD, ODA tools); 2018 DXF stores text as UTF-8. + A preset can choose an older version (#489, `CadOptions.Version`): 2013, 2010, 2007, 2004, 2000, or the source's + own version (older than 2000 becomes 2000). ACadSharp 3.8 can't write a 2007 DWG: chosen, it is written as 2004, + which AutoCAD 2007 opens; kept from a 2007 source, as 2010. Before 2007, text is stored in the drawing's code page + (`$DWGCODEPAGE`), so `CadCodePages` picks the one that holds the most of the drawing's text (its own on a tie; + Korean for Hangul, Japanese for Kana, Big5 before GBK for Chinese, East Asian pages only for East Asian text) and + writes any character it can't hold as `\U+XXXX`. Korean is written as `ANSI_949`, as AutoCAD names it (ACadSharp + needs `KCS5601` to encode it, so the DXF header is renamed after writing). Objects the chosen version doesn't + have are left out with a note: tables and arc dimensions before 2004, multileaders and PDF underlays before 2007, + meshes before 2010. ACadSharp's DWG writer is younger than its reader and its DXF writer, so DXF is the safer format to hand on. Objects ACadSharp does not model (proxy and AEC objects, some 3D and underlay data) are not written; degenerate dimensions it cannot measure are left out of DWG output instead of failing the file. diff --git a/src/Filee.App/Assets/i18n/ar.json b/src/Filee.App/Assets/i18n/ar.json index 3f7676405..25a1de7d0 100644 --- a/src/Filee.App/Assets/i18n/ar.json +++ b/src/Filee.App/Assets/i18n/ar.json @@ -104,6 +104,7 @@ "error.geo_unsupported_shape": "هذا النوع من الأشكال غير مدعوم", "error.geo_too_large": "سيتجاوز ملف Shapefile حد الصيغة البالغ 2 غيغابايت", "notice.pdf_not_smaller": "تعذّر تقليل الحجم، لذا نُسخ الملف كما هو", + "notice.cad_newer_left_out": "حُذفت كائنات لا يدعمها إصدار AutoCAD المختار (أسهم توضيحية متعددة، شبكات …)", "notice.empty_spreadsheet_pages_removed": "تمت إزالة صفحات جدول البيانات الفارغة", "notice.live_photo_pair": "حُفظ كصورة وملف MOV بالاسم نفسه. استوردهما معًا (صور iCloud أو AirDrop أو تطبيق الصور على Mac) لتحصل على Live Photo", "notice.geo_datum_assumed": "المرجع الجيوديسي ليس WGS 84؛ حُوّلت المواقع على افتراض أنه كذلك، وقد تنحرف بمقدار يصل إلى مئات الأمتار", @@ -450,6 +451,9 @@ "presets.name": "الاسم", "presets.target": "التحويل إلى", "presets.same_as_source": "صيغة الملف الأصلي نفسها", + "presets.cad_version": "إصدار AutoCAD", + "presets.cad_version.same": "مثل المصدر", + "presets.cad_version_hint": "تُفتح الإصدارات الأقدم في برامج AutoCAD وCAD الأقدم. قبل 2007 يُحفظ النص بصفحة الترميز التي تناسبه (الكورية، الصينية، اليابانية …) وأي حرف آخر كرمز \\U+. تُحذف الكائنات التي لا يدعمها الإصدار، مثل الأسهم التوضيحية المتعددة والشبكات. يُحفظ DWG بإصدار 2007 كإصدار 2004 الذي يفتحه AutoCAD 2007.", "presets.group.image": "الصورة", "presets.quality": "الجودة", "presets.resize": "تغيير الحجم", @@ -584,6 +588,7 @@ "presets.combine": "جمع كل الملفات في أرشيف واحد", "presets.extract_note": "يُستخرج كل أرشيف إلى مجلد مستقل. يُحدَّد موقعه ضمن الإخراج.", "presets.group.trace": "التتبّع", + "presets.group.cad": "رسم CAD", "presets.trace_note": "يُستخدم عند تتبّع صورة (PNG أو JPG أو غيرها) إلى أشكال متجهية.", "presets.trace_mode": "الوضع", "presets.trace_mode.Color": "ملوّن", diff --git a/src/Filee.App/Assets/i18n/en.json b/src/Filee.App/Assets/i18n/en.json index 74117899b..7ef5e906c 100644 --- a/src/Filee.App/Assets/i18n/en.json +++ b/src/Filee.App/Assets/i18n/en.json @@ -110,6 +110,7 @@ "error.geo_unsupported_shape": "This kind of shape isn't supported", "error.geo_too_large": "The Shapefile would pass the format's 2 GB limit", "notice.pdf_not_smaller": "Couldn't make it smaller, so it was copied as it is", + "notice.cad_newer_left_out": "Objects the chosen AutoCAD version doesn't have (multileaders, meshes …) were left out", "notice.empty_spreadsheet_pages_removed": "Empty spreadsheet pages were removed", "notice.live_photo_pair": "Saved as a photo and a MOV with the same name. Import both together (iCloud Photos, AirDrop or Photos on a Mac) to get a Live Photo", "notice.geo_datum_assumed": "The datum isn't WGS 84; positions were converted as if it were and can be off by up to hundreds of metres", @@ -465,6 +466,9 @@ "presets.name": "Name", "presets.target": "Convert to", "presets.same_as_source": "Same format as the source", + "presets.cad_version": "AutoCAD version", + "presets.cad_version.same": "Same as the source", + "presets.cad_version_hint": "Older versions open in older AutoCAD and CAD programs. Before 2007, text is saved in the code page that fits it (Korean, Chinese, Japanese …) and any other character as a \\U+ code. Objects the version doesn't have, such as multileaders and meshes, are left out. A 2007 DWG is saved as 2004, which AutoCAD 2007 opens.", "presets.group.image": "Image", "presets.quality": "Quality", "presets.resize": "Resize", @@ -599,6 +603,7 @@ "presets.combine": "Put all files into one archive", "presets.extract_note": "Each archive is extracted into a folder of its own. Where it goes is set under Output.", "presets.group.trace": "Tracing", + "presets.group.cad": "CAD drawing", "presets.trace_note": "Used when a picture (PNG, JPG, …) is traced into vector shapes.", "presets.trace_mode": "Mode", "presets.trace_mode.Color": "Colour", diff --git a/src/Filee.App/Assets/i18n/es.json b/src/Filee.App/Assets/i18n/es.json index a0d054174..4327ae459 100644 --- a/src/Filee.App/Assets/i18n/es.json +++ b/src/Filee.App/Assets/i18n/es.json @@ -104,6 +104,7 @@ "error.geo_unsupported_shape": "Este tipo de forma no es compatible", "error.geo_too_large": "El Shapefile superaría el límite de 2 GB del formato", "notice.pdf_not_smaller": "No se pudo reducir su tamaño, por lo que se copió tal cual", + "notice.cad_newer_left_out": "Se omitieron objetos que la versión de AutoCAD elegida no tiene (directrices múltiples, mallas…)", "notice.empty_spreadsheet_pages_removed": "Se eliminaron las páginas vacías de la hoja de cálculo", "notice.live_photo_pair": "Guardado como una foto y un MOV con el mismo nombre. Impórtalos juntos (Fotos de iCloud, AirDrop o Fotos en un Mac) para obtener una Live Photo", "notice.geo_datum_assumed": "El datum no es WGS 84; las posiciones se convirtieron como si lo fuera y pueden desviarse hasta cientos de metros", @@ -450,6 +451,9 @@ "presets.name": "Nombre", "presets.target": "Convertir a", "presets.same_as_source": "El mismo formato que el original", + "presets.cad_version": "Versión de AutoCAD", + "presets.cad_version.same": "Igual que el original", + "presets.cad_version_hint": "Las versiones antiguas se abren en AutoCAD y programas CAD antiguos. Antes de 2007, el texto se guarda en la página de códigos que le corresponde (coreano, chino, japonés…) y cualquier otro carácter como código \\U+. Los objetos que esa versión no tiene, como directrices múltiples y mallas, se omiten. Un DWG 2007 se guarda como 2004, que AutoCAD 2007 abre.", "presets.group.image": "Imagen", "presets.quality": "Calidad", "presets.resize": "Cambiar tamaño", @@ -584,6 +588,7 @@ "presets.combine": "Empaquetar todos los archivos en uno solo", "presets.extract_note": "Cada archivo comprimido se extrae en una carpeta propia. El destino se define en Salida.", "presets.group.trace": "Trazado", + "presets.group.cad": "Dibujo CAD", "presets.trace_note": "Se usa cuando una imagen (PNG, JPG, …) se traza a formas vectoriales.", "presets.trace_mode": "Modo", "presets.trace_mode.Color": "Color", diff --git a/src/Filee.App/Assets/i18n/fr.json b/src/Filee.App/Assets/i18n/fr.json index 078bceb2b..f93cae054 100644 --- a/src/Filee.App/Assets/i18n/fr.json +++ b/src/Filee.App/Assets/i18n/fr.json @@ -110,6 +110,7 @@ "error.geo_unsupported_shape": "Ce type de forme n'est pas pris en charge", "error.geo_too_large": "Le Shapefile dépasserait la limite de 2 Go du format", "notice.pdf_not_smaller": "Impossible de réduire sa taille : le fichier a été copié tel quel", + "notice.cad_newer_left_out": "Des objets que la version d'AutoCAD choisie n'a pas (multilignes de repère, maillages…) ont été omis", "notice.empty_spreadsheet_pages_removed": "Les pages vides de la feuille de calcul ont été supprimées", "notice.live_photo_pair": "Enregistré en une photo et un MOV de même nom. Importez-les ensemble (Photos iCloud, AirDrop ou Photos sur Mac) pour obtenir une Live Photo", "notice.geo_datum_assumed": "Le datum n'est pas WGS 84 ; les positions ont été converties comme s'il l'était et peuvent être décalées de plusieurs centaines de mètres", @@ -465,6 +466,9 @@ "presets.name": "Nom", "presets.target": "Convertir en", "presets.same_as_source": "Même format que l'original", + "presets.cad_version": "Version d'AutoCAD", + "presets.cad_version.same": "Comme l'original", + "presets.cad_version_hint": "Les versions anciennes s'ouvrent dans les anciens AutoCAD et logiciels de CAO. Avant 2007, le texte est enregistré dans la page de codes qui lui convient (coréen, chinois, japonais…) et tout autre caractère sous forme de code \\U+. Les objets que la version n'a pas, comme les multilignes de repère et les maillages, sont omis. Un DWG 2007 est enregistré en 2004, qu'AutoCAD 2007 ouvre.", "presets.group.image": "Image", "presets.quality": "Qualité", "presets.resize": "Redimensionner", @@ -599,6 +603,7 @@ "presets.combine": "Réunir tous les fichiers dans une seule archive", "presets.extract_note": "Chaque archive est extraite dans son propre dossier. Son emplacement se règle dans Sortie.", "presets.group.trace": "Vectorisation", + "presets.group.cad": "Dessin CAO", "presets.trace_note": "Utilisé quand une image (PNG, JPG, …) est vectorisée en formes vectorielles.", "presets.trace_mode": "Mode", "presets.trace_mode.Color": "Couleur", diff --git a/src/Filee.App/Assets/i18n/hi.json b/src/Filee.App/Assets/i18n/hi.json index 7c4e0678d..af5a76197 100644 --- a/src/Filee.App/Assets/i18n/hi.json +++ b/src/Filee.App/Assets/i18n/hi.json @@ -104,6 +104,7 @@ "error.geo_unsupported_shape": "इस तरह का आकार समर्थित नहीं है", "error.geo_too_large": "Shapefile फ़ॉर्मैट की 2 GB सीमा से बड़ी हो जाएगी", "notice.pdf_not_smaller": "इसे छोटा नहीं किया जा सका, इसलिए जैसा है वैसा ही कॉपी कर दिया गया", + "notice.cad_newer_left_out": "चुने गए AutoCAD संस्करण में न होने वाले ऑब्जेक्ट (मल्टीलीडर, मेश …) छोड़ दिए गए", "notice.empty_spreadsheet_pages_removed": "स्प्रेडशीट के खाली पृष्ठ हटा दिए गए", "notice.live_photo_pair": "एक ही नाम की फ़ोटो और MOV के रूप में सहेजा गया। लाइव फ़ोटो पाने के लिए दोनों को साथ में इम्पोर्ट करें (iCloud Photos, AirDrop या Mac पर Photos)", "notice.geo_datum_assumed": "डेटम WGS 84 नहीं है। स्थितियाँ ऐसे कन्वर्ट की गईं मानो वह WGS 84 हो, इसलिए वे कुछ सौ मीटर तक खिसक सकती हैं", @@ -450,6 +451,9 @@ "presets.name": "नाम", "presets.target": "इसमें कन्वर्ट करें", "presets.same_as_source": "स्रोत जैसा ही फ़ॉर्मैट", + "presets.cad_version": "AutoCAD संस्करण", + "presets.cad_version.same": "स्रोत जैसा", + "presets.cad_version_hint": "पुराने संस्करण पुराने AutoCAD और CAD प्रोग्राम में खुलते हैं। 2007 से पहले, टेक्स्ट उसके अनुकूल कोड पेज (कोरियाई, चीनी, जापानी …) में सेव होता है और बाकी हर अक्षर \\U+ कोड के रूप में। जो ऑब्जेक्ट उस संस्करण में नहीं होते, जैसे मल्टीलीडर और मेश, छोड़ दिए जाते हैं। 2007 DWG को 2004 के रूप में सेव किया जाता है, जिसे AutoCAD 2007 खोलता है।", "presets.group.image": "इमेज", "presets.quality": "क्वालिटी", "presets.resize": "आकार बदलें", @@ -584,6 +588,7 @@ "presets.combine": "सभी फ़ाइलों को एक आर्काइव में रखें", "presets.extract_note": "हर आर्काइव अपने अलग फ़ोल्डर में निकाला जाता है। वह कहाँ जाएगा यह आउटपुट में तय होता है।", "presets.group.trace": "ट्रेसिंग", + "presets.group.cad": "CAD ड्रॉइंग", "presets.trace_note": "तब इस्तेमाल होता है जब किसी तस्वीर (PNG, JPG, …) को वेक्टर आकृतियों में ट्रेस किया जाता है।", "presets.trace_mode": "मोड", "presets.trace_mode.Color": "रंगीन", diff --git a/src/Filee.App/Assets/i18n/ja.json b/src/Filee.App/Assets/i18n/ja.json index 287df55c1..9b1b8ee3e 100644 --- a/src/Filee.App/Assets/i18n/ja.json +++ b/src/Filee.App/Assets/i18n/ja.json @@ -104,6 +104,7 @@ "error.geo_unsupported_shape": "この種類の図形には対応していません", "error.geo_too_large": "Shapefile形式の上限(2 GB)を超えてしまいます", "notice.pdf_not_smaller": "サイズを小さくできなかったため、元のままコピーしました", + "notice.cad_newer_left_out": "選んだ AutoCAD のバージョンにないオブジェクト(マルチ引出線、メッシュ …)を省きました", "notice.empty_spreadsheet_pages_removed": "スプレッドシートの空白ページを削除しました", "notice.live_photo_pair": "同じ名前の写真とMOVとして保存しました。両方を一緒に取り込むと(iCloud写真、AirDrop、Macの写真アプリ)Live Photoになります", "notice.geo_datum_assumed": "測地系がWGS 84ではありません。WGS 84とみなして変換したため、位置が最大で数百メートルずれることがあります", @@ -450,6 +451,9 @@ "presets.name": "名前", "presets.target": "変換先", "presets.same_as_source": "元ファイルと同じ形式", + "presets.cad_version": "AutoCAD のバージョン", + "presets.cad_version.same": "元のファイルと同じ", + "presets.cad_version_hint": "古いバージョンは古い AutoCAD や CAD ソフトで開けます。2007 より前は、テキストを内容に合ったコードページ(韓国語、中国語、日本語 …)で保存し、それ以外の文字は \\U+ コードで保存します。そのバージョンにないオブジェクト(マルチ引出線、メッシュなど)は省きます。2007 の DWG は AutoCAD 2007 で開ける 2004 として保存します。", "presets.group.image": "画像", "presets.quality": "品質", "presets.resize": "サイズ変更", @@ -584,6 +588,7 @@ "presets.combine": "すべてのファイルを1つのアーカイブにまとめる", "presets.extract_note": "各アーカイブは個別のフォルダーに展開されます。展開先は「出力」で設定できます。", "presets.group.trace": "トレース", + "presets.group.cad": "CAD 図面", "presets.trace_note": "画像(PNG、JPGなど)をベクター図形にトレースするときに使います。", "presets.trace_mode": "モード", "presets.trace_mode.Color": "カラー", diff --git a/src/Filee.App/Assets/i18n/ko.json b/src/Filee.App/Assets/i18n/ko.json index 519bc2f70..da8404740 100644 --- a/src/Filee.App/Assets/i18n/ko.json +++ b/src/Filee.App/Assets/i18n/ko.json @@ -110,6 +110,7 @@ "error.geo_unsupported_shape": "지원하지 않는 도형 종류예요", "error.geo_too_large": "Shapefile의 한계인 2GB를 넘어요", "notice.pdf_not_smaller": "더 작게 만들 수 없어서 그대로 복사했어요", + "notice.cad_newer_left_out": "선택한 AutoCAD 버전에 없는 개체(다중 지시선, 메시 …)는 빠졌습니다", "notice.empty_spreadsheet_pages_removed": "스프레드시트의 빈 페이지를 제거했어요", "notice.live_photo_pair": "같은 이름의 사진과 MOV로 저장했어요. 두 파일을 함께 가져오면(iCloud 사진, AirDrop, Mac의 사진 앱) 라이브 포토가 돼요", "notice.geo_datum_assumed": "측지계가 WGS 84가 아니라서 같은 것으로 보고 변환했어요. 위치가 수백 m까지 어긋날 수 있어요", @@ -465,6 +466,9 @@ "presets.name": "이름", "presets.target": "변환 형식", "presets.same_as_source": "원본과 같은 형식", + "presets.cad_version": "AutoCAD 버전", + "presets.cad_version.same": "원본과 같게", + "presets.cad_version_hint": "이전 버전은 이전 AutoCAD와 CAD 프로그램에서 열립니다. 2007 이전 버전에서는 텍스트를 내용에 맞는 코드 페이지(한국어, 중국어, 일본어 …)로 저장하고, 그 밖의 문자는 \\U+ 코드로 저장합니다. 해당 버전에 없는 개체(다중 지시선, 메시 등)는 빠집니다. 2007 DWG는 AutoCAD 2007에서 열리는 2004로 저장합니다.", "presets.group.image": "이미지", "presets.quality": "품질", "presets.resize": "크기 조정", @@ -599,6 +603,7 @@ "presets.combine": "모든 파일을 압축 파일 하나로 묶기", "presets.extract_note": "압축 파일마다 따로 폴더를 만들어 풀어요. 저장 위치는 아래 출력에서 정해요.", "presets.group.trace": "벡터화", + "presets.group.cad": "CAD 도면", "presets.trace_note": "그림(PNG, JPG 등)을 벡터 도형으로 바꿀 때 쓰여요.", "presets.trace_mode": "방식", "presets.trace_mode.Color": "컬러", diff --git a/src/Filee.App/Assets/i18n/ne.json b/src/Filee.App/Assets/i18n/ne.json index a5c27c21d..afa6f36d0 100644 --- a/src/Filee.App/Assets/i18n/ne.json +++ b/src/Filee.App/Assets/i18n/ne.json @@ -104,6 +104,7 @@ "error.geo_unsupported_shape": "यस प्रकारको आकार समर्थित छैन", "error.geo_too_large": "Shapefile ढाँचाको २ GB सीमा नाघ्नेछ", "notice.pdf_not_smaller": "यसलाई सानो बनाउन सकिएन, त्यसैले जस्तो छ त्यस्तै प्रतिलिपि गरियो", + "notice.cad_newer_left_out": "छानिएको AutoCAD संस्करणमा नभएका वस्तुहरू (मल्टिलिडर, मेस …) छोडिए", "notice.empty_spreadsheet_pages_removed": "स्प्रेडसिटका खाली पृष्ठहरू हटाइए", "notice.live_photo_pair": "एउटै नामको फोटो र MOV का रूपमा सुरक्षित गरियो। लाइभ फोटो पाउन दुवैलाई सँगै आयात गर्नुहोस् (iCloud Photos, AirDrop वा Mac मा Photos)", "notice.geo_datum_assumed": "डेटम WGS 84 होइन; स्थितिहरू यो WGS 84 भए जस्तै रूपान्तरण गरिए, जसले केही सय मिटरसम्म फरक पार्न सक्छ", @@ -450,6 +451,9 @@ "presets.name": "नाम", "presets.target": "यसमा रूपान्तरण गर्नुहोस्", "presets.same_as_source": "स्रोत जस्तै उही ढाँचा", + "presets.cad_version": "AutoCAD संस्करण", + "presets.cad_version.same": "स्रोत जस्तै", + "presets.cad_version_hint": "पुराना संस्करण पुराना AutoCAD र CAD प्रोग्राममा खुल्छन्। 2007 भन्दा अघि, पाठ त्यसलाई मिल्ने कोड पेज (कोरियाली, चिनियाँ, जापानी …) मा सेभ हुन्छ र अरू कुनै पनि अक्षर \\U+ कोडको रूपमा। त्यो संस्करणमा नभएका वस्तुहरू, जस्तै मल्टिलिडर र मेस, छोडिन्छन्। 2007 DWG लाई 2004 को रूपमा सेभ गरिन्छ, जुन AutoCAD 2007 ले खोल्छ।", "presets.group.image": "छवि", "presets.quality": "गुणस्तर", "presets.resize": "आकार बदल्नुहोस्", @@ -584,6 +588,7 @@ "presets.combine": "सबै फाइलहरू एउटै अभिलेखमा राख्नुहोस्", "presets.extract_note": "प्रत्येक अभिलेख आफ्नै छुट्टै फोल्डरमा निकालिन्छ। यो कहाँ जान्छ भन्ने कुरा आउटपुटमा सेट गरिन्छ।", "presets.group.trace": "ट्रेसिङ", + "presets.group.cad": "CAD रेखाचित्र", "presets.trace_note": "जब तस्बिर (PNG, JPG, …) लाई भेक्टर आकारहरूमा ट्रेस गरिन्छ तब प्रयोग हुन्छ।", "presets.trace_mode": "मोड", "presets.trace_mode.Color": "रङ्गीन", diff --git a/src/Filee.App/Assets/i18n/pl.json b/src/Filee.App/Assets/i18n/pl.json index 89ab9b12a..608cfce62 100644 --- a/src/Filee.App/Assets/i18n/pl.json +++ b/src/Filee.App/Assets/i18n/pl.json @@ -110,6 +110,7 @@ "error.geo_unsupported_shape": "Ten rodzaj kształtu nie jest obsługiwany", "error.geo_too_large": "Shapefile przekroczyłby limit 2 GB tego formatu", "notice.pdf_not_smaller": "Nie udało się zmniejszyć pliku, więc skopiowano go bez zmian", + "notice.cad_newer_left_out": "Pominięto obiekty, których wybrana wersja AutoCAD nie ma (multiodnośniki, siatki…)", "notice.empty_spreadsheet_pages_removed": "Usunięto puste strony arkusza kalkulacyjnego", "notice.live_photo_pair": "Zapisano jako zdjęcie i plik MOV o tej samej nazwie. Zaimportuj je razem (Zdjęcia iCloud, AirDrop lub Zdjęcia na Macu), aby otrzymać Live Photo", "notice.geo_datum_assumed": "Datum nie jest WGS 84; pozycje przeliczono tak, jakby było, więc mogą się różnić nawet o setki metrów", @@ -465,6 +466,9 @@ "presets.name": "Nazwa", "presets.target": "Konwertuj do", "presets.same_as_source": "Ten sam format co źródło", + "presets.cad_version": "Wersja AutoCAD", + "presets.cad_version.same": "Jak w oryginale", + "presets.cad_version_hint": "Starsze wersje otwierają się w starszym AutoCAD i programach CAD. Przed 2007 tekst jest zapisywany w pasującej do niego stronie kodowej (koreański, chiński, japoński…), a każdy inny znak jako kod \\U+. Obiekty, których ta wersja nie ma, np. multiodnośniki i siatki, są pomijane. DWG 2007 jest zapisywany jako 2004, który AutoCAD 2007 otwiera.", "presets.group.image": "Obraz", "presets.quality": "Jakość", "presets.resize": "Zmień rozmiar", @@ -599,6 +603,7 @@ "presets.combine": "Umieść wszystkie pliki w jednym archiwum", "presets.extract_note": "Każde archiwum jest rozpakowywane do własnego folderu. Miejsce ustawisz w sekcji Wynik.", "presets.group.trace": "Wektoryzacja", + "presets.group.cad": "Rysunek CAD", "presets.trace_note": "Używane, gdy obraz (PNG, JPG, …) jest zamieniany na kształty wektorowe.", "presets.trace_mode": "Tryb", "presets.trace_mode.Color": "Kolor", diff --git a/src/Filee.App/Assets/i18n/pt-BR.json b/src/Filee.App/Assets/i18n/pt-BR.json index c0c91ef03..70b575893 100644 --- a/src/Filee.App/Assets/i18n/pt-BR.json +++ b/src/Filee.App/Assets/i18n/pt-BR.json @@ -110,6 +110,7 @@ "error.geo_unsupported_shape": "Este tipo de forma não é compatível", "error.geo_too_large": "O Shapefile passaria do limite de 2 GB do formato", "notice.pdf_not_smaller": "Não deu para deixar menor, então foi copiado como está", + "notice.cad_newer_left_out": "Objetos que a versão do AutoCAD escolhida não tem (multilinhas de chamada, malhas…) ficaram de fora", "notice.empty_spreadsheet_pages_removed": "As páginas vazias da planilha foram removidas", "notice.live_photo_pair": "Salvo como uma foto e um MOV com o mesmo nome. Importe os dois juntos (Fotos do iCloud, AirDrop ou Fotos no Mac) para obter uma Live Photo", "notice.geo_datum_assumed": "O datum não é WGS 84; as posições foram convertidas como se fosse, e podem errar em até centenas de metros", @@ -465,6 +466,9 @@ "presets.name": "Nome", "presets.target": "Converter para", "presets.same_as_source": "O mesmo formato da origem", + "presets.cad_version": "Versão do AutoCAD", + "presets.cad_version.same": "Igual ao original", + "presets.cad_version_hint": "Versões antigas abrem em AutoCAD e programas CAD antigos. Antes de 2007, o texto é salvo na página de código que combina com ele (coreano, chinês, japonês…) e qualquer outro caractere como código \\U+. Objetos que a versão não tem, como multilinhas de chamada e malhas, ficam de fora. Um DWG 2007 é salvo como 2004, que o AutoCAD 2007 abre.", "presets.group.image": "Imagem", "presets.quality": "Qualidade", "presets.resize": "Redimensionar", @@ -599,6 +603,7 @@ "presets.combine": "Colocar todos os arquivos num único arquivo compactado", "presets.extract_note": "Cada arquivo compactado é extraído na própria pasta. O destino fica em Saída.", "presets.group.trace": "Vetorização", + "presets.group.cad": "Desenho CAD", "presets.trace_note": "Usado quando uma imagem (PNG, JPG, …) é vetorizada em formas.", "presets.trace_mode": "Modo", "presets.trace_mode.Color": "Cor", diff --git a/src/Filee.App/Assets/i18n/ru.json b/src/Filee.App/Assets/i18n/ru.json index bf34fd1ef..62d20abf8 100644 --- a/src/Filee.App/Assets/i18n/ru.json +++ b/src/Filee.App/Assets/i18n/ru.json @@ -110,6 +110,7 @@ "error.geo_unsupported_shape": "Такой тип фигуры не поддерживается", "error.geo_too_large": "Shapefile превысил бы предел формата в 2 ГБ", "notice.pdf_not_smaller": "Уменьшить не получилось, файл скопирован как есть", + "notice.cad_newer_left_out": "Объекты, которых нет в выбранной версии AutoCAD (мультивыноски, сети…), не сохранены", "notice.empty_spreadsheet_pages_removed": "Пустые страницы таблицы удалены", "notice.live_photo_pair": "Сохранено как фото и MOV с одинаковым именем. Импортируйте их вместе (Фото iCloud, AirDrop или «Фото» на Mac), чтобы получить Live Photo", "notice.geo_datum_assumed": "Датум отличается от WGS 84; координаты пересчитаны как для WGS 84 и могут сместиться на сотни метров", @@ -465,6 +466,9 @@ "presets.name": "Название", "presets.target": "Конвертировать в", "presets.same_as_source": "Тот же формат, что у исходника", + "presets.cad_version": "Версия AutoCAD", + "presets.cad_version.same": "Как в исходном", + "presets.cad_version_hint": "Старые версии открываются в старых AutoCAD и программах САПР. До 2007 текст сохраняется в подходящей ему кодовой странице (корейской, китайской, японской…), а любой другой символ — кодом \\U+. Объекты, которых в этой версии нет, например мультивыноски и сети, не сохраняются. DWG 2007 сохраняется как 2004, который открывает AutoCAD 2007.", "presets.group.image": "Изображение", "presets.quality": "Качество", "presets.resize": "Размер", @@ -599,6 +603,7 @@ "presets.combine": "Упаковать все файлы в один архив", "presets.extract_note": "Каждый архив распаковывается в свою папку. Куда она попадёт, задаётся в разделе «Сохранение».", "presets.group.trace": "Векторизация", + "presets.group.cad": "Чертёж САПР", "presets.trace_note": "Используется, когда картинка (PNG, JPG, …) превращается в векторные фигуры.", "presets.trace_mode": "Режим", "presets.trace_mode.Color": "Цветной", diff --git a/src/Filee.App/Assets/i18n/th.json b/src/Filee.App/Assets/i18n/th.json index 442f5563a..8e68d0a6d 100644 --- a/src/Filee.App/Assets/i18n/th.json +++ b/src/Filee.App/Assets/i18n/th.json @@ -104,6 +104,7 @@ "error.geo_unsupported_shape": "ไม่รองรับรูปทรงเรขาคณิตประเภทนี้", "error.geo_too_large": "Shapefile จะมีขนาดเกินขีดจำกัดของฟอร์แมตที่ 2 GB", "notice.pdf_not_smaller": "ไม่สามารถบีบอัดให้เล็กลงได้ จึงคัดลอกไฟล์ตามเดิม", + "notice.cad_newer_left_out": "ละวัตถุที่ AutoCAD เวอร์ชันที่เลือกไม่มี (มัลติลีดเดอร์ เมช …) ไว้", "notice.empty_spreadsheet_pages_removed": "ลบหน้าว่างของสเปรดชีตออกแล้ว", "notice.live_photo_pair": "บันทึกเป็นรูปภาพและไฟล์ MOV ที่มีชื่อเดียวกัน นำเข้าทั้งสองไฟล์พร้อมกัน (ผ่านรูปภาพ iCloud, AirDrop หรือ Photos บน Mac) เพื่อใช้งานเป็น Live Photo", "notice.geo_datum_assumed": "เดตัมไม่ใช่ WGS 84 ตำแหน่งถูกแปลงโดยถือว่าเป็น WGS 84 ซึ่งอาจคลาดเคลื่อนได้ถึงหลายร้อยเมตร", @@ -450,6 +451,9 @@ "presets.name": "ชื่อ", "presets.target": "แปลงเป็น", "presets.same_as_source": "ฟอร์แมตเดียวกับต้นฉบับ", + "presets.cad_version": "เวอร์ชัน AutoCAD", + "presets.cad_version.same": "เหมือนต้นฉบับ", + "presets.cad_version_hint": "เวอร์ชันเก่าเปิดได้ใน AutoCAD และโปรแกรม CAD รุ่นเก่า ก่อน 2007 ข้อความจะถูกบันทึกในโค้ดเพจที่เหมาะกับข้อความนั้น (เกาหลี จีน ญี่ปุ่น …) และตัวอักษรอื่นเป็นรหัส \\U+ วัตถุที่เวอร์ชันนั้นไม่มี เช่น มัลติลีดเดอร์และเมช จะถูกละไว้ DWG 2007 จะบันทึกเป็น 2004 ซึ่ง AutoCAD 2007 เปิดได้", "presets.group.image": "รูปภาพ", "presets.quality": "คุณภาพ", "presets.resize": "ปรับขนาด", @@ -584,6 +588,7 @@ "presets.combine": "รวมทุกไฟล์ไว้ในไฟล์บีบอัดเดียว", "presets.extract_note": "ไฟล์บีบอัดแต่ละไฟล์จะแตกออกในโฟลเดอร์ของตัวเอง สามารถกำหนดตำแหน่งได้ที่ 'ผลลัพธ์'", "presets.group.trace": "การแปลงเป็นเวกเตอร์", + "presets.group.cad": "แบบ CAD", "presets.trace_note": "ใช้เมื่อแปลงรูปภาพบิตแมป (PNG, JPG, …) เป็นรูปทรงเวกเตอร์", "presets.trace_mode": "โหมด", "presets.trace_mode.Color": "สี", diff --git a/src/Filee.App/Assets/i18n/zh-CN.json b/src/Filee.App/Assets/i18n/zh-CN.json index d05a00680..bbdc509eb 100644 --- a/src/Filee.App/Assets/i18n/zh-CN.json +++ b/src/Filee.App/Assets/i18n/zh-CN.json @@ -110,6 +110,7 @@ "error.geo_unsupported_shape": "不支持此类形状", "error.geo_too_large": "将超过 Shapefile 2 GB 的上限", "notice.pdf_not_smaller": "无法再缩小,已原样复制", + "notice.cad_newer_left_out": "已省略所选 AutoCAD 版本没有的对象(多重引线、网格……)", "notice.empty_spreadsheet_pages_removed": "已移除电子表格中的空白页面", "notice.live_photo_pair": "已保存为同名的照片和 MOV。将两者一起导入(iCloud 照片、隔空投送或 Mac 上的“照片”)即可得到实况照片", "notice.geo_datum_assumed": "基准面不是 WGS 84,已按相同处理,位置可能偏差数百米", @@ -465,6 +466,9 @@ "presets.name": "名称", "presets.target": "转换为", "presets.same_as_source": "与源文件相同的格式", + "presets.cad_version": "AutoCAD 版本", + "presets.cad_version.same": "与源文件相同", + "presets.cad_version_hint": "旧版本可在旧版 AutoCAD 和 CAD 软件中打开。2007 之前,文字按适合它的代码页(韩文、中文、日文……)保存,其他字符保存为 \\U+ 代码。该版本没有的对象(多重引线、网格等)会被省略。2007 DWG 保存为 AutoCAD 2007 能打开的 2004。", "presets.group.image": "图片", "presets.quality": "质量", "presets.resize": "调整大小", @@ -599,6 +603,7 @@ "presets.combine": "将所有文件放入一个压缩文件", "presets.extract_note": "每个压缩文件解压到各自的文件夹。保存位置在下方“输出”中设置。", "presets.group.trace": "描摹", + "presets.group.cad": "CAD 图纸", "presets.trace_note": "将图片(PNG、JPG 等)描摹为矢量图形时使用。", "presets.trace_mode": "模式", "presets.trace_mode.Color": "彩色", diff --git a/src/Filee.App/Assets/i18n/zh-TW.json b/src/Filee.App/Assets/i18n/zh-TW.json index accf24b86..794acb1aa 100644 --- a/src/Filee.App/Assets/i18n/zh-TW.json +++ b/src/Filee.App/Assets/i18n/zh-TW.json @@ -110,6 +110,7 @@ "error.geo_unsupported_shape": "不支援此類形狀", "error.geo_too_large": "將超過 Shapefile 2 GB 的上限", "notice.pdf_not_smaller": "無法再縮小,已原樣複製", + "notice.cad_newer_left_out": "已略過所選 AutoCAD 版本沒有的物件(複線引線、網格……)", "notice.empty_spreadsheet_pages_removed": "已移除試算表中的空白頁面", "notice.live_photo_pair": "已儲存為同名的照片和 MOV。將兩者一起匯入(iCloud 照片、隔空投送或 Mac 上的「照片」)即可得到原況照片", "notice.geo_datum_assumed": "基準面不是 WGS 84,已按相同處理,位置可能偏差數百公尺", @@ -465,6 +466,9 @@ "presets.name": "名稱", "presets.target": "轉換為", "presets.same_as_source": "與來源檔案相同的格式", + "presets.cad_version": "AutoCAD 版本", + "presets.cad_version.same": "與來源檔案相同", + "presets.cad_version_hint": "舊版本可在舊版 AutoCAD 與 CAD 軟體中開啟。2007 之前,文字以適合它的字碼頁(韓文、中文、日文……)儲存,其他字元儲存為 \\U+ 代碼。該版本沒有的物件(複線引線、網格等)會略過。2007 DWG 儲存為 AutoCAD 2007 能開啟的 2004。", "presets.group.image": "圖片", "presets.quality": "品質", "presets.resize": "調整大小", @@ -599,6 +603,7 @@ "presets.combine": "將所有檔案放入一個壓縮檔", "presets.extract_note": "每個壓縮檔解壓縮到各自的資料夾。儲存位置在下方「輸出」中設定。", "presets.group.trace": "描摹", + "presets.group.cad": "CAD 圖面", "presets.trace_note": "將圖片(PNG、JPG 等)描摹為向量圖形時使用。", "presets.trace_mode": "模式", "presets.trace_mode.Color": "彩色", diff --git a/src/Filee.App/ViewModels/PresetEditorViewModel.cs b/src/Filee.App/ViewModels/PresetEditorViewModel.cs index 5758e1c0c..00f83177b 100644 --- a/src/Filee.App/ViewModels/PresetEditorViewModel.cs +++ b/src/Filee.App/ViewModels/PresetEditorViewModel.cs @@ -93,6 +93,9 @@ .. FormatRegistry.Known WavFormats = Enum.GetValues().Select(v => new Choice(v, loc[$"presets.wav_format.{v}"])).ToList(); ArchiveLevels = Enum.GetValues().Select(v => new Choice(v, loc[$"presets.archive_level.{v}"])).ToList(); TraceModes = Enum.GetValues().Select(v => new Choice(v, loc[$"presets.trace_mode.{v}"])).ToList(); + // Version names stay as AutoCAD calls them, in every language. + CadVersions = Enum.GetValues().Select(v => new Choice(v, + v == Core.Presets.CadVersion.SameAsSource ? loc["presets.cad_version.same"] : "AutoCAD " + v.ToString()["AutoCad".Length..])).ToList(); // A speed set outside the editor (a hand-edited preset file) is offered too, instead of being lost. AnimationSpeeds = [.. ImageOptions.AnimationSpeeds.Append(_edit.Image.PlaybackSpeed).Distinct().Order() .Select(s => new Choice(s, s == 1 ? loc["presets.animation_speed.original"] : SpeedLabel(s)))]; @@ -131,6 +134,7 @@ .. FormatRegistry.Known _slideSeconds = _edit.Media.SlideDuration; _crossfade = _edit.Media.Crossfade; _traceMode = TraceModes.First(m => m.Value == _edit.Trace.Mode); + _cadVersion = CadVersions.First(v => v.Value == _edit.Cad.Version); _animationSpeed = AnimationSpeeds.First(s => s.Value == _edit.Image.PlaybackSpeed); _traceColors = Math.Clamp(_edit.Trace.Colors, TracingOptions.MinColors, TracingOptions.MaxColors); _traceDetail = Math.Clamp(_edit.Trace.Detail, 0, 100); @@ -194,6 +198,9 @@ .. FormatRegistry.Known public IReadOnlyList> ArchiveLevels { get; } public IReadOnlyList> TraceModes { get; } + /// The AutoCAD version a DWG or DXF is written in (CadOptions.Version, #489). + public IReadOnlyList> CadVersions { get; } + /// Playback speeds of animations, 0.25× to 4× (ImageOptions.AnimationSpeed). public IReadOnlyList> AnimationSpeeds { get; } @@ -208,7 +215,7 @@ .. FormatRegistry.Known nameof(ShowAudioChannels), nameof(ShowWav), nameof(VideoCodecs), nameof(VideoCodecChoice), nameof(ShowVideoCodec), nameof(ShowHardwareEncoding), nameof(AudioCodecs), nameof(AudioCodecChoice), nameof(ShowAudioCodec), nameof(ShowArchive), nameof(ShowArchiveOptions), nameof(ShowExtractNote), - nameof(ShowSaveEveryFrame), nameof(ShowAnimationSpeed), nameof(ShowTrace), nameof(ShowExtractMotionPhoto), nameof(ShowSlideshow), + nameof(ShowSaveEveryFrame), nameof(ShowAnimationSpeed), nameof(ShowTrace), nameof(ShowCad), nameof(ShowExtractMotionPhoto), nameof(ShowSlideshow), nameof(ShowLivePhotoChoice), nameof(LivePhotoKinds), nameof(LivePhotoChoice), nameof(IsLivePhoto), nameof(LivePhotoHint))] private Choice _target; @@ -435,6 +442,7 @@ public bool ColorProfileChecked private Choice _traceMode; [ObservableProperty] private int _traceColors; + [ObservableProperty] private Choice _cadVersion; [ObservableProperty] private int _traceDetail; [ObservableProperty] private Choice _animationSpeed; @@ -456,6 +464,7 @@ partial void OnTiffCompressionChanged(Choice? oldValue, Choice< partial void OnWavFormatChanged(Choice? oldValue, Choice newValue) => Choices.Restore(oldValue, newValue, v => WavFormat = v); partial void OnArchiveLevelChanged(Choice? oldValue, Choice newValue) => Choices.Restore(oldValue, newValue, v => ArchiveLevel = v); partial void OnTraceModeChanged(Choice? oldValue, Choice newValue) => Choices.Restore(oldValue, newValue, v => TraceMode = v); + partial void OnCadVersionChanged(Choice? oldValue, Choice newValue) => Choices.Restore(oldValue, newValue, v => CadVersion = v); partial void OnCompressionLevelChanged(Choice? oldValue, Choice newValue) => Choices.Restore(oldValue, newValue, v => CompressionLevel = v); partial void OnAnimationSpeedChanged(Choice? oldValue, Choice newValue) => @@ -540,6 +549,9 @@ partial void OnAnimationSpeedChanged(Choice? oldValue, Choice ne /// drawing, a CAD file) the options are ignored. /// public bool ShowTrace => TargetId is "svg" or "svgz" or "dxf" or "dwg"; + + /// The AutoCAD version, for presets that write DWG or DXF. + public bool ShowCad => !IsTool && TargetId is "dxf" or "dwg"; public bool ShowTraceColors => TraceMode.Value == TracingMode.Color; public bool ShowPercent => Resize.Value == ResizeMode.Percent; public bool ShowLongEdge => Resize.Value == ResizeMode.LongEdge; @@ -643,6 +655,8 @@ static double Seconds(double value, double fallback, double min, double max) => _original.Trace.Mode = TraceMode.Value; _original.Trace.Colors = Math.Clamp(TraceColors, TracingOptions.MinColors, TracingOptions.MaxColors); _original.Trace.Detail = Math.Clamp(TraceDetail, 0, 100); + // Only where it is shown: a version chosen for DWG and left behind by a PNG target is not kept. + _original.Cad.Version = ShowCad ? CadVersion.Value : default; var output = _original.Output; output.Location = Location.Value; diff --git a/src/Filee.App/Views/PresetEditorView.axaml b/src/Filee.App/Views/PresetEditorView.axaml index 717ccbcc8..2007785fb 100644 --- a/src/Filee.App/Views/PresetEditorView.axaml +++ b/src/Filee.App/Views/PresetEditorView.axaml @@ -293,6 +293,20 @@ + + + + + + + + + + + + + + diff --git a/src/Filee.Core/Presets/Preset.cs b/src/Filee.Core/Presets/Preset.cs index 324a971b1..40d31bb0c 100644 --- a/src/Filee.Core/Presets/Preset.cs +++ b/src/Filee.Core/Presets/Preset.cs @@ -99,6 +99,9 @@ public sealed class Preset /// Options used when a raster image is traced into vector shapes (PNG → SVG, DXF ...). public TracingOptions Trace { get; set; } = new(); + /// Options used when a DWG or DXF is written. + public CadOptions Cad { get; set; } = new(); + /// Where and how output files are written. public OutputRule Output { get; set; } = new(); @@ -192,6 +195,7 @@ public bool MakesLivePhotoOf(string sourceFormat) => Media = Media.Clone(), Archive = Archive.Clone(), Trace = Trace.Clone(), + Cad = Cad.Clone(), Output = Output.Clone(), Kind = Kind, SubPresetIds = [.. SubPresetIds], @@ -764,3 +768,29 @@ public sealed class TracingOptions public TracingOptions Clone() => (TracingOptions)MemberwiseClone(); } + +/// The AutoCAD version a DWG or DXF is written in (#489). +public enum CadVersion +{ + /// AutoCAD 2018 (AC1032): still current in AutoCAD 2026. + AutoCad2018, + AutoCad2013, + AutoCad2010, + /// A DWG is written as 2004, which AutoCAD 2007 opens: ACadSharp can't write a 2007 DWG. + AutoCad2007, + AutoCad2004, + AutoCad2000, + /// The version of the drawing converted, within what can be written (2000–2018). + SameAsSource, +} + +/// Options for writing DWG and DXF. +public sealed class CadOptions +{ + /// A version from a newer Filee reads as . + [JsonConverter(typeof(LenientEnumJsonConverter))] + public CadVersion Version { get; set; } + + public CadOptions Clone() => (CadOptions)MemberwiseClone(); +} + diff --git a/src/Filee.Engines/Cad/CadCodePages.cs b/src/Filee.Engines/Cad/CadCodePages.cs new file mode 100644 index 000000000..25047707c --- /dev/null +++ b/src/Filee.Engines/Cad/CadCodePages.cs @@ -0,0 +1,125 @@ +// Text in a DWG or DXF before AutoCAD 2007 is stored in the drawing's code page ($DWGCODEPAGE), not UTF-8 (#489). +// A drawing from 2007 or later often still says ANSI_1252 while holding Korean or Chinese text, and ACadSharp encodes +// with the code page as it is, so saving it as 2004 would turn that text into "?". So the code page is picked from the +// text, and a character it can't hold is written as \U+XXXX, which AutoCAD reads in TEXT, MTEXT and attributes. + +using System.Text; +using ACadSharp; +using ACadSharp.Entities; + +namespace Filee.Engines.Cad; + +internal static class CadCodePages +{ + /// + /// Code pages AutoCAD names ANSI_n, in the order a tie goes after the drawing's own one. The East Asian ones also + /// hold Greek, Cyrillic and each other's Chinese characters, so they come first only for the script they are for + /// (): Korean for Hangul, Japanese for Kana, Chinese for Chinese characters, Big5 before GBK + /// (text both hold, traditional characters, is Big5; simplified text, which Big5 lacks, is GBK). + /// + private static readonly int[] Candidates = [1252, 1250, 1251, 1253, 1254, 1255, 1256, 1257, 1258, 874, 950, 936, 949, 932]; + + /// + /// Gives a drawing about to be written in a pre-2007 version the code page that holds most of its text (its own + /// one when that ties) and escapes what the code page can't hold. Returns the code page. + /// + public static int Prepare(CadDocument document) + { + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + var texts = Texts(document).ToList(); + var page = Choose(texts.Select(t => t.Get()), Number(document.Header.CodePage)); + // ACadSharp 3.8 knows Korean only as KCS5601; CadFile writes the DXF header as ANSI_949, as AutoCAD does. + document.Header.CodePage = page == 949 ? "KCS5601" : $"ANSI_{page}"; + var encoding = Encoding.GetEncoding(page); + foreach (var text in texts) + { + // Only changed text is set back: a dimension without its own text (null) must not get an empty one. + var value = text.Get(); + var escaped = Escape(value, encoding); + if (!ReferenceEquals(value, escaped)) + text.Set(escaped); + } + return page; + } + + /// The candidate that holds the most distinct non-ASCII characters; on a tie. + internal static int Choose(IEnumerable texts, int? current) + { + var characters = texts.SelectMany(t => t).Where(c => c > 0x7F).ToHashSet(); + var first = new List(); + if (current is { } own && Candidates.Contains(own)) + first.Add(own); + if (characters.Any(c => c is >= '가' and <= '힣' or >= 'ᄀ' and <= 'ᇿ' or >= '㄰' and <= '㆏')) + first.Add(949); + if (characters.Any(c => c is >= '぀' and <= 'ヿ')) + first.Add(932); + if (characters.Any(c => c is >= '一' and <= '鿿' or >= '㐀' and <= '䶿' or >= '豈' and <= '﫿')) + first.AddRange([950, 936]); + int[] order = [.. first, .. Candidates.Except(first)]; + return order.Select(page => (Page: page, Held: characters.Count(c => Holds(Encoding.GetEncoding(page), c)))) + .OrderByDescending(p => p.Held).First().Page; + } + + /// The text with every character can't hold as \U+XXXX. + internal static string Escape(string text, Encoding encoding) + { + if (text.All(c => c <= 0x7F || Holds(encoding, c))) + return text; + var result = new StringBuilder(text.Length + 16); + foreach (var c in text) + { + if (c <= 0x7F || Holds(encoding, c)) + result.Append(c); + else if (char.IsSurrogate(c)) + result.Append('?'); // \U+ takes four hex digits: nothing outside the BMP fits + else + result.Append($"\\U+{(int)c:X4}"); + } + return result.ToString(); + } + + private static bool Holds(Encoding encoding, char c) + { + if (char.IsSurrogate(c)) + return false; + var one = c.ToString(); + return encoding.GetString(encoding.GetBytes(one)) == one; + } + + /// The number of a $DWGCODEPAGE name (ANSI_949, GB2312, BIG5 …), or null when it isn't one of them. + internal static int? Number(string? name) => name?.ToUpperInvariant() switch + { + null => null, + "KCS5601" or "JOHAB" => 949, + "GB2312" => 936, + "BIG5" => 950, + "DOS932" or "SHIFT_JIS" => 932, + var upper when upper.StartsWith("ANSI_", StringComparison.Ordinal) && int.TryParse(upper[5..], out var number) => number, + _ => null, + }; + + /// Every text a drawing shows: TEXT and attributes, MTEXT, dimension text, attributes of inserts. + private static IEnumerable<(Func Get, Action Set)> Texts(CadDocument document) + { + foreach (var record in document.BlockRecords) + foreach (var entity in record.Entities) + { + switch (entity) + { + case TextEntity text: + yield return (() => text.Value ?? "", v => text.Value = v); + break; + case MText mtext: + yield return (() => mtext.Value ?? "", v => mtext.Value = v); + break; + case Dimension dimension: + yield return (() => dimension.Text ?? "", v => dimension.Text = v); + break; + case Insert insert: + foreach (var attribute in insert.Attributes) + yield return (() => attribute.Value ?? "", v => attribute.Value = v); + break; + } + } + } +} diff --git a/src/Filee.Engines/Cad/CadConverter.cs b/src/Filee.Engines/Cad/CadConverter.cs index 9b68c6474..0d4b9d9c8 100644 --- a/src/Filee.Engines/Cad/CadConverter.cs +++ b/src/Filee.Engines/Cad/CadConverter.cs @@ -40,6 +40,9 @@ public sealed class CadConverter(ILogger? logger = null) : IConverter, IImageTra [ new("dwg", "dxf"), new("dxf", "dwg"), + // Into another version of the same format (#489). + new("dwg", "dwg"), + new("dxf", "dxf"), .. from source in Sources from target in (string[])["pdf", "svg", .. ImageEncoder.Writable] select ImageEncoder.Edge(source, target), @@ -59,7 +62,8 @@ public Task> ConvertAsync(ConversionStep step, IProgress _logger.LogDebug("{File}: {Message}", path, message)); + if (CadFile.Write(document, path, step.To, step.Preset.Cad.Version, message => _logger.LogDebug("{File}: {Message}", path, message)) > 0) + step.Notice?.Invoke("notice.cad_newer_left_out"); progress?.Report(1); return [path]; } diff --git a/src/Filee.Engines/Cad/CadFile.cs b/src/Filee.Engines/Cad/CadFile.cs index ce16c85f8..e02bd3afc 100644 --- a/src/Filee.Engines/Cad/CadFile.cs +++ b/src/Filee.Engines/Cad/CadFile.cs @@ -1,20 +1,43 @@ -// Reads DWG / DXF files and writes DXF / DWG with ACadSharp (MIT). Output is always AutoCAD 2018 (AC1032): the -// current DWG format (AutoCAD 2018–2026, DWG TrueView, BricsCAD, LibreCAD ...), and for DXF the first-choice -// version whose text is UTF-8, so Korean and Chinese text survive without code page guessing. +// Reads DWG / DXF files and writes DXF / DWG with ACadSharp (MIT). Output is AutoCAD 2018 (AC1032) unless the preset +// asks for an older version (#489): 2018 is the current DWG format (AutoCAD 2018–2026, DWG TrueView, BricsCAD, +// LibreCAD ...), and its text is UTF-8. Before 2007 text is in a code page, which CadCodePages picks from the text. using System.Text; using ACadSharp; using ACadSharp.Exceptions; using ACadSharp.IO; +using Filee.Core.Presets; namespace Filee.Engines.Cad; /// DWG and DXF input and output. internal static class CadFile { - /// Version of every written DWG and DXF. + /// Version a DWG or DXF is written in unless the preset says otherwise. public const ACadVersion OutputVersion = ACadVersion.AC1032; + /// + /// The version to write for the preset's choice. "Same as the source" keeps the drawing's version within what + /// can be written (older than 2000 becomes 2000). ACadSharp can't write a 2007 DWG: chosen, it is written as 2004, + /// which AutoCAD 2007 opens; kept from a 2007 source, as 2010, which keeps its text in UTF-8. + /// + internal static ACadVersion VersionFor(CadVersion choice, ACadVersion source, string format) + { + var version = choice switch + { + CadVersion.AutoCad2013 => ACadVersion.AC1027, + CadVersion.AutoCad2010 => ACadVersion.AC1024, + CadVersion.AutoCad2007 => ACadVersion.AC1021, + CadVersion.AutoCad2004 => ACadVersion.AC1018, + CadVersion.AutoCad2000 => ACadVersion.AC1015, + CadVersion.SameAsSource => source < ACadVersion.AC1015 ? ACadVersion.AC1015 : source > OutputVersion ? OutputVersion : source, + _ => OutputVersion, + }; + if (format == "dwg" && version == ACadVersion.AC1021) + return choice == CadVersion.SameAsSource ? ACadVersion.AC1024 : ACadVersion.AC1018; + return version; + } + /// /// Reads a drawing. Binary and ASCII DXF are detected automatically. Throws /// with a readable message for files that are not drawings, are damaged, or use an unsupported version. @@ -55,8 +78,12 @@ void Notify(object? sender, NotificationEventArgs e) } } - /// Writes the drawing as an AutoCAD 2018 DXF (ASCII) or DWG. - public static void Write(CadDocument document, string path, string format, Action? warning = null) + /// + /// Writes the drawing as an ASCII DXF or a DWG, in AutoCAD 2018 or the version the preset chose. Returns how many + /// objects that version doesn't have were left out. + /// + public static int Write(CadDocument document, string path, string format, CadVersion version = CadVersion.AutoCad2018, + Action? warning = null) { void Notify(object? sender, NotificationEventArgs e) { @@ -64,9 +91,11 @@ void Notify(object? sender, NotificationEventArgs e) warning?.Invoke(e.Message); } - document.Header.Version = OutputVersion; + document.Header.Version = VersionFor(version, document.Header.Version, format); try { + var codePage = document.Header.Version < ACadVersion.AC1021 ? CadCodePages.Prepare(document) : 0; + var leftOut = DropNewerEntities(document, warning); DropStraySequenceEnds(document); if (format == "dwg") { @@ -74,7 +103,12 @@ void Notify(object? sender, NotificationEventArgs e) DwgWriter.Write(path, document, new DwgWriterConfiguration(), Notify); } else + { DxfWriter.Write(path, document, binary: false, new DxfWriterConfiguration(), Notify); + if (codePage == 949) + NameKoreanCodePage(path); + } + return leftOut; } catch (Exception ex) when (ex is not (OperationCanceledException or IOException or UnauthorizedAccessException)) { @@ -83,6 +117,33 @@ void Notify(object? sender, NotificationEventArgs e) } } + /// + /// Leaves out the objects the version being written doesn't have (AutoCAD before it would not open the file): + /// tables and arc dimensions before 2004 (AC1018), multileaders and PDF underlays before 2007 (AC1021), meshes before + /// 2010 (AC1024). Returns how many. + /// + private static int DropNewerEntities(CadDocument document, Action? warning) + { + var version = document.Header.Version; + bool TooNew(ACadSharp.Entities.Entity entity) => entity switch + { + ACadSharp.Entities.TableEntity or ACadSharp.Entities.DimensionArc => version < ACadVersion.AC1018, + ACadSharp.Entities.MultiLeader or ACadSharp.Entities.PdfUnderlay => version < ACadVersion.AC1021, + ACadSharp.Entities.Mesh => version < ACadVersion.AC1024, + _ => false, + }; + var count = 0; + foreach (var record in document.BlockRecords) + foreach (var entity in record.Entities.Where(TooNew).ToList()) + { + record.Entities.Remove(entity); + count++; + } + if (count > 0) + warning?.Invoke($"{count} object(s) newer than {version} were left out."); + return count; + } + /// /// R12 DXF files can leave SEQEND markers behind as loose entities; the writers reject them (DWG) or would /// write them as garbage (DXF). They carry no geometry. @@ -164,6 +225,25 @@ private static Stream OpenDxf(string path) } } + /// + /// The DXF's $DWGCODEPAGE as AutoCAD writes it for Korean, ANSI_949, in place of the KCS5601 that ACadSharp needs + /// to encode the text in it (the reverse of ). + /// + private static void NameKoreanCodePage(string path) + { + var bytes = File.ReadAllBytes(path); + var head = Encoding.ASCII.GetString(bytes, 0, Math.Min(bytes.Length, 64 * 1024)); + var key = head.IndexOf("$DWGCODEPAGE", StringComparison.Ordinal); + var value = key < 0 ? -1 : head.IndexOf("KCS5601", key, Math.Min(64, head.Length - key), StringComparison.Ordinal); + if (value < 0) + return; + var name = "ANSI_949"u8; + using var stream = File.Create(path); + stream.Write(bytes, 0, value); + stream.Write(name); + stream.Write(bytes, value + 7, bytes.Length - value - 7); + } + private static void TryDelete(string path) { try diff --git a/src/Filee.Engines/Vector/SvgDxfWriter.cs b/src/Filee.Engines/Vector/SvgDxfWriter.cs index f09844b00..ff37136b1 100644 --- a/src/Filee.Engines/Vector/SvgDxfWriter.cs +++ b/src/Filee.Engines/Vector/SvgDxfWriter.cs @@ -60,15 +60,15 @@ private SvgDxfWriter(SvgRenderer.Drawing drawing, CancellationToken ct) /// Number of entities written (for tests and the log). public int EntityCount { get; private set; } - /// Writes to as DXF (AutoCAD 2018). + /// Writes to as DXF, in the preset's version (AutoCAD 2018 by default). /// The drawing has no outlines to write. - public static void Write(SvgRenderer.Drawing drawing, string output, CancellationToken ct) + public static void Write(SvgRenderer.Drawing drawing, string output, CancellationToken ct, Core.Presets.CadVersion version = Core.Presets.CadVersion.AutoCad2018) { var writer = new SvgDxfWriter(drawing, ct); writer.Collect(); if (writer.EntityCount == 0) throw new InvalidDataException("The SVG has no shapes that can be written as CAD geometry (only images or empty groups)."); - CadFile.Write(writer._document, output, "dxf"); + CadFile.Write(writer._document, output, "dxf", version); } private void Collect() diff --git a/src/Filee.Engines/Vector/VectorConverter.cs b/src/Filee.Engines/Vector/VectorConverter.cs index ceda51647..78ddfabfe 100644 --- a/src/Filee.Engines/Vector/VectorConverter.cs +++ b/src/Filee.Engines/Vector/VectorConverter.cs @@ -107,7 +107,7 @@ private static IReadOnlyList ToDxf(ConversionStep step, CancellationToke var output = step.Output.Allocate("dxf"); if (output is null) return []; - SvgDxfWriter.Write(drawing, output, ct); + SvgDxfWriter.Write(drawing, output, ct, step.Preset.Cad.Version); return [output]; } diff --git a/tests/Filee.App.Tests/RenderTests.cs b/tests/Filee.App.Tests/RenderTests.cs index 261edbe49..7697987d3 100644 --- a/tests/Filee.App.Tests/RenderTests.cs +++ b/tests/Filee.App.Tests/RenderTests.cs @@ -1646,6 +1646,28 @@ public void Preset_editor_offers_markdown_without_pictures(string language) Assert.False(preset.Document.LeaveOutPictures); } + [AvaloniaFact] + public void Preset_editor_offers_the_autocad_version_for_dwg_and_dxf_only() + { + TestServices.EnsureInitialized("en"); + var preset = new Filee.Core.Presets.Preset { TargetFormat = "dwg", Name = "Old DWG" }; + var vm = new PresetEditorViewModel(preset, AppHost.Get()); + Assert.True(vm.ShowCad); + Assert.Equal(["AutoCAD 2018", "AutoCAD 2013", "AutoCAD 2010", "AutoCAD 2007", "AutoCAD 2004", "AutoCAD 2000", "Same as the source"], + vm.CadVersions.Select(v => v.Label)); + + vm.CadVersion = vm.CadVersions.Single(v => v.Value == Filee.Core.Presets.CadVersion.AutoCad2004); + Assert.True(vm.Apply()); + Assert.Equal(Filee.Core.Presets.CadVersion.AutoCad2004, preset.Cad.Version); + Assert.Equal(Filee.Core.Presets.CadVersion.AutoCad2004, new PresetEditorViewModel(preset, AppHost.Get()).CadVersion.Value); + + // Not for other targets: the choice isn't kept behind them. + vm.Target = vm.Targets.Single(t => t.Value == "png"); + Assert.False(vm.ShowCad); + Assert.True(vm.Apply()); + Assert.Equal(Filee.Core.Presets.CadVersion.AutoCad2018, preset.Cad.Version); + } + [AvaloniaFact] public void Preset_editor_offers_save_as_zip_for_geospatial_targets_only() { diff --git a/tests/Filee.Engines.Tests/CadVersionTests.cs b/tests/Filee.Engines.Tests/CadVersionTests.cs new file mode 100644 index 000000000..5b38f127a --- /dev/null +++ b/tests/Filee.Engines.Tests/CadVersionTests.cs @@ -0,0 +1,154 @@ +// DWG and DXF in an older AutoCAD version (#489): every version reads back, and text survives the code page that +// versions before 2007 store it in. + +using System.Text; +using ACadSharp; +using ACadSharp.Entities; +using ACadSharp.IO; +using ACadSharp.Tables; +using CSMath; +using Filee.Core.Conversion; +using Filee.Core.Presets; +using Filee.Engines.Cad; + +namespace Filee.Engines.Tests; + +public class CadVersionTests(EngineFixture fx) : IClassFixture +{ + private const string Korean = "한글 도면 제목"; + private const string Chinese = "这是中文图纸"; // simplified: mostly not in CP949 + private const string Mixed = "평면도 这是 é"; + + private async Task ConvertAsync(string input, string target, CadVersion version) + { + var job = await fx.ConvertAsync([input], new Preset { TargetFormat = target, Cad = { Version = version }, Output = { FileNamePattern = "{name}_out" } }); + Assert.True(job.State == JobState.Completed, string.Join("; ", job.Files.Select(f => $"{f.ErrorKey} {f.ErrorDetail}"))); + return job.Outputs.Single(); + } + + /// The sample drawing, saved as AutoCAD 2018 with the ANSI_1252 code page newer drawings often keep. + private string Source(string format) + { + var doc = CadBuilder.Sample(); + doc.Header.CodePage = "ANSI_1252"; + var layer = doc.Layers["Drawing"]; + CadBuilder.Add(doc, layer, new TextEntity(Korean) { InsertPoint = new XYZ(0, -10, 0), Height = 3 }); + CadBuilder.Add(doc, layer, new TextEntity(Chinese) { InsertPoint = new XYZ(0, -20, 0), Height = 3 }); + CadBuilder.Add(doc, layer, new MText(Mixed) { InsertPoint = new XYZ(0, -30, 0), Height = 3, RectangleWidth = 80 }); + var folder = fx.NewFolder(); + if (format == "dxf") + return CadBuilder.SaveDxf(doc, folder, "source.dxf"); + var path = Path.Combine(folder, "source.dwg"); + DwgWriter.Write(path, doc); + return path; + } + + private static CadDocument ReadBack(string path) => CadFile.Read(path, Path.GetExtension(path)[1..]); + + /// The texts as shown: escapes decoded. + private static string[] Shown(CadDocument doc) => + [ + .. doc.Entities.OfType().Select(t => CadTextCodes.DecodeText(t.Value)), + .. doc.Entities.OfType().Select(t => CadTextCodes.DecodeText(t.Value)), + ]; + + [Theory] + [InlineData("dwg", CadVersion.AutoCad2018, ACadVersion.AC1032)] + [InlineData("dwg", CadVersion.AutoCad2013, ACadVersion.AC1027)] + [InlineData("dwg", CadVersion.AutoCad2010, ACadVersion.AC1024)] + [InlineData("dwg", CadVersion.AutoCad2007, ACadVersion.AC1018)] // no 2007 DWG writer: 2004, which 2007 opens + [InlineData("dwg", CadVersion.AutoCad2004, ACadVersion.AC1018)] + [InlineData("dwg", CadVersion.AutoCad2000, ACadVersion.AC1015)] + [InlineData("dxf", CadVersion.AutoCad2018, ACadVersion.AC1032)] + [InlineData("dxf", CadVersion.AutoCad2013, ACadVersion.AC1027)] + [InlineData("dxf", CadVersion.AutoCad2010, ACadVersion.AC1024)] + [InlineData("dxf", CadVersion.AutoCad2007, ACadVersion.AC1021)] + [InlineData("dxf", CadVersion.AutoCad2004, ACadVersion.AC1018)] + [InlineData("dxf", CadVersion.AutoCad2000, ACadVersion.AC1015)] + public async Task Each_version_reads_back_with_its_entities_and_text(string format, CadVersion choice, ACadVersion written) + { + var source = Source(format); + var expected = CadBuilder.Census(ReadBack(source)); + + var output = await ConvertAsync(source, format, choice); + + var back = ReadBack(output); + Assert.Equal(written, back.Header.Version); + Assert.Equal(expected, CadBuilder.Census(back)); + var shown = Shown(back); + Assert.Contains(Korean, shown); + Assert.Contains(Chinese, shown); + Assert.Contains(Mixed, shown); + } + + [Fact] + public async Task An_old_dxf_gets_the_code_page_of_its_text_and_escapes_the_rest() + { + var output = await ConvertAsync(Source("dxf"), "dxf", CadVersion.AutoCad2004); + + var raw = Encoding.Latin1.GetString(await File.ReadAllBytesAsync(output, TestContext.Current.CancellationToken)); + // Korean is the script most of the text needs; AutoCAD names it ANSI_949. + Assert.Matches(@"\$DWGCODEPAGE\r?\n\s*3\r?\nANSI_949\r?\n", raw); + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + Assert.Contains(Encoding.Latin1.GetString(Encoding.GetEncoding(949).GetBytes(Korean)), raw); + Assert.Contains(@"\U+8FD9", raw); // 这 isn't in CP949 + } + + [Fact] + public void The_code_page_follows_the_text() + { + Assert.Equal(949, CadCodePages.Choose([Korean], current: 1252)); + Assert.Equal(936, CadCodePages.Choose([Chinese], current: 1252)); + Assert.Equal(950, CadCodePages.Choose(["繁體中文說明"], current: 1252)); + Assert.Equal(932, CadCodePages.Choose(["ひらがなとカタカナ"], current: null)); + Assert.Equal(1251, CadCodePages.Choose(["Привет"], current: null)); + Assert.Equal(1252, CadCodePages.Choose(["Plan A"], current: null)); + Assert.Equal(950, CadCodePages.Choose(["Plan A"], current: 950)); // ASCII only: the drawing keeps its own + Assert.Equal(949, CadCodePages.Number("ANSI_949")); + Assert.Equal(936, CadCodePages.Number("gb2312")); + } + + [Theory] + [InlineData(CadVersion.SameAsSource, ACadVersion.AC1018, "dwg", ACadVersion.AC1018)] + [InlineData(CadVersion.SameAsSource, ACadVersion.AC1021, "dwg", ACadVersion.AC1024)] + [InlineData(CadVersion.SameAsSource, ACadVersion.AC1021, "dxf", ACadVersion.AC1021)] + [InlineData(CadVersion.SameAsSource, ACadVersion.AC1014, "dxf", ACadVersion.AC1015)] + [InlineData(CadVersion.SameAsSource, ACadVersion.AC1009, "dwg", ACadVersion.AC1015)] + [InlineData(CadVersion.AutoCad2018, ACadVersion.AC1015, "dwg", ACadVersion.AC1032)] + public void Same_as_the_source_stays_within_what_can_be_written(CadVersion choice, ACadVersion source, string format, ACadVersion written) => + Assert.Equal(written, CadFile.VersionFor(choice, source, format)); + + [Fact] + public async Task Objects_an_old_version_lacks_are_left_out_with_a_note() + { + var doc = CadBuilder.Sample(); + var lines = doc.Entities.OfType().Count(); + doc.Entities.Add(new MultiLeader()); + doc.Entities.Add(new Mesh()); + var source = CadBuilder.SaveDxf(doc, fx.NewFolder(), "new.dxf"); + + var job = await fx.ConvertAsync([source], new Preset { TargetFormat = "dwg", Cad = { Version = CadVersion.AutoCad2000 } }); + + Assert.Equal(JobState.Completed, job.State); + Assert.Equal("notice.cad_newer_left_out", job.Files[0].NoticeKey); + var back = DwgReader.Read(job.Outputs.Single()); + Assert.Empty(back.Entities.OfType()); + Assert.Empty(back.Entities.OfType()); + Assert.Equal(lines, back.Entities.OfType().Count()); + + // In 2018 they stay, and there's no note. + var current = await fx.ConvertAsync([source], new Preset { TargetFormat = "dxf", Output = { FileNamePattern = "{name}_2018" } }); + Assert.Null(current.Files[0].NoticeKey); + Assert.Single(DxfReader.Read(current.Outputs.Single()).Entities.OfType()); + } + + [Fact] + public async Task A_dwg_is_written_in_another_dwg_version_in_one_step() + { + var route = fx.Catalog.CreatePlanner().Plan("dwg", "dwg"); + Assert.Equal("cad", Assert.Single(route!.Steps).Converter.Id); + + var output = await ConvertAsync(Source("dwg"), "dwg", CadVersion.AutoCad2000); + Assert.Equal("AC1015"u8.ToArray(), (await File.ReadAllBytesAsync(output, TestContext.Current.CancellationToken))[..6]); + } +} From b75684c3af73663be1a79c13e3bbb759addfc1ea Mon Sep 17 00:00:00 2001 From: cre8ive-marketing <168519979+cre8ive-marketing@users.noreply.github.com> Date: Fri, 23 Oct 2026 20:29:29 +0530 Subject: [PATCH 2/4] CAD code pages: count and escape names, one ? per emoji (#497 review) 1. Layer, block, text style, dimension style, linetype and layout names and attribute tags count when the code page is picked, and are escaped like text: a Korean layer in a drawing with only English text gets ANSI_949 instead of coming back as "??". Tests in 2000 and 2004, DXF and DWG, and a mixed-script layer name escaped as \U+XXXX. 2. A character outside the BMP is one "?", not one per surrogate half. 3. ENGINES.md: text in table cells isn't escaped; the DWG header keeps ACadSharp's code page index (KSC5601 / GB2312). --- src/Filee.Engines/Cad/CadCodePages.cs | 34 ++++++++++++-- tests/Filee.Engines.Tests/CadVersionTests.cs | 47 ++++++++++++++++++++ 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/src/Filee.Engines/Cad/CadCodePages.cs b/src/Filee.Engines/Cad/CadCodePages.cs index 25047707c..8a52e611d 100644 --- a/src/Filee.Engines/Cad/CadCodePages.cs +++ b/src/Filee.Engines/Cad/CadCodePages.cs @@ -70,8 +70,10 @@ internal static string Escape(string text, Encoding encoding) { if (c <= 0x7F || Holds(encoding, c)) result.Append(c); - else if (char.IsSurrogate(c)) - result.Append('?'); // \U+ takes four hex digits: nothing outside the BMP fits + else if (char.IsHighSurrogate(c)) + result.Append('?'); // \U+ takes four hex digits: nothing outside the BMP fits, one ? per character + else if (char.IsLowSurrogate(c)) + continue; else result.Append($"\\U+{(int)c:X4}"); } @@ -98,14 +100,36 @@ var upper when upper.StartsWith("ANSI_", StringComparison.Ordinal) && int.TryPar _ => null, }; - /// Every text a drawing shows: TEXT and attributes, MTEXT, dimension text, attributes of inserts. + /// + /// Every text a drawing holds in its code page: TEXT and attributes (values and tags), MTEXT, dimension text, and + /// the names of layers, blocks, text and dimension styles, linetypes and layouts. Korean layer names with English + /// labels are common, so names count when the code page is picked, and are escaped like text. + /// private static IEnumerable<(Func Get, Action Set)> Texts(CadDocument document) { + foreach (var layer in document.Layers) + yield return (() => layer.Name, v => layer.Name = v); + foreach (var style in document.TextStyles) + yield return (() => style.Name, v => style.Name = v); + foreach (var style in document.DimensionStyles) + yield return (() => style.Name, v => style.Name = v); + foreach (var lineType in document.LineTypes) + yield return (() => lineType.Name, v => lineType.Name = v); + foreach (var layout in document.Layouts) + yield return (() => layout.Name, v => layout.Name = v); foreach (var record in document.BlockRecords) + { + // *Model_Space, *Paper_Space and anonymous blocks are ASCII by definition. + if (!record.Name.StartsWith('*')) + yield return (() => record.Name, v => record.Name = v); foreach (var entity in record.Entities) { switch (entity) { + case AttributeDefinition definition: + yield return (() => definition.Value ?? "", v => definition.Value = v); + yield return (() => definition.Tag ?? "", v => definition.Tag = v); + break; case TextEntity text: yield return (() => text.Value ?? "", v => text.Value = v); break; @@ -117,9 +141,13 @@ var upper when upper.StartsWith("ANSI_", StringComparison.Ordinal) && int.TryPar break; case Insert insert: foreach (var attribute in insert.Attributes) + { yield return (() => attribute.Value ?? "", v => attribute.Value = v); + yield return (() => attribute.Tag ?? "", v => attribute.Tag = v); + } break; } } + } } } diff --git a/tests/Filee.Engines.Tests/CadVersionTests.cs b/tests/Filee.Engines.Tests/CadVersionTests.cs index 5b38f127a..158d43f1f 100644 --- a/tests/Filee.Engines.Tests/CadVersionTests.cs +++ b/tests/Filee.Engines.Tests/CadVersionTests.cs @@ -94,6 +94,53 @@ public async Task An_old_dxf_gets_the_code_page_of_its_text_and_escapes_the_rest Assert.Contains(@"\U+8FD9", raw); // 这 isn't in CP949 } + [Theory] + [InlineData("dxf", CadVersion.AutoCad2000)] + [InlineData("dxf", CadVersion.AutoCad2004)] + [InlineData("dwg", CadVersion.AutoCad2000)] + [InlineData("dwg", CadVersion.AutoCad2004)] + public async Task Korean_names_pick_the_code_page_even_with_english_text(string format, CadVersion version) + { + // A layer and a block named in Korean, an attribute tag too, and only English text: common in Korean drawings. + var doc = new CadDocument(); + doc.Header.CodePage = "ANSI_1252"; + var wall = new Layer("벽체") { Color = CadBuilder.Visible }; + doc.Layers.Add(wall); + CadBuilder.Add(doc, wall, new Line(new XYZ(0, 0, 0), new XYZ(100, 0, 0))); + CadBuilder.Add(doc, wall, new TextEntity("Wall A") { InsertPoint = new XYZ(0, 5, 0), Height = 3 }); + CadBuilder.Add(doc, wall, new AttributeDefinition { Tag = "도면번호", Value = "A-101", Height = 3 }); + var source = format == "dxf" ? CadBuilder.SaveDxf(doc, fx.NewFolder(), "walls.dxf") : SaveDwg(doc, "walls.dwg"); + + var back = ReadBack(await ConvertAsync(source, format, version)); + + Assert.Contains(back.Layers, l => l.Name == "벽체"); + Assert.Contains(back.Entities.OfType(), a => a.Tag == "도면번호"); + Assert.Contains("Wall A", Shown(back)); + } + + [Fact] + public async Task Names_and_text_the_code_page_cant_hold_are_escaped_and_an_emoji_is_one_question_mark() + { + var doc = new CadDocument(); + doc.Header.CodePage = "ANSI_1252"; + var layer = new Layer("평면 这") { Color = CadBuilder.Visible }; + doc.Layers.Add(layer); + CadBuilder.Add(doc, layer, new TextEntity("한글 도면 😀 끝") { InsertPoint = new XYZ(0, 0, 0), Height = 3 }); + var source = CadBuilder.SaveDxf(doc, fx.NewFolder(), "mixed.dxf"); + + var back = ReadBack(await ConvertAsync(source, "dxf", CadVersion.AutoCad2004)); + + Assert.Contains(back.Layers, l => CadTextCodes.DecodeText(l.Name) == "평면 这"); + Assert.Contains("한글 도면 ? 끝", Shown(back)); + } + + private string SaveDwg(CadDocument doc, string name) + { + var path = Path.Combine(fx.NewFolder(), name); + DwgWriter.Write(path, doc); + return path; + } + [Fact] public void The_code_page_follows_the_text() { From 0edb87bd584144926317c09e2743cd5f798927c7 Mon Sep 17 00:00:00 2001 From: cre8ive-marketing <168519979+cre8ive-marketing@users.noreply.github.com> Date: Fri, 23 Oct 2026 20:30:26 +0530 Subject: [PATCH 3/4] ENGINES.md: names in the code page choice, one ? per emoji, table cells, the DWG code page index --- docs/ENGINES.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/ENGINES.md b/docs/ENGINES.md index f9c105f1c..366fab74d 100644 --- a/docs/ENGINES.md +++ b/docs/ENGINES.md @@ -862,7 +862,10 @@ and writes the files; Filee's own renderer draws them with SkiaSharp (one drawin which AutoCAD 2007 opens; kept from a 2007 source, as 2010. Before 2007, text is stored in the drawing's code page (`$DWGCODEPAGE`), so `CadCodePages` picks the one that holds the most of the drawing's text (its own on a tie; Korean for Hangul, Japanese for Kana, Big5 before GBK for Chinese, East Asian pages only for East Asian text) and - writes any character it can't hold as `\U+XXXX`. Korean is written as `ANSI_949`, as AutoCAD names it (ACadSharp + writes any character it can't hold as `\U+XXXX` (one `?` for a character outside the BMP). Names count and are + escaped too: layers, blocks, text and dimension styles, linetypes, layouts and attribute tags, so a Korean layer in a + drawing with only English text gets `ANSI_949`. Text inside table cells (2004 and later) isn't escaped. In a DWG, the + header's code page index is ACadSharp's (`KSC5601` / `GB2312`), the DXF says `ANSI_949`; both read back the same. Korean is written as `ANSI_949`, as AutoCAD names it (ACadSharp needs `KCS5601` to encode it, so the DXF header is renamed after writing). Objects the chosen version doesn't have are left out with a note: tables and arc dimensions before 2004, multileaders and PDF underlays before 2007, meshes before 2010. ACadSharp's DWG writer is younger than its reader From bd6f9fa59c8199c8b22573b771dfac3b28a19193 Mon Sep 17 00:00:00 2001 From: KnifeLemon Date: Sat, 24 Oct 2026 01:10:51 +0900 Subject: [PATCH 4/4] =?UTF-8?q?Korean=20CAD=20version=20strings=20in=20the?= =?UTF-8?q?=20app's=20usual=20tone=20(=ED=95=B4=EC=9A=94=EC=B2=B4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- src/Filee.App/Assets/i18n/ko.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Filee.App/Assets/i18n/ko.json b/src/Filee.App/Assets/i18n/ko.json index e235745ad..739f5c090 100644 --- a/src/Filee.App/Assets/i18n/ko.json +++ b/src/Filee.App/Assets/i18n/ko.json @@ -112,7 +112,7 @@ "error.geo_unsupported_shape": "지원하지 않는 도형 종류예요", "error.geo_too_large": "Shapefile의 한계인 2GB를 넘어요", "notice.pdf_not_smaller": "더 작게 만들 수 없어서 그대로 복사했어요", - "notice.cad_newer_left_out": "선택한 AutoCAD 버전에 없는 개체(다중 지시선, 메시 …)는 빠졌습니다", + "notice.cad_newer_left_out": "선택한 AutoCAD 버전에 없는 개체(다중 지시선, 메시 …)는 빼고 저장했어요", "notice.original_kept": "원본은 그대로 두고 결과를 사본으로 저장했어요", "notice.original_in_trash": "원본은 휴지통으로 옮겼고, 결과는 저장된 이름 그대로예요", "notice.original_unchanged": "더 작게 만들지 못해서 원본을 그대로 두었어요", @@ -485,7 +485,7 @@ "presets.same_as_source": "원본과 같은 형식", "presets.cad_version": "AutoCAD 버전", "presets.cad_version.same": "원본과 같게", - "presets.cad_version_hint": "이전 버전은 이전 AutoCAD와 CAD 프로그램에서 열립니다. 2007 이전 버전에서는 텍스트를 내용에 맞는 코드 페이지(한국어, 중국어, 일본어 …)로 저장하고, 그 밖의 문자는 \\U+ 코드로 저장합니다. 해당 버전에 없는 개체(다중 지시선, 메시 등)는 빠집니다. 2007 DWG는 AutoCAD 2007에서 열리는 2004로 저장합니다.", + "presets.cad_version_hint": "이전 버전은 이전 AutoCAD와 CAD 프로그램에서 열려요. 2007 이전 버전은 텍스트를 내용에 맞는 코드 페이지(한국어, 중국어, 일본어 …)로 저장하고, 그 밖의 문자는 \\U+ 코드로 저장해요. 그 버전에 없는 개체(다중 지시선, 메시 등)는 빠져요. 2007 DWG는 AutoCAD 2007에서 열리는 2004로 저장해요.", "presets.group.image": "이미지", "presets.quality": "품질", "presets.resize": "크기 조정",