mirror of
				https://github.com/davidgiven/fluxengine.git
				synced 2025-10-31 11:17:01 -07:00 
			
		
		
		
	
		
			
				
	
	
		
			533 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			533 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
#include "globals.h"
 | 
						|
#include "flags.h"
 | 
						|
#include "sector.h"
 | 
						|
#include "imagewriter/imagewriter.h"
 | 
						|
#include "image.h"
 | 
						|
#include "lib/config.pb.h"
 | 
						|
#include "lib/layout.h"
 | 
						|
#include "logger.h"
 | 
						|
#include <algorithm>
 | 
						|
#include <iostream>
 | 
						|
#include <fstream>
 | 
						|
#include <chrono>
 | 
						|
#include <ctime>
 | 
						|
 | 
						|
/*
 | 
						|
 * Where to get the type of encoding FM or MFM? Now solved with options in
 | 
						|
 * proto config
 | 
						|
 */
 | 
						|
static const char LABEL[] = "IMD archive by fluxengine on"; // 22 karakters
 | 
						|
static uint8_t getModulationandSpeed(
 | 
						|
    int flags, ImdOutputProto::RecordingMode mode)
 | 
						|
{
 | 
						|
    if (flags == 0)
 | 
						|
    {
 | 
						|
        error(
 | 
						|
            "Can't write IMD files with this speed {}, and modulation {}. Did "
 | 
						|
            "you read a real disk?",
 | 
						|
            flags,
 | 
						|
            false);
 | 
						|
    }
 | 
						|
    else
 | 
						|
    {
 | 
						|
        flags = 1000000.0 / flags;
 | 
						|
    }
 | 
						|
 | 
						|
    if ((flags > 950) and
 | 
						|
        (flags < 1050)) // HD disk 5% discrepency is ok 1000*5% = 50 1 us
 | 
						|
    {
 | 
						|
        /* 500 kbps */
 | 
						|
        if (mode == ImdOutputProto::RECMODE_FM)
 | 
						|
        {
 | 
						|
            return 0;
 | 
						|
        }
 | 
						|
        else
 | 
						|
        {
 | 
						|
            return 3;
 | 
						|
        }
 | 
						|
    }
 | 
						|
    else if ((flags > 1475) and (flags < 1575)) // SD disk
 | 
						|
    {
 | 
						|
        /* 300 kbps*/
 | 
						|
        if (mode == ImdOutputProto::RECMODE_FM)
 | 
						|
        {
 | 
						|
            return 1;
 | 
						|
        }
 | 
						|
        else
 | 
						|
        {
 | 
						|
            return 4;
 | 
						|
        }
 | 
						|
    }
 | 
						|
    else if ((flags > 1900) and (flags < 2100)) // DD disk
 | 
						|
    {
 | 
						|
        /* 250 kbps */
 | 
						|
        if (mode == ImdOutputProto::RECMODE_FM)
 | 
						|
        {
 | 
						|
            return 2;
 | 
						|
        }
 | 
						|
        else
 | 
						|
        {
 | 
						|
            return 5;
 | 
						|
        }
 | 
						|
    }
 | 
						|
    else
 | 
						|
    {
 | 
						|
        error(
 | 
						|
            "IMD: Can't write IMD files with this speed {}, and modulation {}. "
 | 
						|
            "Try another format.",
 | 
						|
            flags,
 | 
						|
            false);
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
struct TrackHeader
 | 
						|
{
 | 
						|
    uint8_t ModeValue;
 | 
						|
    uint8_t track;
 | 
						|
    uint8_t Head;
 | 
						|
    uint8_t numSectors;
 | 
						|
    uint8_t SectorSize;
 | 
						|
};
 | 
						|
 | 
						|
static uint8_t setSectorSize(int flags)
 | 
						|
{
 | 
						|
    switch (flags)
 | 
						|
    {
 | 
						|
        case 128:
 | 
						|
            return 0;
 | 
						|
        case 256:
 | 
						|
            return 1;
 | 
						|
        case 512:
 | 
						|
            return 2;
 | 
						|
        case 1024:
 | 
						|
            return 3;
 | 
						|
        case 2048:
 | 
						|
            return 4;
 | 
						|
        case 4096:
 | 
						|
            return 5;
 | 
						|
        case 8192:
 | 
						|
            return 6;
 | 
						|
    }
 | 
						|
    error(
 | 
						|
        "IMD: Sector size {} not in standard range (128, 256, 512, 1024, 2048, "
 | 
						|
        "4096, 8192).",
 | 
						|
        flags);
 | 
						|
}
 | 
						|
 | 
						|
#define SEC_CYL_MAP_FLAG 0x80
 | 
						|
#define SEC_HEAD_MAP_FLAG 0x40
 | 
						|
#define HEAD_MASK 0x3F
 | 
						|
#define END_OF_FILE 0x1A
 | 
						|
 | 
						|
// clang-format off
 | 
						|
/*
 | 
						|
 * IMAGE FILE FORMAT
 | 
						|
 * The overall layout of an ImageDisk .IMD image file is:
 | 
						|
 * IMD v.vv: dd/mm/yyyy hh:mm:ss
 | 
						|
 * Comment (ASCII only - unlimited size)
 | 
						|
 * 1A byte - ASCII EOF character
 | 
						|
 * - For each track on the disk:
 | 
						|
 * 1 byte Mode value							(0-5) see getModulationspeed for definition		
 | 
						|
 * 1 byte Cylinder							(0-n)
 | 
						|
 * 1 byte Head								(0-1)
 | 
						|
 * 1 byte number of sectors in track			(1-n)
 | 
						|
 * 1 byte sector size							(0-6) see getsectorsize for definition
 | 
						|
 * sector numbering map						IMD start numbering sectors with 1.
 | 
						|
 * sector cylinder map (optional)				definied in high byte of head (since head is 0 or 1)
 | 
						|
 * sector head map (optional)					definied in high byte of head (since head is 0 or 1)
 | 
						|
 * sector data records	For each data record:
 | 
						|
 * 	1 byte Sector status 					
 | 
						|
 * 		0: Sector data unavailable - could not be read
 | 
						|
 * 		1: Normal data: (Sector Size) bytes follow
 | 
						|
 * 		2: Compressed: All bytes in sector have same value (xx)
 | 
						|
 * 		3: Normal data with "Deleted-Data address mark"
 | 
						|
 * 		4: Compressed with "Deleted-Data address mark"
 | 
						|
 * 		5: Normal data read with data error
 | 
						|
 * 		6: Compressed read with data error"
 | 
						|
 * 		7: Deleted data read with data error"
 | 
						|
 * 		8: Compressed, Deleted read with data error"
 | 
						|
 * 	sector size of Sector data
 | 
						|
 * <End of file>
 | 
						|
 */
 | 
						|
// clang-format on
 | 
						|
class ImdImageWriter : public ImageWriter
 | 
						|
{
 | 
						|
public:
 | 
						|
    ImdImageWriter(const ImageWriterProto& config): ImageWriter(config) {}
 | 
						|
 | 
						|
    void writeImage(const Image& image)
 | 
						|
    {
 | 
						|
        const Geometry& geometry = image.getGeometry();
 | 
						|
        unsigned numHeads;
 | 
						|
        unsigned numSectors;
 | 
						|
        unsigned numBytes;
 | 
						|
        std::ofstream outputFile(
 | 
						|
            _config.filename(), std::ios::out | std::ios::binary);
 | 
						|
        if (!outputFile.is_open())
 | 
						|
            error("IMD: cannot open output file");
 | 
						|
        unsigned numSectorsinTrack = 0;
 | 
						|
 | 
						|
        numHeads = geometry.numSides;
 | 
						|
        numSectors = geometry.numSectors;
 | 
						|
        numBytes = geometry.sectorSize;
 | 
						|
 | 
						|
        Bytes imagenew;
 | 
						|
        ByteWriter bw(imagenew);
 | 
						|
 | 
						|
        ImdOutputProto::DataRate dataRate = _config.imd().data_rate();
 | 
						|
        if (dataRate == ImdOutputProto::RATE_GUESS)
 | 
						|
        {
 | 
						|
            dataRate = (geometry.numSectors > 10) ? ImdOutputProto::RATE_HD
 | 
						|
                                                  : ImdOutputProto::RATE_DD;
 | 
						|
            if (geometry.sectorSize <= 256)
 | 
						|
                dataRate = ImdOutputProto::RATE_SD;
 | 
						|
            log("IMD: guessing data rate as {}",
 | 
						|
                ImdOutputProto::DataRate_Name(dataRate));
 | 
						|
        }
 | 
						|
 | 
						|
        ImdOutputProto::RecordingMode recordingMode =
 | 
						|
            _config.imd().recording_mode();
 | 
						|
        if (recordingMode == ImdOutputProto::RECMODE_GUESS)
 | 
						|
        {
 | 
						|
            recordingMode = ImdOutputProto::RECMODE_MFM;
 | 
						|
            log("IMD: guessing recording mode as {}",
 | 
						|
                ImdOutputProto::RecordingMode_Name(recordingMode));
 | 
						|
        }
 | 
						|
 | 
						|
        // Give the user a option to give a comment in the IMD file for archive
 | 
						|
        // purposes.
 | 
						|
        auto start = std::chrono::system_clock::now();
 | 
						|
        std::time_t time = std::chrono::system_clock::to_time_t(start);
 | 
						|
 | 
						|
        std::string comment = _config.imd().comment();
 | 
						|
        if (comment.size() == 0)
 | 
						|
        {
 | 
						|
            comment = LABEL;
 | 
						|
            comment.append(" date: ");
 | 
						|
            comment.append(std::ctime(&time));
 | 
						|
        }
 | 
						|
        else
 | 
						|
        {
 | 
						|
            comment.insert(0, "IMD ");
 | 
						|
        }
 | 
						|
        bw.seek(0);
 | 
						|
 | 
						|
        bw.append(comment);
 | 
						|
        bw.write_8(END_OF_FILE);
 | 
						|
        std::string sector_skew;
 | 
						|
        sector_skew.clear();
 | 
						|
        unsigned Status_Sector = 1;
 | 
						|
        bool blnOptionalCylinderMap = false;
 | 
						|
        bool blnOptionalHeadMap = false;
 | 
						|
 | 
						|
        /* Write the actual sector data. */
 | 
						|
        for (int track = 0; track < geometry.numTracks; track++)
 | 
						|
        {
 | 
						|
            for (int head = 0; head < numHeads; head++)
 | 
						|
            {
 | 
						|
                unsigned sectorIdBase = 1; // IMD starts sector numbering with
 | 
						|
                                           // 1;
 | 
						|
                unsigned sectorId = 0;
 | 
						|
                TrackHeader header = {0,
 | 
						|
                    0,
 | 
						|
                    0,
 | 
						|
                    0,
 | 
						|
                    0}; // define something to hold the header values
 | 
						|
                const auto& sector = image.get(track, head, sectorId + 1);
 | 
						|
                if (!sector)
 | 
						|
                { // sector 0 doesnt exist exit with error
 | 
						|
                    // this track, head has no sectors
 | 
						|
                    Status_Sector = 0;
 | 
						|
                    log("IMD: sector {} not found on track {}, head {}\n",
 | 
						|
                        sectorId + 1,
 | 
						|
                        track,
 | 
						|
                        head);
 | 
						|
                    break;
 | 
						|
                }
 | 
						|
                else
 | 
						|
                {
 | 
						|
                    /* Get the header information */
 | 
						|
                    numBytes =
 | 
						|
                        sector->data.size(); // number of bytes can change per
 | 
						|
                                             // sector per track
 | 
						|
                    header.track = track;
 | 
						|
                    header.Head = head;
 | 
						|
                    header.SectorSize = setSectorSize(numBytes);
 | 
						|
                    sector_skew.clear();
 | 
						|
                    numSectorsinTrack = 0;
 | 
						|
                    nanoseconds_t RATE = 0;
 | 
						|
                    if (sector->clock > 0)
 | 
						|
                    {
 | 
						|
                        RATE = 1000000.0 / sector->clock;
 | 
						|
                    }
 | 
						|
                    else
 | 
						|
                    {
 | 
						|
                        switch (dataRate)
 | 
						|
                        {
 | 
						|
                            case ImdOutputProto::RATE_HD:
 | 
						|
                                RATE = 1000;
 | 
						|
                                break;
 | 
						|
                            case ImdOutputProto::RATE_SD:
 | 
						|
                                RATE = 1500;
 | 
						|
                                break;
 | 
						|
                            case ImdOutputProto::RATE_DD:
 | 
						|
                                RATE = 2000;
 | 
						|
                                break;
 | 
						|
                        }
 | 
						|
                    }
 | 
						|
                    header.ModeValue =
 | 
						|
                        getModulationandSpeed(RATE, recordingMode);
 | 
						|
                }
 | 
						|
                // determine number of sectors in track
 | 
						|
                for (int sectorId = 0; sectorId < numSectors; sectorId++)
 | 
						|
                {
 | 
						|
                    const auto& sector = image.get(track, head, sectorId + 1);
 | 
						|
                    if (!sector)
 | 
						|
                    {
 | 
						|
                        break;
 | 
						|
                    }
 | 
						|
                    else
 | 
						|
                    {
 | 
						|
                        numSectorsinTrack++;
 | 
						|
                    }
 | 
						|
                }
 | 
						|
                // determine sector skew and if there are optional cylindermaps
 | 
						|
                // or headermaps
 | 
						|
                for (int sectorId = 0; sectorId < numSectorsinTrack; sectorId++)
 | 
						|
                {
 | 
						|
                    const auto& sector = image.get(track, head, sectorId + 1);
 | 
						|
                    if (!sector)
 | 
						|
                    {
 | 
						|
                        break;
 | 
						|
                    }
 | 
						|
                    else
 | 
						|
                    {
 | 
						|
                        sector_skew.push_back(
 | 
						|
                            (sectorId + sectorIdBase) +
 | 
						|
                            '0'); // fill sectorskew start with 1
 | 
						|
                        if ((sector->physicalTrack) !=
 | 
						|
                            (sector->logicalTrack)) // different physicaltrack
 | 
						|
                                                    // fromn logicaltrack
 | 
						|
                        {
 | 
						|
                            blnOptionalCylinderMap = true;
 | 
						|
                        }
 | 
						|
                        if (sector->logicalSide !=
 | 
						|
                            sector->physicalSide) // different physicalside
 | 
						|
                                                  // fromn logicalside
 | 
						|
                        {
 | 
						|
                            blnOptionalHeadMap = true;
 | 
						|
                        }
 | 
						|
                    }
 | 
						|
                }
 | 
						|
                bw.write_8(header.ModeValue); // 1 byte ModeValue
 | 
						|
                bw.write_8(track);            // 1 byte Cylinder
 | 
						|
                // are there optional cylinder or head maps?
 | 
						|
                if (blnOptionalCylinderMap)
 | 
						|
                {
 | 
						|
                    header.Head = header.Head ^
 | 
						|
                                  SEC_CYL_MAP_FLAG; // if head was 0 (00000000)
 | 
						|
                                                    // it becomes (10000000)
 | 
						|
                }
 | 
						|
                if (blnOptionalHeadMap)
 | 
						|
                {
 | 
						|
                    header.Head = header.Head ^
 | 
						|
                                  SEC_HEAD_MAP_FLAG; // if head was 1 (00000001)
 | 
						|
                                                     // it becomes (01000001)
 | 
						|
                }
 | 
						|
                bw.write_8(head); // 1 byte Head
 | 
						|
                bw.write_8(
 | 
						|
                    numSectorsinTrack); // 1 byte number of sectors in track
 | 
						|
                bw.write_8(header.SectorSize); // 1 byte sector size
 | 
						|
                for (int sectorId = 0; sectorId < numSectorsinTrack; sectorId++)
 | 
						|
                {
 | 
						|
                    bw.write_8(
 | 
						|
                        (sectorId + sectorIdBase)); // sector numbering map
 | 
						|
                }
 | 
						|
                // Write optional cylinder map
 | 
						|
                // The Sector Cylinder Map has one entry for each sector, and
 | 
						|
                // contains the logical Cylinder ID for the corresponding sector
 | 
						|
                // in the Sector Numbering Map.
 | 
						|
                if (blnOptionalCylinderMap)
 | 
						|
                {
 | 
						|
                    // determine how the optional cylinder map looks like
 | 
						|
                    // write the corresponding logical ID
 | 
						|
                    for (int sectorId = 0; sectorId < numSectorsinTrack;
 | 
						|
                         sectorId++)
 | 
						|
                    {
 | 
						|
                        // const auto& sector = sectors.get(track, head,
 | 
						|
                        // sectorId);
 | 
						|
                        bw.write_8(sector->logicalTrack); // 1 byte logical
 | 
						|
                                                          // track
 | 
						|
                    }
 | 
						|
                }
 | 
						|
 | 
						|
                // Write optional sector head map
 | 
						|
                // The Sector Head Map has one entry for each sector, and
 | 
						|
                // contains the logical Head ID for the corresponding sector in
 | 
						|
                // the Sector Numbering Map.
 | 
						|
                if (blnOptionalHeadMap)
 | 
						|
                {
 | 
						|
                    // determine how the optional head map looks like
 | 
						|
                    // write the corresponding logical ID
 | 
						|
                    for (int sectorId = 0; sectorId < numSectorsinTrack;
 | 
						|
                         sectorId++)
 | 
						|
                    {
 | 
						|
                        //	const auto& sector = sectors.get(track, head,
 | 
						|
                        // sectorId);
 | 
						|
                        bw.write_8(sector->logicalSide); // 1 byte logical side
 | 
						|
                    }
 | 
						|
                }
 | 
						|
                // Now read data and write to file
 | 
						|
                for (int sectorId = 0; sectorId < numSectorsinTrack; sectorId++)
 | 
						|
                {
 | 
						|
                    // clang-format off
 | 
						|
                    /*	For each data record:
 | 
						|
                     *	1 byte Sector status 					
 | 
						|
                     *		0: Sector data unavailable - could not be read
 | 
						|
                     *		1: Normal data: (Sector Size) bytes follow
 | 
						|
                     *		2: Compressed: All bytes in sector have same value (xx)
 | 
						|
                     *		3: Normal data with "Deleted-Data address mark"
 | 
						|
                     *		4: Compressed with "Deleted-Data address mark"
 | 
						|
                     *		5: Normal data read with data error
 | 
						|
                     *		6: Compressed read with data error"
 | 
						|
                     *		7: Deleted data read with data error"
 | 
						|
                     *		8: Compressed, Deleted read with data error"
 | 
						|
                     *	sector size of Sector data
 | 
						|
                     */
 | 
						|
                    // clang-format on
 | 
						|
                    // read sector
 | 
						|
                    const auto& sector = image.get(track, head, sectorId + 1);
 | 
						|
                    bool blnCompressable =
 | 
						|
                        false; // Consists the sector of 1 value? if yes then
 | 
						|
                               // compresses IMD this to 1 value
 | 
						|
                    Bytes sectordata(numBytes); // define the sectordata with
 | 
						|
                                                // the size of the sectorsize
 | 
						|
                    Bytes compressed(
 | 
						|
                        1);       // reserve 1 byte for comressed sectordata
 | 
						|
                    uint8_t byte; // value read
 | 
						|
                    uint8_t
 | 
						|
                        byte_previous; // previous value read (to determine if
 | 
						|
                                       // all bytes are equel in this sector)
 | 
						|
                    if (!sector)
 | 
						|
                    {
 | 
						|
                        Status_Sector = 0;
 | 
						|
                        break;
 | 
						|
                    }
 | 
						|
                    else
 | 
						|
                    {
 | 
						|
                        ByteReader br(sector->data); // read the sector data
 | 
						|
                        int i;
 | 
						|
                        // determine if all bytes are the same -> compress and
 | 
						|
                        // sector status = 2
 | 
						|
                        for (i = 0; i < numBytes; i++)
 | 
						|
                        {
 | 
						|
                            byte = br.read_8();
 | 
						|
                            if (i == 0)
 | 
						|
                            {
 | 
						|
                                byte_previous = byte;
 | 
						|
                            }
 | 
						|
                            if (byte_previous == byte)
 | 
						|
                            {
 | 
						|
                                blnCompressable = true;
 | 
						|
                            }
 | 
						|
                            else
 | 
						|
                            {
 | 
						|
                                blnCompressable = false;
 | 
						|
                                break;
 | 
						|
                            }
 | 
						|
                        }
 | 
						|
                        switch (sector->status)
 | 
						|
                        {
 | 
						|
                            // clang-format off
 | 
						|
                            /* fluxengine knows of a few sector statussen but not all of the statussen in IMD.
 | 
						|
							 *  // the statussen are in sector.h. Translation to fluxengine is as follows:
 | 
						|
							 *	Statussen fluxengine							|	Status IMD		
 | 
						|
							 *--------------------------------------------------------------------------------------------------------------------
 | 
						|
							 *  	OK,											|	1, 2 (Normal data: (Sector Size) of (compressed) bytes follow)
 | 
						|
							 *	BAD_CHECKSUM,									|	5, 6, 7, 8
 | 
						|
							 *	MISSING,	  sector not found					|	0 (Sector data unavailable - could not be read)
 | 
						|
							 *	DATA_MISSING, sector present but no data found	|	3, 4
 | 
						|
							 *	CONFLICT,										|
 | 
						|
							 *	INTERNAL_ERROR									|
 | 
						|
							 */
 | 
						|
                            // clang-format on
 | 
						|
                            case Sector::MISSING: /* Sector data unavailable -
 | 
						|
                                                     could not be read */
 | 
						|
 | 
						|
                                Status_Sector = 0;
 | 
						|
                                break;
 | 
						|
 | 
						|
                            case Sector::OK: /* Normal data: (Sector Size) bytes
 | 
						|
                                                follow */
 | 
						|
                                if (blnCompressable) // data is compressable
 | 
						|
                                {
 | 
						|
                                    Status_Sector = 2;
 | 
						|
                                }
 | 
						|
                                else
 | 
						|
                                {
 | 
						|
                                    Status_Sector = 1;
 | 
						|
                                }
 | 
						|
                                break;
 | 
						|
                            case Sector::DATA_MISSING:
 | 
						|
                                Status_Sector =
 | 
						|
                                    3; // we misuse normal data with
 | 
						|
                                       // deleted-data addres mark for this
 | 
						|
                                       // missing data option
 | 
						|
                                break;
 | 
						|
                            // IMD recognizes all of these cases. but fluxengine
 | 
						|
                            // doesnt support them. case 2: /* Compressed: All
 | 
						|
                            // bytes in sector have same value (xx) */ case 3:
 | 
						|
                            // /* Normal data with "Deleted-Data address mark"
 | 
						|
                            // */ case 4: /* Compressed with "Deleted-Data
 | 
						|
                            // address mark"*/
 | 
						|
                            case Sector::BAD_CHECKSUM:
 | 
						|
                                // case 5: /* Normal data read with data error -
 | 
						|
                                // could not be read*/
 | 
						|
                                Status_Sector = 5;
 | 
						|
                                break;
 | 
						|
                                // case 6: /* Compressed read with data error -
 | 
						|
                                // could not be read */ case 7: /* Deleted data
 | 
						|
                                // read with data error - could not be read */
 | 
						|
                                // case 8: /* Compressed, Deleted read with data
 | 
						|
                                // error - could not be read */
 | 
						|
 | 
						|
                            default:
 | 
						|
                                error(
 | 
						|
                                    "IMD: Don't understand IMD files with "
 | 
						|
                                    "sector status {}",
 | 
						|
                                    Status_Sector);
 | 
						|
                        }
 | 
						|
                        bw.write_8(Status_Sector); // 1 byte status sector
 | 
						|
                        if (blnCompressable)
 | 
						|
                        {
 | 
						|
                            bw.write_8(byte);
 | 
						|
                            blnCompressable = false;
 | 
						|
                        }
 | 
						|
                        else
 | 
						|
                        {
 | 
						|
                            bw.append(sector->data);
 | 
						|
                        }
 | 
						|
                        numSectors = numSectorsinTrack;
 | 
						|
                    }
 | 
						|
                    blnOptionalCylinderMap = false;
 | 
						|
                    blnOptionalHeadMap = false;
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
        imagenew.writeTo(outputFile);
 | 
						|
        log("IMD: Written {} tracks, {} heads, {} sectors, {} bytes per "
 | 
						|
            "sector, {} kB total",
 | 
						|
            geometry.numTracks,
 | 
						|
            numHeads,
 | 
						|
            numSectors,
 | 
						|
            numBytes,
 | 
						|
            outputFile.tellp() / 1024);
 | 
						|
    }
 | 
						|
};
 | 
						|
 | 
						|
std::unique_ptr<ImageWriter> ImageWriter::createImdImageWriter(
 | 
						|
    const ImageWriterProto& config)
 | 
						|
{
 | 
						|
    return std::unique_ptr<ImageWriter>(new ImdImageWriter(config));
 | 
						|
}
 |