Seth Hillbrand 77797103f7 Add ability to embed files in various elements
Schematics, symbols, boards and footprints all get the ability to store
files inside their file structures.  File lookups now have a
kicad-embed:// URI to allow various parts of KiCad to refer to files
stored in this manner.

kicad-embed://datasheet.pdf references the file named "datasheet.pdf"
embedded in the document.  Embeds are allowed in schematics, boards,
symbols and footprints.  Currently supported embeddings are Datasheets,
3D Models and drawingsheets

Fixes https://gitlab.com/kicad/code/kicad/-/issues/6918

Fixes https://gitlab.com/kicad/code/kicad/-/issues/2376

Fixes https://gitlab.com/kicad/code/kicad/-/issues/17827
2024-07-15 16:06:55 -07:00

700 lines
18 KiB
C++

/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2015-2016 Cirilo Bernardo <cirilo.bernardo@gmail.com>
* Copyright (C) 2018-2022 KiCad Developers, see AUTHORS.txt for contributors.
* Copyright (C) 2022 CERN
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, you may find one here:
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
* or you may search the http://www.gnu.org website for the version 2 license,
* or you may write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#define GLM_FORCE_RADIANS
#include <mutex>
#include <utility>
#include <wx/datetime.h>
#include <wx/dir.h>
#include <wx/log.h>
#include <wx/stdpaths.h>
#include <boost/version.hpp>
#if BOOST_VERSION >= 106800
#include <boost/uuid/detail/sha1.hpp>
#else
#include <boost/uuid/sha1.hpp>
#endif
#include "3d_cache.h"
#include "3d_info.h"
#include "3d_plugin_manager.h"
#include "sg/scenegraph.h"
#include "plugins/3dapi/ifsg_api.h"
#include <advanced_config.h>
#include <common.h> // For ExpandEnvVarSubstitutions
#include <filename_resolver.h>
#include <paths.h>
#include <pgm_base.h>
#include <project.h>
#include <settings/common_settings.h>
#include <settings/settings_manager.h>
#include <wx_filename.h>
#define MASK_3D_CACHE "3D_CACHE"
static std::mutex mutex3D_cache;
static bool isSHA1Same( const unsigned char* shaA, const unsigned char* shaB ) noexcept
{
for( int i = 0; i < 20; ++i )
{
if( shaA[i] != shaB[i] )
return false;
}
return true;
}
static bool checkTag( const char* aTag, void* aPluginMgrPtr )
{
if( nullptr == aTag || nullptr == aPluginMgrPtr )
return false;
S3D_PLUGIN_MANAGER *pp = (S3D_PLUGIN_MANAGER*) aPluginMgrPtr;
return pp->CheckTag( aTag );
}
static const wxString sha1ToWXString( const unsigned char* aSHA1Sum )
{
unsigned char uc;
unsigned char tmp;
char sha1[41];
int j = 0;
for( int i = 0; i < 20; ++i )
{
uc = aSHA1Sum[i];
tmp = uc / 16;
if( tmp > 9 )
tmp += 87;
else
tmp += 48;
sha1[j++] = tmp;
tmp = uc % 16;
if( tmp > 9 )
tmp += 87;
else
tmp += 48;
sha1[j++] = tmp;
}
sha1[j] = 0;
return wxString::FromUTF8Unchecked( sha1 );
}
class S3D_CACHE_ENTRY
{
public:
S3D_CACHE_ENTRY();
~S3D_CACHE_ENTRY();
void SetSHA1( const unsigned char* aSHA1Sum );
const wxString GetCacheBaseName();
wxDateTime modTime; // file modification time
unsigned char sha1sum[20];
std::string pluginInfo; // PluginName:Version string
SCENEGRAPH* sceneData;
S3DMODEL* renderData;
private:
// prohibit assignment and default copy constructor
S3D_CACHE_ENTRY( const S3D_CACHE_ENTRY& source );
S3D_CACHE_ENTRY& operator=( const S3D_CACHE_ENTRY& source );
wxString m_CacheBaseName; // base name of cache file (a SHA1 digest)
};
S3D_CACHE_ENTRY::S3D_CACHE_ENTRY()
{
sceneData = nullptr;
renderData = nullptr;
memset( sha1sum, 0, 20 );
}
S3D_CACHE_ENTRY::~S3D_CACHE_ENTRY()
{
delete sceneData;
if( nullptr != renderData )
S3D::Destroy3DModel( &renderData );
}
void S3D_CACHE_ENTRY::SetSHA1( const unsigned char* aSHA1Sum )
{
if( nullptr == aSHA1Sum )
{
wxLogTrace( MASK_3D_CACHE, wxT( "%s:%s:%d\n * [BUG] NULL passed for aSHA1Sum" ),
__FILE__, __FUNCTION__, __LINE__ );
return;
}
memcpy( sha1sum, aSHA1Sum, 20 );
}
const wxString S3D_CACHE_ENTRY::GetCacheBaseName()
{
if( m_CacheBaseName.empty() )
m_CacheBaseName = sha1ToWXString( sha1sum );
return m_CacheBaseName;
}
S3D_CACHE::S3D_CACHE()
{
m_FNResolver = new FILENAME_RESOLVER;
m_project = nullptr;
m_Plugins = new S3D_PLUGIN_MANAGER;
}
S3D_CACHE::~S3D_CACHE()
{
FlushCache();
delete m_FNResolver;
delete m_Plugins;
}
SCENEGRAPH* S3D_CACHE::load( const wxString& aModelFile, const wxString& aBasePath,
S3D_CACHE_ENTRY** aCachePtr, const EMBEDDED_FILES* aEmbeddedFiles )
{
if( aCachePtr )
*aCachePtr = nullptr;
wxString full3Dpath = m_FNResolver->ResolvePath( aModelFile, aBasePath, aEmbeddedFiles );
if( full3Dpath.empty() )
{
// the model cannot be found; we cannot proceed
wxLogTrace( MASK_3D_CACHE, wxT( "%s:%s:%d\n * [3D model] could not find model '%s'\n" ),
__FILE__, __FUNCTION__, __LINE__, aModelFile );
return nullptr;
}
// check cache if file is already loaded
std::lock_guard<std::mutex> lock( mutex3D_cache );
std::map< wxString, S3D_CACHE_ENTRY*, rsort_wxString >::iterator mi;
mi = m_CacheMap.find( full3Dpath );
if( mi != m_CacheMap.end() )
{
wxFileName fname( full3Dpath );
if( fname.FileExists() ) // Only check if file exists. If not, it will
{ // use the same model in cache.
bool reload = ADVANCED_CFG::GetCfg().m_Skip3DModelMemoryCache;
wxDateTime fmdate = fname.GetModificationTime();
if( fmdate != mi->second->modTime )
{
unsigned char hashSum[20];
getSHA1( full3Dpath, hashSum );
mi->second->modTime = fmdate;
if( !isSHA1Same( hashSum, mi->second->sha1sum ) )
{
mi->second->SetSHA1( hashSum );
reload = true;
}
}
if( reload )
{
if( nullptr != mi->second->sceneData )
{
S3D::DestroyNode( mi->second->sceneData );
mi->second->sceneData = nullptr;
}
if( nullptr != mi->second->renderData )
S3D::Destroy3DModel( &mi->second->renderData );
mi->second->sceneData = m_Plugins->Load3DModel( full3Dpath,
mi->second->pluginInfo );
}
}
if( nullptr != aCachePtr )
*aCachePtr = mi->second;
return mi->second->sceneData;
}
// a cache item does not exist; search the Filename->Cachename map
return checkCache( full3Dpath, aCachePtr );
}
SCENEGRAPH* S3D_CACHE::Load( const wxString& aModelFile, const wxString& aBasePath, const EMBEDDED_FILES* aEmbeddedFiles )
{
return load( aModelFile, aBasePath, nullptr, aEmbeddedFiles );
}
SCENEGRAPH* S3D_CACHE::checkCache( const wxString& aFileName, S3D_CACHE_ENTRY** aCachePtr )
{
if( aCachePtr )
*aCachePtr = nullptr;
unsigned char sha1sum[20];
S3D_CACHE_ENTRY* ep = new S3D_CACHE_ENTRY;
m_CacheList.push_back( ep );
wxFileName fname( aFileName );
ep->modTime = fname.GetModificationTime();
if( !getSHA1( aFileName, sha1sum ) || m_CacheDir.empty() )
{
// just in case we can't get a hash digest (for example, on access issues)
// or we do not have a configured cache file directory, we create an
// entry to prevent further attempts at loading the file
if( m_CacheMap.emplace( aFileName, ep ).second == false )
{
wxLogTrace( MASK_3D_CACHE,
wxT( "%s:%s:%d\n * [BUG] duplicate entry in map file; key = '%s'" ),
__FILE__, __FUNCTION__, __LINE__, aFileName );
m_CacheList.pop_back();
delete ep;
}
else
{
if( aCachePtr )
*aCachePtr = ep;
}
return nullptr;
}
if( m_CacheMap.emplace( aFileName, ep ).second == false )
{
wxLogTrace( MASK_3D_CACHE,
wxT( "%s:%s:%d\n * [BUG] duplicate entry in map file; key = '%s'" ),
__FILE__, __FUNCTION__, __LINE__, aFileName );
m_CacheList.pop_back();
delete ep;
return nullptr;
}
if( aCachePtr )
*aCachePtr = ep;
ep->SetSHA1( sha1sum );
wxString bname = ep->GetCacheBaseName();
wxString cachename = m_CacheDir + bname + wxT( ".3dc" );
if( !ADVANCED_CFG::GetCfg().m_Skip3DModelFileCache && wxFileName::FileExists( cachename )
&& loadCacheData( ep ) )
return ep->sceneData;
ep->sceneData = m_Plugins->Load3DModel( aFileName, ep->pluginInfo );
if( !ADVANCED_CFG::GetCfg().m_Skip3DModelFileCache && nullptr != ep->sceneData )
saveCacheData( ep );
return ep->sceneData;
}
bool S3D_CACHE::getSHA1( const wxString& aFileName, unsigned char* aSHA1Sum )
{
if( aFileName.empty() )
{
wxLogTrace( MASK_3D_CACHE, wxT( "%s:%s:%d\n * [BUG] empty filename" ),
__FILE__, __FUNCTION__, __LINE__ );
return false;
}
if( nullptr == aSHA1Sum )
{
wxLogTrace( MASK_3D_CACHE, wxT( "%s\n * [BUG] NULL pointer passed for aMD5Sum" ),
__FILE__, __FUNCTION__, __LINE__ );
return false;
}
#ifdef _WIN32
FILE* fp = _wfopen( aFileName.wc_str(), L"rb" );
#else
FILE* fp = fopen( aFileName.ToUTF8(), "rb" );
#endif
if( nullptr == fp )
return false;
boost::uuids::detail::sha1 dblock;
unsigned char block[4096];
size_t bsize = 0;
while( ( bsize = fread( &block, 1, 4096, fp ) ) > 0 )
dblock.process_bytes( block, bsize );
fclose( fp );
unsigned int digest[5];
dblock.get_digest( digest );
// ensure MSB order
for( int i = 0; i < 5; ++i )
{
int idx = i << 2;
unsigned int tmp = digest[i];
aSHA1Sum[idx+3] = tmp & 0xff;
tmp >>= 8;
aSHA1Sum[idx+2] = tmp & 0xff;
tmp >>= 8;
aSHA1Sum[idx+1] = tmp & 0xff;
tmp >>= 8;
aSHA1Sum[idx] = tmp & 0xff;
}
return true;
}
bool S3D_CACHE::loadCacheData( S3D_CACHE_ENTRY* aCacheItem )
{
wxString bname = aCacheItem->GetCacheBaseName();
if( bname.empty() )
{
wxLogTrace( MASK_3D_CACHE,
wxT( " * [3D model] cannot load cached model; no file hash available" ) );
return false;
}
if( m_CacheDir.empty() )
{
wxLogTrace( MASK_3D_CACHE,
wxT( " * [3D model] cannot load cached model; config directory unknown" ) );
return false;
}
wxString fname = m_CacheDir + bname + wxT( ".3dc" );
if( !wxFileName::FileExists( fname ) )
{
wxLogTrace( MASK_3D_CACHE, wxT( " * [3D model] cannot open file '%s'" ), fname.GetData() );
return false;
}
if( nullptr != aCacheItem->sceneData )
S3D::DestroyNode( (SGNODE*) aCacheItem->sceneData );
aCacheItem->sceneData = (SCENEGRAPH*)S3D::ReadCache( fname.ToUTF8(), m_Plugins, checkTag );
if( nullptr == aCacheItem->sceneData )
return false;
return true;
}
bool S3D_CACHE::saveCacheData( S3D_CACHE_ENTRY* aCacheItem )
{
if( nullptr == aCacheItem )
{
wxLogTrace( MASK_3D_CACHE, wxT( "%s:%s:%d\n * NULL passed for aCacheItem" ),
__FILE__, __FUNCTION__, __LINE__ );
return false;
}
if( nullptr == aCacheItem->sceneData )
{
wxLogTrace( MASK_3D_CACHE, wxT( "%s:%s:%d\n * aCacheItem has no valid scene data" ),
__FILE__, __FUNCTION__, __LINE__ );
return false;
}
wxString bname = aCacheItem->GetCacheBaseName();
if( bname.empty() )
{
wxLogTrace( MASK_3D_CACHE,
wxT( " * [3D model] cannot load cached model; no file hash available" ) );
return false;
}
if( m_CacheDir.empty() )
{
wxLogTrace( MASK_3D_CACHE,
wxT( " * [3D model] cannot load cached model; config directory unknown" ) );
return false;
}
wxString fname = m_CacheDir + bname + wxT( ".3dc" );
if( wxFileName::Exists( fname ) )
{
if( !wxFileName::FileExists( fname ) )
{
wxLogTrace( MASK_3D_CACHE,
wxT( " * [3D model] path exists but is not a regular file '%s'" ), fname );
return false;
}
}
return S3D::WriteCache( fname.ToUTF8(), true, (SGNODE*)aCacheItem->sceneData,
aCacheItem->pluginInfo.c_str() );
}
bool S3D_CACHE::Set3DConfigDir( const wxString& aConfigDir )
{
if( !m_ConfigDir.empty() )
return false;
wxFileName cfgdir( ExpandEnvVarSubstitutions( aConfigDir, m_project ), wxEmptyString );
cfgdir.Normalize( FN_NORMALIZE_FLAGS );
if( !cfgdir.DirExists() )
{
cfgdir.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
if( !cfgdir.DirExists() )
{
wxLogTrace( MASK_3D_CACHE,
wxT( "%s:%s:%d\n * failed to create 3D configuration directory '%s'" ),
__FILE__, __FUNCTION__, __LINE__, cfgdir.GetPath() );
return false;
}
}
m_ConfigDir = cfgdir.GetPath();
// inform the file resolver of the config directory
if( !m_FNResolver->Set3DConfigDir( m_ConfigDir ) )
{
wxLogTrace( MASK_3D_CACHE,
wxT( "%s:%s:%d\n * could not set 3D Config Directory on filename resolver\n"
" * config directory: '%s'" ),
__FILE__, __FUNCTION__, __LINE__, m_ConfigDir );
}
// 3D cache data must go to a user's cache directory;
// unfortunately wxWidgets doesn't seem to provide
// functions to retrieve such a directory.
//
// 1. OSX: ~/Library/Caches/kicad/3d/
// 2. Linux: ${XDG_CACHE_HOME}/kicad/3d ~/.cache/kicad/3d/
// 3. MSWin: AppData\Local\kicad\3d
wxFileName cacheDir;
cacheDir.AssignDir( PATHS::GetUserCachePath() );
cacheDir.AppendDir( wxT( "3d" ) );
if( !cacheDir.DirExists() )
{
cacheDir.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
if( !cacheDir.DirExists() )
{
wxLogTrace( MASK_3D_CACHE,
wxT( "%s:%s:%d\n * failed to create 3D cache directory '%s'" ),
__FILE__, __FUNCTION__, __LINE__, cacheDir.GetPath() );
return false;
}
}
m_CacheDir = cacheDir.GetPathWithSep();
return true;
}
bool S3D_CACHE::SetProject( PROJECT* aProject )
{
m_project = aProject;
bool hasChanged = false;
if( m_FNResolver->SetProject( aProject, &hasChanged ) && hasChanged )
{
m_CacheMap.clear();
std::list< S3D_CACHE_ENTRY* >::iterator sL = m_CacheList.begin();
std::list< S3D_CACHE_ENTRY* >::iterator eL = m_CacheList.end();
while( sL != eL )
{
delete *sL;
++sL;
}
m_CacheList.clear();
return true;
}
return false;
}
void S3D_CACHE::SetProgramBase( PGM_BASE* aBase )
{
m_FNResolver->SetProgramBase( aBase );
}
FILENAME_RESOLVER* S3D_CACHE::GetResolver() noexcept
{
return m_FNResolver;
}
std::list< wxString > const* S3D_CACHE::GetFileFilters() const
{
return m_Plugins->GetFileFilters();
}
void S3D_CACHE::FlushCache( bool closePlugins )
{
std::list< S3D_CACHE_ENTRY* >::iterator sCL = m_CacheList.begin();
std::list< S3D_CACHE_ENTRY* >::iterator eCL = m_CacheList.end();
while( sCL != eCL )
{
delete *sCL;
++sCL;
}
m_CacheList.clear();
m_CacheMap.clear();
if( closePlugins )
ClosePlugins();
}
void S3D_CACHE::ClosePlugins()
{
if( m_Plugins )
m_Plugins->ClosePlugins();
}
S3DMODEL* S3D_CACHE::GetModel( const wxString& aModelFileName, const wxString& aBasePath,
const EMBEDDED_FILES* aEmbeddedFiles )
{
S3D_CACHE_ENTRY* cp = nullptr;
SCENEGRAPH* sp = load( aModelFileName, aBasePath, &cp, aEmbeddedFiles );
if( !sp )
return nullptr;
if( !cp )
{
wxLogTrace( MASK_3D_CACHE,
wxT( "%s:%s:%d\n * [BUG] model loaded with no associated S3D_CACHE_ENTRY" ),
__FILE__, __FUNCTION__, __LINE__ );
return nullptr;
}
if( cp->renderData )
return cp->renderData;
S3DMODEL* mp = S3D::GetModel( sp );
cp->renderData = mp;
return mp;
}
void S3D_CACHE::CleanCacheDir( int aNumDaysOld )
{
wxDir dir;
wxString fileSpec = wxT( "*.3dc" );
wxArrayString fileList; // Holds list of ".3dc" files found in cache directory
size_t numFilesFound = 0;
wxFileName thisFile;
wxDateTime lastAccess, thresholdDate;
wxDateSpan durationInDays;
// Calc the threshold date above which we delete cache files
durationInDays.SetDays( aNumDaysOld );
thresholdDate = wxDateTime::Now() - durationInDays;
// If the cache directory can be found and opened, then we'll try and clean it up
if( dir.Open( m_CacheDir ) )
{
thisFile.SetPath( m_CacheDir ); // Set the base path to the cache folder
// Get a list of all the ".3dc" files in the cache directory
numFilesFound = dir.GetAllFiles( m_CacheDir, &fileList, fileSpec );
for( unsigned int i = 0; i < numFilesFound; i++ )
{
// Completes path to specific file so we can get its "last access" date
thisFile.SetFullName( fileList[i] );
// Only get "last access" time to compare against. Don't need the other 2 timestamps.
if( thisFile.GetTimes( &lastAccess, nullptr, nullptr ) )
{
if( lastAccess.IsEarlierThan( thresholdDate ) )
{
// This file is older than the threshold so delete it
wxRemoveFile( thisFile.GetFullPath() );
}
}
}
}
}