This commit is contained in:
2026-10-27 13:12:42 +07:00
parent 0341bfac25
commit 8733c328fa
5 changed files with 35 additions and 134 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

@@ -3,96 +3,24 @@ package game_print_service
import (
"bytes"
_ "embed"
"errors"
"github.com/go-pdf/fpdf"
)
//go:embed assets/paper.jpg
var diplomaPaper []byte
//go:embed assets/diploma.png
var diplomaImage []byte
var errInvalidDiplomaPlace = errors.New("некорректное место диплома")
func diplomaPDF(place int) ([]byte, error) {
ranks := [...]string{"I", "II", "III"}
if place < 1 || place > len(ranks) {
return nil, errInvalidDiplomaPlace
}
func diplomaPDF() ([]byte, error) {
pdf := fpdf.New("P", "mm", "A4", "")
pdf.SetMargins(20, 20, 20)
pdf.SetAutoPageBreak(false, 0)
pdf.AddUTF8FontFromBytes("typewriter", "", typewriterFont)
pdf.SetFont("typewriter", "", 12)
pdf.AddPage()
options := fpdf.ImageOptions{ImageType: "JPG"}
paper := pdf.RegisterImageOptionsReader("diploma-paper", options, bytes.NewReader(diplomaPaper))
options := fpdf.ImageOptions{ImageType: "PNG"}
image := pdf.RegisterImageOptionsReader("diploma", options, bytes.NewReader(diplomaImage))
if err := pdf.Error(); err != nil {
return nil, err
}
height := 210 * paper.Height() / paper.Width()
pdf.ClipRect(0, 0, 210, 297, false)
pdf.ImageOptions("diploma-paper", 0, (297-height)/2, 210, height, false, options, 0, "")
pdf.ClipEnd()
pdf.SetDrawColor(99, 75, 51)
pdf.SetTextColor(61, 45, 34)
pdf.SetFillColor(99, 75, 51)
pdf.SetLineWidth(0.45)
pdf.Rect(12, 12, 186, 273, "D")
pdf.SetLineWidth(0.16)
pdf.Rect(15, 15, 180, 267, "D")
for _, corner := range []fpdf.PointType{{21, 21}, {189, 21}, {21, 276}, {189, 276}} {
x, y := corner.X, corner.Y
pdf.Polygon([]fpdf.PointType{{x, y - 2}, {x + 2, y}, {x, y + 2}, {x - 2, y}}, "F")
pdf.Line(x-4, y, x-2.5, y)
pdf.Line(x+2.5, y, x+4, y)
pdf.Line(x, y-4, x, y-2.5)
pdf.Line(x, y+2.5, x, y+4)
}
pdf.SetFillColor(255, 253, 247)
pdf.Circle(105, 38, 17, "FD")
logo := pdf.RegisterImageOptionsReader("diploma-logo", options, bytes.NewReader(printLogo))
if err := pdf.Error(); err != nil {
return nil, err
}
logoHeight := 28 * logo.Height() / logo.Width()
pdf.ClipCircle(105, 38, max(28, logoHeight)/2+0.1, false)
pdf.ImageOptions("diploma-logo", 91, 38-logoHeight/2, 28, 0, false, options, 0, "")
pdf.ClipEnd()
diplomaText(pdf, 61, 15, "Вечерний детектив")
pdf.Line(75, 73, 135, 73)
diplomaText(pdf, 82, 42, "ДИПЛОМ")
diplomaText(pdf, 116, 13, "Награждается команда")
pdf.SetLineWidth(0.3)
pdf.Line(32, 141, 178, 141)
pdf.TransformBegin()
pdf.TransformRotate(-5, 105, 181)
pdf.SetDrawColor(133, 53, 43)
pdf.SetTextColor(133, 53, 43)
pdf.SetLineWidth(0.55)
pdf.Rect(62, 156, 86, 51, "D")
pdf.SetLineWidth(0.18)
pdf.Rect(64, 158, 82, 47, "D")
diplomaText(pdf, 160, 11, "ЗА")
diplomaText(pdf, 168, 44, ranks[place-1])
diplomaText(pdf, 193, 15, "МЕСТО")
pdf.TransformEnd()
pdf.SetDrawColor(99, 75, 51)
pdf.SetTextColor(61, 45, 34)
diplomaText(pdf, 222, 11, "За проницательность, командную работу")
diplomaText(pdf, 230, 11, "и мастерство расследования")
pdf.SetLineWidth(0.25)
pdf.Line(32, 260, 77, 260)
pdf.Line(112, 260, 178, 260)
pdf.SetFont("typewriter", "", 9)
pdf.SetXY(32, 264)
pdf.CellFormat(45, 5, "Дата", "", 0, "C", false, 0, "")
pdf.SetXY(112, 264)
pdf.CellFormat(66, 5, "Организатор / подпись", "", 0, "C", false, 0, "")
width := min(210.0, 297*image.Width()/image.Height())
height := width * image.Height() / image.Width()
pdf.ImageOptions("diploma", (210-width)/2, (297-height)/2, width, height, false, options, 0, "")
return pdfBytes(pdf)
}
func diplomaText(pdf *fpdf.Fpdf, y, size float64, text string) {
pdf.SetFont("typewriter", "", size)
pdf.SetXY(20, y)
pdf.CellFormat(170, size*0.3528*1.2, text, "", 0, "C", false, 0, "")
}
@@ -2,60 +2,32 @@ package game_print_service
import (
"bytes"
"errors"
"os/exec"
"regexp"
"strconv"
"strings"
"image/png"
"testing"
)
func TestDiplomasUsePortraitA4AndBrandAssets(t *testing.T) {
for _, tc := range []struct {
place int
rank string
}{{1, "I"}, {2, "II"}, {3, "III"}} {
t.Run(tc.rank, func(t *testing.T) {
data, err := diplomaPDF(tc.place)
func TestDiplomaUsesPortraitA4AndBlankArtwork(t *testing.T) {
for _, name := range []string{"blank diploma"} {
t.Run(name, func(t *testing.T) {
data, err := diplomaPDF()
if err != nil {
t.Fatal(err)
}
if bytes.Count(data, []byte("/Type /Page\n")) != 1 || !bytes.Contains(data, []byte("/MediaBox [0 0 595.28 841.89]")) {
t.Fatal("diploma is not one portrait A4 page")
}
if !bytes.Contains(data, diplomaPaper) || !bytes.Contains(data, printLogo) || bytes.Count(data, []byte("/Subtype /Image")) != 2 {
t.Fatal("original paper and logo not embedded")
if bytes.Count(data, []byte("/Subtype /Image")) != 1 || !bytes.Contains(data, []byte("/Width 1054")) || !bytes.Contains(data, []byte("/Height 1492")) {
t.Fatal("diploma artwork missing")
}
if !bytes.Contains(data, []byte("/BaseFont /utf8typewriter")) {
t.Fatal("typewriter font missing")
artwork, err := png.Decode(bytes.NewReader(diplomaImage))
if err != nil {
t.Fatal(err)
}
t.Run("contents", func(t *testing.T) {
if _, err := exec.LookPath("pdftotext"); err != nil {
t.Skip("pdftotext is needed only for optional rendered-content checks")
}
text := extractedPDFText(t, data)
for _, want := range []string{"ДИПЛОМ", "Вечерний детектив", "Награждается команда", "МЕСТО", "Дата", "Организатор / подпись", "мастерство расследования"} {
if !strings.Contains(text, want) {
t.Errorf("missing text %s", want)
}
}
if !regexp.MustCompile(`(?m)^\s*` + tc.rank + `\s*$`).MatchString(text) {
t.Fatalf("wrong award place: %s", text)
}
if regexp.MustCompile(`(?m)^\s*[123]\s*$`).MatchString(text) {
t.Fatal("page number printed")
}
})
})
}
}
func TestDiplomaRejectsUnknownPlace(t *testing.T) {
for _, place := range []int{-1, 0, 4} {
t.Run(strconv.Itoa(place), func(t *testing.T) {
data, err := diplomaPDF(place)
if !errors.Is(err, errInvalidDiplomaPlace) || len(data) != 0 {
t.Fatalf("place %d: data = %d bytes, error = %v", place, len(data), err)
if artwork.Bounds().Dx() != 1054 || artwork.Bounds().Dy() != 1492 {
t.Fatal("unexpected artwork dimensions")
}
if bytes.Contains(data, []byte("/Type /Font")) {
t.Fatal("diploma contains added text font")
}
})
}
@@ -132,6 +132,7 @@ func (s *GamePrintService) Download(ctx context.Context, gameID int) ([]byte, st
{playerFolder + "Вопросы.pdf", func() ([]byte, error) { return questionsPDF(scenario.Name, story.Questions) }},
{playerFolder + "Список пидарасов.pdf", func() ([]byte, error) { return draftPDF("Список пидарасов") }},
{playerFolder + "Черновик расследования.pdf", func() ([]byte, error) { return draftPDF("Черновик расследования") }},
{hostFolder + "Дипломы/Диплом.pdf", diplomaPDF},
{hostFolder + "Ответы.pdf", func() ([]byte, error) { return answersPDF(scenario.Name, story.Questions) }},
{hostFolder + "Лист выдачи улик.pdf", func() ([]byte, error) { return issuancePDF(scenario.Name, teams, clues) }},
}
@@ -147,15 +148,6 @@ func (s *GamePrintService) Download(ctx context.Context, gameID int) ([]byte, st
return nil, "", err
}
}
for place := 1; place <= 3; place++ {
data, err := diplomaPDF(place)
if err != nil {
return nil, "", fmt.Errorf("generate diploma %d: %w", place, err)
}
if err := add(fmt.Sprintf("%sДипломы/%d место.pdf", hostFolder, place), data); err != nil {
return nil, "", err
}
}
if len(teams) > 0 {
data, _, err := s.qr.GenerateTeamQR(ctx, game, teams)
if err != nil {
@@ -151,12 +151,21 @@ func TestDownloadPrintMaterials(t *testing.T) {
playerFolder + "Правила.pdf", playerFolder + "Введение.pdf", playerFolder + "Вопросы.pdf",
playerFolder + "Список пидарасов.pdf", playerFolder + "Черновик расследования.pdf",
hostFolder + "Ответы.pdf", hostFolder + "Лист выдачи улик.pdf",
hostFolder + "Дипломы/1 место.pdf", hostFolder + "Дипломы/2 место.pdf", hostFolder + "Дипломы/3 место.pdf",
hostFolder + "Дипломы/Диплом.pdf",
} {
if !bytes.HasPrefix(entries[name], []byte("%PDF-")) {
t.Errorf("missing PDF %s", name)
}
}
diplomas := 0
for name := range entries {
if strings.HasPrefix(name, hostFolder+"Дипломы/") {
diplomas++
}
}
if diplomas != 1 {
t.Fatalf("diplomas = %d, want one blank diploma", diplomas)
}
_, hasQR := entries[hostFolder+"QR команд.pdf"]
_, hasExternal := entries[hostFolder+"Внешние материалы.pdf"]
if hasQR != tc.wantQR || hasExternal != tc.wantExternal {
@@ -169,7 +178,7 @@ func TestDownloadPrintMaterials(t *testing.T) {
if !bytes.Equal(entries[playerFolder+"Улики/001 - Письмо _ улика.pdf"], f.files["clue.pdf"]) {
t.Fatal("original clue was changed or missing")
}
wantEntries := 12
wantEntries := 10
if tc.wantQR {
wantEntries++
}