Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Select PS Eraser points properly for over_exclude (cf #2689) #4191

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

skef
Copy link
Contributor

@skef skef commented Feb 25, 2020

Closes #2689

@linusromer
Copy link
Contributor

@skef I can confirm that it works now for the old EPS-file from 2016. However, I did some other tests and they failed:

How it should look:

eraser-problems-2-4

How it looks in FF:

eraser-problems-2-4-ff

(The roundings of the corners are a different issue.)

eps files

@linusromer
Copy link
Contributor

@skef By the way: I have written an own PS-import function in python for FontForge in https://github.com/linusromer/mf2outline/blob/master/mf2outline.py which is far from complete. However, it works quite well with EPS from MetaPost. It uses the python package booleanOperations (because removeOverlap from FontForge was buggy) and processes each path step by step (with temporary steps for stroked paths which use an own function for the outlines but I think that has improved meanwhile in FF). Maybe this could be a hint for this issue.

# own postscript interpreter for a postscript file "eps" 
# into the glyph "glyph"
def import_ps(eps,glyph):
	from booleanOperations import BooleanOperationManager
	from booleanOperations.booleanGlyph import BooleanGlyph
	with open(eps, "r") as epsfile:
		# read through the lines and write them continously  as contours
		# in a fontforge char
		# this is specialized for Metapost Output and useless for
		# general postscript
		#
		# some variable declarations
		rawglyph = [] # will be filled with raw contours
		is_white = False # any other color than white will be interpreted as black
		linewidth = 1 # just default
		stack = [] # ps stack (coordinate of points, pen dimensions etc.)
		ctm = [1.0,0.0,0.0,1.0,0.0,0.0] # current transformation matrix
		dash = 0
		linecap = "round"
		linejoin = "round"
		miterlimit = 10
		contour = []
		stroke_follows_fill = False 
		gsave_contour = [] # (list) bezier path that is saved by gsave
		gsave_ctm = [1.0,0.0,0.0,1.0,0.0,0.0] # current transformation matrix that is saved by gsave
		gsave_is_white = False # "color" saved by gsave
		#
		# okay, let's start:
		for line in epsfile:
			if line[0] != "%" and len(line)>0: # ignore comments
				if "gsave fill grestore stroke" in line: # this happens just so often, so we treat it as special case
					stroke_follows_fill = True # pay attention...
				words = line.split()
				for word in words: # go through the words
					# showpage will have no effect
					if isolate_number(word) != None:
						stack.append(isolate_number(word))
					elif word == "newpath":
						contour = [] # (list) bezier path 
					elif (word == "moveto") or (word == "lineto"):
						contour.append([(stack[-2],stack[-1])])
						stack = stack[:-2]
					elif word == "curveto":
						contour.append([
						(stack[-6],stack[-5]),
						(stack[-4],stack[-3]),
						(stack[-2],stack[-1])
						])
						stack = stack[:-6]
					elif word == "closepath":
						if (len(contour) > 1) and \
						(contour[0][0][0] != contour[-1][-1][0]) or \
						(contour[0][0][1] != contour[-1][-1][1]):
							contour.append([(contour[0][0][0],contour[0][0][1])])
					elif word == "setrgbcolor":
						if stack[-1] == 1 and \
						stack[-2] == 1 and \
						stack[-3] == 1:
							is_white = True
						else:
							is_white = False
						stack = stack[:-3]
					elif word == "setlinewidth":
						linewidth = stack.pop()
					elif word == "setdash":
						dash = stack.pop()
					elif word == "setlinecap":
						linecapcode = stack.pop()
						if linecapcode == 0:
							linecap = "butt"
						elif linecapcode == 2:
							linecap = "square"
						else:
							linecap = "round"
					elif word == "setlinejoin":
						linejoincode = stack.pop()
						if linejoincode == 0:
							linejoin = "miter"
						elif linejoincode == 2:
							linejoin = "bevel"
						else:
							linejoin = "round"
					elif word == "setmiterlimit":
						miterlimit = stack.pop()
					elif word == "scale":
						sy=stack.pop()
						sx=stack.pop()
						ctm=[ctm[0]*sx,ctm[1]*sy,ctm[2]*sx,ctm[3]*sy,ctm[4]*sx,ctm[5]*sy]
					elif word == "concat":
						f=stack.pop()
						e=stack.pop()
						d=stack.pop()
						c=stack.pop()
						b=stack.pop()
						a=stack.pop()
						ctm=[a*ctm[0]+c*ctm[1],b*ctm[0]+d*ctm[1],\
						a*ctm[2]+c*ctm[3],b*ctm[2]+d*ctm[3],\
						a*ctm[4]+c*ctm[5]+e,b*ctm[4]+d*ctm[5]+f]
					elif word == "dtransform":
						dy=stack.pop()
						dx=stack.pop()
						stack.extend(homogeneous(ctm[:4],dx,dy))
					elif word == "idtransform":
						dy=stack.pop()
						dx=stack.pop()
						stack.extend(homogeneous(invertmatrix(ctm[:4]),dx,dy))
					elif word == "transform":
						y=stack.pop()
						x=stack.pop()
						stack.extend(homogeneous(ctm,x,y))
					elif word == "itransform":
						y=stack.pop()
						x=stack.pop()
						stack.extend(homogeneous(invertmatrix(ctm),x,y))
					elif word == "exch":
						last = stack.pop()
						secondlast = stack.pop()
						stack.extend([last,secondlast])
					#elif word == "truncate":
						# truncate is problematic, as it produces
						# results that are not exact
						# I do not know, why this is in METAPOST
						# stack[len(stack)-1]=int(stack[len(stack)-1])
					elif word == "pop":
						stack.pop()
					elif word == "fill" and not stroke_follows_fill:
						if is_white:
							if windingnumber(contour) > 0:
								contour = bezierreverse(contour) # make counterclockwise
							rawglyph = rawDifference(rawglyph,[contour]) 
						else:
							if windingnumber(contour) < 0:
								contour = bezierreverse(contour) # make clockwise
							rawglyph.append(contour) #rawglyph = romerUnion(rawglyph,[contour])
					elif word == "stroke":		
						# We have to determine the angle and the 
						# axis of the ellipse that is the product of
						# a circle with radius=linewidth with the 
						# ctm (current transformation matrix.
						# One would have to consider also shearing,
						# but METAPOST makes the ctm (for pens)
						# always as a product of rotation matrix and 
						# a diagonal (non-uniform scaling) matrix.
						# Hence, we make a quick an dirty computation
						if ctm[0] == ctm[1]:
							alpha = .25*math.pi
						else:
							alpha = math.atan(ctm[1]/ctm[0])
						pen_x = abs(linewidth*ctm[0]/math.cos(alpha))
						pen_y = abs(linewidth*ctm[3]/math.cos(alpha))
						if stroke_follows_fill:
							tempcontour = bezierouteroutline(contour,pen_x,pen_y,alpha)
							stroke_follows_fill = False
						else:
							tempcontour = bezieroutline(contour,pen_x,pen_y,alpha)
						if is_white:
							if windingnumber(tempcontour) > 0:
								tempcontour = bezierreverse(tempcontour) # make counterclockwise
							rawglyph = rawDifference(rawglyph,[tempcontour]) 
						else:
							if windingnumber(tempcontour) < 0:
								tempcontour = bezierreverse(tempcontour) # make clockwise
							rawglyph.append(tempcontour) #rawglyph = romerUnion(rawglyph,[tempcontour])
					elif word == "gsave":
						gsave_contour = list(contour) # clone contour 
						gsave_ctm = ctm 
						gsave_is_white = is_white
					elif word == "grestore":
						contour = list(gsave_contour)
						ctm = gsave_ctm 
						is_white = gsave_is_white
		# now fill the raw glyph into a fontforge glyph:
		#rawglyphrounded = roundRawGlyph(rawglyph,10)
		for c in rawglyph:
			glyph.foreground += rawPathToFontforgeContour(c)
			if not args.raw:
				glyph.removeOverlap()

@skef
Copy link
Contributor Author

skef commented Feb 25, 2020

The missing corner rounding should be fixed by #4183, which is waiting on review.

FontForge's exclude function does appear to act strangely with nested contours.

From a very quick look based on your examples it appears that

  1. All "eraser" contours should be treated as "removing", and therefore clockwise in FontForge's algorithm. This is effectively a variant on "correct direction".
  2. FontForge's weird treatment of nested contours can be ameliorated by first removing the overlap between the non-erased contours and then running "exclude"

These aren't difficult changes so I'll give it a shot and update the draft PR, probably sometime this evening. Problem 2 seems like a bug separate from PS input, though, so once I confirm the fix is possible I'll ask about potentially changing the main algorithm.

@skef
Copy link
Contributor Author

skef commented Feb 26, 2020

(I merged #4183 and rebased to fix the join problem.)

@linusromer I worked around the immediate problems by running Remove Overlap on both SplineSets separately before running Exclude. The three examples now work as shown in your illustration.

I realize that incrementally debugging FontForge's implementation may not be of much interest but unless and until I can track down a proper reference on this subject you're my main resource. Still, feel free to bow out now or whenever.

@linusromer
Copy link
Contributor

@skef Yes, the three old EPS work now (and the rounded corners, too). However, the following still won't work:
fontforge-eraser-problem-5-6.zip

@skef skef mentioned this pull request Mar 6, 2020
@frank-trampe
Copy link
Contributor

@skef, what remains on this one?

@skef
Copy link
Contributor Author

skef commented Apr 14, 2021

I think this one might remain a draft for a while. The current version represents progress but it's not done and I'm not sure its a priority.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

PostScript (eps,ps) import with erasers does not work properly
3 participants