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 }