Про формат SVG и Inkscape

Jul 27, 2024 02:58

Снова "провалился в кроличью нору". Наконец-то вывел и реализовал интерполяцию движения в плоскости при помощи некоммутирующих дуальных комплексных чисел, попутно научился ими пользоваться, причём мне показалось, что под 2D-игры и прочую 2D-графику они весьма интересны. Это одно из самых экономичных представлений произвольного движения, имеет интуитивно понятное умножение "слева" и "справа", и прямо "из коробки" позволяет делать параллакс, когда у нас несколько "плоскостей", которые двигаются с разными скоростями, включая и совсем "задник", который остаётся неизменным.

Накатал текста аж на 45 килобайт, но подумал, что никого это не впечатлит, если примером применения опять станут пресловутые гармошки. Надо демонстрацию покрасивше сделать, и с этой целью решил научиться парсить SVG-файл, не весь целиком, а хотя бы элемент Path. Штука оказалась весьма своеобразной, равно как и его взаимоотношения с Inkscape'ом, да и откуда есть появился Inkscape...

Мой первый успешный парсинг/рендер "корешка" из LaTeX:



Сам по себе SVG (Scalable Vector Graphics) имеет XML синтаксис. Возьмём "корешок", сгенерированный из LaTeX:


Его можно открыть в текстовом редакторе, будет вот такое:

Здесь использовано пять элементов path, описывающих символ корня, икс, игрек, двойку и плюс. Затем двойка использована дважды, а все остальные символы - по одному разу.

Непосредственно описание "пути" содержится в аттрибуте "d", т.е data. Ровно в этом месте авторам вдруг безумно захотелось поэкономить интернет-трафик, и они начали экономить на спичках!

Весь этот "путь" состоит из команд, каждая из которых описана одной-единственной буквой:
- MoveTo, или M: переместить "перо" на новую позицию, ничего при этом не рисуя. Ровно с этой команды начинается каждый путь, чтобы было ясно, откуда начинать вести кривую. Если буква большая, значит координаты абсолютные, если маленькая - то относительные, т.е смещение относительно прошлого положения "пера".
- LineTo, или L: нарисовать прямую линию от текущего положения до указанных координат. То же самое: большая буква - координаты абсолютные, маленькая буква - относительные.
- Horizontal, или H: горизонтальная прямая, поэтому координата нужна всего одна.
- Vertical, или V: вертикальная прямая, та же история.
- CurveTo, хотя я её запомнил как CubicBezier, короче, C: нарисовать кубическую кривую Безье от текущего положения. Первыми передаются координаты первой управляющей точки, затем второй, и под конец - конечная точка кривой,
- Smooth, а я подумал Symmetric, в общем, S: всё та же кривая Безье, но первую управляющую точку не передаём - она расположена зеркально относительно последней управляющей точки, если у нас последней командой тоже был Безье. А если была прямая, то первая управляющая точка совпадает просто с первой точкой, в результате чего кривая в этом месте максимально спрямлённая.
- QuadraticBezier, Q: квадратичная кривая Безье, обходящаяся одной контрольной точкой. Передаём сначала её, потом конечную точку.
- T, вообще не знаю, от чего это аббревиатура, в общем, снова квадратичный Безье, но контрольную точку не передаём - она зеркально отражена, если последним был тоже квадратичный Безье, либо совпадает с первой точкой, что вообще сведёт его к обычной прямой линии между двумя точками, но вот захотелось...
- Arc, A: дуга, т.е кусочек эллипса. Передаются размеры большой и малой оси эллипса, его наклон, два булёвых значения, как именно этот эллипс "пристроить", и конечная точка. Странно, что они эти булёвы значения не включили в оси эллипса, придав им знаки, ну вот захотелось так.
- Zамкнуть, или Z: команда без аргументов. Говорит провести прямую линию от текущего места в начало кривой, и в целом объявить эту кривую замкнутой.

Вроде бы кажется логичным... А потом начинается копеечная экономия. Все ненужные разделители можно убрать! Ввёл команду - сразу пиши число, безо всяких пробелов. Одно число написал, а второе у тебя отрицательное - пиши прям слитно, парсер должен догадаться, что как только посреди первого числа вдруг минус появился, значит это уже следующее число началось! Или если оно положительное, меньше единицы, и десятичная точка в первом числе уже была, пиши это число прям сразу, ведь запись .5 вполне нормальная, и снова парсер сообразит: раз вторая точка, значит, за ней уже следующее число.

Нужно несколько одинаковых команд подряд, например, кубические Безье - напиши C всего один раз, а потом сыпь аргументами до посинения! Отсчитав первые 6 аргументов и увидев следующие, парсер должен понять: значит, пошла ещё одна Безье, и всё это прочитать.

Но с командой M (MoveTo) такой фокус не проходит: если за первыми двумя аргументами последовали ещё и ещё, стандарт призывает интерпретировать все последующие пары как неявную команду L (LineTo), т.к просто двигать точку туда-сюда, ничего не рисуя, бессмысленно.

Я это дело СЛУЧАЙНО поизучал по документации на SVG 2.0, не зная, что он ещё нифига не принят, и, очень вероятно, уже и не будет принят, т.к "всем пофиг". Конкретно для path они предлагали буквально несколько нововведений. Во-первых, команда B (Bearing), которая меняет смысл относительных координат. Если раньше нужно было к текущему X прибавить dX, к текущему Y прибавить dY, то теперь dX по сути означает сдвиг "вперёд", а dY - "влево по ходу движения", ну а направление движения - тот самый Bearing, который по умолчанию нулевой, но можно его задавать. Идея была - сделать что-то вроде "черепашьей графики".

А второе нововведение ещё усложняет парсер, поскольку предлагает воспринимать Z не только как обособленную команду, но и ставить её вместо последней пары координат, например, в кубическом Безье. Это будет означать: конечной точкой этой кривой Безье станет начальная точка всей кривой целиком, и на этом построение завершаем, считая кривую замкнутой.

Мне стало интересно посмотреть, насколько "оптимизированный код" даёт тот же самый Inkscape, для чего я нарисовал в нём дугу и немудрёную кривулину (не через окружность, а через кривые Безье):



и потом открыл файл в текстовом редакторе. Первое, что я увидел - аттрибуты sodipodi повсюду. Вот где описывается дуга:

Мне поначалу казалось, что это какая-то аббревиатура, на манер MISO/MOSI в SPI. В спецификации на SVG ничего подобного не нашёл, но в целом поиск в интернете дал результаты. Оказалось, что Inkscape - это форк от графического редактора Sodipodi, который, в свою очередь форк от редактора Gill, сделанный одним человеком из Эстонии с русской фамилией. В англоязычном интервью он объяснял, что sodipodi - это эстонское слово, означающее мешанину, но лично мне кажется, корректный русский перевод здесь - каляка-маляка.

Ещё тогда внутреннее представление объектов в векторном редакторе не вполне соответствовало представлению в SVG, поэтому, чтобы не потерять данные, вся дополнительная информация и записывалась в эти аттрибуты sodipodi, которые обычные читалки тупо игнорируют. И даже когда sodipodi превратился в Inkscape, представление осталось старым.

А вот моя кривулина:

Прямо в самом начале особенность на особенности сидит! Сначала команда m, с маленькой буквы, т.е относительные координаты, по которым установить начало кривой. Но ведь мы пока не знаем, где у нас "перо" стоит! На это в спецификации есть ответ: начальное положение (0;0) в начале каждого Path, поэтому что m, что M должны вести себя одинаково. А далее, за первой парой координат следует вторая пара координат. Это уже ПРЯМАЯ ЛИНИЯ, причём раз m была с маленькой буквы, значит и здесь кординаты относительные!

С другой стороны, пробелов Inkscape не жалеет, он стоит и после m, и перед "-161", хотя минус позволял бы пробел и не ставить. Так оно всё-таки понятнее. Ещё и X,Y отделяет запятой, а пары отделяет пробелами, хотя с точки зрения формата это вообще одно и то же, просто delimeter и всё тут!

А ещё видим интересную вещь, два нуля в аргументах c (т.е кубический Безье с отн. координатами). Это означает, что первая управляющая точка совпадает с первой точкой. Ровно в этом случае можно было бы поставить s вместо c, и нули эти выкинуть. Но сохраняльщик такой фигнёй не страдает, как минимум, в кривой, нарисованной "от руки". Я попробовал ручками внести такое изменение в файл и снова открыть его в Inkscape: открылся без проблем, показал ровно ту же кривулину, и при последующем сохранении под новым именем даже сохранил эту s вместо c.

Довольно долго соображал, как эту штуку сколько-нибудь "красиво" отпарсить. Мне в конечном итоге надо было получить набор точек, которые затем можно подвергать движению, и набор индексов, по которым из этих точек составляются кривые Безье, для старого доброго GDIшного вызова PolyBezier. Чем хороши Безье - они описываются ИСКЛЮЧИТЕЛЬНО ТОЧКАМИ, т.е все они преобразуются единообразно. С дугами дела хуже, как в SVG, так и в GDI. В SVG есть два "скаляра", вдруг затесавшихся среди векторов: это длины большой и малой оси эллипса. В случае движения они преобразовываться не должны, хотя мы можем захотеть ещё и масштабирование добавить, оно естественным образом в некомм. дуал. компл. число тоже входит, а тогда и эти длины придётся масштабировать! А в GDI, в команде Arc, как будто бы, всё на координатах, только вот незадача - эллипс этот поворачивать нельзя, он просто вписывается в прямоугольник с горизонтальными и вертикальными сторонами. А значит, при произвольном движении (включая вращение) у нас этот эллипс поведёт себя очень нехорошо... В итоге решил пока на дуги забить: позже можно будет их худо-бедно приблизить тем же Безье (может, несколькими), зато не мучаться.

Пришёл к выводу, что непосредственно прочитывая новую буковку (имя команды), мы ничего особо делать и не должны, просто обновить значение CurCmd. А выполняем мы команду в тот момент, когда закончили парсинг очередного числа (наткнувшись на что-то ещё), и обнаружили, что оно уже "лишнее", т.е относится уже к следующей команде, имя которой хранится у нас в CurCmd. Тогда оно получается сколько-нибудь единообразно, но команду Z придётся обработать отдельно! Ибо у неё аргументы отсутствуют, и стоит она чаще всего последней.

Немножко костыльно/велосипедно, зато быстро написалось: после громких раздумий буквально за часик, и довольно компактно (200 строк кода, около 8 КБ), и даже с заделом под SVG 2.0, ну всяко же бывает! Другие "пути" из той формулы:







Что интересно, когда формулы LaTeX преобразуются в SVG, там, пока что, ни разу не встретились ни дуги, ни квадратичные Безье, ни "симметричная версия" (S вместо C) кубических Безье.

Может пригодиться ещё и для лазерных дел, автоматизировать разделку исходных файлов под резку и гравировку, так чтобы он вырезал сначала внутренние отверстия, а только потом деталь с отверстиями из общей фанерки. А ещё "материализовывал клоны" автоматически (LaserGRBL клоны в упор не признаёт!) Сейчас всё это ручками, приятного не очень много, кажется временами, что "лобзиком тупо быстрее"!

странные девайсы, математика, бред, программки

Previous post Next post
Up