Check-in [af566d903e]
Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Comment: | prepare magick v7 support, minor parameter rework, dissolve duration_arg(), experiment with more convert options |
---|---|
Timelines: | family | ancestors | descendants | both | trunk |
Files: | files | file ages | folders |
SHA1: |
af566d903e86f9a751621a03b607e842 |
User & Date: | mario 2022-10-09 16:36:20 |
2022-10-10
| ||
07:45 | translatable=no for combo boxes check-in: 33dbf985e5 user: mario tags: trunk | |
2022-10-09
| ||
16:36 | prepare magick v7 support, minor parameter rework, dissolve duration_arg(), experiment with more convert options check-in: af566d903e user: mario tags: trunk | |
2022-10-08
| ||
17:41 | hex_color int(alpha), -background None fallback (semi-transparency likely requires a `-channel A -ordered-dither checker` pattern) check-in: c3d85c855b user: mario tags: trunk | |
Changes to inkscape/export_gif.py.
8 9 10 11 12 13 14 | # id: org.include-once.inkscape.export-gif # license: MITL # version: 1.1 # state: stable # depends: bin:inkscape (>= 1.1), bin:convert, python (>= 3.6), python:svgelements # pylint: disable=line-too-long, missing-module-docstring, bad-whitespace # config: | | | | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | # id: org.include-once.inkscape.export-gif # license: MITL # version: 1.1 # state: stable # depends: bin:inkscape (>= 1.1), bin:convert, python (>= 3.6), python:svgelements # pylint: disable=line-too-long, missing-module-docstring, bad-whitespace # config: # { name: file, type: file, mode: file, value: "~/anim.gif", description: "Target GIF filename", help: "Output filename should end in .gif for actual results. Alternatively can be `APNG:dir/filename.apng` with ImageMagick 7." } # { name: mode, type: select, select: "PNG→ImageMagick (better quality)|SVG→ImageMagick (simple drawings)|PNG→Pillow (builtin - a bit faster)|JavaScript→SVG (embed anim code)", value: PNG+ImageMagick, description: Conversion mode, help: "There's different backends to assemble the GIF. Pillow also works on Windows without convert.exe installed. The JavaScript mode embeds some code, does not actually generate a GIF output file." } # { name: delay, type: float, value: 0.35, min: 0.01, max: 20, precision: 2, description: "Delay between slides (seconds)", "Can be overriden on a per-slide basis with [--delay=2.5]. Note that convert(1) usually uses »ticks« (*10ms), but this export_gif option is really in seconds." } # { name: loop, type: int, value: 0, description: "Loop limit (0 for endless)" } # [ hidden: 1, disabled: save_as, type: str, value: "0", description: used in saveas_gif.inx, note: "have to use `str` here, cause .inx `false` is not recognized for hidden attributes" ] # { xml: <image width="505" height="50">export_gif.svg</image> } # { name: notebook, type: notebook, value: system, description: "Additional flags and help" } # { category: ImageMagick, name: fuzz, type: select, select: "0%|5%|10%|20%|30%|50%", value: "10%", description: "Fuzzing/dither", help: "Detect similar colors for optimization" } # { category: ImageMagick, name: layers, type: select, select: "optimize|coalesce|compare-any|compare-clear|compare-overlay|composite|dispose|flatten|merge|mosaic|optimize-frame|optimize-plus|optimize-transparency|remove-dups|remove-zero|trim-bounds", value: "optimize", description: "Accumulation/combination of -layers", help: "Can influence optimization or layer combination. Specifically coalesce/merge are pre-interpreted by export_gif to produce accumulating slides." } # { category: ImageMagick, name: extra, type: select, select: "-quiet|-optimize|-size 640x360|-alpha background|-auto-gamma|-auto-level|-coalesce|-colors 64|-dither FloydSteinberg|-limit disk 1MB|-reverse|-monochrome|-transparent white", value: "", description: "Extra args", help: "Some default convert -args." } # { category: ImageMagick, name: extra2, type: str, value: "", description: "Custom args", help: "Specify additional convert(1) options." } # { category: ImageMagick, name: background, type: color, appearance: colorbutton, value: "0", description: "Background color for transparent layers", help: "Only works for SVG→ImageMagick option." } # { category: System, name: preview, type: bool, value: 0, description: "Preview result file", help: "Should bring up default image viewer on resulting GIF (via xdg-open, or start… on Windows)" } # { category: System, name: keep_tmp, type: bool, value: 0, description: "Keep temporary files", help: "Will retain the frame PNGs in the fixed directory /tmp/inkscape.export_gif/" } # { category: System, name: reload_svg, type: bool, value: 0, description: "Reload SVG in Inkscape", help: "Averts the warning popup of lacking result data. But is quite redundant for this tool. And should only be enabled if it's becoming too obnoxious." } # { category: System, name: export_background, type: bool, value: 0, description: "Force background application in PNG export", help: "Uses --export-background for generating PNG slides prior assembly. (See ImageMagick for color tab.)" } # { category: Animation, name: subframes, type: int, value: 5, description: "Subframes per ❮animation❯ slide", help: "Can be overridden per [animate=25] or [steps=25] in layer label." } |
107 108 109 110 111 112 113 114 115 116 117 118 119 120 | import os import tempfile from argparse import Namespace import re import functools import textwrap import shlex import math import inkex from inkex.utils import strargs from inkex.tween import StyleInterpolator, TransformInterpolator from inkex.transforms import Transform | > | 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | import os import tempfile from argparse import Namespace import re import functools import textwrap import shlex import shutil import math import inkex from inkex.utils import strargs from inkex.tween import StyleInterpolator, TransformInterpolator from inkex.transforms import Transform |
169 170 171 172 173 174 175 | if not self.options.keep_tmp: os.unlink(self.options.file) def effect(self): """ Generate output file when invoked """ layers = list(self.get_layers()) fixed_layers = [layer.id for layer in layers if layer.is_fixed] | | | 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | if not self.options.keep_tmp: os.unlink(self.options.file) def effect(self): """ Generate output file when invoked """ layers = list(self.get_layers()) fixed_layers = [layer.id for layer in layers if layer.is_fixed] temp_files_args = [] # tuples of filename,{-args} # more parameter handling if re.search("JavaScript", self.options.mode, re.I): return self.inject_javascript() self.make_temp() up_until_mode = re.search("merge|compo|coal|plus", self.options.layers) preconvert_png = re.search("PNG", self.options.mode, re.I) |
197 198 199 200 201 202 203 | if layer.animate or self.is_animatable(layer): # produces multiple subframes anim = AnimationSteps( parent = self, layer = layer, svg = self.export_layers(dest="", show=[layer.id] + fixed_layers + merge_layers) ) | | | 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 | if layer.animate or self.is_animatable(layer): # produces multiple subframes anim = AnimationSteps( parent = self, layer = layer, svg = self.export_layers(dest="", show=[layer.id] + fixed_layers + merge_layers) ) temp_files_args.extend(list(anim.export())) else: # current frame into .svg/.png dest = self.export_layers( dest = f"{self.tempdir}/{self.index}.{layer.label}.{img_ext}", show = [layer.id] + fixed_layers + merge_layers ) temp_files_args.append([dest, layer.args]) |
224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 | os.system(('start %s' if self.win32 else 'xdg-open %r 2>&1 &') % self.options.file) if self.options.save_as: self.save(self.options.output) elif self.options.reload_svg: inkex.base.SvgOutputMixin.save(self, self.options.output) def convert_png(self, svg_path, png_path): """ Use the convenience wrapper (more likely referencing the current inkscape binary) """ extra = {} if self.options.background and self.options.export_background: color = inkex.Color(self.options.background) extra.update({ "export_background": self.hex_color(color, with_alpha=False), "export_background_opacity": "{:.2f}".format(color.alpha), }) inkex.command.inkscape( | > > > > > > > > > > > < | | | | < < < < < < < < < | 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 | os.system(('start %s' if self.win32 else 'xdg-open %r 2>&1 &') % self.options.file) if self.options.save_as: self.save(self.options.output) elif self.options.reload_svg: inkex.base.SvgOutputMixin.save(self, self.options.output) @staticmethod def hex_color(color, with_alpha=True): """ convert ints or inkex.Color tuple to #RRGGBBAA string """ color = inkex.Color(color) formatted = "#{:02X}{:02X}{:02X}".format(*color.to_rgb()) if with_alpha: formatted += "{:02X}".format(int(255 * min(1.0, color.alpha))) return formatted def convert_png(self, svg_path, png_path): """ Use the convenience wrapper (more likely referencing the current inkscape binary) """ extra = {} if self.options.background and self.options.export_background: color = inkex.Color(self.options.background) extra.update({ "export_background": self.hex_color(color, with_alpha=False), "export_background_opacity": "{:.2f}".format(color.alpha), # 5%/compareOverlay/-coalesce per channel [--alpha=set] [--channel=A] [--ordered-dither=checks] has some effect # compareClear/-coalesce [--channel=A --ordered-dither=4x4 --dispose=-1] }) inkex.command.inkscape( svg_path, export_filename=png_path, export_type="png", export_area_page=True, export_text_to_path=True, **extra, #export_dpi=96, --export-width --export-height ) def imagick_gif(self, temp_files_args): """ Run `convert` using list of exported slides """ inkex.command.call(*[ shutil.which("magick") or "convert", "-delay", float(self.options.delay) * 100, "-loop", self.options.loop, "-layers", self.options.layers, "-fuzz", self.options.fuzz, # ToDo: distribute some flags into temp_files_args? (-blur -colors -compose -cycle -fx -gamma -grayscale -ordered-dither -resample -resize -transparent ) "-background", self.hex_color(self.options.background) if self.options.background else "None", *shlex.split(self.options.extra), *shlex.split(self.options.extra2), *self.zip_args(temp_files_args), *[os.path.expanduser(self.options.file)] ]) @staticmethod def zip_args(temp_files_args): """ unzip files and possible -args (fitted list of dicts) """ for filename, arg_list in temp_files_args: for key, val in arg_list.items(): if key == "write": continue |
287 288 289 290 291 292 293 | frames[0].save( os.path.expanduser(self.options.file), save_all = True, append_images = frames[1:], version = "GIF89a", optimize = True, disposal = 1, # no disposal | | < < < < < < | 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 | frames[0].save( os.path.expanduser(self.options.file), save_all = True, append_images = frames[1:], version = "GIF89a", optimize = True, disposal = 1, # no disposal duration = [float(args[1].get("delay", self.options.delay)) * 1000 for args in temp_files_args], loop = self.options.loop, ) def clean_up(self): """ remove tmp files + dir, automatically invoked by base.run() """ super().clean_up() if self.options.keep_tmp or not self.tempdir or not os.path.exists(self.tempdir): return for file_name in os.listdir(self.tempdir): os.unlink(self.tempdir + "/" + file_name) |
342 343 344 345 346 347 348 | with open(dest, "wb") as svg_fp: svg_fp.write(svg.tostring()) if dest_png: self.convert_png(dest, dest_png) return dest_png or dest def get_layers(self): | | | | | | | | 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 | with open(dest, "wb") as svg_fp: svg_fp.write(svg.tostring()) if dest_png: self.convert_png(dest, dest_png) return dest_png or dest def get_layers(self): """ Per default all layers are exported, label tags [bg], [exclude], [merge] yield special treatment """ for layer in self.svg.xpath('./svg:g[@inkscape:groupmode="layer"][@inkscape:label]'): label = layer.attrib["{http://www.inkscape.org/namespaces/inkscape}label"] animate = re.findall(r"\[(?:steps|animate)(?:=(\d+))?\]", label) yield Namespace( id = layer.attrib["id"], label = re.sub(r"\W+", "_", label), is_fixed = bool(re.search(r"fixed|\[(is_?fixe?d?|foreground)\]", label)), is_sticky = bool(re.search(r"background|\[(bg)\]", label)), no_frame = bool(re.search(r"fixed|\[(merge|no_frame|exclude)\]", label)), do_merge = bool(re.search(r"\[(merge|join|group)\]", label)), animate = int(([i for i in animate if i] or [self.options.subframes])[0]) if animate else 0, tags = re.findall(r"\[(\w+)\]", label), args = dict(re.findall(r"--(\w[\w\-]+)=(\w[^\s,;\]\)]*)", label)) ) def inject_javascript(self): """ insert JavaScript version into SVG document """ script = inkex.Script() |
471 472 473 474 475 476 477 | self.layer = layer self.frames = layer.animate self.delay = float(layer.args.get("delay", self.gif.options.delay)) self.layer.args.update({ "delay": self.delay / self.frames # frame time split between animation steps }) | | | 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 | self.layer = layer self.frames = layer.animate self.delay = float(layer.args.get("delay", self.gif.options.delay)) self.layer.args.update({ "delay": self.delay / self.frames # frame time split between animation steps }) def export(self): """ applies transforms in each subframe, yields filename+args list """ transforms = [] for anim in self.svg.xpath("//svg:animate"): transforms.append([self.animate_style(anim, **anim.attrib), self.pace(anim)]) for anim in self.svg.xpath("//svg:animateTransform"): transforms.append([self.animate_transform(anim, **anim.attrib), self.pace(anim)]) for anim in self.svg.xpath("//svg:animateMotion"): |