dots/config/inkscape/extensions/org.inkscape.extension.30306/scientific_inkscape/homogenizer.py
2026-06-05 13:11:08 +02:00

396 lines
16 KiB
Python

#!/usr/bin/env python
# coding=utf-8
#
# Copyright (c) 2023 David Burghoff <burghoff@utexas.edu>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
import dhelpers as dh
import inkex
from inkex import TextElement, FlowRoot, FlowPara, Tspan, Transform, Group, FlowSpan
from inkex.text.cache import BaseElementCache
otp_support_tags = BaseElementCache.otp_support_tags
from inkex.text.utils import default_style_atts
from applytransform_mod import fuseTransform
import math
badels = (
inkex.NamedView,
inkex.Defs,
inkex.Metadata,
inkex.ForeignObject,
inkex.SVGfont,
inkex.FontFace,
inkex.MissingGlyph,
)
dispprofile = False
class Homogenizer(inkex.EffectExtension):
# def document_path(self):
# return 'test'
def add_arguments(self, pars):
pars.add_argument("--tab", help="The selected UI-tab when OK was pressed")
pars.add_argument(
"--setfontsize", type=inkex.Boolean, default=False, help="Set font size?"
)
pars.add_argument("--fontsize", type=float, default=8, help="New font size")
pars.add_argument(
"--fixtextdistortion",
type=inkex.Boolean,
default=False,
help="Fix distorted text?",
)
pars.add_argument("--fontmodes", type=int, default=1, help="Font size options")
pars.add_argument(
"--setfontfamily",
type=inkex.Boolean,
default=False,
help="Set font family?",
)
pars.add_argument("--fontfamily", type=str, default="", help="New font family")
# pars.add_argument("--setreplacement", type=inkex.Boolean, default=False,help="Replace missing fonts?")
# pars.add_argument("--replacement", type=str, default='', help="Missing fon replacement");
pars.add_argument(
"--setstroke", type=inkex.Boolean, default=False, help="Set stroke width?"
)
pars.add_argument(
"--setstrokew", type=float, default=1, help="New stroke width"
)
pars.add_argument(
"--strokemodes", type=int, default=1, help="Stroke width options"
)
pars.add_argument(
"--clearclipmasks", type=inkex.Boolean, default=False, help="Clear clips and masks"
)
pars.add_argument(
"--fusetransforms",
type=inkex.Boolean,
default=False,
help="Fuse transforms to paths?",
)
pars.add_argument(
"--plotaware",
type=inkex.Boolean,
default=False,
help="Plot-aware text scaling?",
)
def effect(self):
if dispprofile:
import cProfile, pstats, io
from pstats import SortKey
pr = cProfile.Profile()
pr.enable()
setfontsize = self.options.setfontsize
# setfontsize = (self.options.fontmodes>1);
fontsize = self.options.fontsize
setfontfamily = self.options.setfontfamily
fontfamily = self.options.fontfamily
setstroke = self.options.setstroke
setstrokew = self.options.setstrokew
fixtextdistortion = self.options.fixtextdistortion
sel0 = [self.svg.selection[ii] for ii in range(len(self.svg.selection))]
# should work with both v1.0 and v1.1
sel = [v for el in sel0 for v in el.descendants2()]
itag = inkex.Image.ctag
if all([el.tag == itag for el in sel]):
inkex.utils.errormsg(
"""Thanks for using Scientific Inkscape!
It appears that you're attempting to homogenize a raster Image object. Please note that Inkscape is mainly for working with vector images, not raster images. Vector images preserve all of the information used to generate them, whereas raster images do not. Read about the difference here:
https://en.wikipedia.org/wiki/Vector_graphics
Unfortunately, this means that there is not much the Homogenizer can do to edit raster images. If you want to edit a raster image, you will need to use a program like Photoshop or GIMP.
"""
)
quit()
elif self.options.plotaware and any([not isinstance(k, Group) for k in sel0]):
inkex.utils.errormsg(
"Plot-aware scaling requires that every selected object be a grouped plot."
)
return
sela = [el for el in sel if not (isinstance(el, badels))]
sel = [
el
for el in sel
if isinstance(el, (TextElement, Tspan, FlowRoot, FlowPara, FlowSpan))
]
if setfontfamily or setfontsize or fixtextdistortion:
tels = [d for d in sel if isinstance(d, (TextElement, FlowRoot))]
if not self.options.plotaware:
bbs = dh.BB2(self.svg, tels, False)
else:
aels = [d for el in sel0 for d in el.descendants2()]
bbs = dh.BB2(self.svg, aels, False)
if setfontsize:
# Get all font sizes and scale factors
onept = self.svg.cdocsize.unittouu("1pt")
szs = dict()
for el in tels:
cszs = [c.tfs / onept for ln in el.parsed_text.lns for c in ln.chrs]
if len(cszs) > 0:
szs[el] = max(cszs)
# Determine scale and/or size
fixedscale = False
try:
if self.options.fontmodes == 3:
fixedscale = True
elif self.options.fontmodes == 4:
fixedscale = True
fontsize = fontsize / max(szs.values()) * 100
elif self.options.fontmodes == 5:
from statistics import mean
fontsize = mean(szs.values())
elif self.options.fontmodes == 6:
from statistics import median
fontsize = median(szs.values())
elif self.options.fontmodes == 7:
fontsize = min(szs.values())
elif self.options.fontmodes == 8:
fontsize = max(szs.values())
except ValueError:
fontsize = 12
from inkex.text import parser
for el in szs:
for d in reversed(el.descendants2()):
sty = d.cspecified_style
if el == d or "font-size" in sty:
dfs, sf, utdfs = dh.composed_width(d, "font-size")
if dfs==0:
continue
bshift = parser.TChar.get_baseline(sty, d.getparent())
if bshift != 0 or "%" in sty.get("font-size", ""):
# Convert sub/superscripts into relative size
pfs, sf, _ = dh.composed_width(d.getparent(), "font-size")
d.cstyle["font-size"] = f"{dfs / pfs * 100:.2f}%"
else:
# Set absolute size
scl = (
fontsize * onept / dfs
if not fixedscale
else fontsize / 100
)
nfs = utdfs * scl
nfs = f"{nfs:.2f}" if abs(nfs) > 1 else "{:.3g}".format(nfs)
d.cstyle["font-size"] = nfs.rstrip("0").rstrip(".") + "px"
if fixtextdistortion:
# make a new transform that removes bad scaling and shearing (see General_affine_transformation.nb)
for el in sel:
ct = el.ccomposed_transform
detv = ct.a * ct.d - ct.b * ct.c
if detv!=0:
signdet = -1 * (detv < 0) + (detv >= 0)
sqrtdet = math.sqrt(abs(detv))
magv = math.sqrt(ct.b**2 + ct.a**2)
ctnew = Transform(
[
[ct.a * sqrtdet / magv, -ct.b * sqrtdet * signdet / magv, ct.e],
[ct.b * sqrtdet / magv, ct.a * sqrtdet * signdet / magv, ct.f],
]
)
dh.global_transform(el, (ctnew @ (-ct)))
if setfontfamily:
from inkex.text.font_properties import inkscape_spec_to_css
sty = inkscape_spec_to_css(fontfamily)
if sty is None:
dh.idebug('Font seems to be invalid—check its spelling.')
import sys
sys.exit()
# If any type of Font Style is being set, reset the others to default
if any(k in sty for k in ["font-weight", "font-style", "font-stretch"]):
for k in ["font-weight", "font-style", "font-stretch"]:
sty.setdefault(k, default_style_atts[k])
for el in reversed(sel):
for k,v in sty.items():
el.cstyle[k] = v
el.cstyle["-inkscape-font-specification"] = None
from inkex.text import parser
dh.character_fixer(tels)
if setfontfamily or setfontsize or fixtextdistortion:
bbs2 = dh.BB2(self.svg, tels, True)
if not self.options.plotaware:
for el in sel:
myid = el.get_id()
if (
isinstance(el, (TextElement, FlowRoot))
and myid in bbs
and myid in bbs2
):
bb = bbs[el.get_id()]
bb2 = bbs2[el.get_id()]
tx = (bb2[0] + bb2[2] / 2) - (bb[0] + bb[2] / 2)
ty = (bb2[1] + bb2[3] / 2) - (bb[1] + bb[3] / 2)
trl = Transform("translate({0}, {1})".format(-tx, -ty))
dh.global_transform(el, trl)
else:
from scale_plots import (
geometric_bbox,
Find_Plot_Area,
trtf,
appendInt,
)
gbbs = {elid: geometric_bbox(el, fbb).sbb for elid, fbb in bbs.items()}
for i0, g in enumerate(sel0):
pels = [k for k in g if k.get_id() in bbs] # plot elements list
vl, hl, lvel, lhel = Find_Plot_Area(pels, gbbs)
if lvel is None or lhel is None:
lvel = None
lhel = None
# Display warning and proceed
numgroup = str(i0 + 1) + appendInt(i0 + 1)
inkex.utils.errormsg(
"A box-like plot area could not be automatically detected on the "
+ numgroup
+ " selected plot (group ID "
+ g.get_id()
+ ").\n\nDraw a box with a stroke to define the plot area."
+ "\nAdjustment will still be performed, but the results may not be ideal."
)
bbp = dh.bbox(None)
# plot area
for el in pels:
if el.get_id() in [lvel, lhel]:
bbp = bbp.union(gbbs[el.get_id()])
for el in g.descendants2():
if el in tels:
bb1 = dh.bbox(bbs[el.get_id()])
bb2 = dh.bbox(bbs2[el.get_id()])
if bbp.isnull:
dx = bb1.xc - bb2.xc
dy = bb1.yc - bb2.yc
else:
# For elements outside the plot area, adjust position to maintain
# the scaled distance to the plot area
if bb1.xc < bbp.x1:
dx = (bbp.x1 - bb2.x2) - (
bbp.x1 - bb1.x2
) * bb2.w / bb1.w
elif bb1.xc > bbp.x2:
dx = (bb1.x1 - bbp.x2) * bb2.w / bb1.w - (
bb2.x1 - bbp.x2
)
else:
dx = bb1.xc - bb2.xc
if bb1.yc < bbp.y1:
dy = (bbp.y1 - bb2.y2) - (
bbp.y1 - bb1.y2
) * bb2.h / bb1.h
elif bb1.yc > bbp.y2:
dy = (bb1.y1 - bbp.y2) * bb2.h / bb1.h - (
bb2.y1 - bbp.y2
)
else:
dy = bb1.yc - bb2.yc
tr2 = trtf(dx, dy)
dh.global_transform(el, tr2)
if setstroke:
szd = dict()
sfd = dict()
szs = []
for el in sela:
sw, sf, _ = dh.composed_width(el, "stroke-width")
elid = el.get_id()
szd[elid] = sw
sfd[elid] = sf
if sw is not None:
szs.append(sw)
fixedscale = False
if self.options.strokemodes == 2:
setstrokew = self.svg.cdocsize.unittouu(str(setstrokew) + "px")
elif self.options.strokemodes == 3:
fixedscale = True
elif self.options.strokemodes == 5:
from statistics import mean
setstrokew = mean(szs)
elif self.options.strokemodes == 6:
from statistics import median
setstrokew = median(szs)
elif self.options.strokemodes == 7:
setstrokew = min(szs)
elif self.options.strokemodes == 8:
setstrokew = max(szs)
for el in sela:
elid = el.get_id()
if not (szd[elid] is None):
if not (fixedscale):
newsize = setstrokew
else:
newsize = szd[elid] * (setstrokew / 100)
if sfd[elid]==0:
continue
el.cstyle["stroke-width"] = str(newsize / sfd[elid]) + "px"
if self.options.fusetransforms:
for el in sela:
if el.tag in otp_support_tags:
# Fuse the composed transform onto the path
el.ctransform = el.ccomposed_transform
fuseTransform(el)
el.ctransform = -el.getparent().ccomposed_transform
if self.options.clearclipmasks:
for el in sela:
el.cstyle['clip-path'] = 'none'
el.cstyle['mask'] = 'none'
if dispprofile:
pr.disable()
s = io.StringIO()
sortby = SortKey.CUMULATIVE
ps = pstats.Stats(pr, stream=s).sort_stats(sortby)
ps.print_stats()
dh.debug(s.getvalue())
if __name__ == "__main__":
dh.Run_SI_Extension(Homogenizer(), "Homogenizer")