From 229515fbd73f7b521b558c9b8f4cc20d85618ace Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Tue, 3 Dec 2024 04:48:18 +0100 Subject: [PATCH] DXF: add creation option to set and DXF system variables --- autotest/ogr/ogr_dxf.py | 102 +++++++++++++- doc/source/drivers/vector/dxf.rst | 34 +++-- ogr/ogrsf_frmts/dxf/ogr_dxf.h | 3 + ogr/ogrsf_frmts/dxf/ogrdxfdatasource.cpp | 4 + ogr/ogrsf_frmts/dxf/ogrdxfdriver.cpp | 22 +++ ogr/ogrsf_frmts/dxf/ogrdxfwriterds.cpp | 163 ++++++++++++++++++++++- 6 files changed, 313 insertions(+), 15 deletions(-) diff --git a/autotest/ogr/ogr_dxf.py b/autotest/ogr/ogr_dxf.py index 161cef93883d..3f54f375b271 100644 --- a/autotest/ogr/ogr_dxf.py +++ b/autotest/ogr/ogr_dxf.py @@ -17,7 +17,7 @@ import ogrtest import pytest -from osgeo import gdal, ogr +from osgeo import gdal, ogr, osr ############################################################################### @@ -4020,3 +4020,103 @@ def test_ogr_dxf_read_closed_polyline_with_bulge(): f = lyr.GetNextFeature() g = f.GetGeometryRef() assert g.GetGeometryType() == ogr.wkbPolygon + + +############################################################################### + + +def test_ogr_dxf_write_INSUNITS(tmp_vsimem): + + filename = str(tmp_vsimem / "out.dxf") + + with ogr.GetDriverByName("DXF").CreateDataSource( + filename, options=["INSUNITS=METERS"] + ) as ds: + pass + with ogr.Open(filename) as ds: + assert ds.GetMetadataItem("$INSUNITS", "DXF_HEADER_VARIABLES") == "6" + + with ogr.GetDriverByName("DXF").CreateDataSource( + filename, options=["INSUNITS=21"] + ) as ds: + pass + with ogr.Open(filename) as ds: + assert ds.GetMetadataItem("$INSUNITS", "DXF_HEADER_VARIABLES") == "21" + + with gdal.quiet_errors(): + with ogr.GetDriverByName("DXF").CreateDataSource( + filename, options=["INSUNITS=INVALID"] + ) as ds: + pass + with ogr.Open(filename) as ds: + assert ds.GetMetadataItem("$INSUNITS", "DXF_HEADER_VARIABLES") == " 1" + + with ogr.GetDriverByName("DXF").CreateDataSource( + filename, options=["INSUNITS=FROM_CRS"] + ) as ds: + srs = osr.SpatialReference() + srs.ImportFromEPSG(32631) + ds.CreateLayer("test", srs=srs) + with ogr.Open(filename) as ds: + assert ds.GetMetadataItem("$INSUNITS", "DXF_HEADER_VARIABLES") == "6" + + # No CRS + with gdal.quiet_errors(): + with ogr.GetDriverByName("DXF").CreateDataSource( + filename, options=["INSUNITS=FROM_CRS"] + ) as ds: + pass + with ogr.Open(filename) as ds: + assert ds.GetMetadataItem("$INSUNITS", "DXF_HEADER_VARIABLES") == " 1" + + # Not a projected CRS + with gdal.quiet_errors(): + with ogr.GetDriverByName("DXF").CreateDataSource( + filename, options=["INSUNITS=FROM_CRS"] + ) as ds: + srs = osr.SpatialReference() + srs.ImportFromEPSG(4326) + ds.CreateLayer("test", srs=srs) + with ogr.Open(filename) as ds: + assert ds.GetMetadataItem("$INSUNITS", "DXF_HEADER_VARIABLES") == " 1" + + # Unknown linear units + with gdal.quiet_errors(): + with ogr.GetDriverByName("DXF").CreateDataSource( + filename, options=["INSUNITS=FROM_CRS"] + ) as ds: + srs = osr.SpatialReference() + srs.ImportFromProj4("+proj=merc +to_meter=2") + ds.CreateLayer("test", srs=srs) + with ogr.Open(filename) as ds: + assert ds.GetMetadataItem("$INSUNITS", "DXF_HEADER_VARIABLES") == " 1" + + +############################################################################### + + +def test_ogr_dxf_write_MEASUREMENT(tmp_vsimem): + + filename = str(tmp_vsimem / "out.dxf") + + with ogr.GetDriverByName("DXF").CreateDataSource( + filename, options=["MEASUREMENT=METRIC"] + ) as ds: + pass + with ogr.Open(filename) as ds: + assert ds.GetMetadataItem("$MEASUREMENT", "DXF_HEADER_VARIABLES") == "1" + + with ogr.GetDriverByName("DXF").CreateDataSource( + filename, options=["MEASUREMENT=1"] + ) as ds: + pass + with ogr.Open(filename) as ds: + assert ds.GetMetadataItem("$MEASUREMENT", "DXF_HEADER_VARIABLES") == "1" + + with gdal.quiet_errors(): + with ogr.GetDriverByName("DXF").CreateDataSource( + filename, options=["MEASUREMENT=INVALID"] + ) as ds: + pass + with ogr.Open(filename) as ds: + assert ds.GetMetadataItem("$MEASUREMENT", "DXF_HEADER_VARIABLES") == " 0" diff --git a/doc/source/drivers/vector/dxf.rst b/doc/source/drivers/vector/dxf.rst index 30b348621b34..4d2fad9921ae 100644 --- a/doc/source/drivers/vector/dxf.rst +++ b/doc/source/drivers/vector/dxf.rst @@ -405,6 +405,30 @@ The driver supports the following dataset creation options: Override the trailer file used - in place of trailer.dxf located in the GDAL_DATA directory. +- .. dsco:: FIRST_ENTITY + :choices: + + Identifier of first entity + +- .. dsco:: INSUNITS + :choices: HEADER_VALUE, FROM_CRS, UNITLESS, INCHES, FEET, MILLIMETERS, CENTIMETERS, METERS, US_SURVEY_FEET + :default: HEADER_VALUE + :since: 3.11 + + Drawing units for the model space (`$INSUNITS `__ system variable). + Defaults to the value of the header template, which is ``INCHES``. + The special value ``FROM_CRS`` means that the linear units of the CRS of + the layer, when it is a projected CRS, are used to set ``$INSUNITS``. + +- .. dsco:: MEASUREMENT + :choices: HEADER_VALUE, IMPERIAL, METRIC + :default: HEADER_VALUE + :since: 3.11 + + Drawing units for the model space (`$MEASUREMENT `__ system variable). + Defaults to the value of the header template, which is ``IMPERIAL``. + + The header and trailer templates can be complete DXF files. The driver will scan them and only extract the needed portions (portion before or after the ENTITIES section). @@ -474,16 +498,6 @@ It is assumed that patterns are using "g" (georeferenced) units for defining the line pattern. If not, the scaling of the DXF patterns is likely to be wrong - potentially very wrong. -Units -~~~~~ - -GDAL writes DXF files with measurement units set to "Imperial - Inches". -If you need to change the units, edit the -`$MEASUREMENT `__ -and -`$INSUNITS `__ -variables in the header template. - -------------- diff --git a/ogr/ogrsf_frmts/dxf/ogr_dxf.h b/ogr/ogrsf_frmts/dxf/ogr_dxf.h index 7b84a419c8a0..355caa171c2a 100644 --- a/ogr/ogrsf_frmts/dxf/ogr_dxf.h +++ b/ogr/ogrsf_frmts/dxf/ogr_dxf.h @@ -957,6 +957,9 @@ class OGRDXFWriterDS final : public GDALDataset bool m_bHeaderFileIsTemp = false; bool m_bTrailerFileIsTemp = false; + OGRSpatialReference m_oSRS{}; + std::string m_osINSUNITS = "HEADER_VALUE"; + std::string m_osMEASUREMENT = "HEADER_VALUE"; public: OGRDXFWriterDS(); diff --git a/ogr/ogrsf_frmts/dxf/ogrdxfdatasource.cpp b/ogr/ogrsf_frmts/dxf/ogrdxfdatasource.cpp index 52a3dc74323c..5c3d26b5e0df 100644 --- a/ogr/ogrsf_frmts/dxf/ogrdxfdatasource.cpp +++ b/ogr/ogrsf_frmts/dxf/ogrdxfdatasource.cpp @@ -821,6 +821,8 @@ bool OGRDXFDataSource::ReadHeaderSection() } oHeaderVariables[l_osName] = szLineBuf; + GDALDataset::SetMetadataItem(l_osName.c_str(), szLineBuf, + "DXF_HEADER_VARIABLES"); } if (nCode < 0) { @@ -857,6 +859,8 @@ bool OGRDXFDataSource::ReadHeaderSection() } oHeaderVariables[l_osName] = szLineBuf; + GDALDataset::SetMetadataItem(l_osName.c_str(), szLineBuf, + "DXF_HEADER_VARIABLES"); } if (nCode < 0) { diff --git a/ogr/ogrsf_frmts/dxf/ogrdxfdriver.cpp b/ogr/ogrsf_frmts/dxf/ogrdxfdriver.cpp index 707a9ee1389b..9ae41e4ac95b 100644 --- a/ogr/ogrsf_frmts/dxf/ogrdxfdriver.cpp +++ b/ogr/ogrsf_frmts/dxf/ogrdxfdriver.cpp @@ -129,6 +129,28 @@ void RegisterOGRDXF() "file' default='trailer.dxf'/>" " " + " " ""); poDriver->SetMetadataItem( diff --git a/ogr/ogrsf_frmts/dxf/ogrdxfwriterds.cpp b/ogr/ogrsf_frmts/dxf/ogrdxfwriterds.cpp index 5442a8f5df2b..a6b0c2607a83 100644 --- a/ogr/ogrsf_frmts/dxf/ogrdxfwriterds.cpp +++ b/ogr/ogrsf_frmts/dxf/ogrdxfwriterds.cpp @@ -270,6 +270,11 @@ int OGRDXFWriterDS::Open(const char *pszFilename, char **papszOptions) if (CSLFetchNameValue(papszOptions, "FIRST_ENTITY") != nullptr) nNextFID = atoi(CSLFetchNameValue(papszOptions, "FIRST_ENTITY")); + m_osINSUNITS = + CSLFetchNameValueDef(papszOptions, "INSUNITS", m_osINSUNITS.c_str()); + m_osMEASUREMENT = CSLFetchNameValueDef(papszOptions, "MEASUREMENT", + m_osMEASUREMENT.c_str()); + /* -------------------------------------------------------------------- */ /* Prescan the header and trailer for entity codes. */ /* -------------------------------------------------------------------- */ @@ -317,12 +322,17 @@ int OGRDXFWriterDS::Open(const char *pszFilename, char **papszOptions) /* ICreateLayer() */ /************************************************************************/ -OGRLayer * -OGRDXFWriterDS::ICreateLayer(const char *pszName, - const OGRGeomFieldDefn * /*poGeomFieldDefn*/, - CSLConstList /*papszOptions*/) +OGRLayer *OGRDXFWriterDS::ICreateLayer(const char *pszName, + const OGRGeomFieldDefn *poGeomFieldDefn, + CSLConstList /*papszOptions*/) { + if (poGeomFieldDefn) + { + const auto poSRS = poGeomFieldDefn->GetSpatialRef(); + if (poSRS) + m_oSRS = *poSRS; + } if (EQUAL(pszName, "blocks") && poBlocksLayer == nullptr) { poBlocksLayer = new OGRDXFBlocksWriterLayer(this); @@ -529,6 +539,151 @@ bool OGRDXFWriterDS::TransferUpdateHeader(VSILFILE *fpOut) } } + // Patch INSUNITS + if (nCode == 9 && EQUAL(szLineBuf, "$INSUNITS") && + m_osINSUNITS != "HEADER_VALUE") + { + if (!WriteValue(fpOut, nCode, szLineBuf)) + return false; + nCode = oHeaderDS.ReadValue(szLineBuf, sizeof(szLineBuf)); + if (nCode == 70) + { + int nVal = -1; + if (m_osINSUNITS == "FROM_CRS") + { + if (m_oSRS.IsEmpty()) + { + CPLError(CE_Warning, CPLE_AppDefined, + "Layer has no CRS. Using default value of " + "INSUNIT from template header file"); + } + else if (m_oSRS.IsProjected()) + { + const char *pszUnits = nullptr; + const double dfUnits = m_oSRS.GetLinearUnits(&pszUnits); + if (std::fabs(dfUnits - 1) <= 1e-10) + { + nVal = 6; + } + else if (std::fabs(dfUnits - + CPLAtof(SRS_UL_FOOT_CONV)) <= 1e-10) + { + nVal = 2; + } + else if (std::fabs(dfUnits - + CPLAtof(SRS_UL_US_FOOT_CONV)) <= + 1e-10) + { + nVal = 21; + } + else + { + CPLError(CE_Warning, CPLE_AppDefined, + "Could not translate CRS unit %s to " + "INSUNIT. Using default value from " + "template header file", + pszUnits); + } + } + else + { + CPLError(CE_Warning, CPLE_AppDefined, + "Layer CRS is not a projected CRS. " + "Using default value of INSUNIT from " + "template header file"); + } + } + else + { + static const struct + { + const char *pszValue; + int nValue; + } INSUNITSMap[] = { + {"UNITLESS", 0}, + {"INCHES", 1}, + {"FEET", 2}, + {"MILLIMETERS", 4}, + {"CENTIMETERS", 5}, + {"METERS", 6}, + {"US_SURVEY_FEET", 21}, + }; + + for (const auto &sTuple : INSUNITSMap) + { + if (m_osINSUNITS == sTuple.pszValue || + m_osINSUNITS == CPLSPrintf("%d", sTuple.nValue)) + { + nVal = sTuple.nValue; + break; + } + } + if (nVal < 0) + { + CPLError(CE_Warning, CPLE_AppDefined, + "Could not translate INSUNITS=%s. " + "Using default value from template header " + "file", + m_osINSUNITS.c_str()); + } + } + + if (nVal >= 0) + { + if (!WriteValue(fpOut, nCode, CPLSPrintf("%d", nVal))) + return false; + + continue; + } + } + } + + // Patch MEASUREMENT + if (nCode == 9 && EQUAL(szLineBuf, "$MEASUREMENT") && + m_osMEASUREMENT != "HEADER_VALUE") + { + if (!WriteValue(fpOut, nCode, szLineBuf)) + return false; + nCode = oHeaderDS.ReadValue(szLineBuf, sizeof(szLineBuf)); + if (nCode == 70) + { + int nVal = -1; + + static const struct + { + const char *pszValue; + int nValue; + } MEASUREMENTMap[] = { + {"IMPERIAL", 0}, + {"METRIC", 1}, + }; + + for (const auto &sTuple : MEASUREMENTMap) + { + if (m_osMEASUREMENT == sTuple.pszValue || + m_osMEASUREMENT == CPLSPrintf("%d", sTuple.nValue)) + { + nVal = sTuple.nValue; + break; + } + } + if (nVal < 0) + { + CPLError(CE_Warning, CPLE_AppDefined, + "Could not translate MEASUREMENT=%s. " + "Using default value from template header file", + m_osMEASUREMENT.c_str()); + } + else + { + if (!WriteValue(fpOut, nCode, CPLSPrintf("%d", nVal))) + return false; + + continue; + } + } + } + // Copy over the source line. if (!WriteValue(fpOut, nCode, szLineBuf)) return false;