xref: /aosp_15_r20/external/harfbuzz_ng/generate_notice.py (revision 2d1272b857b1f7575e6e246373e1cb218663db8a)
1#!/usr/bin/env python3
2
3from enum import Enum
4from pathlib import Path
5from typing import Sequence
6from typing import Tuple
7from fontTools import ttLib
8import tempfile
9import subprocess
10import json
11import argparse
12import contextlib
13import os
14import re
15import sys
16
17# list of specific files to be ignored.
18IGNORE_FILE_NAME = [
19  # Exclude myself
20  "generate_notice.py",
21
22  # License files
23  "LICENSE",
24  "LICENSE_APACHE2.TXT",
25  "LICENSE_FSFAP.TXT",
26  "LICENSE_GPLv2.TXT",
27  "LICENSE_GPLv2_WITH_AUTOCONF_EXCEPTION.TXT",
28  "LICENSE_GPLv3_WITH_AUTOCONF_EXCEPTION.TXT",
29  "LICENSE_HPND_SELL_VARIANT.TXT",
30  "LICENSE_ISC.TXT",
31  "LICENSE_MIT_MODERN_VARIANT.TXT",
32  "LICENSE_OFL.TXT",
33  "METADATA",
34  "MODULE_LICENSE_MIT",
35  "NOTICE",
36
37  # dictionary which has Copyright word
38  "perf/texts/en-words.txt",
39
40  # broken unreadable font file for fuzzing target
41  "test/fuzzing/fonts/sbix-extents.ttf",
42
43  # ???
44  "xkcd.png",
45]
46
47IGNORE_DIR_IF_NO_COPYRIGHT = [
48    "test",
49    "perf",
50]
51
52NO_COPYRIGHT_FILES = [
53  ".ci/build-win32.sh",
54  ".ci/build-win64.sh",
55  ".ci/deploy-docs.sh",
56  ".ci/publish_release_artifact.sh",
57  ".ci/requirements-fonttools.in",
58  ".ci/requirements-fonttools.txt",
59  ".ci/requirements.in",
60  ".ci/requirements.txt",
61  ".ci/win32-cross-file.txt",
62  ".ci/win64-cross-file.txt",
63  ".circleci/config.yml",
64  ".clang-format",
65  ".codecov.yml",
66  ".editorconfig",
67  ".github/dependabot.yml",
68  ".github/workflows/arm-ci.yml",
69  ".github/workflows/cifuzz.yml",
70  ".github/workflows/configs-build.yml",
71  ".github/workflows/coverity-scan.yml",
72  ".github/workflows/linux-ci.yml",
73  ".github/workflows/macos-ci.yml",
74  ".github/workflows/msvc-ci.yml",
75  ".github/workflows/msys2-ci.yml",
76  ".github/workflows/scorecard.yml",
77  "AUTHORS",
78  "BUILD.md",
79  "CMakeLists.txt",
80  "CONFIG.md",
81  "NEWS",
82  "OWNERS",
83  "README.android",
84  "README.md",
85  "README.python.md",
86  "RELEASING.md",
87  "SECURITY.md",
88  "TESTING.md",
89  "TEST_MAPPING",
90  "THANKS",
91  "docs/HarfBuzz.png",
92  "docs/HarfBuzz.svg",
93  "docs/features.dot",
94  "docs/harfbuzz-docs.xml",
95  "docs/harfbuzz-overrides.txt",
96  "docs/harfbuzz-sections.txt",
97  "docs/meson.build",
98  "docs/repacker.md",
99  "docs/serializer.md",
100  "docs/subset-preprocessing.md",
101  "docs/usermanual-buffers-language-script-and-direction.xml",
102  "docs/usermanual-clusters.xml",
103  "docs/usermanual-fonts-and-faces.xml",
104  "docs/usermanual-getting-started.xml",
105  "docs/usermanual-glyph-information.xml",
106  "docs/usermanual-install-harfbuzz.xml",
107  "docs/usermanual-integration.xml",
108  "docs/usermanual-object-model.xml",
109  "docs/usermanual-opentype-features.xml",
110  "docs/usermanual-shaping-concepts.xml",
111  "docs/usermanual-utilities.xml",
112  "docs/usermanual-what-is-harfbuzz.xml",
113  "docs/version.xml.in",
114  "docs/wasm-shaper.md",
115  "harfbuzz.doap",
116  "meson.build",
117  "meson_options.txt",
118  "replace-enum-strings.cmake",
119  "src/ArabicPUASimplified.txt",
120  "src/ArabicPUATraditional.txt",
121  "src/OT/Layout/GPOS/Anchor.hh",
122  "src/OT/Layout/GPOS/AnchorFormat1.hh",
123  "src/OT/Layout/GPOS/AnchorFormat2.hh",
124  "src/OT/Layout/GPOS/AnchorFormat3.hh",
125  "src/OT/Layout/GPOS/AnchorMatrix.hh",
126  "src/OT/Layout/GPOS/ChainContextPos.hh",
127  "src/OT/Layout/GPOS/Common.hh",
128  "src/OT/Layout/GPOS/ContextPos.hh",
129  "src/OT/Layout/GPOS/CursivePos.hh",
130  "src/OT/Layout/GPOS/CursivePosFormat1.hh",
131  "src/OT/Layout/GPOS/ExtensionPos.hh",
132  "src/OT/Layout/GPOS/GPOS.hh",
133  "src/OT/Layout/GPOS/LigatureArray.hh",
134  "src/OT/Layout/GPOS/MarkArray.hh",
135  "src/OT/Layout/GPOS/MarkBasePos.hh",
136  "src/OT/Layout/GPOS/MarkBasePosFormat1.hh",
137  "src/OT/Layout/GPOS/MarkLigPos.hh",
138  "src/OT/Layout/GPOS/MarkLigPosFormat1.hh",
139  "src/OT/Layout/GPOS/MarkMarkPos.hh",
140  "src/OT/Layout/GPOS/MarkMarkPosFormat1.hh",
141  "src/OT/Layout/GPOS/MarkRecord.hh",
142  "src/OT/Layout/GPOS/PairPos.hh",
143  "src/OT/Layout/GPOS/PairPosFormat1.hh",
144  "src/OT/Layout/GPOS/PairPosFormat2.hh",
145  "src/OT/Layout/GPOS/PairSet.hh",
146  "src/OT/Layout/GPOS/PairValueRecord.hh",
147  "src/OT/Layout/GPOS/PosLookup.hh",
148  "src/OT/Layout/GPOS/PosLookupSubTable.hh",
149  "src/OT/Layout/GPOS/SinglePos.hh",
150  "src/OT/Layout/GPOS/SinglePosFormat1.hh",
151  "src/OT/Layout/GPOS/SinglePosFormat2.hh",
152  "src/OT/Layout/GPOS/ValueFormat.hh",
153  "src/OT/Layout/GSUB/AlternateSet.hh",
154  "src/OT/Layout/GSUB/AlternateSubst.hh",
155  "src/OT/Layout/GSUB/AlternateSubstFormat1.hh",
156  "src/OT/Layout/GSUB/ChainContextSubst.hh",
157  "src/OT/Layout/GSUB/Common.hh",
158  "src/OT/Layout/GSUB/ContextSubst.hh",
159  "src/OT/Layout/GSUB/ExtensionSubst.hh",
160  "src/OT/Layout/GSUB/GSUB.hh",
161  "src/OT/Layout/GSUB/Ligature.hh",
162  "src/OT/Layout/GSUB/LigatureSet.hh",
163  "src/OT/Layout/GSUB/LigatureSubst.hh",
164  "src/OT/Layout/GSUB/LigatureSubstFormat1.hh",
165  "src/OT/Layout/GSUB/MultipleSubst.hh",
166  "src/OT/Layout/GSUB/MultipleSubstFormat1.hh",
167  "src/OT/Layout/GSUB/ReverseChainSingleSubst.hh",
168  "src/OT/Layout/GSUB/ReverseChainSingleSubstFormat1.hh",
169  "src/OT/Layout/GSUB/Sequence.hh",
170  "src/OT/Layout/GSUB/SingleSubst.hh",
171  "src/OT/Layout/GSUB/SingleSubstFormat1.hh",
172  "src/OT/Layout/GSUB/SingleSubstFormat2.hh",
173  "src/OT/Layout/GSUB/SubstLookup.hh",
174  "src/OT/Layout/GSUB/SubstLookupSubTable.hh",
175  "src/OT/Var/VARC/VARC.cc",
176  "src/OT/Var/VARC/VARC.hh",
177  "src/OT/Var/VARC/coord-setter.hh",
178  "src/OT/glyf/CompositeGlyph.hh",
179  "src/OT/glyf/Glyph.hh",
180  "src/OT/glyf/GlyphHeader.hh",
181  "src/OT/glyf/SimpleGlyph.hh",
182  "src/OT/glyf/SubsetGlyph.hh",
183  "src/OT/glyf/composite-iter.hh",
184  "src/OT/glyf/glyf-helpers.hh",
185  "src/OT/glyf/glyf.hh",
186  "src/OT/glyf/loca.hh",
187  "src/OT/glyf/path-builder.hh",
188  "src/addTable.py",
189  "src/check-c-linkage-decls.py",
190  "src/check-externs.py",
191  "src/check-header-guards.py",
192  "src/check-includes.py",
193  "src/check-libstdc++.py",
194  "src/check-static-inits.py",
195  "src/check-symbols.py",
196  "src/fix_get_types.py",
197  "src/gen-arabic-joining-list.py",
198  "src/gen-arabic-pua.py",
199  "src/gen-arabic-table.py",
200  "src/gen-def.py",
201  "src/gen-emoji-table.py",
202  "src/gen-harfbuzzcc.py",
203  "src/gen-hb-version.py",
204  "src/gen-indic-table.py",
205  "src/gen-os2-unicode-ranges.py",
206  "src/gen-ragel-artifacts.py",
207  "src/gen-tag-table.py",
208  "src/gen-ucd-table.py",
209  "src/gen-use-table.py",
210  "src/gen-vowel-constraints.py",
211  "src/harfbuzz-cairo.pc.in",
212  "src/harfbuzz-config.cmake.in",
213  "src/harfbuzz-gobject.pc.in",
214  "src/harfbuzz-icu.pc.in",
215  "src/harfbuzz-subset.cc",
216  "src/harfbuzz-subset.pc.in",
217  "src/harfbuzz.cc",
218  "src/harfbuzz.pc.in",
219  "src/hb-ot-shaper-arabic-joining-list.hh",
220  "src/hb-ot-shaper-arabic-pua.hh",
221  "src/hb-ot-shaper-arabic-table.hh",
222  "src/hb-ot-shaper-indic-table.cc",
223  "src/hb-ot-shaper-use-table.hh",
224  "src/hb-ot-shaper-vowel-constraints.cc",
225  "src/hb-ot-tag-table.hh",
226  "src/hb-ucd-table.hh",
227  "src/hb-unicode-emoji-table.hh",
228  "src/justify.py",
229  "src/meson.build",
230  "src/ms-use/IndicPositionalCategory-Additional.txt",
231  "src/ms-use/IndicShapingInvalidCluster.txt",
232  "src/ms-use/IndicSyllabicCategory-Additional.txt",
233  "src/relative_to.py",
234  "src/sample.py",
235  "src/test-use-table.cc",
236  "src/update-unicode-tables.make",
237  "src/wasm/graphite/Makefile",
238  "src/wasm/graphite/shape.cc",
239  "src/wasm/rust/harfbuzz-wasm/Cargo.toml",
240  "src/wasm/rust/harfbuzz-wasm/src/lib.rs",
241  "src/wasm/sample/c/Makefile",
242  "src/wasm/sample/c/shape-fallback.cc",
243  "src/wasm/sample/c/shape-ot.cc",
244  "src/wasm/sample/rust/hello-wasm/Cargo.toml",
245  "src/wasm/sample/rust/hello-wasm/src/lib.rs",
246  "subprojects/.gitignore",
247  "subprojects/cairo.wrap",
248  "subprojects/freetype2.wrap",
249  "subprojects/glib.wrap",
250  "subprojects/google-benchmark.wrap",
251  "subprojects/packagefiles/ragel/meson.build",
252  "subprojects/ragel.wrap",
253  "util/meson.build",
254  "util/test-hb-subset-parsing.c",
255]
256
257class CommentType(Enum):
258  C_STYLE_BLOCK = 1  # /* ... */
259  C_STYLE_BLOCK_AS_LINE = 2  # /* ... */ but uses multiple lines of block comments.
260  C_STYLE_LINE = 3 # // ...
261  SCRIPT_STYLE_HASH = 4 #  # ...
262  OPENTYPE_NAME = 5
263  OPENTYPE_COLLECTION_NAME = 6
264  UNKNOWN = 10000
265
266
267# Helper function of showing error message and immediate exit.
268def fatal(msg: str):
269  sys.stderr.write(str(msg))
270  sys.stderr.write("\n")
271  sys.exit(1)
272
273
274def warn(msg: str):
275  sys.stderr.write(str(msg))
276  sys.stderr.write("\n")
277
278def debug(msg: str):
279  # sys.stderr.write(str(msg))
280  # sys.stderr.write("\n")
281  pass
282
283
284def cleanup_and_join(out_lines: Sequence[str]):
285  while not out_lines[-1].strip():
286    out_lines.pop(-1)
287
288  # If all lines starts from empty space, strip it out.
289  while all([len(x) == 0 or x[0] == ' ' for x in out_lines]):
290    out_lines = [x[1:] for x in out_lines]
291
292  if not out_lines:
293    fatal("Failed to get copyright info")
294  return "\n".join(out_lines)
295
296
297def get_comment_type(copyright_line: str, path_str: str) -> CommentType:
298  # vms_make.com contains multiple copyright header as a string constants.
299  if copyright_line.startswith("#"):
300    return CommentType.SCRIPT_STYLE_HASH
301  if copyright_line.startswith("//"):
302    return CommentType.C_STYLE_LINE
303  return CommentType.C_STYLE_BLOCK
304
305def extract_copyright_font(path_str: str) -> str:
306  path = Path(path_str)
307  if path.suffix in ['.ttf', '.otf', '.dfont']:
308    return extract_from_opentype_name(path, 0)
309  elif path.suffix in ['.ttc', '.otc']:
310    return extract_from_opentype_collection_name(path)
311
312
313# Extract copyright notice and returns next index.
314def extract_copyright_at(lines: Sequence[str], i: int, path: str) -> Tuple[str, int]:
315  commentType = get_comment_type(lines[i], path)
316
317  if commentType == CommentType.C_STYLE_BLOCK:
318    return extract_from_c_style_block_at(lines, i, path)
319  elif commentType == CommentType.C_STYLE_LINE:
320    return extract_from_c_style_lines_at(lines, i, path)
321  elif commentType == CommentType.SCRIPT_STYLE_HASH:
322    return extract_from_script_hash_at(lines, i, path)
323  else:
324    fatal("Uknown comment style: %s" % lines[i])
325
326def extract_from_opentype_collection_name(path: str) -> str:
327
328  with open(path, mode="rb") as f:
329    head = f.read(12)
330
331  if head[0:4].decode() != 'ttcf':
332    fatal('Invalid magic number for TTC file: %s' % path)
333  numFonts = int.from_bytes(head[8:12], byteorder="big")
334
335  licenses = set()
336  for i in range(0, numFonts):
337    license = extract_from_opentype_name(path, i)
338    licenses.add(license)
339
340  return '\n\n'.join(licenses)
341
342def extract_from_opentype_name(path: str, index: int) -> str:
343
344  def get_preferred_name(nameID: int, ttf):
345    def get_score(platID: int, encID: int):
346      if platID == 3 and encID == 10:
347        return 0
348      elif platID == 0 and encID == 6:
349        return 1
350      elif platID == 0 and encID == 4:
351        return 2
352      elif platID == 3 and encID == 1:
353        return 3
354      elif platID == 0 and encID == 3:
355        return 4
356      elif platID == 0 and encID == 2:
357        return 5
358      elif platID == 0 and encID == 1:
359        return 6
360      elif platID == 0 and encID == 0:
361        return 7
362      else:
363        return 10000
364
365    best_score = 1000000
366    best_name = None
367
368    if 'name' not in ttf:
369      return None
370
371    for name in ttf['name'].names:
372      if name.nameID != nameID:
373        continue
374
375      score = get_score(name.platformID, name.platEncID)
376      if score < best_score:
377        best_score = score
378        best_name = name
379
380    return best_name
381
382  def get_notice_from_cff(ttf):
383    if 'CFF ' not in ttf:
384      return None
385
386    # Looks like there is no way of getting Notice line in CFF table.
387    # Use the line that has "Copyright" in the string pool.
388    cff = ttf['CFF '].cff
389    for string in cff.strings:
390      if 'Copyright' in string:
391        return string
392    return None
393
394  with contextlib.closing(ttLib.TTFont(path, 0, fontNumber=index)) as ttf:
395    copyright = get_preferred_name(0, ttf)
396    if not copyright:
397      copyright = get_notice_from_cff(ttf)
398    if not copyright:
399      return None
400
401    license_description = get_preferred_name(13, ttf)
402
403    if license_description:
404      copyright = str(copyright) + "\n\n" + str(license_description)
405    else:
406      copyright = str(copyright)
407
408    license_url = get_preferred_name(14, ttf)
409
410    if license_url:
411      copyright = str(copyright) + "\n\n" + str(license_url)
412    else:
413      copyright = str(copyright)
414
415    return copyright
416
417def extract_from_c_style_lines_at(
418    lines: Sequence[str], i: int, path: str) -> Tuple[str, int]:
419  def is_copyright_end(line):
420    if line.startswith("//"):
421      return False
422    else:
423      return True
424  start = i
425  while i < len(lines):
426    if is_copyright_end(lines[i]):
427      break
428    i += 1
429  end = i
430
431  if start == end:
432    fatal("Failed to get copyright info")
433
434  out_lines = []
435  for line in lines[start:end]:
436    if line.startswith("//# "):  # Andorid.bp uses //# style
437      out_lines.append(line[4:])
438    elif line.startswith("//#"):  # Andorid.bp uses //# style
439      out_lines.append(line[3:])
440    elif line.startswith("// "):
441      out_lines.append(line[3:])
442    elif line == "//":
443      out_lines.append(line[2:])
444    else:
445      out_lines.append(line)
446
447  return (cleanup_and_join(out_lines), i + 1)
448
449
450def extract_from_script_hash_at(
451    lines: Sequence[str], i: int, path: str) -> Tuple[str, int]:
452  if lines[i].strip()[0] != "#":
453    return (None, i + 1)
454  def is_copyright_end(lines: str, i: int) -> bool:
455    if "#" not in lines[i]:
456      return True
457    # treat double spacing as end of license header
458    if lines[i] == "#" and lines[i+1] == "#":
459      return True
460    return False
461
462  start = i
463  while i < len(lines):
464    if is_copyright_end(lines, i):
465      break
466    i += 1
467  end = i
468
469  if start == end:
470    fatal("Failed to get copyright info")
471
472  out_lines = []
473  for line in lines[start:end]:
474    if line.startswith("# "):
475      out_lines.append(line[2:])
476    elif line == "#":
477      out_lines.append(line[1:])
478    else:
479      out_lines.append(line)
480
481  return (cleanup_and_join(out_lines), i + 1)
482
483
484def extract_from_c_style_block_at(
485    lines: Sequence[str], i: int, path: str) -> Tuple[str, int]:
486
487  def is_copyright_end(lines: str, i: int) -> bool:
488    if "*/" in lines[i]:
489      return True
490    if lines[i] == " *" and lines[i + 1] == " *":
491      return True
492    if lines[i] == "" and lines[i + 1] == "":
493      return True
494    return False
495
496  start = i
497  i += 1 # include at least one line
498  while i < len(lines):
499    if is_copyright_end(lines, i):
500      break
501    i += 1
502  end = i + 1
503
504  out_lines = []
505  for line in lines[start:end]:
506    clean_line = line
507
508    # Strip begining "/*" chars
509    if clean_line.startswith("/* "):
510      clean_line = clean_line[3:]
511    if clean_line == "/*":
512      clean_line = clean_line[2:]
513
514    # Strip ending "*/" chars
515    if clean_line.endswith(" */"):
516      clean_line = clean_line[:-3]
517    if clean_line.endswith("*/"):
518      clean_line = clean_line[:-2]
519
520    # Strip starting " *" chars
521    if clean_line.startswith(" * "):
522      clean_line = clean_line[3:]
523    if clean_line == " *":
524      clean_line = clean_line[2:]
525
526    # hb-aots-tester.cpp has underline separater which can be dropped.
527    if path.endswith("test/shape/data/aots/hb-aots-tester.cpp"):
528      clean_line = clean_line.replace("_", "")
529
530    # Strip trailing spaces
531    clean_line = clean_line.rstrip()
532
533    out_lines.append(clean_line)
534
535  return (cleanup_and_join(out_lines), i + 1)
536
537
538# Returns true if the line shows the start of copyright notice.
539def is_copyright_line(line: str, path: str) -> bool:
540  if "Copyright" not in line:
541    return False
542
543  # For avoiding unexpected mismatches, exclude quoted Copyright string.
544  if "`Copyright'" in line:
545    return False
546  if "\"Copyright\"" in line:
547    return False
548
549  if "OpCode_Copyright" in line:
550    return False
551
552  if path.endswith("src/hb-ot-name.h") and "HB_OT_NAME_ID_COPYRIGHT" in line:
553    return False
554
555  return True
556
557def assert_mandatory_copyright(path_str: str):
558  path = Path(path_str)
559  toplevel_dir = str(path).split(os.sep)[0]
560
561  if toplevel_dir in IGNORE_DIR_IF_NO_COPYRIGHT:
562    return
563
564  fatal("%s does not contain Copyright line" % path)
565
566
567# Extract the copyright notice and put it into copyrights arg.
568def do_file(path: str, copyrights: set, no_copyright_files: set):
569  raw = Path(path).read_bytes()
570  basename = os.path.basename(path)
571  dirname = os.path.dirname(path)
572
573  is_font = (dirname.endswith('./test/fuzzing/fonts') or
574             Path(path).suffix in ['.ttf', '.otf', '.dfont', '.ttc', '.otc'])
575
576  if is_font:
577    notice = extract_copyright_font(path)
578    if not notice:
579      assert_mandatory_copyright(path)
580      return
581
582    if not notice in copyrights:
583      copyrights[notice] = []
584    copyrights[notice].append(path)
585  else:
586    try:
587      content = raw.decode("utf-8")
588    except UnicodeDecodeError:
589      content = raw.decode("iso-8859-1")
590
591    if not "Copyright" in content:
592      if path in no_copyright_files:
593        no_copyright_files.remove(path)
594      else:
595        assert_mandatory_copyright(path)
596      return
597
598    lines = content.splitlines()
599
600    # The COPYING in the in-house dir has full OFL license with description.
601    # Use the OFL license description body.
602    if path.endswith("test/shape/data/in-house/COPYING") or path.endswith("test/COPYING"):
603      notice = cleanup_and_join(lines[9:])
604      copyrights.setdefault(notice, [])
605      copyrights[notice].append(path)
606      return
607
608    # The COPYING in the top dir has MIT-Modern-Variant license with description.
609    # Use the entire file as a license notice.
610    if path.endswith("COPYING") and str(Path(path)) == 'COPYING':
611      notice = cleanup_and_join(lines)
612      copyrights.setdefault(notice, [])
613      copyrights[notice].append(path)
614      return
615
616    i = 0
617    license_found = False
618    while i < len(lines):
619      if is_copyright_line(lines[i], path):
620        (notice, nexti) = extract_copyright_at(lines, i, path)
621        if notice:
622          copyrights.setdefault(notice, [])
623          copyrights[notice].append(path)
624          license_found = True
625
626        i = nexti
627      else:
628        i += 1
629
630    if not license_found:
631      assert_mandatory_copyright(path)
632
633def do_check(path, format):
634  if not path.endswith('/'): # make sure the path ends with slash
635    path = path + '/'
636
637  file_to_ignore = set([os.path.join(path, x) for x in IGNORE_FILE_NAME])
638  no_copyright_files = set([os.path.join(path, x) for x in NO_COPYRIGHT_FILES])
639  copyrights = {}
640
641  for directory, sub_directories, filenames in os.walk(path):
642    # skip .git directory
643    if ".git" in sub_directories:
644      sub_directories.remove(".git")
645
646    for fname in filenames:
647      fpath = os.path.join(directory, fname)
648      if fpath in file_to_ignore:
649        file_to_ignore.remove(fpath)
650        continue
651
652      do_file(fpath, copyrights, no_copyright_files)
653
654  if len(file_to_ignore) != 0:
655    fatal("Following files are listed in IGNORE_FILE_NAME but doesn't exists,.\n"
656          + "\n".join(file_to_ignore))
657
658  if len(no_copyright_files) != 0:
659    fatal("Following files are listed in NO_COPYRIGHT_FILES but doesn't exists.\n"
660          + "\n".join(no_copyright_files))
661
662  if format == Format.notice:
663    print_notice(copyrights, False)
664  elif format == Format.notice_with_filename:
665    print_notice(copyrights, True)
666  elif format == Format.html:
667    print_html(copyrights)
668  elif format == Format.json:
669    print_json(copyrights)
670
671def print_html(copyrights):
672  print('<html>')
673  print("""
674  <head>
675    <style>
676      table {
677        font-family: monospace
678      }
679
680      table tr td {
681        padding: 10px 10px 10px 10px
682      }
683    </style>
684  </head>
685  """)
686  print('<body>')
687  print('<table border="1" style="border-collapse:collapse">')
688  for notice in sorted(copyrights.keys()):
689    files = sorted(copyrights[notice])
690
691    print('<tr>')
692    print('<td>')
693    print('<ul>')
694    for file in files:
695      print('<li>%s</li>' % file)
696    print('</ul>')
697    print('</td>')
698
699    print('<td>')
700    print('<p>%s</p>' % notice.replace('\n', '<br>'))
701    print('</td>')
702
703    print('</tr>')
704
705
706  print('</table>')
707  print('</body></html>')
708
709def print_notice(copyrights, print_file):
710  # print the copyright in sorted order for stable output.
711  for notice in sorted(copyrights.keys()):
712    if print_file:
713      files = sorted(copyrights[notice])
714      print("\n".join(files))
715      print()
716    print(notice)
717    print()
718    print("-" * 67)
719    print()
720
721def print_json(copyrights):
722  print(json.dumps(copyrights))
723
724class Format(Enum):
725  notice = 'notice'
726  notice_with_filename = 'notice_with_filename'
727  html = 'html'
728  json = 'json'
729
730  def __str__(self):
731    return self.value
732
733def main():
734  parser = argparse.ArgumentParser(description="Collect notice headers.")
735  parser.add_argument("--format", dest="format", type=Format, choices=list(Format),
736                      default=Format.notice, help="print filename before the license notice")
737  parser.add_argument("--target", dest="target", action='store',
738                      required=True, help="target directory to collect notice headers")
739  res = parser.parse_args()
740  do_check(res.target, res.format)
741
742if __name__ == "__main__":
743  main()
744
745