github.com/kintar/etxt@v0.0.9/emask/impl_faux.go (about)

     1  package emask
     2  
     3  import "math"
     4  import "math/bits"
     5  import "image"
     6  import "image/draw"
     7  
     8  import "golang.org/x/image/vector"
     9  import "golang.org/x/image/math/fixed"
    10  import "golang.org/x/image/font/sfnt"
    11  
    12  import "github.com/kintar/etxt/efixed"
    13  
    14  // A rasterizer to draw oblique and faux-bold text. For high quality
    15  // results, please use the font's italic and bold versions directly
    16  // instead of these fake effects.
    17  //
    18  // In general, the performance of FauxRasterizer without effects is very
    19  // similar to [DefaultRasterizer]. Using reasonable skew factors for
    20  // oblique text tends to increase the rasterization time around 15%, and
    21  // using faux-bold increases the rasterization time in 60%, but it depends
    22  // a lot on how extreme the effects are.
    23  //
    24  // This rasterizer was created mostly to serve as an example of how to
    25  // create modified rasterizers, featuring both modification of glyph
    26  // control points (oblique) and post-processing of the generated mask
    27  // (faux-bold).
    28  type FauxRasterizer struct {
    29  	// same fields as DefaultRasterizer
    30  	rasterizer     vector.Rasterizer
    31  	onChange       func(Rasterizer)
    32  	auxOnChange    func(*FauxRasterizer)
    33  	maskAdjust     image.Point
    34  	cacheSignature uint64
    35  	normOffsetX    float64
    36  	normOffsetY    float64
    37  	hasInitSig     bool // flag for initialized signature
    38  
    39  	skewing float64 // between -1 (45 degrees) and 1 (-45 degrees)
    40  	// (quantized to be representable without loss
    41  	//  in 16 bits)
    42  
    43  	// extra width (faux-bold) related fields
    44  	xwidth        float64
    45  	xwidthFract   float64 // the fractional part of xwidth (quantized to 1/64ths)
    46  	xwidthWhole   uint16  // the whole part of xwidth
    47  	xwidthTailMod uint16
    48  	xwidthTail    []uint8 // internal implementation detail
    49  }
    50  
    51  // Sets the oblique skewing factor. Values outside the [-1, 1] range will
    52  // be clamped. -1 corresponds to a counter-clockwise angle of 45 degrees
    53  // from the vertical. 1 corresponds to -45 degrees.
    54  //
    55  // In general, most italic fonts have an italic angle between -6 and -9
    56  // degrees, which would correspond to skew factors in the [0.13, 0.2] range.
    57  func (self *FauxRasterizer) SetSkewFactor(factor float64) {
    58  	// normalize and store new skewing factor
    59  	if factor == 0 {
    60  		if self.skewing == 0 {
    61  			return
    62  		}
    63  		self.skewing = 0
    64  		self.cacheSignature = self.cacheSignature & 0xFFFF0FFF0000FFFF
    65  	} else {
    66  		if factor > 1.0 {
    67  			factor = 1.0
    68  		}
    69  		if factor < -1.0 {
    70  			factor = -1.0
    71  		}
    72  		quantized := int32(factor * 32768)
    73  		if quantized == 0 {
    74  			if factor > 0 {
    75  				quantized = 1
    76  			}
    77  			if factor < 0 {
    78  				quantized = -1
    79  			}
    80  		}
    81  		newSkewing := float64(quantized) / 32768
    82  		if self.skewing == newSkewing {
    83  			return
    84  		}
    85  		self.skewing = newSkewing
    86  
    87  		// update cache signature
    88  		offset := int32(32768)
    89  		if quantized > 0 {
    90  			offset = 32767
    91  		} // allow reaching 1 skew factor
    92  		sigMark := uint16(quantized + offset)
    93  		self.cacheSignature = self.cacheSignature & 0xFFFF0FFF0000FFFF
    94  		self.cacheSignature |= 0x0000100000000000 // flag for "active italics"
    95  		self.cacheSignature |= uint64(sigMark) << 16
    96  	}
    97  
    98  	self.notifyChange()
    99  }
   100  
   101  // Gets the skewing factor [-1.0, 1.0] used for the oblique style.
   102  func (self *FauxRasterizer) GetSkewFactor() float64 { return self.skewing }
   103  
   104  // Sets the extra width for the faux-bold. Values outside the [0, 1024]
   105  // range will be clamped. Fractional values are allowed, but internally
   106  // the decimal part will be quantized to 1/64ths of a pixel.
   107  //
   108  // Important: when extra width is used for faux-bold, the glyphs will
   109  // become wider. If you want to adapt the positioning of the glyphs to
   110  // account for this widening, you can use an esizer.AdvancePadSizer,
   111  // link the rasterizer to it through SetAuxOnChangeFunc and update
   112  // the padding with the value of [FauxRasterizer.GetExtraWidth](), for
   113  // example.
   114  func (self *FauxRasterizer) SetExtraWidth(extraWidth float64) {
   115  	// normalize and store new skewing factor
   116  	if extraWidth <= 0 {
   117  		if self.xwidth == 0 {
   118  			return
   119  		} // shortcut
   120  		self.xwidth = 0
   121  		self.xwidthWhole = 0
   122  		self.xwidthFract = 0
   123  		self.cacheSignature = self.cacheSignature & 0xFFF0FFFFFFFF0000
   124  	} else {
   125  		if extraWidth > 1024.0 {
   126  			extraWidth = 1024
   127  		}
   128  		quantized := uint32(extraWidth * 64)
   129  		if quantized >= 65536 {
   130  			quantized = 65535
   131  		}
   132  		if quantized == 0 {
   133  			quantized = 1
   134  		}
   135  		newExtraWidth := float64(quantized) / 64
   136  		if self.xwidth == newExtraWidth {
   137  			return
   138  		} // shortcut
   139  		self.xwidth = newExtraWidth
   140  
   141  		// compute whole part for the given extra width
   142  		wholeFloat, fractFloat := math.Modf(self.xwidth)
   143  		self.xwidthWhole = uint16(wholeFloat)
   144  		self.xwidthFract = fractFloat
   145  		if len(self.xwidthTail) < int(self.xwidthWhole) {
   146  			if self.xwidthTail == nil {
   147  				self.xwidthTail = make([]uint8, 8)
   148  				self.xwidthTailMod = 7
   149  			} else {
   150  				targetSize := uint16RoundToNextPow2(self.xwidthWhole)
   151  				if targetSize == 1 {
   152  					panic("unreachable")
   153  				}
   154  				self.xwidthTailMod = targetSize - 1
   155  				if uint16(cap(self.xwidthTail)) >= targetSize {
   156  					self.xwidthTail = self.xwidthTail[0:targetSize]
   157  				} else {
   158  					self.xwidthTail = make([]uint8, targetSize)
   159  				}
   160  			}
   161  		}
   162  
   163  		// update cache signature
   164  		self.cacheSignature = self.cacheSignature & 0xFFF0FFFFFFFF0000
   165  		self.cacheSignature |= 0x00000B0000000000 // flag for "active bold"
   166  		self.cacheSignature |= uint64(uint16(quantized))
   167  	}
   168  
   169  	self.notifyChange()
   170  }
   171  
   172  // round the given uint16 to the next power of two (stays
   173  // as it is if the value is already a power of two)
   174  func uint16RoundToNextPow2(value uint16) uint16 {
   175  	if value == 1 {
   176  		return 2
   177  	}
   178  	if bits.OnesCount16(value) <= 1 {
   179  		return value
   180  	} // (already a pow2)
   181  	return uint16(1) << (16 - bits.LeadingZeros16(value))
   182  }
   183  
   184  // Gets the extra width (in pixels, possibly fractional)
   185  // used for the faux-bold style.
   186  func (self *FauxRasterizer) GetExtraWidth() float64 { return self.xwidth }
   187  
   188  // Satisfies the [UserCfgCacheSignature] interface.
   189  func (self *FauxRasterizer) SetHighByte(value uint8) {
   190  	self.cacheSignature &= 0x00FFFFFFFFFFFFFF
   191  	self.cacheSignature |= uint64(value) << 56
   192  	self.notifyChange()
   193  }
   194  
   195  // Satisfies the [Rasterizer] interface. The cache signature for the
   196  // faux rasterizer has the following shape:
   197  //   - 0xFF00000000000000 bits for [UserCfgCacheSignature]'s high byte.
   198  //   - 0x00FF000000000000 bits being 0xFA (self signature byte).
   199  //   - 0x0000F00000000000 bits being 0x1 if italics are enabled.
   200  //   - 0x00000F0000000000 bits being 0xB if bold is enabled.
   201  //   - 0x00000000FFFF0000 bits encoding the skewing [-1, 1] as [0, 65535],
   202  //     with the zero skewing not having a representation here (signatures
   203  //     are still different due to the "italics-enabled" flag).
   204  //   - 0x000000000000FFFF bits encoding the extra bold width in 64ths of
   205  //     a pixel and encoded as a uint16.
   206  func (self *FauxRasterizer) CacheSignature() uint64 {
   207  	// initialize "FA" signature (standing for FAUX) bits if relevant
   208  	if !self.hasInitSig {
   209  		self.hasInitSig = true
   210  		self.cacheSignature |= 0x00FA000000000000
   211  	}
   212  
   213  	// return cache signature
   214  	return self.cacheSignature
   215  }
   216  
   217  func (self *FauxRasterizer) fixedToFloat32Coords(point fixed.Point26_6) (float32, float32) {
   218  	// apply skewing here!
   219  	fx := float64(point.X) / 64
   220  	fy := float64(point.Y) / 64
   221  	x := fx - fy*self.skewing + self.normOffsetX
   222  	y := fy + self.normOffsetY
   223  	return float32(x), float32(y)
   224  }
   225  
   226  // Satisfies the [Rasterizer] interface.
   227  func (self *FauxRasterizer) Rasterize(outline sfnt.Segments, fract fixed.Point26_6) (*image.Alpha, error) {
   228  	self.newOutline(outline, fract)
   229  	mask := image.NewAlpha(self.rasterizer.Bounds())
   230  	processOutline(self, outline)
   231  	self.rasterizer.Draw(mask, mask.Bounds(), image.Opaque, image.Point{})
   232  	mask.Rect = mask.Rect.Add(self.maskAdjust)
   233  
   234  	if self.xwidth > 0 {
   235  		self.applyExtraWidth(mask.Pix, mask.Stride)
   236  	}
   237  	return mask, nil
   238  }
   239  
   240  // Like [FauxRasterizer.SetOnChangeFunc], but not reserved for internal
   241  // Renderer use. This is provided so you can link a custom esizer.Sizer to
   242  // the rasterizer and get notified when its configuration changes.
   243  func (self *FauxRasterizer) SetAuxOnChangeFunc(onChange func(*FauxRasterizer)) {
   244  	self.auxOnChange = onChange
   245  }
   246  
   247  func (self *FauxRasterizer) notifyChange() {
   248  	if self.onChange != nil {
   249  		self.onChange(self)
   250  	}
   251  	if self.auxOnChange != nil {
   252  		self.auxOnChange(self)
   253  	}
   254  }
   255  
   256  func (self *FauxRasterizer) newOutline(outline sfnt.Segments, fract fixed.Point26_6) error {
   257  	glyphBounds := outline.Bounds()
   258  
   259  	// adjust the bounds accounting for skewing
   260  	if self.skewing != 0 {
   261  		shiftA := efixed.FromFloat64RoundAwayZero((float64(glyphBounds.Min.Y) / 64) * self.skewing)
   262  		shiftB := efixed.FromFloat64RoundAwayZero((float64(glyphBounds.Max.Y) / 64) * self.skewing)
   263  		if self.skewing >= 0 { // don't make me explain...
   264  			glyphBounds.Min.X -= shiftB
   265  			glyphBounds.Max.X -= shiftA
   266  		} else { // ...I don't actually know what I'm doing
   267  			glyphBounds.Min.X -= shiftA
   268  			glyphBounds.Max.X -= shiftB
   269  		}
   270  	}
   271  
   272  	// adjust the bounds accounting for faux-bold extra width
   273  	if self.xwidth > 0 {
   274  		glyphBounds.Max.X += fixed.Int26_6(int32(math.Ceil(self.xwidth)) << 6)
   275  	}
   276  
   277  	// similar to default rasterizer
   278  	size, normOffset, adjust := figureOutBounds(glyphBounds, fract)
   279  	self.maskAdjust = adjust
   280  	self.normOffsetX = float64(normOffset.X) / 64
   281  	self.normOffsetY = float64(normOffset.Y) / 64
   282  	self.rasterizer.Reset(size.X, size.Y)
   283  	self.rasterizer.DrawOp = draw.Src
   284  	return nil
   285  }
   286  
   287  // ==== EXTRA WIDTH COMPUTATIONS ====
   288  // I got very traumatized trying to figure out all this fake-bold stuff.
   289  // Just use a proper bold font and leave me alone.
   290  //
   291  // ...
   292  //
   293  // Better faux-bold would have to be done through shape expansion anyway,
   294  // working directly with the outline points, but that's tricky to do (e.g:
   295  // github.com/libass/libass/blob/7bf4bee0fc9a1d6257a105a3c19df6cf08733f8e/
   296  // libass/ass_outline.c#L499)... but even freetype's faux-bold is not perfect.
   297  
   298  func (self *FauxRasterizer) applyExtraWidth(pixels []uint8, stride int) {
   299  	// extra width is applied independently to each row
   300  	for x := 0; x < len(pixels); x += stride {
   301  		self.applyRowExtraWidth(pixels[x:x+stride], pixels, x, stride)
   302  	}
   303  }
   304  
   305  func (self *FauxRasterizer) applyRowExtraWidth(row []uint8, pixels []uint8, start int, stride int) {
   306  	var peakAlpha uint8
   307  	var twoPixSwap bool // flag for "two-pixel-stem" fix
   308  
   309  	// for each row, the idea is to ascend to the biggest alpha
   310  	// values first, and then when falling apply the extra width,
   311  	// mostly as a keep-max-of-last-n-alpha-values.
   312  	for index := 0; index < len(row); {
   313  		index, peakAlpha = self.extraWidthRowAscend(row, index)
   314  		if peakAlpha == 0 {
   315  			return
   316  		}
   317  		peakAlpha, twoPixSwap = self.peakAlphaFix(row, index, pixels, start, stride, peakAlpha)
   318  		index = self.extraWidthRowFall(row, index, peakAlpha, twoPixSwap)
   319  	}
   320  }
   321  
   322  func (self *FauxRasterizer) peakAlphaFix(row []uint8, index int, pixels []uint8, start int, stride int, peakAlpha uint8) (uint8, bool) {
   323  	if peakAlpha == 255 || self.xwidthWhole == 0 {
   324  		return peakAlpha, false
   325  	}
   326  
   327  	// check boundaries
   328  	if index < 2 {
   329  		return peakAlpha, false
   330  	}
   331  	if index+1 >= len(row) {
   332  		return peakAlpha, false
   333  	}
   334  	aboveIndex := (start + index - 1 - stride)
   335  	belowIndex := (start + index - 1 + stride)
   336  	if aboveIndex < 0 || belowIndex > len(pixels) {
   337  		return peakAlpha, false
   338  	}
   339  
   340  	// "in stem" heuristic
   341  	pixAbove := (pixels[aboveIndex-1] > 0 || pixels[aboveIndex] > 0 || pixels[aboveIndex+1] > 0)
   342  	pixBelow := (pixels[belowIndex-1] > 0 || pixels[belowIndex] > 0 || pixels[belowIndex+1] > 0)
   343  	if !pixAbove || !pixBelow {
   344  		return peakAlpha, false
   345  	}
   346  
   347  	// handle the edge case of two-pixel stem
   348  	if index >= 3 && row[index] == 0 && row[index-2] != 0 && row[index-3] == 0 {
   349  		return 255, true // two-pix-stem swap is necessary!
   350  	}
   351  
   352  	return 255, false
   353  }
   354  
   355  // Returns the first index after the alpha peak, along with the peak value.
   356  func (self *FauxRasterizer) extraWidthRowAscend(row []uint8, index int) (int, uint8) {
   357  	peakAlpha := uint8(0)
   358  	prevAlpha := uint8(0)
   359  	for ; index < len(row); index++ {
   360  		currAlpha := row[index]
   361  		if currAlpha == prevAlpha {
   362  			continue
   363  		}
   364  		if currAlpha < prevAlpha {
   365  			return index, peakAlpha
   366  		}
   367  		if currAlpha > peakAlpha {
   368  			peakAlpha = currAlpha
   369  		}
   370  		prevAlpha = currAlpha
   371  	}
   372  	return 0, 0
   373  }
   374  
   375  // The tricky part. As mentioned before, the main idea is to
   376  // keep-max-of-last-n-alpha-values, but... *trauma intensifies*
   377  func (self *FauxRasterizer) extraWidthRowFall(row []uint8, index int, peakAlpha uint8, twoPixSwap bool) int {
   378  	// apply the whole width part...
   379  	whole := self.xwidthWhole
   380  	if whole == 0 { // ...unless there's no whole part, I guess
   381  		return self.extraWidthRowFractFall(row, index, peakAlpha)
   382  	}
   383  
   384  	peakAlphaIndex := index - 1
   385  	realPeakAlpha := row[peakAlphaIndex]
   386  	for n := uint16(0); n < whole; n++ {
   387  		currAlpha := row[index]
   388  		if currAlpha >= peakAlpha {
   389  			row[peakAlphaIndex] = peakAlpha
   390  			return index
   391  		}
   392  		row[index] = peakAlpha
   393  		self.xwidthTail[n] = currAlpha
   394  		index += 1
   395  	}
   396  
   397  	// two-pixel-stem swap correction
   398  	if twoPixSwap {
   399  		row[peakAlphaIndex] = peakAlpha
   400  		row[index-1] = realPeakAlpha
   401  		self.xwidthTail[0] = realPeakAlpha
   402  		peakAlpha = realPeakAlpha
   403  	}
   404  
   405  	// we are done with the whole width peak part. now... what's this?
   406  	mod := self.xwidthTailMod
   407  	if whole > 1 {
   408  		self.backfixTail(whole)
   409  	}
   410  
   411  	// prepare variables to propagate the tail
   412  	tailIndex := uint16(0)
   413  	prevAlpha := peakAlpha
   414  	prevTailAdd := peakAlpha
   415  	if twoPixSwap {
   416  		tailIndex = 1
   417  	}
   418  
   419  	// propagate the tail
   420  	for index < len(row) {
   421  		tailAlpha := self.xwidthTail[tailIndex]
   422  		newAlpha := self.interpolateForExtraWidth(prevAlpha, tailAlpha)
   423  		currAlpha := row[index]
   424  		if currAlpha >= newAlpha {
   425  			return index
   426  		} // not falling anymore
   427  		row[index] = newAlpha
   428  
   429  		// put current alpha on the tail
   430  		newTailIndex := (tailIndex + whole) & mod
   431  		self.xwidthTail[newTailIndex] = currAlpha
   432  		if currAlpha > prevTailAdd { // tests recommended me this.
   433  			self.backfixTailGen(whole, newTailIndex, mod)
   434  		}
   435  		prevTailAdd = currAlpha
   436  		tailIndex = (tailIndex + 1) & mod
   437  
   438  		// please let's go to the next value already
   439  		prevAlpha = tailAlpha
   440  		index += 1
   441  	}
   442  	return index
   443  }
   444  
   445  // while we were filling a row with peak values, maybe the values
   446  // that we were overwritting had some ups and downs, and while in
   447  // other parts of the code we can control for that manually, in
   448  // this part they might have gone unnoticed. this function corrects
   449  // this and normalizes the tail to their max possible values.
   450  // expects the tail to start at index = 0.
   451  func (self *FauxRasterizer) backfixTail(whole uint16) {
   452  	// this code is so ugly
   453  	i := whole - 1
   454  	max := self.xwidthTail[i]
   455  	i -= 1
   456  	for {
   457  		value := self.xwidthTail[i]
   458  		if value > max {
   459  			max = value
   460  		} else if value < max {
   461  			self.xwidthTail[i] = max
   462  		}
   463  		if i == 0 {
   464  			return
   465  		}
   466  		i -= 1
   467  	}
   468  }
   469  
   470  // like backfixTail, but without starting at 0
   471  func (self *FauxRasterizer) backfixTailGen(whole uint16, lastIndex uint16, mod uint16) {
   472  	if whole <= 1 {
   473  		return
   474  	}
   475  	max := self.xwidthTail[lastIndex]
   476  	whole -= 1
   477  	if lastIndex > 0 {
   478  		lastIndex -= 1
   479  	} else {
   480  		lastIndex = mod
   481  	}
   482  	for {
   483  		value := self.xwidthTail[lastIndex]
   484  		if value > max {
   485  			max = value
   486  		} else if value < max {
   487  			self.xwidthTail[lastIndex] = max
   488  		}
   489  		whole -= 1
   490  		if whole == 0 {
   491  			return
   492  		}
   493  		if lastIndex > 0 {
   494  			lastIndex -= 1
   495  		} else {
   496  			lastIndex = mod
   497  		} // can you hear gofmt scream already?
   498  	}
   499  }
   500  
   501  // like extraWidthRowFall, but when the whole part of the extra
   502  // width is zero and there's only a fractional part to add
   503  func (self *FauxRasterizer) extraWidthRowFractFall(row []uint8, index int, peakAlpha uint8) int {
   504  	prevAlpha := peakAlpha
   505  	for ; index < len(row); index++ {
   506  		currAlpha := row[index]
   507  		newAlpha := self.interpolateForExtraWidth(prevAlpha, currAlpha)
   508  		if currAlpha >= newAlpha {
   509  			return index
   510  		}
   511  		row[index] = newAlpha
   512  		prevAlpha = currAlpha
   513  	}
   514  	return index
   515  }
   516  
   517  func (self *FauxRasterizer) interpolateForExtraWidth(prevAlpha, currAlpha uint8) uint8 {
   518  	// I have reasons to believe this operation can't overflow,
   519  	// but don't kill me if it ever does, just report it.
   520  	// Note: I originally used pre-computed tables for the products.
   521  	//       They are indeed generally faster, but for me it wasn't enough
   522  	//       to deserve the extra computations when setting the extra width
   523  	//       (nor the 512 extra bytes of space required).
   524  	// Note2: I also tried to optimize for self.xwidthFract == 0, but it
   525  	//        did not help, neither here nor earlier in the process. Maybe
   526  	//        operating with ones and zeros are fast paths anyway.
   527  	prevWeight := uint8(self.xwidthFract * float64(prevAlpha))
   528  	currWeight := uint8((1 - self.xwidthFract) * float64(currAlpha))
   529  	return prevWeight + currWeight
   530  }
   531  
   532  // ==== FROM HERE ON METHODS ARE THE SAME AS DEFAULT RASTERIZER ====
   533  // (...so you can ignore them, nothing new here)
   534  
   535  // See [DefaultRasterizer.MoveTo]().
   536  func (self *FauxRasterizer) MoveTo(point fixed.Point26_6) {
   537  	x, y := self.fixedToFloat32Coords(point)
   538  	self.rasterizer.MoveTo(x, y)
   539  }
   540  
   541  // See [DefaultRasterizer.LineTo]().
   542  func (self *FauxRasterizer) LineTo(point fixed.Point26_6) {
   543  	x, y := self.fixedToFloat32Coords(point)
   544  	self.rasterizer.LineTo(x, y)
   545  }
   546  
   547  // See [DefaultRasterizer.QuadTo]().
   548  func (self *FauxRasterizer) QuadTo(control, target fixed.Point26_6) {
   549  	cx, cy := self.fixedToFloat32Coords(control)
   550  	tx, ty := self.fixedToFloat32Coords(target)
   551  	self.rasterizer.QuadTo(cx, cy, tx, ty)
   552  }
   553  
   554  // See [DefaultRasterizer.CubeTo]().
   555  func (self *FauxRasterizer) CubeTo(controlA, controlB, target fixed.Point26_6) {
   556  	cax, cay := self.fixedToFloat32Coords(controlA)
   557  	cbx, cby := self.fixedToFloat32Coords(controlB)
   558  	tx, ty := self.fixedToFloat32Coords(target)
   559  	self.rasterizer.CubeTo(cax, cay, cbx, cby, tx, ty)
   560  }
   561  
   562  // Satisfies the [Rasterizer] interface.
   563  func (self *FauxRasterizer) SetOnChangeFunc(onChange func(Rasterizer)) {
   564  	self.onChange = onChange
   565  }