qr.go raw

   1  package main
   2  
   3  // Minimal QR code encoder for npub strings.
   4  // Generates version-4-M (33x33 modules, medium EC) alphanumeric QR codes
   5  // and renders them as an SVG string with a center logo cutout.
   6  //
   7  // npub1... is 63 chars. Version 4-M alphanumeric capacity = 78 chars. Plenty.
   8  // Medium EC (~15% correction) tolerates the center logo area.
   9  
  10  // qrSVG generates a QR code SVG string for the given data.
  11  // The SVG has a white background, black modules, and a centered logo placeholder.
  12  func qrSVG(data string, size int, logoSVG string) string {
  13  	mods := qrEncode(data)
  14  	if mods == nil {
  15  		return ""
  16  	}
  17  	n := len(mods)
  18  	// Module size in SVG units.
  19  	quiet := 2 // quiet zone modules
  20  	total := n + quiet*2
  21  	modSize := size / total
  22  	if modSize < 1 {
  23  		modSize = 1
  24  	}
  25  	svgSize := total * modSize
  26  
  27  	svg := "<svg xmlns='http://www.w3.org/2000/svg' width='" + itoa(svgSize) + "' height='" + itoa(svgSize) + "' viewBox='0 0 " + itoa(svgSize) + " " + itoa(svgSize) + "'>"
  28  	svg += "<rect width='100%' height='100%' fill='white'/>"
  29  
  30  	// Center logo area (mask out modules).
  31  	logoMods := 9 // 9x9 module area in center
  32  	logoStart := (n - logoMods) / 2
  33  	logoEnd := logoStart + logoMods
  34  
  35  	for y := 0; y < n; y++ {
  36  		for x := 0; x < n; x++ {
  37  			if mods[y][x]&1 == 1 {
  38  				// Skip modules in logo area.
  39  				if x >= logoStart && x < logoEnd && y >= logoStart && y < logoEnd {
  40  					continue
  41  				}
  42  				px := (x + quiet) * modSize
  43  				py := (y + quiet) * modSize
  44  				svg += "<rect x='" + itoa(px) + "' y='" + itoa(py) + "' width='" + itoa(modSize) + "' height='" + itoa(modSize) + "' fill='black'/>"
  45  			}
  46  		}
  47  	}
  48  
  49  	// Center logo.
  50  	if logoSVG != "" {
  51  		logoPixStart := (logoStart + quiet) * modSize
  52  		logoPixSize := logoMods * modSize
  53  		svg += "<g transform='translate(" + itoa(logoPixStart) + "," + itoa(logoPixStart) + ")'>"
  54  		svg += "<rect width='" + itoa(logoPixSize) + "' height='" + itoa(logoPixSize) + "' fill='white' rx='4'/>"
  55  		pad := logoPixSize / 6
  56  		inner := logoPixSize - pad*2
  57  		svg += "<g transform='translate(" + itoa(pad) + "," + itoa(pad) + ")'>"
  58  		// Inject logo SVG, rewriting width/height.
  59  		svg += "<svg width='" + itoa(inner) + "' height='" + itoa(inner) + "' viewBox='0 0 100 100'>"
  60  		// Strip outer <svg> tags from logoSVG and inject content.
  61  		svg += extractSVGContent(logoSVG)
  62  		svg += "</svg></g></g>"
  63  	}
  64  
  65  	svg += "</svg>"
  66  	return svg
  67  }
  68  
  69  // extractSVGContent strips the outer <svg ...> and </svg> tags, returning inner content.
  70  func extractSVGContent(s string) string {
  71  	// Find end of opening <svg ...> tag.
  72  	i := 0
  73  	for i < len(s) && s[i] != '<' {
  74  		i++
  75  	}
  76  	if i >= len(s) {
  77  		return s
  78  	}
  79  	// Find closing > of opening tag.
  80  	j := i + 1
  81  	for j < len(s) && s[j] != '>' {
  82  		j++
  83  	}
  84  	if j >= len(s) {
  85  		return s
  86  	}
  87  	start := j + 1
  88  	// Find </svg>.
  89  	end := len(s)
  90  	for k := len(s) - 1; k >= 6; k-- {
  91  		if s[k] == '>' && k >= 5 && s[k-5:k+1] == "</svg>" {
  92  			end = k - 5
  93  			break
  94  		}
  95  	}
  96  	return s[start:end]
  97  }
  98  
  99  // --- QR encoder: Version 4-M, alphanumeric mode ---
 100  
 101  const qrSize = 33 // Version 4 = 33x33
 102  
 103  // qrEncode produces the module matrix for a version-4-M alphanumeric QR code.
 104  func qrEncode(data string) [][]byte {
 105  	gfInit()
 106  	// Encode data bits.
 107  	bits := qrAlphanumericBits(data)
 108  	if bits == nil {
 109  		return nil
 110  	}
 111  	// Add terminator + padding.
 112  	bits = qrPadBits(bits)
 113  	// Compute error correction.
 114  	codewords := bitsToBytes(bits)
 115  	ecWords := rsEncode(codewords)
 116  	// Interleave (single block for v4-M).
 117  	var allBits []byte
 118  	for _, b := range codewords {
 119  		allBits = append(allBits, byteToBits(b)...)
 120  	}
 121  	for _, b := range ecWords {
 122  		allBits = append(allBits, byteToBits(b)...)
 123  	}
 124  	// Remainder bits for version 4: 7.
 125  	for i := 0; i < 7; i++ {
 126  		allBits = append(allBits, 0)
 127  	}
 128  
 129  	// Place modules.
 130  	mods := make([][]byte, qrSize)
 131  	for i := range mods {
 132  		mods[i] = make([]byte, qrSize)
 133  	}
 134  	// reserved = 2 means "function pattern, don't overwrite"
 135  	qrPlaceFinderPatterns(mods)
 136  	qrPlaceAlignmentPattern(mods)
 137  	qrPlaceTimingPatterns(mods)
 138  	qrPlaceDarkModule(mods)
 139  	qrReserveFormatArea(mods)
 140  	qrReserveVersionArea(mods) // no-op for v4
 141  	qrPlaceData(mods, allBits)
 142  	qrApplyBestMask(mods)
 143  	return mods
 144  }
 145  
 146  // Alphanumeric character set.
 147  const alphaChars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:"
 148  
 149  func alphaVal(c byte) int {
 150  	for i := 0; i < len(alphaChars); i++ {
 151  		if alphaChars[i] == c {
 152  			return i
 153  		}
 154  	}
 155  	return -1
 156  }
 157  
 158  func qrAlphanumericBits(data string) []byte {
 159  	udata := toUpper(data)
 160  	// Mode indicator: 0010 (alphanumeric).
 161  	bits := []byte{0, 0, 1, 0}
 162  	// Character count: 9 bits for version 4.
 163  	count := len(udata)
 164  	for i := 8; i >= 0; i-- {
 165  		bits = append(bits, byte((count>>uint(i))&1))
 166  	}
 167  	// Encode pairs.
 168  	for i := 0; i+1 < len(udata); i += 2 {
 169  		v1 := alphaVal(udata[i])
 170  		v2 := alphaVal(udata[i+1])
 171  		if v1 < 0 || v2 < 0 {
 172  			return nil
 173  		}
 174  		val := v1*45 + v2
 175  		for b := 10; b >= 0; b-- {
 176  			bits = append(bits, byte((val>>uint(b))&1))
 177  		}
 178  	}
 179  	if len(udata)%2 == 1 {
 180  		v := alphaVal(udata[len(udata)-1])
 181  		if v < 0 {
 182  			return nil
 183  		}
 184  		for b := 5; b >= 0; b-- {
 185  			bits = append(bits, byte((v>>uint(b))&1))
 186  		}
 187  	}
 188  	return bits
 189  }
 190  
 191  func toUpper(s string) string {
 192  	b := make([]byte, len(s))
 193  	for i := 0; i < len(s); i++ {
 194  		c := s[i]
 195  		if c >= 'a' && c <= 'z' {
 196  			c -= 32
 197  		}
 198  		b[i] = c
 199  	}
 200  	return string(b)
 201  }
 202  
 203  // v4-M: 80 data codewords total.
 204  const v4MDataCodewords = 80
 205  
 206  func qrPadBits(bits []byte) []byte {
 207  	totalBits := v4MDataCodewords * 8
 208  	// Add terminator (up to 4 zeros).
 209  	term := 4
 210  	if totalBits-len(bits) < 4 {
 211  		term = totalBits - len(bits)
 212  	}
 213  	for i := 0; i < term; i++ {
 214  		bits = append(bits, 0)
 215  	}
 216  	// Pad to byte boundary.
 217  	for len(bits)%8 != 0 {
 218  		bits = append(bits, 0)
 219  	}
 220  	// Pad bytes.
 221  	padBytes := []byte{0xEC, 0x11}
 222  	idx := 0
 223  	for len(bits) < totalBits {
 224  		bits = append(bits, byteToBits(padBytes[idx%2])...)
 225  		idx++
 226  	}
 227  	return bits[:totalBits]
 228  }
 229  
 230  func bitsToBytes(bits []byte) []byte {
 231  	n := len(bits) / 8
 232  	out := make([]byte, n)
 233  	for i := 0; i < n; i++ {
 234  		var v byte
 235  		for j := 0; j < 8; j++ {
 236  			v = v<<1 | bits[i*8+j]
 237  		}
 238  		out[i] = v
 239  	}
 240  	return out
 241  }
 242  
 243  func byteToBits(b byte) []byte {
 244  	bits := make([]byte, 8)
 245  	for i := 7; i >= 0; i-- {
 246  		bits[7-i] = (b >> uint(i)) & 1
 247  	}
 248  	return bits
 249  }
 250  
 251  // --- Reed-Solomon for v4-M: 1 block, 80 data codewords, 18 EC codewords ---
 252  
 253  const v4MECCodewords = 18
 254  
 255  // GF(256) with primitive polynomial 0x11d.
 256  var gfExp [512]byte
 257  var gfLog [256]byte
 258  var gfReady bool
 259  
 260  func gfInit() {
 261  	if gfReady {
 262  		return
 263  	}
 264  	gfReady = true
 265  	v := 1
 266  	for i := 0; i < 255; i++ {
 267  		gfExp[i] = byte(v)
 268  		gfLog[v] = byte(i)
 269  		v <<= 1
 270  		if v >= 256 {
 271  			v ^= 0x11d
 272  		}
 273  	}
 274  	for i := 255; i < 512; i++ {
 275  		gfExp[i] = gfExp[i-255]
 276  	}
 277  }
 278  
 279  func gfMul(a, b byte) byte {
 280  	if a == 0 || b == 0 {
 281  		return 0
 282  	}
 283  	return gfExp[int(gfLog[a])+int(gfLog[b])]
 284  }
 285  
 286  func rsEncode(data []byte) []byte {
 287  	// Generator polynomial for 18 EC codewords.
 288  	gen := rsGeneratorPoly(v4MECCodewords)
 289  	result := make([]byte, v4MECCodewords)
 290  	for _, d := range data {
 291  		factor := d ^ result[0]
 292  		copy(result, result[1:])
 293  		result[v4MECCodewords-1] = 0
 294  		for i := 0; i < v4MECCodewords; i++ {
 295  			result[i] ^= gfMul(gen[i], factor)
 296  		}
 297  	}
 298  	return result
 299  }
 300  
 301  func rsGeneratorPoly(n int) []byte {
 302  	poly := []byte{1}
 303  	for i := 0; i < n; i++ {
 304  		newPoly := make([]byte, len(poly)+1)
 305  		for j := 0; j < len(poly); j++ {
 306  			newPoly[j] ^= poly[j]
 307  			newPoly[j+1] ^= gfMul(poly[j], gfExp[i])
 308  		}
 309  		poly = newPoly
 310  	}
 311  	return poly[1:] // drop leading 1
 312  }
 313  
 314  // --- Module placement ---
 315  
 316  func setMod(mods [][]byte, x, y int, val byte) {
 317  	if x >= 0 && x < qrSize && y >= 0 && y < qrSize {
 318  		mods[y][x] = val
 319  	}
 320  }
 321  
 322  func qrPlaceFinderPatterns(mods [][]byte) {
 323  	place := func(cx, cy int) {
 324  		for dy := -4; dy <= 4; dy++ {
 325  			for dx := -4; dx <= 4; dx++ {
 326  				x, y := cx+dx, cy+dy
 327  				if x < 0 || x >= qrSize || y < 0 || y >= qrSize {
 328  					continue
 329  				}
 330  				adx, ady := dx, dy
 331  				if adx < 0 {
 332  					adx = -adx
 333  				}
 334  				if ady < 0 {
 335  					ady = -ady
 336  				}
 337  				mx := adx
 338  				if ady > mx {
 339  					mx = ady
 340  				}
 341  				var v byte
 342  				switch {
 343  				case mx == 4:
 344  					v = 2 // separator (white, reserved)
 345  				case mx == 0 || mx == 2:
 346  					v = 3 // black, reserved
 347  				case mx == 1:
 348  					v = 2 // white, reserved
 349  				case mx == 3:
 350  					v = 3 // black, reserved
 351  				}
 352  				mods[y][x] = v
 353  			}
 354  		}
 355  	}
 356  	place(3, 3)              // top-left
 357  	place(qrSize-4, 3)      // top-right
 358  	place(3, qrSize-4)      // bottom-left
 359  }
 360  
 361  func qrPlaceAlignmentPattern(mods [][]byte) {
 362  	// Version 4: alignment pattern at (26, 26).
 363  	cx, cy := 26, 26
 364  	for dy := -2; dy <= 2; dy++ {
 365  		for dx := -2; dx <= 2; dx++ {
 366  			adx, ady := dx, dy
 367  			if adx < 0 {
 368  				adx = -adx
 369  			}
 370  			if ady < 0 {
 371  				ady = -ady
 372  			}
 373  			mx := adx
 374  			if ady > mx {
 375  				mx = ady
 376  			}
 377  			var v byte
 378  			if mx == 1 {
 379  				v = 2 // white, reserved
 380  			} else {
 381  				v = 3 // black, reserved
 382  			}
 383  			mods[cy+dy][cx+dx] = v
 384  		}
 385  	}
 386  }
 387  
 388  func qrPlaceTimingPatterns(mods [][]byte) {
 389  	for i := 8; i < qrSize-8; i++ {
 390  		v := byte(3) // black reserved
 391  		if i%2 == 1 {
 392  			v = 2 // white reserved
 393  		}
 394  		if mods[6][i] == 0 {
 395  			mods[6][i] = v
 396  		}
 397  		if mods[i][6] == 0 {
 398  			mods[i][6] = v
 399  		}
 400  	}
 401  }
 402  
 403  func qrPlaceDarkModule(mods [][]byte) {
 404  	mods[qrSize-8][8] = 3 // always dark
 405  }
 406  
 407  func qrReserveFormatArea(mods [][]byte) {
 408  	// Around top-left finder.
 409  	for i := 0; i < 9; i++ {
 410  		if mods[8][i] == 0 {
 411  			mods[8][i] = 2
 412  		}
 413  		if mods[i][8] == 0 {
 414  			mods[i][8] = 2
 415  		}
 416  	}
 417  	// Below top-right finder.
 418  	for i := 0; i < 8; i++ {
 419  		if mods[8][qrSize-1-i] == 0 {
 420  			mods[8][qrSize-1-i] = 2
 421  		}
 422  	}
 423  	// Right of bottom-left finder.
 424  	for i := 0; i < 7; i++ {
 425  		if mods[qrSize-1-i][8] == 0 {
 426  			mods[qrSize-1-i][8] = 2
 427  		}
 428  	}
 429  }
 430  
 431  func qrReserveVersionArea(mods [][]byte) {
 432  	// Version info blocks only for version >= 7. No-op for v4.
 433  }
 434  
 435  func qrPlaceData(mods [][]byte, bits []byte) {
 436  	idx := 0
 437  	// Data is placed in 2-column strips, right to left, bottom to top / top to bottom alternating.
 438  	x := qrSize - 1
 439  	upward := true
 440  	for x > 0 {
 441  		if x == 6 {
 442  			x-- // skip timing column
 443  		}
 444  		for row := 0; row < qrSize; row++ {
 445  			y := row
 446  			if upward {
 447  				y = qrSize - 1 - row
 448  			}
 449  			for dx := 0; dx <= 1; dx++ {
 450  				cx := x - dx
 451  				if mods[y][cx] != 0 {
 452  					continue // reserved
 453  				}
 454  				if idx < len(bits) {
 455  					mods[y][cx] = bits[idx] // 0 or 1
 456  				}
 457  				idx++
 458  			}
 459  		}
 460  		upward = !upward
 461  		x -= 2
 462  	}
 463  }
 464  
 465  // --- Masking ---
 466  
 467  // v4-M format info strings (pre-computed for masks 0-7).
 468  // Format: error correction level M (01) + mask pattern + BCH error correction.
 469  var formatBits = [8][15]byte{
 470  	{1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0}, // mask 0
 471  	{1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0}, // mask 1
 472  	{1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1}, // mask 2
 473  	{1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1}, // mask 3
 474  	{1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0}, // mask 4
 475  	{1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0}, // mask 5
 476  	{1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1}, // mask 6
 477  	{1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1}, // mask 7
 478  }
 479  
 480  func maskFunc(mask int, x, y int) bool {
 481  	switch mask {
 482  	case 0:
 483  		return (x+y)%2 == 0
 484  	case 1:
 485  		return y%2 == 0
 486  	case 2:
 487  		return x%3 == 0
 488  	case 3:
 489  		return (x+y)%3 == 0
 490  	case 4:
 491  		return (y/2+x/3)%2 == 0
 492  	case 5:
 493  		return (x*y)%2+(x*y)%3 == 0
 494  	case 6:
 495  		return ((x*y)%2+(x*y)%3)%2 == 0
 496  	case 7:
 497  		return ((x+y)%2+(x*y)%3)%2 == 0
 498  	}
 499  	return false
 500  }
 501  
 502  func isData(v byte) bool {
 503  	return v <= 1 // 0 or 1 are data modules
 504  }
 505  
 506  func qrApplyBestMask(mods [][]byte) {
 507  	bestMask := 0
 508  	bestScore := 1<<31 - 1
 509  	for m := 0; m < 8; m++ {
 510  		// Apply mask.
 511  		for y := 0; y < qrSize; y++ {
 512  			for x := 0; x < qrSize; x++ {
 513  				if isData(mods[y][x]) && maskFunc(m, x, y) {
 514  					mods[y][x] ^= 1
 515  				}
 516  			}
 517  		}
 518  		score := qrPenaltyScore(mods)
 519  		// Undo mask.
 520  		for y := 0; y < qrSize; y++ {
 521  			for x := 0; x < qrSize; x++ {
 522  				if isData(mods[y][x]) && maskFunc(m, x, y) {
 523  					mods[y][x] ^= 1
 524  				}
 525  			}
 526  		}
 527  		if score < bestScore {
 528  			bestScore = score
 529  			bestMask = m
 530  		}
 531  	}
 532  	// Apply best mask permanently.
 533  	for y := 0; y < qrSize; y++ {
 534  		for x := 0; x < qrSize; x++ {
 535  			if isData(mods[y][x]) && maskFunc(bestMask, x, y) {
 536  				mods[y][x] ^= 1
 537  			}
 538  		}
 539  	}
 540  	// Write format info.
 541  	qrWriteFormatInfo(mods, bestMask)
 542  	// Convert reserved modules: 2→0 (white), 3→1 (black).
 543  	for y := 0; y < qrSize; y++ {
 544  		for x := 0; x < qrSize; x++ {
 545  			if mods[y][x] == 2 {
 546  				mods[y][x] = 0
 547  			} else if mods[y][x] == 3 {
 548  				mods[y][x] = 1
 549  			}
 550  		}
 551  	}
 552  }
 553  
 554  func qrWriteFormatInfo(mods [][]byte, mask int) {
 555  	fb := formatBits[mask]
 556  	// Horizontal: left of top-left finder.
 557  	positions := [15][2]int{
 558  		{0, 8}, {1, 8}, {2, 8}, {3, 8}, {4, 8}, {5, 8}, {7, 8}, {8, 8},
 559  		{8, 7}, {8, 5}, {8, 4}, {8, 3}, {8, 2}, {8, 1}, {8, 0},
 560  	}
 561  	for i, pos := range positions {
 562  		v := byte(2)
 563  		if fb[i] == 1 {
 564  			v = 3
 565  		}
 566  		mods[pos[1]][pos[0]] = v
 567  	}
 568  	// Vertical: below top-right + right of bottom-left.
 569  	positions2 := [15][2]int{
 570  		{8, qrSize - 1}, {8, qrSize - 2}, {8, qrSize - 3}, {8, qrSize - 4},
 571  		{8, qrSize - 5}, {8, qrSize - 6}, {8, qrSize - 7},
 572  		{qrSize - 8, 8}, {qrSize - 7, 8}, {qrSize - 6, 8}, {qrSize - 5, 8},
 573  		{qrSize - 4, 8}, {qrSize - 3, 8}, {qrSize - 2, 8}, {qrSize - 1, 8},
 574  	}
 575  	for i, pos := range positions2 {
 576  		v := byte(2)
 577  		if fb[i] == 1 {
 578  			v = 3
 579  		}
 580  		mods[pos[1]][pos[0]] = v
 581  	}
 582  }
 583  
 584  func qrPenaltyScore(mods [][]byte) int {
 585  	score := 0
 586  	// Rule 1: 5+ same-color in a row/column.
 587  	for y := 0; y < qrSize; y++ {
 588  		run := 1
 589  		for x := 1; x < qrSize; x++ {
 590  			if (mods[y][x]&1) == (mods[y][x-1]&1) {
 591  				run++
 592  			} else {
 593  				if run >= 5 {
 594  					score += run - 2
 595  				}
 596  				run = 1
 597  			}
 598  		}
 599  		if run >= 5 {
 600  			score += run - 2
 601  		}
 602  	}
 603  	for x := 0; x < qrSize; x++ {
 604  		run := 1
 605  		for y := 1; y < qrSize; y++ {
 606  			if (mods[y][x]&1) == (mods[y-1][x]&1) {
 607  				run++
 608  			} else {
 609  				if run >= 5 {
 610  					score += run - 2
 611  				}
 612  				run = 1
 613  			}
 614  		}
 615  		if run >= 5 {
 616  			score += run - 2
 617  		}
 618  	}
 619  	// Rule 2: 2x2 same-color blocks.
 620  	for y := 0; y < qrSize-1; y++ {
 621  		for x := 0; x < qrSize-1; x++ {
 622  			c := mods[y][x] & 1
 623  			if (mods[y][x+1]&1) == c && (mods[y+1][x]&1) == c && (mods[y+1][x+1]&1) == c {
 624  				score += 3
 625  			}
 626  		}
 627  	}
 628  	// Rule 4: proportion of dark modules.
 629  	dark := 0
 630  	total := qrSize * qrSize
 631  	for y := 0; y < qrSize; y++ {
 632  		for x := 0; x < qrSize; x++ {
 633  			if mods[y][x]&1 == 1 {
 634  				dark++
 635  			}
 636  		}
 637  	}
 638  	pct := dark * 100 / total
 639  	dev := pct - 50
 640  	if dev < 0 {
 641  		dev = -dev
 642  	}
 643  	score += (dev / 5) * 10
 644  	return score
 645  }
 646