Merge remote-tracking branch 'origin/main' into v2.17.x
[dcpomatic.git] / src / wx / verify_dcp_result_panel.cc
1 /*
2     Copyright (C) 2018 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 "file_dialog.h"
24 #include "verify_dcp_result_panel.h"
25 #include "wx_util.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 VerifyDCPResultPanel::VerifyDCPResultPanel(wxWindow* parent)
46         : wxPanel(parent, wxID_ANY)
47 {
48         auto sizer = new wxBoxSizer(wxVERTICAL);
49         auto notebook = new wxNotebook(this, wxID_ANY);
50         sizer->Add(notebook, 1, wxEXPAND | wxALL, DCPOMATIC_DIALOG_BORDER);
51
52         _pages[dcp::VerificationNote::Type::ERROR] = new wxRichTextCtrl(notebook, wxID_ANY, wxEmptyString, wxDefaultPosition, {400, 300}, wxRE_READONLY);
53         notebook->AddPage(_pages[dcp::VerificationNote::Type::ERROR], _("Errors"));
54         _pages[dcp::VerificationNote::Type::BV21_ERROR] = new wxRichTextCtrl(notebook, wxID_ANY, wxEmptyString, wxDefaultPosition, {400, 300}, wxRE_READONLY);
55         notebook->AddPage(_pages[dcp::VerificationNote::Type::BV21_ERROR], _("SMPTE Bv2.1 errors"));
56         _pages[dcp::VerificationNote::Type::WARNING] = new wxRichTextCtrl(notebook, wxID_ANY, wxEmptyString, wxDefaultPosition, {400, 300}, wxRE_READONLY);
57         notebook->AddPage(_pages[dcp::VerificationNote::Type::WARNING], _("Warnings"));
58
59         _summary = new wxStaticText(this, wxID_ANY, wxT(""));
60         sizer->Add(_summary, 0, wxALL, DCPOMATIC_DIALOG_BORDER);
61
62         auto save_sizer = new wxBoxSizer(wxHORIZONTAL);
63         _save_text_report = new Button(this, _("Save report as text..."));
64         save_sizer->Add(_save_text_report, 0, wxALL, DCPOMATIC_SIZER_GAP);
65         _save_html_report = new Button(this, _("Save report as HTML..."));
66         save_sizer->Add(_save_html_report, 0, wxALL, DCPOMATIC_SIZER_GAP);
67         sizer->Add(save_sizer);
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         _save_text_report->bind(&VerifyDCPResultPanel::save_text_report, this);
78         _save_html_report->bind(&VerifyDCPResultPanel::save_html_report, this);
79
80         _save_text_report->Enable(false);
81         _save_html_report->Enable(false);
82 }
83
84
85 void
86 VerifyDCPResultPanel::fill(shared_ptr<VerifyDCPJob> job)
87 {
88         if (job->finished_ok() && job->result().notes.empty()) {
89                 _summary->SetLabel(_("DCP validates OK."));
90                 return;
91         }
92
93         map<dcp::VerificationNote::Type, int> counts;
94         counts[dcp::VerificationNote::Type::WARNING] = 0;
95         counts[dcp::VerificationNote::Type::BV21_ERROR] = 0;
96         counts[dcp::VerificationNote::Type::ERROR] = 0;
97
98         auto add_bullet = [this](dcp::VerificationNote::Type type, wxString message) {
99                 _pages[type]->BeginStandardBullet(N_("standard/diamond"), 1, 50);
100                 _pages[type]->WriteText(message);
101                 _pages[type]->Newline();
102                 _pages[type]->EndStandardBullet();
103         };
104
105         auto add = [&counts, &add_bullet](dcp::VerificationNote note, wxString message) {
106                 if (note.reference_hash()) {
107                         message.Replace("%reference_hash", std_to_wx(note.reference_hash().get()));
108                 }
109                 if (note.calculated_hash()) {
110                         message.Replace("%calculated_hash", std_to_wx(note.calculated_hash().get()));
111                 }
112                 if (note.frame()) {
113                         message.Replace("%frame", std_to_wx(dcp::raw_convert<string>(note.frame().get())));
114                         message.Replace(
115                                 "%timecode",
116                                 std_to_wx(
117                                         dcp::Time(note.frame().get(), note.frame_rate().get(), note.frame_rate().get()).as_string(dcp::Standard::SMPTE)
118                                         ));
119                 }
120                 if (note.note()) {
121                         message.Replace("%n", std_to_wx(note.note().get()));
122                 }
123                 if (note.file()) {
124                         message.Replace("%f", std_to_wx(note.file()->filename().string()));
125                 }
126                 if (note.line()) {
127                         message.Replace("%l", std_to_wx(dcp::raw_convert<string>(note.line().get())));
128                 }
129                 if (note.component()) {
130                         message.Replace("%component", std_to_wx(dcp::raw_convert<string>(note.component().get())));
131                 }
132                 if (note.size()) {
133                         message.Replace("%size", std_to_wx(dcp::raw_convert<string>(note.size().get())));
134                 }
135                 if (note.id()) {
136                         message.Replace("%id", std_to_wx(note.id().get()));
137                 }
138                 if (note.other_id()) {
139                         message.Replace("%other_id", std_to_wx(note.other_id().get()));
140                 }
141                 add_bullet(note.type(), message);
142                 counts[note.type()]++;
143         };
144
145         if (job->finished_in_error() && job->error_summary() != "") {
146                 /* We have an error that did not come from dcp::verify */
147                 add_bullet(dcp::VerificationNote::Type::ERROR, std_to_wx(job->error_summary()));
148                 ++counts[dcp::VerificationNote::Type::ERROR];
149         }
150
151         for (auto i: job->result().notes) {
152                 switch (i.code()) {
153                 case dcp::VerificationNote::Code::FAILED_READ:
154                         add(i, _("Could not read DCP (%n)"));
155                         break;
156                 case dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES:
157                         add(i, _("The hash (%reference_hash) of the CPL %n in the PKL does not agree with the CPL file (%calculated_hash).  This probably means that the CPL file is corrupt."));
158                         break;
159                 case dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_RATE:
160                         add(i, _("The picture in a reel has a frame rate of %n, which is not valid."));
161                         break;
162                 case dcp::VerificationNote::Code::INCORRECT_PICTURE_HASH:
163                         add(i, _("The hash (%calculated_hash) of the picture asset %f does not agree with the PKL file (%reference_hash).  This probably means that the asset file is corrupt."));
164                         break;
165                 case dcp::VerificationNote::Code::MISMATCHED_PICTURE_HASHES:
166                         add(i, _("The PKL and CPL hashes disagree for picture asset %f."));
167                         break;
168                 case dcp::VerificationNote::Code::INCORRECT_SOUND_HASH:
169                         add(i, _("The hash (%calculated_hash) of the sound asset %f does not agree with the PKL file (%reference_hash).  This probably means that the asset file is corrupt."));
170                         break;
171                 case dcp::VerificationNote::Code::MISMATCHED_SOUND_HASHES:
172                         add(i, _("The PKL and CPL hashes disagree for sound asset %f."));
173                         break;
174                 case dcp::VerificationNote::Code::EMPTY_ASSET_PATH:
175                         add(i, _("An asset has an empty path in the ASSETMAP."));
176                         break;
177                 case dcp::VerificationNote::Code::MISSING_ASSET:
178                         add(i, _("The asset %f is missing."));
179                         break;
180                 case dcp::VerificationNote::Code::MISMATCHED_STANDARD:
181                         add(i, _("Parts of the DCP are written according to the Interop standard and parts according to SMPTE."));
182                         break;
183                 case dcp::VerificationNote::Code::INVALID_XML:
184                         if (i.line()) {
185                                 add(i, _("The XML in %f is malformed on line %l (%n)."));
186                         } else {
187                                 add(i, _("The XML in %f is malformed (%n)."));
188                         }
189                         break;
190                 case dcp::VerificationNote::Code::MISSING_ASSETMAP:
191                         add(i, _("No ASSETMAP or ASSETMAP.xml file was found."));
192                         break;
193                 case dcp::VerificationNote::Code::INVALID_INTRINSIC_DURATION:
194                         add(i, _("The asset %n has an intrinsic duration of less than 1 second, which is invalid."));
195                         break;
196                 case dcp::VerificationNote::Code::INVALID_DURATION:
197                         add(i, _("The asset %n has a duration of less than 1 second, which is invalid."));
198                         break;
199                 case dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
200                         add(i, _("At least one frame of the video asset %f is over the limit of 250Mbit/s."));
201                         break;
202                 case dcp::VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
203                         add(i, _("Frame %frame (timecode %timecode) in asset %f has an instantaneous bit rate that is close to the limit of 250Mbit/s."));
204                         break;
205                 case dcp::VerificationNote::Code::EXTERNAL_ASSET:
206                         add(i, _("This DCP refers to at the asset %n in another DCP (and perhaps others), so it is a \"version file\" (VF)"));
207                         break;
208                 case dcp::VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD:
209                         add(i, _("The asset %f is 3D but its MXF is marked as 2D."));
210                         break;
211                 case dcp::VerificationNote::Code::INVALID_STANDARD:
212                         add(i, _("This DCP uses the Interop standard, but it should be made with SMPTE."));
213                         break;
214                 case dcp::VerificationNote::Code::INVALID_LANGUAGE:
215                         add(i, _("The invalid language tag %n is used."));
216                         break;
217                 case dcp::VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS:
218                         add(i, _("The video asset %f uses the invalid image size %n."));
219                         break;
220                 case dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K:
221                         add(i, _("The video asset %f uses the invalid frame rate %n."));
222                         break;
223                 case dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K:
224                         add(i, _("The video asset %f uses the frame rate %n which is invalid for 4K video."));
225                         break;
226                 case dcp::VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D:
227                         add(i, _("The video asset %f uses the frame rate %n which is invalid for 3D video."));
228                         break;
229                 case dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES:
230                         add(i, _("The XML in the closed caption asset %f takes up %n bytes which is over the 256KB limit."));
231                         break;
232                 case dcp::VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES:
233                         add(i, _("The timed text asset %f takes up %n bytes which is over the 115MB limit."));
234                         break;
235                 case dcp::VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES:
236                         add(i, _("The fonts in the timed text asset %f take up %n bytes which is over the 10MB limit."));
237                         break;
238                 case dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE:
239                         add(i, _("The subtitle asset %f contains no <Language> tag."));
240                         break;
241                 case dcp::VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES:
242                         add(i, _("Not all subtitle assets specify the same <Language> tag."));
243                         break;
244                 case dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME:
245                         add(i, _("The subtitle asset %f contains no <StartTime> tag."));
246                         break;
247                 case dcp::VerificationNote::Code::INVALID_SUBTITLE_START_TIME:
248                         add(i, _("The subtitle asset %f has a <StartTime> which is not zero."));
249                         break;
250                 case dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME:
251                         add(i, _("The first subtitle or closed caption happens before 4s into the first reel."));
252                         break;
253                 case dcp::VerificationNote::Code::INVALID_SUBTITLE_DURATION:
254                         add(i, _("At least one subtitle lasts less than 15 frames."));
255                         break;
256                 case dcp::VerificationNote::Code::INVALID_SUBTITLE_SPACING:
257                         add(i, _("At least one pair of subtitles is separated by less than 2 frames."));
258                         break;
259                 case dcp::VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT:
260                         add(i, _("There are more than 3 subtitle lines in at least one place."));
261                         break;
262                 case dcp::VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH:
263                         add(i, _("There are more than 52 characters in at least one subtitle line."));
264                         break;
265                 case dcp::VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH:
266                         add(i, _("There are more than 79 characters in at least one subtitle line."));
267                         break;
268                 case dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT:
269                         add(i, _("There are more than 3 closed caption lines in at least one place."));
270                         break;
271                 case dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH:
272                         add(i, _("There are more than 32 characters in at least one closed caption line."));
273                         break;
274                 case dcp::VerificationNote::Code::INVALID_SOUND_FRAME_RATE:
275                         add(i, _("The sound asset %f has an invalid frame rate of %n."));
276                         break;
277                 case dcp::VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT:
278                         add(i, _("The CPL %n has no <AnnotationText> tag."));
279                         break;
280                 case dcp::VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT:
281                         add(i, _("The CPL %n has an <AnnotationText> which is not the same as its <ContentTitleText>."));
282                         break;
283                 case dcp::VerificationNote::Code::MISMATCHED_ASSET_DURATION:
284                         add(i, _("At least one asset in a reel does not have the same duration as the others."));
285                         break;
286                 case dcp::VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS:
287                         add(i, _("The DCP has subtitles but at least one reel has no subtitle asset."));
288                         break;
289                 case dcp::VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS:
290                         add(i, _("The DCP has closed captions but not every reel has the same number of closed caption assets."));
291                         break;
292                 case dcp::VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT:
293                         add(i, _("The subtitle asset %n has no <EntryPoint> tag."));
294                         break;
295                 case dcp::VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT:
296                         add(i, _("Subtitle asset %n has a non-zero <EntryPoint>."));
297                         break;
298                 case dcp::VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT:
299                         add(i, _("The closed caption asset %n has no <EntryPoint> tag."));
300                         break;
301                 case dcp::VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT:
302                         add(i, _("Closed caption asset %n has a non-zero <EntryPoint>."));
303                         break;
304                 case dcp::VerificationNote::Code::MISSING_HASH:
305                         add(i, _("The asset %n has no <Hash> in the CPL."));
306                         break;
307                 case dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE:
308                         add(i, _("The DCP is a feature but has no FFEC (first frame of end credits) marker."));
309                         break;
310                 case dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE:
311                         add(i, _("The DCP is a feature but has no FFMC (first frame of moving credits) marker."));
312                         break;
313                 case dcp::VerificationNote::Code::MISSING_FFOC:
314                         add(i, _("The DCP has no FFOC (first frame of content) marker."));
315                         break;
316                 case dcp::VerificationNote::Code::MISSING_LFOC:
317                         add(i, _("The DCP has no LFOC (last frame of content) marker."));
318                         break;
319                 case dcp::VerificationNote::Code::INCORRECT_FFOC:
320                         add(i, _("The DCP has a FFOC of %n instead of 1."));
321                         break;
322                 case dcp::VerificationNote::Code::INCORRECT_LFOC:
323                         add(i, _("The DCP has a LFOC of %n instead of the reel duration minus one."));
324                         break;
325                 case dcp::VerificationNote::Code::MISSING_CPL_METADATA:
326                         add(i, _("The CPL %n has no CPL metadata tag."));
327                         break;
328                 case dcp::VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER:
329                         add(i, _("The CPL %n has no CPL metadata version number tag."));
330                         break;
331                 case dcp::VerificationNote::Code::MISSING_EXTENSION_METADATA:
332                         add(i, _("The CPL %n has no CPL extension metadata tag."));
333                         break;
334                 case dcp::VerificationNote::Code::INVALID_EXTENSION_METADATA:
335                         add(i, _("The CPL %f has an invalid CPL extension metadata tag (%n)"));
336                         break;
337                 case dcp::VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT:
338                         add(i, _("The CPL %n has encrypted content but is not signed."));
339                         break;
340                 case dcp::VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT:
341                         add(i, _("The PKL %n has encrypted content but is not signed."));
342                         break;
343                 case dcp::VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL:
344                         add(i, _("The PKL %n has an <AnnotationText> which does not match its CPL's <ContentTitleText>."));
345                         break;
346                 case dcp::VerificationNote::Code::PARTIALLY_ENCRYPTED:
347                         add(i, _("The DCP has encrypted content, but not all its assets are encrypted."));
348                         break;
349                 case dcp::VerificationNote::Code::INVALID_JPEG2000_CODESTREAM:
350                         add(i, _("A picture frame has an invalid JPEG2000 codestream (%n)"));
351                         break;
352                 case dcp::VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K:
353                         add(i, _("A 2K JPEG2000 frame has %n guard bits instead of 1."));
354                         break;
355                 case dcp::VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_4K:
356                         add(i, _("A 4K JPEG2000 frame has %n guard bits instead of 2."));
357                         break;
358                 case dcp::VerificationNote::Code::INVALID_JPEG2000_TILE_SIZE:
359                         add(i, _("A JPEG2000 tile size does not match the image size."));
360                         break;
361                 case dcp::VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_WIDTH:
362                         add(i, _("A JPEG2000 frame has a code-block width of %n instead of 32."));
363                         break;
364                 case dcp::VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_HEIGHT:
365                         add(i, _("A JPEG2000 frame has a code-block height of %n instead of 32."));
366                         break;
367                 case dcp::VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_2K:
368                         add(i, _("A 2K JPEG2000 frame has %n POC marker(s) instead of 0."));
369                         break;
370                 case dcp::VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_4K:
371                         add(i, _("A 4K JPEG2000 frame has %n POC marker(s) instead of 1."));
372                         break;
373                 case dcp::VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER:
374                         add(i, _("A JPEG2000 frame contains an invalid POC marker (%n)."));
375                         break;
376                 case dcp::VerificationNote::Code::INVALID_JPEG2000_POC_MARKER_LOCATION:
377                         add(i, _("A JPEG2000 frame contains POC marker in an invalid location."));
378                         break;
379                 case dcp::VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_2K:
380                         add(i, _("A 2K JPEG2000 frame contains %n tile parts instead of 3."));
381                         break;
382                 case dcp::VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_4K:
383                         add(i, _("A 4K JPEG2000 frame contains %n tile parts instead of 6."));
384                         break;
385                 case dcp::VerificationNote::Code::MISSING_JPEG200_TLM_MARKER:
386                         add(i, _("A JPEG2000 frame has no TLM marker."));
387                         break;
388                 case dcp::VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY:
389                         add(i, _("A subtitle lasts longer than the reel it is in."));
390                         break;
391                 case dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID:
392                         add(i, _("The Resource ID in a timed text MXF did not match the ID of the contained XML."));
393                         break;
394                 case dcp::VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID:
395                         add(i, _("The Asset ID in a timed text MXF is the same as the Resource ID or that of the contained XML."));
396                         break;
397                 case dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION:
398                 {
399                         vector<string> parts;
400                         boost::split(parts, i.note().get(), boost::is_any_of(" "));
401                         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])));
402                         break;
403                 }
404                 case dcp::VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED:
405                         add(i, _("Part of the DCP could not be checked because no KDM was available."));
406                         break;
407                 case dcp::VerificationNote::Code::EMPTY_TEXT:
408                         add(i, _("At least one <Text> node in a subtitle or closed caption is empty."));
409                         break;
410                 case dcp::VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN:
411                         add(i, _("Some closed <Text> or <Image> nodes have different vertical alignments within a <Subtitle>."));
412                         break;
413                 case dcp::VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING:
414                         add(i, _("Some closed captions are not listed in the order of their vertical position."));
415                         break;
416                 case dcp::VerificationNote::Code::UNEXPECTED_ENTRY_POINT:
417                         add(i, _("There is a <EntryPoint> tag inside a <MainMarkers>."));
418                         break;
419                 case dcp::VerificationNote::Code::UNEXPECTED_DURATION:
420                         add(i, _("There is a <Duration> tag inside a <MainMarkers>."));
421                         break;
422                 case dcp::VerificationNote::Code::INVALID_CONTENT_KIND:
423                         add(i, _("An invalid <ContentKind> %n has been used."));
424                         break;
425                 case dcp::VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA:
426                         add(i, _("The <MainPictureActiveArea> is either not a multiple of 2, or is bigger than an asset."));
427                         break;
428                 case dcp::VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL:
429                         add(i, _("The PKL %n has more than one asset with the same ID."));
430                         break;
431                 case dcp::VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP:
432                         add(i, _("The ASSETMAP %n has more than one asset with the same ID."));
433                         break;
434                 case dcp::VerificationNote::Code::MISSING_SUBTITLE:
435                         add(i, _("The subtitle asset %n contains no subtitles."));
436                         break;
437                 case dcp::VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE:
438                         add(i, _("<IssueDate> has an invalid value %n"));
439                         break;
440                 case dcp::VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS:
441                         add(i, _("Sound assets do not all have the same channel count."));
442                         break;
443                 case dcp::VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION:
444                         add(i, _("<MainSoundConfiguration> describes incorrect number of channels (%n)"));
445                         break;
446                 case dcp::VerificationNote::Code::MISSING_FONT:
447                         add(i, _("The font file for font ID \"%n\" was not found, or was not referred to in the ASSETMAP."));
448                         break;
449                 case dcp::VerificationNote::Code::INVALID_JPEG2000_TILE_PART_SIZE:
450                         add(i, _("Frame %frame has an image component that is too large (component %component is %size bytes in size)."));
451                         break;
452                 case dcp::VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT:
453                         add(i, _("The XML in the subtitle asset %n has more than one namespace declaration."));
454                         break;
455                 case dcp::VerificationNote::Code::MISSING_LOAD_FONT_FOR_FONT:
456                         add(i, _("A subtitle or closed caption refers to a font with ID %id that does not have a corresponding <LoadFont> node."));
457                         break;
458                 case dcp::VerificationNote::Code::MISSING_LOAD_FONT:
459                         add(i, _("The SMPTE subtitle asset %id has <Text> nodes but no <LoadFont> node"));
460                         break;
461                 case dcp::VerificationNote::Code::MISMATCHED_ASSET_MAP_ID:
462                         add(i, _("The asset with ID %id in the asset map actually has an id of %other_id"));
463                         break;
464                 case dcp::VerificationNote::Code::EMPTY_CONTENT_VERSION_LABEL_TEXT:
465                         add(i, _("The <LabelText> in a <ContentVersion> in CPL %id is empty"));
466                         break;
467                 case dcp::VerificationNote::Code::INVALID_CPL_NAMESPACE:
468                         add(i, _("The CPL %id has an invalid namespace %n"));
469                         break;
470                 case dcp::VerificationNote::Code::MISSING_CPL_CONTENT_VERSION:
471                         add(i, _("The CPL %id has no <ContentVersion> tag"));
472                         break;
473                 case dcp::VerificationNote::Code::MATCHING_CPL_HASHES:
474                 case dcp::VerificationNote::Code::CORRECT_PICTURE_HASH:
475                 case dcp::VerificationNote::Code::VALID_PICTURE_FRAME_SIZES_IN_BYTES:
476                 case dcp::VerificationNote::Code::VALID_RELEASE_TERRITORY:
477                 case dcp::VerificationNote::Code::VALID_CPL_ANNOTATION_TEXT:
478                 case dcp::VerificationNote::Code::MATCHING_PKL_ANNOTATION_TEXT_WITH_CPL:
479                 case dcp::VerificationNote::Code::ALL_ENCRYPTED:
480                 case dcp::VerificationNote::Code::NONE_ENCRYPTED:
481                 case dcp::VerificationNote::Code::VALID_CONTENT_KIND:
482                 case dcp::VerificationNote::Code::VALID_MAIN_PICTURE_ACTIVE_AREA:
483                 case dcp::VerificationNote::Code::VALID_CONTENT_VERSION_LABEL_TEXT:
484                         /* These are all "OK" messages which we don't report here */
485                         break;
486                 }
487         }
488
489         wxString summary_text;
490
491         if (counts[dcp::VerificationNote::Type::ERROR] == 1) {
492                 /// TRANSLATORS: this will be used at the start of a string like "1 error, 2 Bv2.1 errors and 3 warnings."
493                 summary_text = _("1 error, ");
494         } else {
495                 /// TRANSLATORS: this will be used at the start of a string like "1 error, 2 Bv2.1 errors and 3 warnings."
496                 summary_text = wxString::Format("%d errors, ", counts[dcp::VerificationNote::Type::ERROR]);
497         }
498
499         if (counts[dcp::VerificationNote::Type::BV21_ERROR] == 1) {
500                 /// TRANSLATORS: this will be used in the middle of a string like "1 error, 2 Bv2.1 errors and 3 warnings."
501                 summary_text += _("1 Bv2.1 error, ");
502         } else {
503                 /// TRANSLATORS: this will be used in the middle of a string like "1 error, 2 Bv2.1 errors and 3 warnings."
504                 summary_text += wxString::Format("%d Bv2.1 errors, ", counts[dcp::VerificationNote::Type::BV21_ERROR]);
505         }
506
507         if (counts[dcp::VerificationNote::Type::WARNING] == 1) {
508                 /// TRANSLATORS: this will be used at the end of a string like "1 error, 2 Bv2.1 errors and 3 warnings."
509                 summary_text += _("and 1 warning.");
510         } else {
511                 /// TRANSLATORS: this will be used at the end of a string like "1 error, 2 Bv2.1 errors and 3 warnings."
512                 summary_text += wxString::Format("and %d warnings.", counts[dcp::VerificationNote::Type::WARNING]);
513         }
514
515         _summary->SetLabel(summary_text);
516
517         if (counts[dcp::VerificationNote::Type::ERROR] == 0) {
518                 add_bullet(dcp::VerificationNote::Type::ERROR, _("No errors found."));
519         }
520
521         if (counts[dcp::VerificationNote::Type::BV21_ERROR] == 0) {
522                 add_bullet(dcp::VerificationNote::Type::BV21_ERROR, _("No SMPTE Bv2.1 errors found."));
523         }
524
525         if (counts[dcp::VerificationNote::Type::WARNING] == 0) {
526                 add_bullet(dcp::VerificationNote::Type::WARNING, _("No warnings found."));
527         }
528
529         _job = job;
530         _save_text_report->Enable(true);
531         _save_html_report->Enable(true);
532 }
533
534
535 template <class T>
536 void save(wxWindow* parent, wxString filter, dcp::VerificationResult const& result)
537 {
538         FileDialog dialog(parent, _("Verification report"), filter, wxFD_SAVE | wxFD_OVERWRITE_PROMPT, "SaveVerificationReport");
539         if (!dialog.show()) {
540                 return;
541         }
542
543         T formatter(dialog.path());
544         dcp::verify_report(result, formatter);
545 }
546
547
548 void
549 VerifyDCPResultPanel::save_text_report()
550 {
551         if (_job) {
552                 save<dcp::TextFormatter>(this, wxT("Text files (*.txt)|*.txt"), _job->result());
553         }
554 }
555
556
557 void
558 VerifyDCPResultPanel::save_html_report()
559 {
560         if (_job) {
561                 save<dcp::HTMLFormatter>(this, wxT("HTML files (*.htm;*html)|*.htm;*.html"), _job->result());
562         }
563 }