diff --git a/bin/evening_detective_server b/bin/evening_detective_server index 7a22491..51054db 100755 Binary files a/bin/evening_detective_server and b/bin/evening_detective_server differ diff --git a/internal/services/game_print_service/assets/paper.jpg b/internal/services/game_print_service/assets/paper.jpg new file mode 100644 index 0000000..ec7c0f0 Binary files /dev/null and b/internal/services/game_print_service/assets/paper.jpg differ diff --git a/internal/services/game_print_service/diploma.go b/internal/services/game_print_service/diploma.go new file mode 100644 index 0000000..104fd07 --- /dev/null +++ b/internal/services/game_print_service/diploma.go @@ -0,0 +1,98 @@ +package game_print_service + +import ( + "bytes" + _ "embed" + "errors" + + "github.com/go-pdf/fpdf" +) + +//go:embed assets/paper.jpg +var diplomaPaper []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 + } + 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)) + 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, "") + 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, "") +} diff --git a/internal/services/game_print_service/diploma_test.go b/internal/services/game_print_service/diploma_test.go new file mode 100644 index 0000000..8cb17f8 --- /dev/null +++ b/internal/services/game_print_service/diploma_test.go @@ -0,0 +1,62 @@ +package game_print_service + +import ( + "bytes" + "errors" + "os/exec" + "regexp" + "strconv" + "strings" + "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) + 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.Contains(data, []byte("/BaseFont /utf8typewriter")) { + t.Fatal("typewriter font missing") + } + 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) + } + }) + } +} diff --git a/internal/services/game_print_service/service.go b/internal/services/game_print_service/service.go index e957fad..78ade96 100644 --- a/internal/services/game_print_service/service.go +++ b/internal/services/game_print_service/service.go @@ -147,6 +147,15 @@ 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 { diff --git a/internal/services/game_print_service/service_test.go b/internal/services/game_print_service/service_test.go index bcd618b..4a00d88 100644 --- a/internal/services/game_print_service/service_test.go +++ b/internal/services/game_print_service/service_test.go @@ -151,6 +151,7 @@ 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", } { if !bytes.HasPrefix(entries[name], []byte("%PDF-")) { t.Errorf("missing PDF %s", name) @@ -168,7 +169,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 := 9 + wantEntries := 12 if tc.wantQR { wantEntries++ } @@ -314,6 +315,12 @@ func TestPrintArchiveUsesScenarioTitle(t *testing.T) { if strings.Contains(name, "QR команд") { continue } + if strings.HasPrefix(name, hostFolder+"Дипломы/") { + if strings.Contains(text, f.scenarioName) { + t.Fatal("scenario title printed in universal diploma") + } + continue + } if name == playerFolder+"Правила.pdf" || name == playerFolder+"Черновик расследования.pdf" || name == playerFolder+"Список пидарасов.pdf" { if strings.Contains(text, f.scenarioName) { t.Fatalf("scenario title printed in %s", name)