We value your input!
Please participate in Archicad 28 Home Screen and Tooltips/Quick Tutorials survey

Archicad C++ API
About Archicad add-on development using the C++ API.

Differences of GX::Image::WriteToFile(...) on Windows and MacOS

DavidKahlEnscape
Participant

Hello everyone,

 

We are developing an Archicad Add-On for Windows and MacOS. We are seeing inconsistent behavior of the GX::Image::WriteToFile(...) function between both platforms. On Windows, everything works as expected. On MacOS, we are getting a log entry “error 20: Not a directory” (macOS system error?) from within the Archicad API, followed by a return code -7003.

The passed path is checked and valid. We can write files to that path directly, so there shouldn’t be any issues with access rights either. The image file is valid too, and we can use the above mentioned function to write it to a location outside the Archicad library.
We are currently implementing a workaround where we copy it into the library via IO::FileSystem::Copy(...), but this cannot be the intended logic, right?

 

This occurs with the latest Archicad 27 Technical Preview.

 

Two questions:

1. Are there any differences in the expected behavior of GX::Image::WriteToFile(...) between Windows and MacOS?2. Where can we find the official documentation for this function and / or class?

 

7 REPLIES 7
Akos Somorjai
Graphisoft
Graphisoft

Hi,

 

Theoretically, there shouldn't be any difference between Mac and Windows.

 

Could you please tell me more about the location you are passing to the function? What does `location.GetStatus()` tell you before you pass it to `WriteToFile()` ? Does the path you are writing to exist?

 

Best, Akos

DavidKahlEnscape
Participant

Hey,

yes, `location.GetStatus()` tells us that it is valid. We currently do IO::FileSystem::Copy(...) to that location as a workaround and it succeeds.

 

I will post a write-up of the code we have and the states the variables are in later.

 

Best regards,
David

DavidKahlEnscape
Participant

Hello again,

to give a bit more context: we get the path to an image on the user's system and want to write it into the Archicad embedded library. I will first show a snippet of the way we use the Archicad API. Our workaround is also in there but commented out.

 

    ACResult<IO::Location> CreateEmbeddedImageTest(const GS::uchar_t* sourcePath)
    {
        static const auto enscapeFolder = L("EnscapeTextures");
        API_SpecFolderID folderId = API_EmbeddedProjectLibraryFolderID;
        
        // get project library folder
        IO::Location libFolderLocation;
        if (auto errCode = ACAPI_ProjectSettings_GetSpecFolder(&folderId, &libFolderLocation); errCode != NoError)
        {
            return error(errCode);
        }

        if (auto errCode = IO::Folder { libFolderLocation }.GetStatus(); errCode != NoError)
        {
            return error(errCode);
        }

        // ensure enscape texture sub folder exists
        if (auto errCode = libFolderLocation.AppendToLocal(IO::Name { enscapeFolder }); errCode != NoError)
        {
            return error(errCode);
        }

        if (auto errCode = IO::fileSystem.CreateFolderTree(libFolderLocation); errCode != NoError)
        {
            return error(errCode);
        }

        IO::Folder enscapeTextureFolder { libFolderLocation };
        if (auto errCode = enscapeTextureFolder.GetStatus(); errCode != NoError)
        {
            return error(errCode);
        }

        IO::Location sourceLocation { sourcePath };
        IO::File sourceFile { sourceLocation, IO::File::OnNotFound::Ignore };

        logDebug(GS::UniString::SPrintf("Reading image from location '%T'.", sourceLocation.ToLogText().ToPrintf()));

        if (auto errCode = sourceFile.GetStatus(); errCode != NoError)
        {
            return error(errCode);
        }

        if (sourceFile.IsReadable())
        {
            IO::Name sourceFileName;
            if (auto errCode = sourceLocation.GetLastLocalName(&sourceFileName); errCode != NoError)
            {
                return error(errCode);
            }
            
            IO::Location targetLocation { libFolderLocation, sourceFileName };
            
            logDebug(GS::UniString::SPrintf("Attempting to write image to location '%T'.", targetLocation.ToLogText().ToPrintf()));

            // This call needs to check for == NoError because of "IO::File::OnNotFound::Fail"
            if (auto err = IO::File(targetLocation, IO::File::OnNotFound::Fail).GetStatus(); err == NoError)
            {
                logDebug(GS::UniString::SPrintf("Deleting old image from location '%T'.", targetLocation.ToLogText().ToPrintf()));

                // Delete so that we can create a new file with the same name in the next call
                IO::fileSystem.Delete(targetLocation);
            }

            auto image = GX::Image(sourceLocation);

            // just to show the image is valid...
            logDebug(GS::UniString::SPrintf("Writing image '%T' (H %i x W %i, %i bit) to file...", 
                sourceLocation.ToLogText().ToPrintf(),
                image.GetHeight(),
                image.GetWidth(),
                image.GetColorDepth()));

            // Here, we now use Copy to work around error on Mac, instead of the line below that
            // auto errCode = IO::fileSystem.Copy(sourceLocation, targetLocation);
            auto errCode = image.WriteToFile(targetLocation, image.GetTypeID());

            if (errCode != NoError)
            {
                logDebug(GS::UniString::SPrintf("Failed to write image '%T' to the embedded library.", targetLocation.ToLogText().ToPrintf()));
                return error(errCode);
            }

            API_LibPart libPart {
                .typeID = APILib_PictID,
                .location = &targetLocation,
            };

            if (auto errCode = ACAPI_LibraryPart_Register(&libPart); errCode != NoError)
            {
                return error(errCode);
            }

            return ok(targetLocation);
        }
        else
        {
            return ok(sourceLocation);
        }
    }

 
This is the log output.

15:57:39.645 [1] | DEBUG | Reading image from location '/Users/{username}/Enscape/ImportedTextures/0b593a9043a7a5013eb5b6ce3b56cf0d/Carpet.jpg'.
15:57:39.645 [1] | DEBUG | Attempting to write image to location '/Users/{username}/Library/Application Support/Graphisoft/AutoSave-27/Emb_1037365571/content/EnscapeTextures/Carpet.jpg'.
15:57:39.646 [1] | DEBUG | Writing image '/Users/{username}/Enscape/ImportedTextures/0b593a9043a7a5013eb5b6ce3b56cf0d/Carpet.jpg' (H 1024 x W 1024, 32 bit) to file...
2023-09-14 15:57:39.652013+0200 Archicad[25010:2669540] [Archicad] IIOImageWriteSession:121: cannot create: '/Users/{username}/Library/Application Support/Graphisoft/AutoSave-27/Emb_1037365571/content/EnscapeTextures/.Carpet.jpg-sWPY'
         error = 20 (Not a directory)
15:57:39.652 [1] | DEBUG | Failed to write image '/Users/{username}/Library/Application Support/Graphisoft/AutoSave-27/Emb_1037365571/content/EnscapeTextures/Carpet.jpg' to the embedded library.

 

Best regards,
David

DavidKahlEnscape
Participant

Edit: Deleted duplicate post.

Akos Somorjai
Graphisoft
Graphisoft

Hi David,

 

The reason why it behaves differently on Mac and Windows is that the underlying mechanism is platform dependent; it uses ImageIO on the Mac.

From your code it seems that you definitely know that the image you are copying is valid, so it’s OK to use the FileSystem::Copy to move it to the embedded library.

 

I guess '{username}’ is replaced by a valid user on the test computer...

 

I’ll try to reproduce this on my computer later during the week; I’m on an offsite workshop on Monday and Tuesday.

 

Best, Akos

DavidKahlEnscape
Participant

Hi Akos,

 

okay, that is good to know that the workaround sounds valid! Still, it would be great if you could reproduce the behavior so that maybe we can end up with one code path on both platforms.

 

Yes, the developer that created the snippet decided to replace his name with '{username}'. It is a valid user on his machine.

 

Best regards,
David 

Hi David,

You've mentioned, that you can use the function to write outside of the embedded library.
I've also had issues with writing directly to the embedded library, but in different situations.
So my initial guess would be that it has more to do with the embedded lib than with the specific write function.

What worked best for me so far was to use APIEnv_CopyFilesIntoLibraryID after deleting the target file first with APIEnv_DeleteEmbeddedLibItemID.
Something like:

 

IO::Location targetFile (embeddedLibLoc, fileName);
err = ACAPI_Environment (APIEnv_DeleteEmbeddedLibItemID, &targetFile);
if (err != NoError) { return err; }

GS::Array<IO::Location> filesToCopy{ file.GetLocation () };
bool overwriteIfExists = true;
err = ACAPI_Environment (APIEnv_CopyFilesIntoLibraryID, &embeddedLibLoc,
	&filesToCopy, &overwriteIfExists);
if (err != NoError) { return err; }

err = ACAPI_Automate (APIDo_ReloadLibrariesID);
if (err != NoError) { return err; }

 

Best,
Bernd