diff --git a/internal/services/game_print_service/assets/diploma.png b/internal/services/game_print_service/assets/diploma.png new file mode 100644 index 0000000..dbe8b2a Binary files /dev/null and b/internal/services/game_print_service/assets/diploma.png differ diff --git a/internal/services/game_print_service/diploma.go b/internal/services/game_print_service/diploma.go index 104fd07..686f52e 100644 --- a/internal/services/game_print_service/diploma.go +++ b/internal/services/game_print_service/diploma.go @@ -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, "") -} diff --git a/internal/services/game_print_service/diploma_test.go b/internal/services/game_print_service/diploma_test.go index 8cb17f8..1cfaf06 100644 --- a/internal/services/game_print_service/diploma_test.go +++ b/internal/services/game_print_service/diploma_test.go @@ -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") } }) } diff --git a/internal/services/game_print_service/service.go b/internal/services/game_print_service/service.go index 78ade96..eff5044 100644 --- a/internal/services/game_print_service/service.go +++ b/internal/services/game_print_service/service.go @@ -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 { diff --git a/internal/services/game_print_service/service_test.go b/internal/services/game_print_service/service_test.go index bf0d981..7a3fe0b 100644 --- a/internal/services/game_print_service/service_test.go +++ b/internal/services/game_print_service/service_test.go @@ -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++ }