WIP: Cope with changes to verify.
[dcpomatic.git] / src / wx / verify_dcp_dialog.cc
1 /*
2     Copyright (C) 2018-2021 Carl Hetherington <cth@carlh.net>
3
4     This file is part of DCP-o-matic.
5
6     DCP-o-matic is free software; you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation; either version 2 of the License, or
9     (at your option) any later version.
10
11     DCP-o-matic is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15
16     You should have received a copy of the GNU General Public License
17     along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
18
19 */
20
21
22 #include "dcpomatic_button.h"
23 #include "verify_dcp_dialog.h"
24 #include "wx_util.h"
25 #include "lib/scope_guard.h"
26 #include "lib/verify_dcp_job.h"
27 #include <dcp/raw_convert.h>
28 #include <dcp/verify.h>
29 #include <dcp/verify_report.h>
30 #include <dcp/warnings.h>
31 LIBDCP_DISABLE_WARNINGS
32 #include <wx/richtext/richtextctrl.h>
33 #include <wx/notebook.h>
34 LIBDCP_ENABLE_WARNINGS
35 #include <boost/algorithm/string.hpp>
36
37
38 using std::list;
39 using std::map;
40 using std::shared_ptr;
41 using std::string;
42 using std::vector;
43
44
45 VerifyDCPDialog::VerifyDCPDialog (wxWindow* parent, shared_ptr<VerifyDCPJob> job)
46         : wxDialog (parent, wxID_ANY, _("DCP verification"), wxDefaultPosition, {600, 400})
47         , _result(job->result())
48 {
49         auto sizer = new wxBoxSizer (wxVERTICAL);
50         auto notebook = new wxNotebook (this, wxID_ANY);
51         sizer->Add (notebook, 1, wxEXPAND | wxALL, DCPOMATIC_DIALOG_BORDER);
52
53         map<dcp::VerificationNote::Type, wxRichTextCtrl*> pages;
54         pages[dcp::VerificationNote::Type::ERROR] = new wxRichTextCtrl (notebook, wxID_ANY, wxEmptyString, wxDefaultPosition, {400, 300}, wxRE_READONLY);
55         notebook->AddPage (pages[dcp::VerificationNote::Type::ERROR], _("Errors"));
56         pages[dcp::VerificationNote::Type::BV21_ERROR] = new wxRichTextCtrl (notebook, wxID_ANY, wxEmptyString, wxDefaultPosition, {400, 300}, wxRE_READONLY);
57         notebook->AddPage (pages[dcp::VerificationNote::Type::BV21_ERROR], _("SMPTE Bv2.1 errors"));
58         pages[dcp::VerificationNote::Type::WARNING] = new wxRichTextCtrl (notebook, wxID_ANY, wxEmptyString, wxDefaultPosition, {400, 300}, wxRE_READONLY);
59         notebook->AddPage (pages[dcp::VerificationNote::Type::WARNING], _("Warnings"));
60
61         auto summary = new wxStaticText (this, wxID_ANY, wxT(""));
62         sizer->Add (summary, 0, wxALL, DCPOMATIC_DIALOG_BORDER);
63
64         auto save_report = new Button(this, _("Save report..."));
65         sizer->Add(save_report, 0, wxLEFT, DCPOMATIC_DIALOG_BORDER);
66         save_report->bind(&VerifyDCPDialog::save_report, this);
67
68         auto buttons = CreateStdDialogButtonSizer (0);
69         sizer->Add (CreateSeparatedSizer(buttons), wxSizerFlags().Expand().DoubleBorder());
70         buttons->SetAffirmativeButton (new wxButton (this, wxID_OK));
71         buttons->Realize ();
72
73         SetSizer (sizer);
74         sizer->Layout ();
75         sizer->SetSizeHints (this);
76
77         for (auto const& i: pages) {
78                 i.second->GetCaret()->Hide();
79         }
80
81         if (job->finished_ok() && _result.notes.empty()) {
82                 summary->SetLabel (_("DCP validates OK."));
83                 return;
84         }
85
86         map<dcp::VerificationNote::Type, int> counts;
87         counts[dcp::VerificationNote::Type::WARNING] = 0;
88         counts[dcp::VerificationNote::Type::BV21_ERROR] = 0;
89         counts[dcp::VerificationNote::Type::ERROR] = 0;
90
91         auto add_bullet = [&pages](dcp::VerificationNote::Type type, wxString message) {
92                 if (pages.find(type) != pages.end()) {
93                         pages[type]->BeginStandardBullet(N_("standard/diamond"), 1, 50);
94                         pages[type]->WriteText(message);
95                         pages[type]->Newline();
96                         pages[type]->EndStandardBullet();
97                 }
98         };
99
100         auto add = [&counts, &add_bullet](dcp::VerificationNote note, wxString message) {
101                 if (note.cpl_id()) {
102                         message.Replace("%cpl_id", std_to_wx(note.cpl_id().get()));
103                 }
104                 if (note.note()) {
105                         message.Replace("%n", std_to_wx(note.note().get()));
106                 }
107                 if (note.file()) {
108                         message.Replace("%f", std_to_wx(note.file()->filename().string()));
109                 }
110                 if (note.line()) {
111                         message.Replace("%l", std_to_wx(dcp::raw_convert<string>(note.line().get())));
112                 }
113                 if (note.frame()) {
114                         message.Replace("%frame", std_to_wx(dcp::raw_convert<string>(note.frame().get())));
115                 }
116                 if (note.component()) {
117                         message.Replace("%component", std_to_wx(dcp::raw_convert<string>(note.component().get())));
118                 }
119                 if (note.size()) {
120                         message.Replace("%size", std_to_wx(dcp::raw_convert<string>(note.size().get())));
121                 }
122                 if (note.id()) {
123                         message.Replace("%id", std_to_wx(note.id().get()));
124                 }
125                 if (note.other_id()) {
126                         message.Replace("%other_id", std_to_wx(note.other_id().get()));
127                 }
128                 add_bullet (note.type(), message);
129                 counts[note.type()]++;
130         };
131
132         if (job->finished_in_error() && job->error_summary() != "") {
133                 /* We have an error that did not come from dcp::verify */
134                 add_bullet (dcp::VerificationNote::Type::ERROR, std_to_wx(job->error_summary()));
135         }
136
137         for (auto i: job->result().notes) {
138                 switch (i.code()) {
139                 case dcp::VerificationNote::Code::FAILED_READ:
140                         add (i, std_to_wx(*i.note()));
141                         break;
142                 case dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES:
143                         add(i, _("The hash of the CPL %cpl_id in the PKL does not agree with the CPL file.  This probably means that the CPL file is corrupt."));
144                         break;
145                 case dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_RATE:
146                         add(i, _("The picture in a reel has a frame rate of %n, which is not valid."));
147                         break;
148                 case dcp::VerificationNote::Code::INCORRECT_PICTURE_HASH:
149                         add(i, _("The hash of the picture asset %f does not agree with the PKL file.  This probably means that the asset file is corrupt."));
150                         break;
151                 case dcp::VerificationNote::Code::MISMATCHED_PICTURE_HASHES:
152                         add(i, _("The PKL and CPL hashes disagree for picture asset %f."));
153                         break;
154                 case dcp::VerificationNote::Code::INCORRECT_SOUND_HASH:
155                         add(i, _("The hash of the sound asset %f does not agree with the PKL file.  This probably means that the asset file is corrupt."));
156                         break;
157                 case dcp::VerificationNote::Code::MISMATCHED_SOUND_HASHES:
158                         add(i, _("The PKL and CPL hashes disagree for sound asset %f."));
159                         break;
160                 case dcp::VerificationNote::Code::EMPTY_ASSET_PATH:
161                         add(i, _("An asset has an empty path in the ASSETMAP."));
162                         break;
163                 case dcp::VerificationNote::Code::MISSING_ASSET:
164                         add(i, _("The asset %f is missing."));
165                         break;
166                 case dcp::VerificationNote::Code::MISMATCHED_STANDARD:
167                         add(i, _("Parts of the DCP are written according to the Interop standard and parts according to SMPTE."));
168                         break;
169                 case dcp::VerificationNote::Code::INVALID_XML:
170                         if (i.line()) {
171                                 add(i, _("The XML in %f is malformed on line %l (%n)."));
172                         } else {
173                                 add(i, _("The XML in %f is malformed (%n)."));
174                         }
175                         break;
176                 case dcp::VerificationNote::Code::MISSING_ASSETMAP:
177                         add(i, _("No ASSETMAP or ASSETMAP.xml file was found."));
178                         break;
179                 case dcp::VerificationNote::Code::INVALID_INTRINSIC_DURATION:
180                         add(i, _("The asset %n has an intrinsic duration of less than 1 second, which is invalid."));
181                         break;
182                 case dcp::VerificationNote::Code::INVALID_DURATION:
183                         add(i, _("The asset %n has a duration of less than 1 second, which is invalid."));
184                         break;
185                 case dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
186                         add(i, _("At least one frame of the video asset %f is over the limit of 250Mbit/s."));
187                         break;
188                 case dcp::VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
189                         add(i, _("At least one frame of the video asset %f is close to the limit of 250MBit/s."));
190                         break;
191                 case dcp::VerificationNote::Code::EXTERNAL_ASSET:
192                         add(i, _("This DCP refers to at the asset %n in another DCP (and perhaps others), so it is a \"version file\" (VF)"));
193                         break;
194                 case dcp::VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD:
195                         add(i, _("The asset %f is 3D but its MXF is marked as 2D."));
196                         break;
197                 case dcp::VerificationNote::Code::INVALID_STANDARD:
198                         add(i, _("This DCP uses the Interop standard, but it should be made with SMPTE."));
199                         break;
200                 case dcp::VerificationNote::Code::INVALID_LANGUAGE:
201                         add(i, _("The invalid language tag %n is used."));
202                         break;
203                 case dcp::VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS:
204                         add(i, _("The video asset %f uses the invalid image size %n."));
205                         break;
206                 case dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K:
207                         add(i, _("The video asset %f uses the invalid frame rate %n."));
208                         break;
209                 case dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K:
210                         add(i, _("The video asset %f uses the frame rate %n which is invalid for 4K video."));
211                         break;
212                 case dcp::VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D:
213                         add(i, _("The video asset %f uses the frame rate %n which is invalid for 3D video."));
214                         break;
215                 case dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES:
216                         add(i, _("The XML in the closed caption asset %f takes up %n bytes which is over the 256KB limit."));
217                         break;
218                 case dcp::VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES:
219                         add(i, _("The timed text asset %f takes up %n bytes which is over the 115MB limit."));
220                         break;
221                 case dcp::VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES:
222                         add(i, _("The fonts in the timed text asset %f take up %n bytes which is over the 10MB limit."));
223                         break;
224                 case dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE:
225                         add(i, _("The subtitle asset %f contains no <Language> tag."));
226                         break;
227                 case dcp::VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES:
228                         add(i, _("Not all subtitle assets specify the same <Language> tag."));
229                         break;
230                 case dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME:
231                         add(i, _("The subtitle asset %f contains no <StartTime> tag."));
232                         break;
233                 case dcp::VerificationNote::Code::INVALID_SUBTITLE_START_TIME:
234                         add(i, _("The subtitle asset %f has a <StartTime> which is not zero."));
235                         break;
236                 case dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME:
237                         add(i, _("The first subtitle or closed caption happens before 4s into the first reel."));
238                         break;
239                 case dcp::VerificationNote::Code::INVALID_SUBTITLE_DURATION:
240                         add(i, _("At least one subtitle lasts less than 15 frames."));
241                         break;
242                 case dcp::VerificationNote::Code::INVALID_SUBTITLE_SPACING:
243                         add(i, _("At least one pair of subtitles is separated by less than 2 frames."));
244                         break;
245                 case dcp::VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT:
246                         add(i, _("There are more than 3 subtitle lines in at least one place."));
247                         break;
248                 case dcp::VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH:
249                         add(i, _("There are more than 52 characters in at least one subtitle line."));
250                         break;
251                 case dcp::VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH:
252                         add(i, _("There are more than 79 characters in at least one subtitle line."));
253                         break;
254                 case dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT:
255                         add(i, _("There are more than 3 closed caption lines in at least one place."));
256                         break;
257                 case dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH:
258                         add(i, _("There are more than 32 characters in at least one closed caption line."));
259                         break;
260                 case dcp::VerificationNote::Code::INVALID_SOUND_FRAME_RATE:
261                         add(i, _("The sound asset %f has an invalid frame rate of %n."));
262                         break;
263                 case dcp::VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT:
264                         add(i, _("The CPL %cpl_id has no <AnnotationText> tag."));
265                         break;
266                 case dcp::VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT:
267                         add(i, _("The CPL %cpl_id has an <AnnotationText> which is not the same as its <ContentTitleText>."));
268                         break;
269                 case dcp::VerificationNote::Code::MISMATCHED_ASSET_DURATION:
270                         add(i, _("At least one asset in a reel does not have the same duration as the others."));
271                         break;
272                 case dcp::VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS:
273                         add(i, _("The DCP has subtitles but at least one reel has no subtitle asset."));
274                         break;
275                 case dcp::VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS:
276                         add(i, _("The DCP has closed captions but not every reel has the same number of closed caption assets."));
277                         break;
278                 case dcp::VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT:
279                         add(i, _("The subtitle asset %n has no <EntryPoint> tag."));
280                         break;
281                 case dcp::VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT:
282                         add(i, _("Subtitle asset %n has a non-zero <EntryPoint>."));
283                         break;
284                 case dcp::VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT:
285                         add(i, _("The closed caption asset %n has no <EntryPoint> tag."));
286                         break;
287                 case dcp::VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT:
288                         add(i, _("Closed caption asset %n has a non-zero <EntryPoint>."));
289                         break;
290                 case dcp::VerificationNote::Code::MISSING_HASH:
291                         add(i, _("The asset %n has no <Hash> in the CPL."));
292                         break;
293                 case dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE:
294                         add(i, _("The DCP is a feature but has no FFEC (first frame of end credits) marker."));
295                         break;
296                 case dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE:
297                         add(i, _("The DCP is a feature but has no FFMC (first frame of moving credits) marker."));
298                         break;
299                 case dcp::VerificationNote::Code::MISSING_FFOC:
300                         add(i, _("The DCP has no FFOC (first frame of content) marker."));
301                         break;
302                 case dcp::VerificationNote::Code::MISSING_LFOC:
303                         add(i, _("The DCP has no LFOC (last frame of content) marker."));
304                         break;
305                 case dcp::VerificationNote::Code::INCORRECT_FFOC:
306                         add(i, _("The DCP has a FFOC of %n instead of 1."));
307                         break;
308                 case dcp::VerificationNote::Code::INCORRECT_LFOC:
309                         add(i, _("The DCP has a LFOC of %n instead of the reel duration minus one."));
310                         break;
311                 case dcp::VerificationNote::Code::MISSING_CPL_METADATA:
312                         add(i, _("The CPL %cpl_id has no CPL metadata tag."));
313                         break;
314                 case dcp::VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER:
315                         add(i, _("The CPL %cpl_id has no CPL metadata version number tag."));
316                         break;
317                 case dcp::VerificationNote::Code::MISSING_EXTENSION_METADATA:
318                         add(i, _("The CPL %cpl_id has no CPL extension metadata tag."));
319                         break;
320                 case dcp::VerificationNote::Code::INVALID_EXTENSION_METADATA:
321                         add(i, _("The CPL %f has an invalid CPL extension metadata tag (%n)"));
322                         break;
323                 case dcp::VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT:
324                         add(i, _("The CPL %cpl_id has encrypted content but is not signed."));
325                         break;
326                 case dcp::VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT:
327                         add(i, _("The PKL %n has encrypted content but is not signed."));
328                         break;
329                 case dcp::VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL:
330                         add(i, _("The PKL %n has an <AnnotationText> which does not match its CPL's <ContentTitleText>."));
331                         break;
332                 case dcp::VerificationNote::Code::PARTIALLY_ENCRYPTED:
333                         add(i, _("The DCP has encrypted content, but not all its assets are encrypted."));
334                         break;
335                 case dcp::VerificationNote::Code::INVALID_JPEG2000_CODESTREAM:
336                         add(i, _("A picture frame has an invalid JPEG2000 codestream (%n)"));
337                         break;
338                 case dcp::VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K:
339                         add(i, _("A 2K JPEG2000 frame has %n guard bits instead of 1."));
340                         break;
341                 case dcp::VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_4K:
342                         add(i, _("A 4K JPEG2000 frame has %n guard bits instead of 2."));
343                         break;
344                 case dcp::VerificationNote::Code::INVALID_JPEG2000_TILE_SIZE:
345                         add(i, _("A JPEG2000 tile size does not match the image size."));
346                         break;
347                 case dcp::VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_WIDTH:
348                         add(i, _("A JPEG2000 frame has a code-block width of %n instead of 32."));
349                         break;
350                 case dcp::VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_HEIGHT:
351                         add(i, _("A JPEG2000 frame has a code-block height of %n instead of 32."));
352                         break;
353                 case dcp::VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_2K:
354                         add(i, _("A 2K JPEG2000 frame has %n POC marker(s) instead of 0."));
355                         break;
356                 case dcp::VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_4K:
357                         add(i, _("A 4K JPEG2000 frame has %n POC marker(s) instead of 1."));
358                         break;
359                 case dcp::VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER:
360                         add(i, _("A JPEG2000 frame contains an invalid POC marker (%n)."));
361                         break;
362                 case dcp::VerificationNote::Code::INVALID_JPEG2000_POC_MARKER_LOCATION:
363                         add(i, _("A JPEG2000 frame contains POC marker in an invalid location."));
364                         break;
365                 case dcp::VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_2K:
366                         add(i, _("A 2K JPEG2000 frame contains %n tile parts instead of 3."));
367                         break;
368                 case dcp::VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_4K:
369                         add(i, _("A 4K JPEG2000 frame contains %n tile parts instead of 6."));
370                         break;
371                 case dcp::VerificationNote::Code::MISSING_JPEG200_TLM_MARKER:
372                         add(i, _("A JPEG2000 frame has no TLM marker."));
373                         break;
374                 case dcp::VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY:
375                         add(i, _("A subtitle lasts longer than the reel it is in."));
376                         break;
377                 case dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID:
378                         add(i, _("The Resource ID in a timed text MXF did not match the ID of the contained XML."));
379                         break;
380                 case dcp::VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID:
381                         add(i, _("The Asset ID in a timed text MXF is the same as the Resource ID or that of the contained XML."));
382                         break;
383                 case dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION:
384                 {
385                         vector<string> parts;
386                         boost::split (parts, i.note().get(), boost::is_any_of(" "));
387                         add(i, wxString::Format(_("The reel duration (%s) of some timed text is not the same as the ContainerDuration (%s) of its MXF."), std_to_wx(parts[0]), std_to_wx(parts[1])));
388                         break;
389                 }
390                 case dcp::VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED:
391                         add(i, _("Part of the DCP could not be checked because no KDM was available."));
392                         break;
393                 case dcp::VerificationNote::Code::EMPTY_TEXT:
394                         add(i, _("At least one <Text> node in a subtitle or closed caption is empty."));
395                         break;
396                 case dcp::VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN:
397                         add(i, _("Some closed <Text> or <Image> nodes have different vertical alignments within a <Subtitle>."));
398                         break;
399                 case dcp::VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING:
400                         add(i, _("Some closed captions are not listed in the order of their vertical position."));
401                         break;
402                 case dcp::VerificationNote::Code::UNEXPECTED_ENTRY_POINT:
403                         add(i, _("There is a <EntryPoint> tag inside a <MainMarkers>."));
404                         break;
405                 case dcp::VerificationNote::Code::UNEXPECTED_DURATION:
406                         add(i, _("There is a <Duration> tag inside a <MainMarkers>."));
407                         break;
408                 case dcp::VerificationNote::Code::INVALID_CONTENT_KIND:
409                         add(i, _("An invalid <ContentKind> %n has been used."));
410                         break;
411                 case dcp::VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA:
412                         add(i, _("The <MainPictureActiveArea> is either not a multiple of 2, or is bigger than an asset."));
413                         break;
414                 case dcp::VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL:
415                         add(i, _("The PKL %n has more than one asset with the same ID."));
416                         break;
417                 case dcp::VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP:
418                         add(i, _("The ASSETMAP %n has more than one asset with the same ID."));
419                         break;
420                 case dcp::VerificationNote::Code::MISSING_SUBTITLE:
421                         add(i, _("The subtitle asset %n contains no subtitles."));
422                         break;
423                 case dcp::VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE:
424                         add(i, _("<IssueDate> has an invalid value %n"));
425                         break;
426                 case dcp::VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS:
427                         add(i, _("Sound assets do not all have the same channel count."));
428                         break;
429                 case dcp::VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION:
430                         add(i, _("<MainSoundConfiguration> describes incorrect number of channels (%n)"));
431                         break;
432                 case dcp::VerificationNote::Code::MISSING_FONT:
433                         add(i, _("The font file for font ID \"%n\" was not found, or was not referred to in the ASSETMAP."));
434                         break;
435                 case dcp::VerificationNote::Code::INVALID_JPEG2000_TILE_PART_SIZE:
436                         add(i, _("Frame %frame has an image component that is too large (component %component is %size bytes in size)."));
437                         break;
438                 case dcp::VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT:
439                         add(i, _("The XML in the subtitle asset %n has more than one namespace declaration."));
440                         break;
441                 case dcp::VerificationNote::Code::MISSING_LOAD_FONT_FOR_FONT:
442                         add(i, _("A subtitle or closed caption refers to a font with ID %id that does not have a corresponding <LoadFont> node."));
443                         break;
444                 case dcp::VerificationNote::Code::MISSING_LOAD_FONT:
445                         add(i, _("The SMPTE subtitle asset %id has <Text> nodes but no <LoadFont> node"));
446                         break;
447                 case dcp::VerificationNote::Code::MISMATCHED_ASSET_MAP_ID:
448                         add(i, _("The asset with ID %id in the asset map actually has an id of %other_id"));
449                         break;
450                 case dcp::VerificationNote::Code::EMPTY_CONTENT_VERSION_LABEL_TEXT:
451                         add(i, _("The <LabelText> in a <ContentVersion> in CPL %cpl_id is empty"));
452                         break;
453                 case dcp::VerificationNote::Code::ALL_ENCRYPTED:
454                 case dcp::VerificationNote::Code::NONE_ENCRYPTED:
455                 case dcp::VerificationNote::Code::MATCHING_CPL_HASHES:
456                 case dcp::VerificationNote::Code::CORRECT_PICTURE_HASH:
457                 case dcp::VerificationNote::Code::VALID_PICTURE_FRAME_SIZES_IN_BYTES:
458                 case dcp::VerificationNote::Code::VALID_CONTENT_KIND:
459                 case dcp::VerificationNote::Code::VALID_RELEASE_TERRITORY:
460                 case dcp::VerificationNote::Code::VALID_CONTENT_VERSION_LABEL_TEXT:
461                 case dcp::VerificationNote::Code::VALID_CPL_ANNOTATION_TEXT:
462                 case dcp::VerificationNote::Code::MATCHING_PKL_ANNOTATION_TEXT_WITH_CPL:
463                 case dcp::VerificationNote::Code::VALID_MAIN_PICTURE_ACTIVE_AREA:
464                         /* We don't make our own messages for "OK" notes, i.e. things that are used in verification reports */
465                         break;
466                 }
467         }
468
469         wxString summary_text;
470
471         if (counts[dcp::VerificationNote::Type::ERROR] == 1) {
472                 /// TRANSLATORS: this will be used at the start of a string like "1 error, 2 Bv2.1 errors and 3 warnings."
473                 summary_text = _("1 error, ");
474         } else {
475                 /// TRANSLATORS: this will be used at the start of a string like "1 error, 2 Bv2.1 errors and 3 warnings."
476                 summary_text = wxString::Format("%d errors, ", counts[dcp::VerificationNote::Type::ERROR]);
477         }
478
479         if (counts[dcp::VerificationNote::Type::BV21_ERROR] == 1) {
480                 /// TRANSLATORS: this will be used in the middle of a string like "1 error, 2 Bv2.1 errors and 3 warnings."
481                 summary_text += _("1 Bv2.1 error, ");
482         } else {
483                 /// TRANSLATORS: this will be used in the middle of a string like "1 error, 2 Bv2.1 errors and 3 warnings."
484                 summary_text += wxString::Format("%d Bv2.1 errors, ", counts[dcp::VerificationNote::Type::BV21_ERROR]);
485         }
486
487         if (counts[dcp::VerificationNote::Type::WARNING] == 1) {
488                 /// TRANSLATORS: this will be used at the end of a string like "1 error, 2 Bv2.1 errors and 3 warnings."
489                 summary_text += _("and 1 warning.");
490         } else {
491                 /// TRANSLATORS: this will be used at the end of a string like "1 error, 2 Bv2.1 errors and 3 warnings."
492                 summary_text += wxString::Format("and %d warnings.", counts[dcp::VerificationNote::Type::WARNING]);
493         }
494
495         summary->SetLabel(summary_text);
496
497         if (counts[dcp::VerificationNote::Type::ERROR] == 0) {
498                 add_bullet (dcp::VerificationNote::Type::ERROR, _("No errors found."));
499         }
500
501         if (counts[dcp::VerificationNote::Type::BV21_ERROR] == 0) {
502                 add_bullet (dcp::VerificationNote::Type::BV21_ERROR, _("No SMPTE Bv2.1 errors found."));
503         }
504
505         if (counts[dcp::VerificationNote::Type::WARNING] == 0) {
506                 add_bullet (dcp::VerificationNote::Type::WARNING, _("No warnings found."));
507         }
508 }
509
510
511 void
512 VerifyDCPDialog::save_report()
513 {
514         auto d = new wxFileDialog(
515                 this, _("Select report file"), wxEmptyString, wxT("report.html"), wxT("HTML files (*.htm,*.html)|*.htm,*.html"),
516                 wxFD_SAVE | wxFD_OVERWRITE_PROMPT
517                 );
518
519         ScopeGuard sg = [d]() { d->Destroy(); };
520
521         if (d->ShowModal() == wxID_OK) {
522                 dcp::verify_report(_result, wx_to_std(d->GetPath()));
523         }
524 }
525