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

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

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

Мой первый успешный парсинг/рендер "корешка" из LaTeX:
Про формат SVG и Inkscape BigSqrt.png

Сам по себе SVG (Scalable Vector Graphics) имеет XML синтаксис. Возьмём "корешок", сгенерированный из LaTeX:
Про формат SVG и Inkscape \sqrt{x^2+y^2}

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



 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 


Здесь использовано пять элементов 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, для чего я нарисовал в нём дугу и немудрёную кривулину (не через окружность, а через кривые Безье):
Про формат SVG и Inkscape myTestImages.png

и потом открыл файл в текстовом редакторе. Первое, что я увидел - аттрибуты 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, ну всяко же бывает! Другие "пути" из той формулы:

Про формат SVG и Inkscape BigPlus.png

Про формат SVG и Inkscape bigX.png

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


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

Оставить комментарий

Архив записей в блогах:
Некоторые политически активные участники сообществе несколько раз поднимали здесь тему легализации марихуаны, которую пропагандируют обе оппозиционные партии. Данный ход конечно не лишён политического смысла для этих партий. Факт роста их поддержки молодыми избирателями - налицо. Трудно ус ...
Вчера подо постом был комментарий – «Десять лет и разговаривала и объясняла, что это не правильно… (что мужчина не берёт на себя финансовые обязательства перед семьёй). Я содержу себя с сына сама! Продукты преимущественно он теперь он покупает, я просто почти перестала это делать». ...
Статью под одноименным названием с красноречивым подзаголовком "Мифы и факты ...
Это был самый торопившийся человек в целой России, - так Достоевский назвал Белинского. Однажды они встретились у Знаменской церкви в Петербурге. Белинский сказал, что выходил гулять и идёт домой. " - Я сюда часто захожу взглянуть, как идёт постройка..." постройка (вокзала ...
Сегодня мне исполнилось 25. Этот праздник я никогда не любила, даже маленькая говорила маме: "Давай гости придут, подарят подарки и уйдут". С 20 я уже начала плакать каждое свое 27 сентября. Я его и не праздновала: мне никогда не хотелось собирать своих друзей, мне и телефонную трубку-то ...