Files
dwsJasonandClaude Opus 4.8 4161e4bd3f Add loose-file input: encode a directory of C1 images; bump to v1.06
The encoder now accepts a directory as <input> in addition to .c2 and
.gsla files. When the input path is a directory, it is scanned for loose
C1 images (Apple IIgs $C1/0000 Super Hi-Res screens) which are gathered
into the same array of 0x8000-byte C1 frames that a .c2 decodes into, then
fed through the existing GSLA encode path.

- New C1File loader (source/c1_file.{h,cpp}), mirroring C2File:
  scans the directory (dirent / FindFirstFile), keeps files ending in
  .c1 or #c10000 (case-insensitive), natural/numeric-sorts them so frame2
  precedes frame10, validates each is exactly 0x8000 bytes (warns + skips
  otherwise).
- main.cpp: auto-detect directory vs file (no new flag), factor the
  encode+round-trip-verify block into shared encodeFramesToGsla() used by
  both the .c2 and directory paths, update help text.
- Wire c1_file.cpp into Makefile and the Visual Studio project.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 15:35:25 -04:00

274 lines
6.8 KiB
C++

//
// C++ Loader
// For a directory of loose C1 files (Apple IIgs $C1/0000 Super Hi-Res screens)
//
// See c1_file.h for the format/behavior summary.
//
#include "c1_file.h"
#include <stdio.h>
#include <stdlib.h>
#include <string>
#include <vector>
#include <algorithm>
#include <cstring> // memcpy
#include <cctype> // tolower, isdigit
#include <cerrno> // errno
#ifdef _WIN32
#include <windows.h>
#else
#include <dirent.h>
#endif
// A C1 frame is exactly this many bytes (raw Super Hi-Res screen)
static const size_t kC1FrameSize = 0x8000;
//------------------------------------------------------------------------------
// Local helpers
//------------------------------------------------------------------------------
static std::string toLower(const std::string& s)
{
std::string result = s;
for (size_t idx = 0; idx < result.size(); ++idx)
{
result[idx] = (char)tolower((unsigned char)result[idx]);
}
return result;
}
// Case insensitive "does S end with SUFFIX"
static bool endsWith(const std::string& S, const std::string& SUFFIX)
{
if (SUFFIX.size() > S.size())
return false;
std::string s = toLower(S);
std::string suffix = toLower(SUFFIX);
return 0 == s.compare(s.size() - suffix.size(), suffix.size(), suffix);
}
// Is this filename a C1 image (by extension/naming convention)?
static bool isC1Filename(const std::string& name)
{
return endsWith(name, ".c1") || endsWith(name, "#c10000");
}
// Natural/numeric comparison: like a normal string compare, except runs of
// digits are compared as integers, so "frame2" sorts before "frame10".
static bool naturalLess(const std::string& a, const std::string& b)
{
std::string la = toLower(a);
std::string lb = toLower(b);
size_t i = 0;
size_t j = 0;
while (i < la.size() && j < lb.size())
{
bool aDigit = 0 != isdigit((unsigned char)la[i]);
bool bDigit = 0 != isdigit((unsigned char)lb[j]);
if (aDigit && bDigit)
{
// Skip leading zeros
size_t aStart = i;
size_t bStart = j;
while (aStart < la.size() && '0' == la[aStart]) ++aStart;
while (bStart < lb.size() && '0' == lb[bStart]) ++bStart;
// Find end of each digit run
size_t aEnd = aStart;
size_t bEnd = bStart;
while (aEnd < la.size() && isdigit((unsigned char)la[aEnd])) ++aEnd;
while (bEnd < lb.size() && isdigit((unsigned char)lb[bEnd])) ++bEnd;
size_t aLen = aEnd - aStart;
size_t bLen = bEnd - bStart;
// Longer significant-digit run is the larger number
if (aLen != bLen)
return aLen < bLen;
// Same length, compare digit by digit
for (size_t k = 0; k < aLen; ++k)
{
if (la[aStart + k] != lb[bStart + k])
return la[aStart + k] < lb[bStart + k];
}
// Numerically equal; shorter original run (more leading zeros) sorts first
size_t aRun = aEnd - i;
size_t bRun = bEnd - j;
if (aRun != bRun)
return aRun < bRun;
i = aEnd;
j = bEnd;
}
else
{
if (la[i] != lb[j])
return la[i] < lb[j];
++i;
++j;
}
}
// Shorter string sorts first when one is a prefix of the other
return (la.size() - i) < (lb.size() - j);
}
// Enumerate the regular-file names in a directory (just the names, not paths).
static std::vector<std::string> listDirectoryFiles(const char* pDirectoryPath)
{
std::vector<std::string> names;
#ifdef _WIN32
std::string pattern = pDirectoryPath;
if (!pattern.empty() && pattern.back() != '\\' && pattern.back() != '/')
pattern += '\\';
pattern += '*';
WIN32_FIND_DATAA findData;
HANDLE hFind = FindFirstFileA(pattern.c_str(), &findData);
if (INVALID_HANDLE_VALUE != hFind)
{
do
{
if (0 == (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
{
names.push_back(findData.cFileName);
}
} while (FindNextFileA(hFind, &findData));
FindClose(hFind);
}
#else
DIR* pDir = opendir(pDirectoryPath);
if (pDir)
{
struct dirent* pEntry = nullptr;
while (nullptr != (pEntry = readdir(pDir)))
{
std::string name = pEntry->d_name;
if ("." == name || ".." == name)
continue;
names.push_back(name);
}
closedir(pDir);
}
#endif
return names;
}
// Join a directory path and a filename with the platform separator.
static std::string joinPath(const std::string& dir, const std::string& name)
{
#ifdef _WIN32
const char sep = '\\';
#else
const char sep = '/';
#endif
if (dir.empty())
return name;
char last = dir[dir.size() - 1];
if (last == '/' || last == '\\')
return dir + name;
return dir + sep + name;
}
//------------------------------------------------------------------------------
C1File::C1File(const char *pDirectoryPath)
: m_widthPixels(320)
, m_heightPixels(200)
{
LoadFromDirectory(pDirectoryPath);
}
//------------------------------------------------------------------------------
C1File::~C1File()
{
// Free Up the memory
for (unsigned int idx = 0; idx < m_pC1PixelMaps.size(); ++idx)
{
delete[] m_pC1PixelMaps[idx];
m_pC1PixelMaps[ idx ] = nullptr;
}
}
//------------------------------------------------------------------------------
void C1File::LoadFromDirectory(const char* pDirectoryPath)
{
// Free Up any existing memory
for (unsigned int idx = 0; idx < m_pC1PixelMaps.size(); ++idx)
{
delete[] m_pC1PixelMaps[idx];
m_pC1PixelMaps[ idx ] = nullptr;
}
m_pC1PixelMaps.clear();
//--------------------------------------------------------------------------
// Gather the C1 filenames
std::vector<std::string> allNames = listDirectoryFiles(pDirectoryPath);
std::vector<std::string> c1Names;
for (size_t idx = 0; idx < allNames.size(); ++idx)
{
if (isC1Filename(allNames[idx]))
c1Names.push_back(allNames[idx]);
}
// Natural sort so the frames play back in the expected order
std::sort(c1Names.begin(), c1Names.end(), naturalLess);
// Load each frame
for (size_t idx = 0; idx < c1Names.size(); ++idx)
{
std::string fullPath = joinPath(pDirectoryPath, c1Names[idx]);
FILE* pFile = nullptr;
#ifdef _WIN32
errno_t err = fopen_s(&pFile, fullPath.c_str(), "rb");
#else
pFile = fopen(fullPath.c_str(), "rb");
int err = (pFile == nullptr) ? errno : 0; // errno_t is Windows-only (Annex K)
#endif
if (0 != err || nullptr == pFile)
{
printf("WARNING: Could not open %s - skipping\n", fullPath.c_str());
continue;
}
fseek(pFile, 0, SEEK_END);
size_t length = ftell(pFile); // get file size
fseek(pFile, 0, SEEK_SET);
if (kC1FrameSize != length)
{
printf("WARNING: %s is %d bytes, expected %d (0x8000) - skipping\n",
fullPath.c_str(), (int)length, (int)kC1FrameSize);
fclose(pFile);
continue;
}
unsigned char* pFrame = new unsigned char[kC1FrameSize];
size_t bytesRead = fread(pFrame, sizeof(unsigned char), kC1FrameSize, pFile);
fclose(pFile);
if (kC1FrameSize != bytesRead)
{
printf("WARNING: %s short read (%d of %d bytes) - skipping\n",
fullPath.c_str(), (int)bytesRead, (int)kC1FrameSize);
delete[] pFrame;
continue;
}
m_pC1PixelMaps.push_back(pFrame);
}
}