220 lines
5.3 KiB
Go
220 lines
5.3 KiB
Go
package standard
|
|
|
|
import (
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
|
|
"github.com/yeqown/go-qrcode/v2"
|
|
"github.com/yeqown/go-qrcode/writer/standard/imgkit"
|
|
|
|
"github.com/fogleman/gg"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
var _ qrcode.Writer = (*Writer)(nil)
|
|
|
|
var (
|
|
ErrNilWriter = errors.New("nil writer")
|
|
)
|
|
|
|
// Writer is a writer that writes QR Code to io.Writer.
|
|
type Writer struct {
|
|
option *outputImageOptions
|
|
|
|
closer io.WriteCloser
|
|
}
|
|
|
|
// New creates a standard writer.
|
|
func New(filename string, opts ...ImageOption) (*Writer, error) {
|
|
if _, err := os.Stat(filename); err != nil && os.IsExist(err) {
|
|
// custom path got: "file exists"
|
|
log.Printf("could not find path: %s, then save to %s", filename, _defaultFilename)
|
|
filename = _defaultFilename
|
|
}
|
|
|
|
fd, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "create file failed")
|
|
}
|
|
|
|
return NewWithWriter(fd, opts...), nil
|
|
}
|
|
|
|
func NewWithWriter(writeCloser io.WriteCloser, opts ...ImageOption) *Writer {
|
|
dst := defaultOutputImageOption()
|
|
for _, opt := range opts {
|
|
opt.apply(dst)
|
|
}
|
|
|
|
if writeCloser == nil {
|
|
panic("writeCloser could not be nil")
|
|
}
|
|
|
|
return &Writer{
|
|
option: dst,
|
|
closer: writeCloser,
|
|
}
|
|
}
|
|
|
|
const (
|
|
_defaultFilename = "default.jpeg"
|
|
_defaultPadding = 40
|
|
)
|
|
|
|
func (w Writer) Write(mat qrcode.Matrix) error {
|
|
return drawTo(w.closer, mat, w.option)
|
|
}
|
|
|
|
func (w Writer) Close() error {
|
|
if w.closer == nil {
|
|
return nil
|
|
}
|
|
|
|
if err := w.closer.Close(); !errors.Is(err, os.ErrClosed) {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (w Writer) Attribute(dimension int) *Attribute {
|
|
return w.option.preCalculateAttribute(dimension)
|
|
}
|
|
|
|
func drawTo(w io.Writer, mat qrcode.Matrix, option *outputImageOptions) (err error) {
|
|
if option == nil {
|
|
option = defaultOutputImageOption()
|
|
}
|
|
|
|
if w == nil {
|
|
return ErrNilWriter
|
|
}
|
|
|
|
img := draw(mat, option)
|
|
|
|
// DONE(@yeqown): support file format specified config option
|
|
if err = option.imageEncoder.Encode(w, img); err != nil {
|
|
err = fmt.Errorf("imageEncoder.Encode failed: %v", err)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// draw deal QRCode's matrix to be an image.Image. Notice that if anyone changed this function,
|
|
// please also check the function outputImageOptions.preCalculateAttribute().
|
|
func draw(mat qrcode.Matrix, opt *outputImageOptions) image.Image {
|
|
top, right, bottom, left := opt.borderWidths[0], opt.borderWidths[1], opt.borderWidths[2], opt.borderWidths[3]
|
|
// closer as image width, h as image height
|
|
w := mat.Width()*opt.qrBlockWidth() + left + right
|
|
h := mat.Height()*opt.qrBlockWidth() + top + bottom
|
|
dc := gg.NewContext(w, h)
|
|
|
|
// draw background
|
|
dc.SetColor(opt.backgroundColor())
|
|
dc.DrawRectangle(0, 0, float64(w), float64(h))
|
|
dc.Fill()
|
|
|
|
// qrcode block draw context
|
|
ctx := &DrawContext{
|
|
Context: dc,
|
|
x: 0.0,
|
|
y: 0.0,
|
|
w: opt.qrBlockWidth(),
|
|
h: opt.qrBlockWidth(),
|
|
color: color.Black,
|
|
}
|
|
shape := opt.getShape()
|
|
|
|
var (
|
|
halftoneImg image.Image
|
|
halftoneW = float64(opt.qrBlockWidth()) / 3.0
|
|
)
|
|
if opt.halftoneImg != nil {
|
|
halftoneImg = imgkit.Binaryzation(
|
|
imgkit.Scale(opt.halftoneImg, image.Rect(0, 0, mat.Width()*3, mat.Width()*3), nil),
|
|
60,
|
|
)
|
|
|
|
//_ = imgkit.Save(halftoneImg, "mask.jpeg")
|
|
}
|
|
|
|
// iterate the matrix to Draw each pixel
|
|
mat.Iterate(qrcode.IterDirection_ROW, func(x int, y int, v qrcode.QRValue) {
|
|
// Draw the block
|
|
ctx.x, ctx.y = float64(x*opt.qrBlockWidth()+left), float64(y*opt.qrBlockWidth()+top)
|
|
ctx.w, ctx.h = opt.qrBlockWidth(), opt.qrBlockWidth()
|
|
ctx.color = opt.translateToRGBA(v)
|
|
|
|
// DONE(@yeqown): make this abstract to Shapes
|
|
switch typ := v.Type(); typ {
|
|
case qrcode.QRType_FINDER:
|
|
shape.DrawFinder(ctx)
|
|
case qrcode.QRType_DATA:
|
|
if halftoneImg == nil {
|
|
shape.Draw(ctx)
|
|
return
|
|
}
|
|
|
|
ctx2 := &DrawContext{
|
|
Context: ctx.Context,
|
|
w: int(halftoneW),
|
|
h: int(halftoneW),
|
|
}
|
|
// only halftone image enabled and current block is Data.
|
|
for i := 0; i < 3; i++ {
|
|
for j := 0; j < 3; j++ {
|
|
ctx2.x, ctx2.y = ctx.x+float64(i)*halftoneW, ctx.y+float64(j)*halftoneW
|
|
ctx2.color = halftoneImg.At(x*3+i, y*3+j)
|
|
if i == 1 && j == 1 {
|
|
ctx2.color = ctx.color
|
|
// only center block keep the origin color.
|
|
}
|
|
shape.Draw(ctx2)
|
|
}
|
|
}
|
|
default:
|
|
shape.Draw(ctx)
|
|
}
|
|
|
|
// EOFn
|
|
})
|
|
|
|
// DONE(@yeqown): add logo image
|
|
if opt.logoImage() != nil {
|
|
// Draw logo image into rgba
|
|
bound := opt.logo.Bounds()
|
|
upperLeft, lowerRight := bound.Min, bound.Max
|
|
logoWidth, logoHeight := lowerRight.X-upperLeft.X, lowerRight.Y-upperLeft.Y
|
|
|
|
if !validLogoImage(w, h, logoWidth, logoHeight) {
|
|
log.Printf("w=%d, h=%d, logoW=%d, logoH=%d, logo is over than 1/5 of QRCode \n",
|
|
w, h, logoWidth, logoHeight)
|
|
goto done
|
|
}
|
|
|
|
// DONE(@yeqown): calculate the xOffset and yOffset which point(xOffset, yOffset)
|
|
//should icon upper-left to start
|
|
dc.DrawImage(opt.logoImage(), (w-logoWidth)/2, (h-logoHeight)/2)
|
|
}
|
|
done:
|
|
return dc.Image()
|
|
}
|
|
|
|
func validLogoImage(qrWidth, qrHeight, logoWidth, logoHeight int) bool {
|
|
return qrWidth >= 5*logoWidth && qrHeight >= 5*logoHeight
|
|
}
|
|
|
|
// Attribute contains basic information of generated image.
|
|
type Attribute struct {
|
|
// width and height of image
|
|
W, H int
|
|
// in the order of "top, right, bottom, left"
|
|
Borders [4]int
|
|
// the length of block edges
|
|
BlockWidth int
|
|
}
|