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