Введение
На сегодняшний день QR-коды (quick-response) широко используются в различных сферах. Структура QR-кода была разработана в Японии Масахиро Хара.
Хочу поделиться с читателями «Хабрахабра» способом формирования QR-кода в формате машинной вышивки Tajima DST. Данный метод позволяет исключить ручные операции по формированию QR-кода и последующего преобразования полученной картинки в дизайн машинной вышивки. Если у вас или ваших знакомых есть вышивальная машина, то загрузив полученный файл в память машины и выполнив вышивку, можно получить следующее:
Проблема
Ни в одной программе для работы с машинной вышивкой нет функции формирования QR-кода. Чтобы получить файл вышивки QR-кода, необходимо сначала получить картинку QR-кода в любом позволяющем это сделать online-сервисе, затем преобразовать её в блоки стежков средствами редактора машинной вышивки. Для одного или двух QR-кодов такой подход приемлем. Для формирования неограниченного количества QR-кодов необходимо исключить ручные операции.
Решение
Сформируем самостоятельно QR-код с помощью сторонней библиотеки, затем по полученной матрице для каждой колонки построим набор стежков для непрерывно идущих ячеек матрицы. Для качества QR-кода опорные квадраты сформируем отдельно, для этого разделим матрицу на шесть областей:
Вышивка протестирована на вышивальной машине модели Brother NV 90E.
?
Описание входных данных
Входная строка может быть в формате электронной визитной карточки (VCARD), может содержать информацию о географическом положении (GEO). Также это может быть просто текст или строка URL.
Входные данные в виде VCARD:
BEGIN:VCARD
VERSION:3.0
FN:к.м.н., пр. Василий Иванович Квакин
N:Квакин;Василий;Иванович;пр.,к.м.н.
ORG:Рога и Копыта
URL:http://ru.wikipedia.org/Вася_Квакин
EMAIL;TYPE=INTERNET:vasya.kvakin@example.com
END:VCARD
Полное описание формата VCARD здесь.
Входные данные в виде географических координат, первая координата — долгота, вторая — широта:
GEO:30.31616, 59.95015
Описание приложения
Приложение написано на C#. Используется библиотека MessagingToolkit.QRCode, позволяющая создавать QR-код по входящей информационной строке. Библиотека устанавливается пакетом с nuget.org через консоль менеджера пакетов:
PM> Install-Package MessagingToolkit.QRCode
Матрица QR-кода формируется в виде двумерного массива логических значений.
Получив матрицу QR-кода, перейдём к следующему шагу – формированию списка линий для формирования по ним последовательностей стежков.
Будем считать линию набором последовательно идущих ячеек QR-кода без пропусков. Линии могут быть как вертикальные так горизонтальные в случае опорных рамок QR-кода. Набор линий используется для формирования блоков стежков.
Три опорных прямоугольника располагаются по углам QR-code. Разделим матрицу на 6 областей. Первая область — это левый верхний прямоугольник, стежки которого формируются в первую очередь. Стежки для прямоугольника формируются последовательно для всех его сторон, а не вертикальными колонками как в общем случае. Затем формируются стежки для линий находящихся между опорными левым верхним и левым нижним опорными прямоугольниками. Стежки для нечетных колонок формируются сверху вниз. Для четных колонок стежки формируются снизу вверх. Такая последовательность стежков исключает длинные переходы нити снизу вверх и наоборот. Четвёртая область — самая большая область, формируется аналогично второй. Пятая область – опорный прямоугольник, находящийся в правом верхнем углу. Шестая область – завершающая, стежки для колонок в ней формируются также: нечётные сверху вниз, чётные снизу вверх.
Класс QRCodeCreator
Класс использует пространство имён MessagingToolkit.QRCode.Codec для формирования матрицы QR-кода в следующем методе:
using System.Text;
using MessagingToolkit.QRCode.Codec;
namespace EmbroideryFile.QRCode
{
internal class QRCodeCreator
{
public bool[][] GetQRCodeMatrix(string DataToEncode)
{
if (string.IsNullOrEmpty(DataToEncode)) return new bool[1][];
QRCodeEncoder qrCodeEncoder = new QRCodeEncoder();
qrCodeEncoder.CharacterSet = "UTF8";
qrCodeEncoder.QRCodeEncodeMode = QRCodeEncoder.ENCODE_MODE.BYTE;
qrCodeEncoder.QRCodeScale = 1;
qrCodeEncoder.QRCodeVersion = -1;
qrCodeEncoder.QRCodeErrorCorrect = QRCodeEncoder.ERROR_CORRECTION.L;
return qrCodeEncoder.CalQrcode(Encoding.UTF8.GetBytes(DataToEncode));
}
}
}
CharacterSet устанавливаем UTF8, для возможности кодирования символов кириллицы.
Свойству QRCodeErrorCorrect присваиваем значение QRCodeEncoder.ERROR_CORRECTION.L — низкий уровень избыточности при кодировании.
Считаем, что излишняя избыточность данных при чтении не нужна.
Если в файле входной строке присутствуют символы кириллицы, то файл должен быть обязательно сохранён в кодировке UTF8.
Экземпляр этого класса создаётся в конструкторе класса QRCodeStitcher.
?
Класс QRCodeStitcher
Формирование всех видов блоков стежков реализовано в этом классе.Это обеспечивается следующими этапами:
- Формирование списка непрерывных линий для каждой из 6-ти областей;
- Генерация стежков для каждой области по списку линий.
Для формирования списка вертикальных линий по матрице QR-кода выполняем проход по ячейкам вертикальных колонок и при пустой текущей ячейке добавляем текущую линию в результирующий список. Исключение составляют опорные квадраты расположенные по краям QR-кода. Каждый элемент списка содержит данные о начальной точке, конечной точке линии, её длине, а также признак попадания самой нижней ячейки линии в последнюю строку матрицы QR-кода.
При формировании стежков для вертикальных линий перемещения по осям ординат и абсцисс имеют фиксированные значения: dX = 25; dY = 2; Размер ячейки QR-кода также зафиксирован: cellSize = 25 единиц. Единицы измерения здесь 0.1 мм.
Модель данных линии представлена в виде следующей структуры:
public struct Line
{
public Coords Dot1 { get; set; }
public Coords Dot2 { get; set; }
public int Length { get; set; }
public bool Lowest { get; set; }
}
Следующий метод формирует блоки стежков для всех 6-ти описанных ранее областей QR-кода:
private List<List<Coords>> GenerateQRCodeStitchesBoxed()
{
var blocks = new List<List<Coords>>();
int rectSize = GetRectSize();
// левый верхний прямоугольник
blocks.AddRange(GetRectangleSatin(0, 0, rectSize - 1, rectSize - 1));
// левый верхний квадрат
blocks.Add(GenerateBoxStitchBlock(2, 2, rectSize - 4));
// область между верхним и нижним прямоугольниками
blocks.AddRange(GetSatinStitches(GetLaneList(0, rectSize + 1, rectSize, _dimension - rectSize - 1)));
// левый нижний прямоугольник
blocks.AddRange(GetRectangleSatin(0, _dimension - rectSize, rectSize - 1, _dimension - 1));
// левый нижний внутренний квадрат
blocks.Add(GenerateBoxStitchBlock(2, _dimension - rectSize + 2, rectSize - 4));
// средняя область
blocks.AddRange(GetSatinStitches(GetLaneList(rectSize + 1, 0, _dimension - rectSize - 1,
_dimension - 1)));
// правый верхний прямоугольник
blocks.AddRange(GetRectangleSatin(_dimension - rectSize, 0, _dimension - 1, rectSize - 1));
// правый верхний внутренний квадрат
blocks.Add(GenerateBoxStitchBlock(_dimension - rectSize + 2, 2, rectSize - 4));
// область под правым верхним прямоугольником
blocks.AddRange(GetSatinStitches(GetLaneList(_dimension - rectSize, rectSize + 1, _dimension - 1,
_dimension - 1)));
return blocks;
}
Метод GetRectangleSatin() создаёт блоки для квадратов стежков по координатам крайних ячеек:
IEnumerable<List<Coords>> GetRectangleSatin(int x1, int y1, int x2, int y2)
{
int LeftX = (x1 > x2) ? x2 : x1;
int TopY = (y1 > y2) ? y2 : y1;
int RightX = (x1 < x2) ? x2 : x1;
var BottomY = (y1 < y2) ? y2 : y1;
int length = RightX - LeftX;
var rect = new List<List<Coords>>();
rect.Add(GenerateVerticalColumnStitchBlock(LeftX, TopY, length));
rect.Add(GenerateHorizonColumnStitchBlock(LeftX, BottomY, length));
rect.Add(ReverseCoords(GenerateVerticalColumnStitchBlock(RightX, TopY + 1, length)));
rect.Add(ReverseCoords(GenerateHorizonColumnStitchBlock(LeftX + 1, TopY, length)));
return rect;
}
Следующий метод создаёт для генерации внутреннего квадрата опорных областей QR-кода:
/// <summary>
/// Создаёт список стежков для заполненного квадрата
/// </summary>
/// <param name="cellHorizonPos">Горизонтальная позиция верхней левой ячейки квадрата </param>
/// <param name="cellVerticalPos">Вертикальная позиция верхней левой ячейки квадрата </param>
/// <param name="boxSize">Размер квадрата</param>
/// <returns>Список координат</returns>
private List<Coords> GenerateBoxStitchBlock(int cellHorizonPos, int cellVerticalPos, int boxSize)
{
var block = new List<Coords>();
int y = 0; int x = 0;
int startX = cellHorizonPos * _cellSize;
int startY = cellVerticalPos * _cellSize;
block.Add(new Coords { X = startX, Y = startY });
while (y < _cellSize * boxSize)
{
while (x < _cellSize * boxSize - _dX)
{
x = x + _dX;
block.Add(new Coords{ X = startX + x, Y = startY + y });
}
x = boxSize * _cellSize;
block.Add(new Coords { X = startX + x, Y = startY + y });
y = y + _dY;
while (x > _dX)
{
x = x - _dX;
block.Add(new Coords { X = startX + x, Y = startY + y });
}
x = 0;
block.Add(new Coords { X = startX + x, Y = startY + y });
y = y + _dY;
}
return block;
}
Блоки стежков для последовательности вертикальных линий формируются в следующем методе:
/// <summary>
/// Формирует список блоков стежков по списку непрерывных вертикальных линий
/// </summary>
private List<List<Coords>> GetSatinStitches(List<Line> lanes)
{
List<List<Coords>> blockList = new List<List<Coords>>();
foreach (var lane in lanes)
{
List<Coords> satin = null;
if (((lane.Length == 1) && ((lane.Dot1.X % 2) == 0)) ||
((lane.Length > 1) && (lane.Dot2.Y > lane.Dot1.Y)))
satin = GenerateVerticalColumnStitchBlock(lane.Dot1.X, lane.Dot1.Y, lane.Length);
else
satin = ReverseCoords(GenerateVerticalColumnStitchBlock(lane.Dot2.X, lane.Dot2.Y, lane.Length));
blockList.Add(satin);
}
return blockList;
}
Список линий формируется для областей 2, 4, 6 в следующем методе. Проверка завершения линии выполняется в методах ConsumeRelativeCellDown() и ConsumeRelativeCellUp().
/// <summary>
/// Возвращает список вертикальных линий для указанной по угловым ячейкам области
/// </summary>
/// <param name="x1">X координата крайней ячейки области</param>
/// <param name="y1">Y координата крайней ячейки области</param>
/// <param name="x2">X координата крайней ячейки области</param>
/// <param name="y2">Y координата крайней ячейки области</param>
/// <returns></returns>
private List<Line> GetLaneList(int x1, int y1, int x2, int y2)
{
try
{
if (_lines != null) _lines.Clear();
if (y1 > y2)
{
_topY = y2;
_bottomY = y1;
}
else
{
_topY = y1;
_bottomY = y2;
}
if (x1 > x2)
{
_leftX = x2;
_rightX = x1;
}
else
{
_leftX = x1;
_rightX = x2;
}
for (int j = _leftX; j <= _rightX; j = j + 2) //X
{
_state = false;
for (int i = _topY; i <= _bottomY; i++) // Y
{
ConsumeRelativeCellDown(j, i);
}
if (j >= _rightX) break;
_state = false;
for (int i = _bottomY; i >= _topY; i--) // Y
{
ConsumeRelativeCellUp(j + 1, i);
}
}
return _lines;
}
catch (Exception ex)
{
Trace.WriteLine(string.Format("GetLineList(): {0}",ex));
throw;
}
}
Метод ConsumeRelativeCellDown() вызывается при формировании списка линий для чётной колонки QR-кода.
/// <summary>
/// Проверка прерывания текущей линии при проходе сверху вниз
/// </summary>
/// <param name="j"></param>
/// <param name="i"></param>
void ConsumeRelativeCellDown(int j, int i)
{
if (_cells[j][i] == true)
{
// начало линии в верхней строке области
if ((i == _topY))
{
_dot1 = new Coords() { X = j, Y = i };
_curLane.Dot1 = _dot1;
_laneLen = 1;
_state = true;
}
else if ((_state == false))
{
// одиночная ячейка внизу матрицы
if (i == _bottomY)
{
_dot1 = new Coords() { X = j, Y = i };
_curLane.Dot1 = _dot1;
_dot2 = new Coords() { X = j, Y = i };
_curLane.Dot2 = _dot2;
_curLane.Length = 1;
_curLane.Lowest = true;
_endLaneFlag = true;
}
// начало линии
else
{
_dot1 = new Coords() { X = j, Y = i };
_curLane.Dot1 = _dot1;
_laneLen = 1;
_state = true;
}
}
else if ((i == _bottomY))
{
// конец линии внизу
_dot2 = new Coords() { X = j, Y = i };
_curLane.Dot2 = _dot2;
_curLane.Length = ++_laneLen;
_curLane.Lowest = true;
_endLaneFlag = true;
} // линия продолжается
else
{
_laneLen++;
}
}
// конец линии, не крайняя ячейка
else if (_state == true)
{
_dot2 = new Coords() { X = j, Y = i - 1 };
_curLane.Dot2 = _dot2;
_curLane.Length = _laneLen;
_state = false;
_endLaneFlag = true;
}
if (_endLaneFlag == true)
{
_lines.Add(_curLane);
_endLaneFlag = false;
}
}
Метод ConsumeRelativeCellUp() вызывается при формировании списка линий для нечётной колонки QR-кода.
void ConsumeRelativeCellUp(int j, int i)
{
if (_cells[j][i] == true)
{
// начало линии внизу
if ((i == _bottomY))
{
_dot1 = new Coords { X = j, Y = i };
_curLane.Dot1 = _dot1;
_laneLen = 1;
_state = true;
}
else if ((_state == false))
{
// одинокая ячейка
if (i == _topY)
{
_dot1 = new Coords { X = j, Y = i };
_curLane.Dot1 = _dot1;
_dot2 = new Coords { X = j, Y = i };
_curLane.Dot2 = _dot2;
_curLane.Length = 1;
_curLane.Lowest = true;
_endLaneFlag = true;
}
// начало линии
else
{
_dot1 = new Coords { X = j, Y = i };
_curLane.Dot1 = _dot1;
_laneLen = 1;
_state = true;
}
}
else if ((i == _topY))
{
// end of lane at the top
_dot2 = new Coords { X = j, Y = i };
_curLane.Dot2 = _dot2;
_curLane.Length = ++_laneLen;
_curLane.Lowest = true;
_endLaneFlag = true;
} // линия продолжается
else
{
_laneLen++;
}
}
// конец линии, не крайняя строка
else if (_state)
{
_dot2 = new Coords { X = j, Y = i + 1 };
_curLane.Dot2 = _dot2;
_curLane.Length = _laneLen;
_state = false;
_endLaneFlag = true;
}
if (_endLaneFlag)
{
_lines.Add(_curLane);
_endLaneFlag = false;
}
}
Чётные колонки вышиваются сверху вниз, нечётные снизу вверх, это позволяет исключить длинные стежки перемещения нити при переходе к следующей колонке ячеек QR-кода. Следующий код реализует логику добавления стежков в линию:
/// <summary>
/// Формирование стежков вертикальной линии из соответсвующей позиции
/// </summary>
/// <param name="cellHorizonPos">абсцисса верхней ячейки линии</param>
/// <param name="cellVerticalPos">ордината </param>
/// <param name="length"></param>
private List<Coords> GenerateVerticalColumnStitchBlock(int cellHorizonPos, int cellVerticalPos, int length)
{
var block = new List<Coords>();
int curX, curY;
int columnLength = _cellSize * length;
int startX = cellHorizonPos * _cellSize;
int startY = cellVerticalPos * _cellSize;
block.Add(new Coords { X = startX + _cellSize, Y = startY });
for (curY = 0; curY < columnLength; curY = curY + _dY)
{
for (curX = (curY == 0) ? 0 : _dX; (curX < _cellSize) && (curY < columnLength); curX = curX + _dX)
{
block.Add(new Coords { X = startX + curX, Y = startY + curY });
curY = curY + _dY;
}
int edgedX = _cellSize - (curX - _dX);
int edgedY = edgedX * _dY / _dX;
curX = _cellSize;
curY = curY + edgedY - _dY;
block.Add(new Coords { X = startX + curX, Y = startY + curY });
curY = curY + _dY;
for (curX = _cellSize - _dX; (curX > 0) && (curY < columnLength); curX = curX - _dX)
{
block.Add(new Coords { X = startX + curX, Y = startY + curY });
curY = curY + _dY;
}
edgedX = curX + _dX;
edgedY = edgedX * _dY / _dX;
curY = curY + edgedY - _dY;
block.Add(new Coords { X = startX, Y = startY + curY });
}
curX = _cellSize;
curY = columnLength;
block.Add(new Coords { X = startX + curX, Y = startY + curY });
return block;
}
Класс QrcodeDst
В конструкторе класса создаются экземпляры классов DstFile и QrCodeStitcher.
public QrcodeDst()
{
_dst = new DstFile();
_stitchGen = new QrCodeStitcher();
}
Класс имеет следующий метод установки свойства:
public QRCodeStitchInfo QrStitchInfo
{
set { _stitchGen.Info = value; }
}
В классе QrcodeDst реализован метод FillStreamWithDst(Stream stream) выполняющий сохранение QR-кода в формате машинной вышивки Tajima DST.Метод GetQRCodeStitchBlocks() обеспечивает формирование блоков стежков для вышивки в виде списка списков координат с дополнительной информацией является ли первый стежок стежком перехода или останова. Свойство QrStitchInfo класса QrcodeDst предназначено для получения входной информации в виде строки, для хранения матрицы QR-кода.
Метод класса DstFile WriteStitchesToDstStream() принимает в качестве параметров список блоков координат и экземпляр Stream для записи в него данных стежков в формате машинной вышивки.
Следующий фрагмент кода читает данные для кодирования из файла и использует экземпляр QrcodeDst для сохранения последовательностей стежков QR-кода в файл машинной вышивки:
var qrcodeGen = new QrcodeDst();
using (var inputStreamReader = new StreamReader(fileName))
{
var text = inputStreamReader.ReadToEnd();
using (Stream outStream = new FileStream(outputPath, FileMode.Create, FileAccess.Write))
{
if (qrcodeGen != null)
{
qrcodeGen.QrStitchInfo = new QRCodeStitchInfo {QrCodeText = text};
qrcodeGen.FillStream(outStream);
}
}
}
Формат файла для сохранения вышивки описан в следующем параграфе.
Формат DST файла
Для преобразование координат стежков в байты использовано описание формата DST файла отсюда. Последовательность стежков хранится в виде кодированных смещений относительно предыдущего стежка. То есть в файле хранятся команды на перемещение нити, с указанием типа стежка.
Возможные типы стежков:
• Обычный
• Переход
• Останов
Стежок останова, позволяет сменить нить, если это делается вручную.
DST файл имеет заголовок, данные стежков начинаются с 512-ого байта при нумерации байтов
с нуля.
Стежок кодируется тремя байтами:
Номер бита | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
Байт 1 | y+1 | y-1 | y+9 | y-9 | x-9 | x+9 | x-1 | x+1 |
Байт 2 | y+3 | y-3 | y+27 | y-27 | x-27 | x+27 | x-3 | x+3 |
Байт 3 | переход | останов | y+81 | y-81 | x-81 | x+81 | Установлен всегда | Установлен всегда |
Биты перехода и останова могут быть установлены одновременно. Это необходимо при длинном переходе и одновременной смене нити.
DST файл обязательно должен заканчиваться тремя байтами: 00 00 F3.
Ниже приведён код возвращающий байты стежка по значениям перемещения нити относительно предыдущей позиции:
byte[] encode_record(int x, int y, DstStitchType stitchType)
{
byte b0, b1, b2;
b0 = b1 = b2 = 0;
byte[] b = new byte[3];
// следующие значение преобразовать невозможно >+121 or < -121.
if (x >= +41) { b2 += setbit(2); x -= 81; };
if (x <= -41) { b2 += setbit(3); x += 81; };
if (x >= +14) { b1 += setbit(2); x -= 27; };
if (x <= -14) { b1 += setbit(3); x += 27; };
if (x >= +5) { b0 += setbit(2); x -= 9; };
if (x <= -5) { b0 += setbit(3); x += 9; };
if (x >= +2) { b1 += setbit(0); x -= 3; };
if (x <= -2) { b1 += setbit(1); x += 3; };
if (x >= +1) { b0 += setbit(0); x -= 1; };
if (x <= -1) { b0 += setbit(1); x += 1; };
if (x != 0)
{
throw;
};
if (y >= +41) { b2 += setbit(5); y -= 81; };
if (y <= -41) { b2 += setbit(4); y += 81; };
if (y >= +14) { b1 += setbit(5); y -= 27; };
if (y <= -14) { b1 += setbit(4); y += 27; };
if (y >= +5) { b0 += setbit(5); y -= 9; };
if (y <= -5) { b0 += setbit(4); y += 9; };
if (y >= +2) { b1 += setbit(7); y -= 3; };
if (y <= -2) { b1 += setbit(6); y += 3; };
if (y >= +1) { b0 += setbit(7); y -= 1; };
if (y <= -1) { b0 += setbit(6); y += 1; };
if (y != 0)
{
throw;
};
switch (stitchType)
{
case DstStitchType.NORMAL:
b2 += (byte)3;
break;
case DstStitchType.END:
b2 = (byte)243;
b0 = b1 = (byte)0;
break;
case DstStitchType.JUMP:
b2 += (byte)131;
break;
case DstStitchType.STOP:
b2 += (byte)195;
break;
default:
b2 += 3;
break;
};
b[0] = b0; b[1] = b1; b[2] = b2;
return b;
}
Формирования машинной вышивки QR-кода можно посмотреть по ссылке.
Скачать исходный код формирования машинной вышивки QR-кода можно скачать по следующей ссылке.
Загрузить консольное приложение формирующее файл вышивки можно по сылке.
В папке с приложением находятся необходимые библиотеки, исполняемый файл и текстовый файл содержащий информацию для кодирования.
Для запуска приложения наберите следующее в командной строке:
qrcodegen.exe test.asc
Приложение формирует файл с расширением .DST в папке с приложением. Возможно формирование векторного файла SVG и растрового файла PNG. Файл может быть открыть в программе для редактирования машинной вышивки, например http://florianisoftware.com.
Ссылки по теме
• Site of Nathan Crawford – Код с этого сайта использован как основа для формирования PNG файла машинной вышивки.
• Rudolf?s Homepage Описание формата Taijama DST
• Embroidermodder site — Embroidermodder бесплатный инструмент для работы дизайнами машинной вышивки
Комментарии (8)
Kolegg
12.05.2015 22:18У меня есть дальний родственник, который постоянно мучается (точнее мучаюсь я) с ломаной программой для вышивки (там инструкция по установке на страницу). Насколько хороша и легка в освоении программа с embroidermodder.org? Как у нее с оптимизацией рисунка по количеству длинных нитей, которые нужно обрезать?
Den367 Автор
12.05.2015 22:49Первая версия программы embroidermodder написана под Windows, и более не развивается.
Естественно, что коммерческие программы (floriani, embird) обладают большей функциональностью чем embroidermodder.
Embroidermodder2 — программа с открытым исходным кодом, это её основной плюс.Находится в стадии развития.
Оптимизации количества длинных линий на данный момент не обнаружено.
Функционал достачно прост, никаких излишеств:
koltykov
16.05.2015 10:21В Wilcom встроен Corel Draw, в котором есть модуль генерации QR-кодов. Код генерируется и конвертируется в Wilcom за пару щелчков мышкой. Затем в любой нужный формат конвертируется.
Den367 Автор
18.05.2015 11:08Спасибо за комментарий. Wilcom Embroidery Studio — серьёзный программный продукт, но и цена на него такая же серьёзная.
koltykov
18.05.2015 11:57Ну машинная вышивка все же для большинства бизнес :)
А когда этим занимаешься профессионально, то на хорошее ПО и денег не жалко, т.к. в любом случае они окупятся. Я перепробовал много разных ПО: PE Desing, Embird и др. Wilcom из всех мне больше всего понравился, даже я сказал бы так — на голову выше остальных. Если сделают фотостежок как в PE Design, то ему не будет равных.
zikher
странный формат… для кодирования 242 значений использовать 10 бит…