summaryrefslogtreecommitdiff
path: root/lib/Image/ExifTool/BuildTagLookup.pm
blob: 4b348d83a9a1d1126436e5cffa34aef1c1d03eab (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
#------------------------------------------------------------------------------
# File:         BuildTagLookup.pm
#
# Description:  Utility to build tag lookup tables in Image::ExifTool::TagLookup.pm
#
# Revisions:    12/31/2004 - P. Harvey Created
#               02/15/2005 - PH Added ability to generate TagNames documentation
#
# Notes:        Documentation for the tag tables may either be placed in the
#               %docs hash below or in a NOTES entry in the table itself, and
#               individual tags may have their own Notes entry.
#------------------------------------------------------------------------------

package Image::ExifTool::BuildTagLookup;

use strict;
require Exporter;

BEGIN {
    # prevent ExifTool from loading the user config file
    $Image::ExifTool::configFile = '';
    $Image::ExifTool::debug = 1; # enabled debug messages
}

use vars qw($VERSION @ISA);
use Image::ExifTool qw(:Utils :Vars);
use Image::ExifTool::Exif;
use Image::ExifTool::Shortcuts;
use Image::ExifTool::HTML qw(EscapeHTML);
use Image::ExifTool::IPTC;
use Image::ExifTool::XMP;
use Image::ExifTool::Canon;
use Image::ExifTool::Nikon;
use Image::ExifTool::Sony;
use Image::ExifTool::Validate;
use Image::ExifTool::MacOS;

$VERSION = '3.52';
@ISA = qw(Exporter);

sub NumbersFirst($$);
sub SortedTagTablekeys($);

my $createDate = 'Feb 15, 2005';

# global variables to control sorting order of table entries
my $numbersFirst = 1;   # set to -1 to sort numbers last, or 2 to put negative numbers last
my $caseInsensitive;    # flag to ignore case when sorting tag names

# list of all tables in plug-in modules
my @pluginTables = ('Image::ExifTool::MWG::Composite');

# colors for html pages
my $noteFont = "<span class=n>";
my $noteFontSmall = "<span class='n s'>";

my $docType = q{<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
        "http://www.w3.org/TR/html4/loose.dtd">
};

my $homePage = 'https://exiftool.org';

# tweak the ordering of tables in the documentation
my %tweakOrder = (
  # this    => comes after this
  # -------    -----------------
    JPEG    => '-',     # JPEG comes first
    IPTC    => 'Exif',  # put IPTC after EXIF,
    GPS     => 'XMP',   # etc...
    Composite => 'Extra',
    CBOR    => 'JSON',
    GeoTiff => 'GPS',
    CanonVRD=> 'CanonCustom',
    DJI     => 'Casio',
    FLIR    => 'DJI',
    FujiFilm => 'FLIR',
    Kodak   => 'JVC',
    Leaf    => 'Kodak',
    Minolta => 'Leaf',
    Motorola => 'Minolta',
    Nikon   => 'Motorola',
    NikonCustom => 'Nikon',
    NikonCapture => 'NikonCustom',
    Nintendo => 'NikonCapture',
    Pentax  => 'Panasonic',
    SonyIDC => 'Sony',
    Unknown => 'SonyIDC',
    DNG     => 'Unknown',
    PrintIM => 'ICC_Profile',
    Vorbis  => 'Ogg',
    ID3     => 'PostScript',
    MinoltaRaw => 'KyoceraRaw',
    KyoceraRaw => 'CanonRaw',
    SigmaRaw => 'PanasonicRaw',
    Lytro   => 'SigmaRaw',
    PhotoMechanic => 'FotoStation',
    Microsoft     => 'PhotoMechanic',
   'Microsoft::MP'=> 'Microsoft::MP1',
    GIMP    => 'Microsoft',
   'Nikon::CameraSettingsD300' => 'Nikon::ShotInfoD300b',
   'Pentax::LensData' => 'Pentax::LensInfo2',
   'Sony::SRF2' => 'Sony::SRF',
    DarwinCore => 'AFCP',
   'MWG::Regions' => 'MWG::Composite',
   'MWG::Keywords' => 'MWG::Regions',
   'MWG::Collections' => 'MWG::Keywords',
   'GoPro::fdsc' => 'GoPro::KBAT',
);

# list of all recognized Format strings
# (not a complete list, but this is all we use so far)
# (also, formats like "var_X[num]" are allowed for any valid X)
my %formatOK = (
    %Image::ExifTool::Exif::formatNumber,
    0 => 1,
    1 => 1,
    2 => 1, # (writable for docs only)
    real    => 1,
    integer => 1,
    date    => 1,
    boolean => 1,
    rational  => 1,
   'lang-alt' => 1,
    fixed16u => 1,
    fixed16s => 1,
    fixed32u => 1,
    fixed32s => 1,
    extended => 1,
    resize   => 1,
    digits   => 1,
    int16uRev => 1,
    int32uRev => 1,
    rational32u => 1,
    rational32s => 1,
    pstring     => 1,
    var_string  => 1,
    var_int16u  => 1,
    var_pstr32  => 1,
    var_ustr32  => 1,
    var_ue7     => 1, # (BPG)
    # Matroska
    signed      => 1,
    unsigned    => 1,
    utf8        => 1,
    Unicode     => 1, # (Microsoft Xtra)
    GUID        => 1, # (Microsoft Xtra)
    vt_filetime => 1, # (Microsoft Xtra)
);

# Descriptions for the TagNames documentation
# (descriptions may also be defined in tag table NOTES)
# Note: POD headers in these descriptions start with '~' instead of '=' to keep
# from confusing POD parsers which apparently parse inside quoted strings.
my %docs = (
    PodHeader => q{
~head1 NAME

Image::ExifTool::TagNames - ExifTool tag name documentation

~head1 DESCRIPTION

This document contains a complete list of ExifTool tag names, organized into
tables based on information type.  Tag names are used to reference specific
meta information extracted from or written to a file.

~head1 TAG TABLES
},
    ExifTool => q{
The tables listed below give the names of all tags recognized by ExifTool.
They contain a total of $$self{COUNT}{'total tags'} tags, with $$self{COUNT}{'unique tag names'} unique tag names.
},
    ExifTool2 => q{
B<Tag ID>, B<Index#> or B<Sequence> is given in the first column of each
table.  A B<Tag ID> is the computer-readable equivalent of a tag name, and
is the identifier that is actually stored in the file.  B<Index#> refers to
the offset of a value when found at a fixed position within a data block
(B<#> is the multiplier for calculating a byte offset: B<1>, B<2>, B<4> or
B<8>).  These offsets may have a decimal part which is used only to
differentiate tags with values stored at the same position.  (Note that
writable tags within binary data blocks are not individually deletable,
and the usual alternative is to set them to a value of zero.)  B<Sequence>
gives the order of values for a serial data stream.

A B<Tag Name> is the handle by which the information is accessed in
ExifTool.  In some instances, more than one name may correspond to a single
tag ID.  In these cases, the actual name used depends on the context in
which the information is found.  Valid characters in a tag name are A-Z,
a-z, 0-9, hyphen (-) and underline (_).  Case is not significant.  A
question mark (C<?>) after a tag name indicates that the information is
either not understood, not verified, or not very useful -- these tags are
not extracted by ExifTool unless the L<Unknown|../ExifTool.html#Unknown> (-u) option is enabled.  Be
aware that some tag names are different than the descriptions printed out by
default when extracting information with exiftool.  To see the tag names
instead of the descriptions, use C<exiftool -s>.

The B<Writable> column indicates whether the tag is writable by ExifTool.
Anything but a C<no> in this column means the tag is writable.  A C<yes>
indicates writable information that is either unformatted or written using
the existing format.  Other expressions give details about the format of the
stored value, and vary depending on the general type of information.  The
format name may be followed by a number in square brackets to indicate the
number of values written, or the number of characters in a fixed-length
string (including a null terminator which is added if required).

A plus sign (C<+>) after an entry in the B<Writable> column indicates a
I<List> tag which supports multiple values and allows individual values to
be added and deleted.  A slash (C</>) indicates a tag that ExifTool will
I<Avoid> when writing.  These will be edited but not created if another
same-named tag may be created instead.  To create these tags, the group
should be specified.  A tilde (C<~>) indicates a tag this is writable only
when the print conversion is disabled (by setting L<PrintConv|../ExifTool.html#PrintConv> to 0, using the
-n option, or suffixing the tag name with a C<#> character). An exclamation
point (C<!>) indicates a tag that is considered I<Unsafe> to write under
normal circumstances.  These tags are not written unless specified
explicitly (ie. not when wildcards or "all" are used), and care should be
taken when editing them manually since they may affect the way an image is
rendered.  An asterisk (C<*>) indicates a I<Protected> tag which is not
writable directly, but is written automatically by ExifTool (often when a
corresponding L<Composite|Image::ExifTool::TagNames/Composite Tags> or
L<Extra|Image::ExifTool::TagNames/Extra Tags> tag is written). A colon
(C<:>) indicates a I<Mandatory> tag which may be added automatically when
writing (use the API L<NoMandatory|../ExifTool.html#NoMandatory> option to avoid creating mandatory EXIF
tags).  Normally MakerNotes tags may not be deleted individually, but a
caret (C<^>) indicates a I<Deletable> MakerNotes tag.

The HTML version of these tables also lists possible B<Values> for
discrete-valued tags, as well as B<Notes> for some tags.  The B<Values> are
listed with the computer-readable values on the left of the equals sign
(C<=>), and the human-readable values on the right.  The human-readable
values are used by default when reading and writing, but the
computer-readable values may be accessed by disabling the value conversion
with the -n option on the command line, by setting the L<PrintConv|../ExifTool.html#PrintConv> option to 0
in the API, or or on a per-tag basis by adding a hash (C<#>) after the tag
name.

B<Note>: If you are familiar with common meta-information tag names, you may
find that some ExifTool tag names are different than expected.  The usual
reason for this is to make the tag names more consistent across different
types of meta information.  To determine a tag name, either consult this
documentation or run C<exiftool -s> on a file containing the information in
question.

I<(This documentation is the result of decades of research, testing and
reverse engineering, and is the most complete metadata tag list available
anywhere on the internet.  It is provided not only for ExifTool users, but
more importantly as a public service to help augment the collective
knowledge, and is often used as a primary source of information in the
development of other metadata software.  Please help keep this documentation
as accurate and complete as possible, and feed any new discoveries back to
ExifTool.  A big thanks to everyone who has helped with this so far!)>
},
    EXIF => q{
EXIF stands for "Exchangeable Image File Format".  This type of information
is formatted according to the TIFF specification, and may be found in JPG,
TIFF, PNG, JP2, PGF, MIFF, HDP, PSP and XCF images, as well as many
TIFF-based RAW images, and even some AVI and MOV videos.

The EXIF meta information is organized into different Image File Directories
(IFD's) within an image.  The names of these IFD's correspond to the
ExifTool family 1 group names.  When writing EXIF information, the default
B<Group> listed below is used unless another group is specified.

Mandatory tags (indicated by a colon after the B<Writable> type) may be
added automatically with default values when creating a new IFD, and the IFD
is removed automatically when deleting tags if only default-valued mandatory
tags remain.

The table below lists all EXIF tags.  Also listed are TIFF, DNG, HDP and
other tags which are not part of the EXIF specification, but may co-exist
with EXIF tags in some images.  Tags which are part of the EXIF 2.32
specification have an underlined B<Tag Name> in the HTML version of this
documentation.  See
L<https://web.archive.org/web/20190624045241if_/http://www.cipa.jp:80/std/documents/e/DC-008-Translation-2019-E.pdf>
for the official EXIF 2.32 specification.
},
    GPS => q{
These GPS tags are part of the EXIF standard, and are stored in a separate
IFD within the EXIF information.

ExifTool is very flexible about the input format when writing lat/long
coordinates, and will accept from 1 to 3 floating point numbers (for decimal
degrees, degrees and minutes, or degrees, minutes and seconds) separated by
just about anything, and will format them properly according to the EXIF
specification.

Some GPS tags have values which are fixed-length strings. For these, the
indicated string lengths include a null terminator which is added
automatically by ExifTool.  Remember that the descriptive values are used
when writing (eg. 'Above Sea Level', not '0') unless the print conversion is
disabled (with '-n' on the command line or the L<PrintConv|../ExifTool.html#PrintConv> option in the API,
or by suffixing the tag name with a C<#> character).

When adding GPS information to an image, it is important to set all of the
following tags: GPSLatitude, GPSLatitudeRef, GPSLongitude, GPSLongitudeRef,
and GPSAltitude and GPSAltitudeRef if the altitude is known.  ExifTool will
write the required GPSVersionID tag automatically if new a GPS IFD is added
to an image.
},
    XMP => q{
XMP stands for "Extensible Metadata Platform", an XML/RDF-based metadata
format which is being pushed by Adobe.  Information in this format can be
embedded in many different image file types including JPG, JP2, TIFF, GIF,
EPS, PDF, PSD, IND, INX, PNG, DJVU, SVG, PGF, MIFF, XCF, CRW, DNG and a
variety of proprietary TIFF-based RAW images, as well as MOV, AVI, ASF, WMV,
FLV, SWF and MP4 videos, and WMA and audio formats supporting ID3v2
information.

The XMP B<Tag ID>'s aren't listed because in most cases they are identical
to the B<Tag Name> (aside from differences in case).  Tags with different
ID's are mentioned in the B<Notes> column of the HTML version of this
document.

All XMP information is stored as character strings.  The B<Writable> column
specifies the information format:  C<string> is an unformatted string,
C<integer> is a string of digits (possibly beginning with a '+' or '-'),
C<real> is a floating point number, C<rational> is entered as a floating
point number but stored as two C<integer> strings separated by a '/'
character, C<date> is a date/time string entered in the format "YYYY:mm:dd
HH:MM:SS[.ss][+/-HH:MM]" but some partial date/time formats are also
accepted (see L<https://exiftool.org/faq.html#Q5>), C<boolean> is either
"True" or "False" (but "true" and "false" may be written as a ValueConv
value for compatibility with non-conforming applications), C<struct>
indicates a structured tag, and C<lang-alt> is a tag that supports alternate
languages.

When reading, C<struct> tags are extracted only if the L<Struct|../ExifTool.html#Struct> (-struct)
option is used.  Otherwise the corresponding I<Flattened> tags, indicated by
an underline (C<_>) after the B<Writable> type, are extracted.  When
copying, by default both structured and flattened tags are available, but
the flattened tags are considered "unsafe" so they aren't copied unless
specified explicitly.  The L<Struct|../ExifTool.html#Struct> option may be disabled by setting Struct
to 0 via the API or with --struct on the command line to copy only flattened
tags, or enabled by setting Struct to 1 via the API or with -struct on the
command line to copy only as structures.  When writing, the L<Struct|../ExifTool.html#Struct> option
has no effect, and both structured and flattened tags may be written.  See
L<https://exiftool.org/struct.html> for more details.

Individual languages for C<lang-alt> tags are accessed by suffixing the tag
name with a '-', followed by an RFC 3066 language code (eg. "XMP:Title-fr",
or "Rights-en-US").  (See L<http://www.ietf.org/rfc/rfc3066.txt> for the RFC
3066 specification.)  A C<lang-alt> tag with no language code accesses the
"x-default" language, but causes other languages for this tag to be deleted
when writing.  The "x-default" language code may be specified when writing
to preserve other existing languages (eg. "XMP-dc:Description-x-default"). 
When reading, "x-default" is not specified.

The XMP tags are organized according to schema B<Namespace> in the following
tables.  The ExifTool family 1 group names are derived from the namespace
prefixes by adding a leading "XMP-" (eg. "XMP-dc").  A few of the longer
prefixes have been shortened (as mentioned in the documentation below) to
avoid excessively long ExifTool group names.  The tags of any namespace may
be deleted as a group by specifying the family 1 group name (eg.
"-XMP-dc:all=" on the command line).  This includes namespaces which are not
pre-defined by ExifTool.

In cases where a tag name exists in more than one namespace, less common
namespaces are avoided when writing.  However, a specific namespace may be
written by providing a family 1 group name for the tag (eg. XMP-crs:Contrast
or XMP-exif:Contrast).  When deciding on which tags to add to an image,
using standard schemas such as L<dc|/XMP dc Tags>, L<xmp|/XMP xmp Tags>,
L<iptcCore|/XMP iptcCore Tags> and L<iptcExt|/XMP iptcExt Tags> is
recommended if possible.

For structures, the heading of the first column is B<Field Name>.  Field
names are very similar to tag names, except they are used to identify fields
inside structures instead of stand-alone tags.  See
L<the Field Name section of the Structured Information documentation|https://exiftool.org/struct.html#Fields> for more
details.

ExifTool will extract XMP information even if it is not listed in these
tables, but other tags are not writable unless added as user-defined tags in
the L<ExifTool config file|../config.html>.  For example, the C<pdfx> namespace doesn't have a
predefined set of tag names because it is used to store application-defined
PDF information, so although this information will be extracted, it is only
writable if the corresponding user-defined tags have been created.

The tables below list tags from the official XMP specification (with an
underlined B<Namespace> in the HTML version of this documentation), as well
as extensions from various other sources.  See
L<http://www.adobe.com/devnet/xmp/> for the official XMP specification.
},
    IPTC => q{
The tags listed below are part of the International Press Telecommunications
Council (IPTC) and the Newspaper Association of America (NAA) Information
Interchange Model (IIM).  This is an older meta information format, slowly
being phased out in favor of XMP -- the newer IPTCCore specification uses
XMP format.  IPTC information may be found in JPG, TIFF, PNG, MIFF, PS, PDF,
PSD, XCF and DNG images.

IPTC information is separated into different records, each of which has its
own set of tags.  See
L<http://www.iptc.org/std/IIM/4.1/specification/IIMV4.1.pdf> for the
official IPTC IIM specification.

This specification dictates a length for ASCII (C<string> or C<digits>) and
binary (C<undef>) values.  These lengths are given in square brackets after
the B<Writable> format name.  For tags where a range of lengths is allowed,
the minimum and maximum lengths are separated by a comma within the
brackets.  When writing, ExifTool issues a minor warning and truncates the
value if it is longer than allowed by the IPTC specification.  Minor errors
may be ignored with the L<IgnoreMinorErrors|../ExifTool.html#IgnoreMinorErrors> (-m) option, allowing longer
values to be written, but beware that values like this may cause problems
for some other IPTC readers. ExifTool will happily read IPTC values of any
length.

Separate IPTC date and time tags may be written with a combined date/time
value and ExifTool automagically takes the appropriate part of the date/time
string depending on whether a date or time tag is being written.  This is
very useful when copying date/time values to IPTC from other metadata
formats.

IPTC time values include a timezone offset.  If written with a value which
doesn't include a timezone then the current local timezone offset is used
(unless written with a combined date/time, in which case the local timezone
offset at the specified date/time is used, which may be different due to
changes in daylight savings time).

Note that it is not uncommon for IPTC to be found in non-standard locations
in JPEG and TIFF-based images.  When reading, the family 1 group name has a
number added for non-standard IPTC ("IPTC2", "IPTC3", etc), but when writing
only "IPTC" may be specified as the group.  To keep the IPTC consistent,
ExifTool updates tags in all existing IPTC locations, but will create a new
IPTC group only in the standard location.
},
    QuickTime => q{
The QuickTime format is used for many different types of audio, video and
image files (most notably, MOV/MP4 videos and HEIC/CR3 images).  ExifTool
extracts standard meta information and a variety of audio, video and image
parameters, as well as proprietary information written by many camera
models.  Tags with a question mark after their name are not extracted unless
the L<Unknown|../ExifTool.html#Unknown> option is set.

When writing, ExifTool creates both QuickTime and XMP tags by default, but
the group may be specified to write one or the other separately.  If no
location is specified, newly created QuickTime tags are added in the
L<ItemList|Image::ExifTool::TagNames/QuickTime ItemList Tags> location if
possible, otherwise in
L<UserData|Image::ExifTool::TagNames/QuickTime UserData Tags>, and
finally in L<Keys|Image::ExifTool::TagNames/QuickTime Keys Tags>,
but this order may be changed by setting the PREFERRED level of the
appropriate table in the config file (see
L<example.config|../config.html#PREF> in the full distribution for an
example).  Note that some tags with the same name but different ID's may
exist in the same location, but the family 7 group names may be used to
differentiate these.  ExifTool currently writes only top-level metadata in
QuickTime-based files; it extracts other track-specific and timed metadata,
but can not yet edit tags in these locations (with the exception of
track-level date/time tags).

Beware that the Keys tags are actually stored inside the ItemList in the
file, so deleting the ItemList group as a block (ie. C<-ItemList:all=>) also
deletes Keys tags.  Instead, to preserve Keys tags the ItemList tags may be
deleted individually with C<-QuickTime:ItemList:all=>.

Alternate language tags may be accessed for
L<ItemList|Image::ExifTool::TagNames/QuickTime ItemList Tags> and
L<Keys|Image::ExifTool::TagNames/QuickTime Keys Tags> tags by adding
a 3-character ISO 639-2 language code and an optional ISO 3166-1 alpha 2
country code to the tag name (eg. "ItemList:Artist-deu" or
"ItemList::Artist-deu-DE").  Most
L<UserData|Image::ExifTool::TagNames/QuickTime UserData Tags> tags support a
language code, but without a country code.  If no language code is specified
when writing, the default language is written and alternate languages for
the tag are deleted.  Use the "und" language code to write the default
language without deleting alternate languages.  Note that "eng" is treated
as a default language when reading, but not when writing.

According to the specification, integer-format QuickTime date/time tags
should be stored as UTC.  Unfortunately, digital cameras often store local
time values instead (presumably because they don't know the time zone).  For
this reason, by default ExifTool does not assume a time zone for these
values.  However, if the API L<QuickTimeUTC|../ExifTool.html#QuickTimeUTC> option is set, then ExifTool will
assume these values are properly stored as UTC, and will convert them to
local time when extracting.

When writing string-based date/time tags, the system time zone is added if
the PrintConv option is enabled and no time zone is specified.  This is
because Apple software may display crazy values if the time zone is missing
for some tags.

By default ExifTool will remove null padding from some QuickTime containers
in Canon CR3 files when writing, but the
L<QuickTimePad|../ExifTool.html#QuickTimePad> option may be used to preserve
the original size by padding with nulls if necessary.

See
L<https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/>
for the official specification.
},
    Photoshop => q{
Photoshop tags are found in PSD and PSB files, as well as inside embedded
Photoshop information in many other file types (JPEG, TIFF, PDF, PNG to name
a few).

Many Photoshop tags are marked as Unknown (indicated by a question mark
after the tag name) because the information they provide is not very useful
under normal circumstances.  These unknown tags are not extracted unless the
L<Unknown|../ExifTool.html#Unknown> (-u) option is used.  See
L<http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/> for the
official specification

Photoshop path tags (Tag ID's 0x7d0 to 0xbb5) are not defined by default,
but a config file included in the full ExifTool distribution
(config_files/photoshop_paths.config) contains the tag definitions to allow
access to this information.
},
    PrintIM => q{
The format of the PrintIM information is known, however no PrintIM tags have
been decoded.  Use the L<Unknown|../ExifTool.html#Unknown> (-u) option to extract PrintIM information.
},
    GeoTiff => q{
ExifTool extracts the following tags from GeoTIFF images.  See
L<https://web.archive.org/web/20070820121549/http://www.remotesensing.org/geotiff/spec/geotiffhome.html>
for the complete GeoTIFF specification.  Also included in the table below
are ChartTIFF tags (see
L<https://web.archive.org/web/20020828193928/http://www.charttiff.com/whitepapers.shtml>).
GeoTIFF tags are not writable individually, but they may be copied en mass
via the block tags GeoTiffDirectory, GeoTiffDoubleParams and
GeoTiffAsciiParams.
},
    JFIF => q{
The following information is extracted from the JPEG JFIF header.  See
L<https://www.w3.org/Graphics/JPEG/jfif3.pdf> for the JFIF 1.02
specification.
},
    Kodak => q{
Many Kodak models don't store the maker notes in standard IFD format, and
these formats vary with different models.  Some information has been
decoded, but much of the Kodak information remains unknown.
},
    'Kodak SpecialEffects' => q{
The Kodak SpecialEffects and Borders tags are found in sub-IFD's within the
Kodak JPEG APP3 "Meta" segment.
},
    Minolta => q{
These tags are used by Minolta, Konica/Minolta as well as some Sony cameras.
Minolta doesn't make things easy for decoders because the meaning of some
tags and the location where some information is stored is different for
different camera models.  (Take MinoltaQuality for example, which may be
located in 5 different places.)
},
    Olympus => q{
Tags 0x0000 through 0x0103 are used by some older Olympus cameras, and are
the same as Konica/Minolta tags.  These tags are also used for some models
from other brands such as Acer, BenQ, Epson, Hitachi, HP, Maginon, Minolta,
Pentax, Ricoh, Samsung, Sanyo, SeaLife, Sony, Supra and Vivitar.
},
    Panasonic => q{
These tags are used in Panasonic/Leica cameras.
},
    Pentax => q{
These tags are used in Pentax/Asahi cameras.
},
    CanonRaw => q{
These tags apply to CRW-format Canon RAW files and information in the APP0
"CIFF" segment of JPEG images.  When writing CanonRaw/CIFF information, the
length of the information is preserved (and the new information is truncated
or padded as required) unless B<Writable> is C<resize>. Currently, only
JpgFromRaw and ThumbnailImage are allowed to change size.  See
L<https://exiftool.org/canon_raw.html> for a description of the Canon CRW
format.

CRW images also support the addition of a CanonVRD trailer, which in turn
supports XMP.  This trailer is created automatically if necessary when
ExifTool is used to write XMP to a CRW image.
},
    NikonCustom => q{
Unfortunately, the NikonCustom settings are stored in a binary data block
which changes from model to model.  This means that significant effort must
be spent in decoding these for each model, usually requiring hundreds of
test images from a dedicated Nikon owner.  For this reason, the NikonCustom
settings have not been decoded for all models.  The tables below list the
custom settings for the currently supported models.
},
    Unknown => q{
The following tags are decoded in unsupported maker notes.  Use the L<Unknown|../ExifTool.html#Unknown>
(-u) option to display other unknown tags.
},
    PDF => q{
The tags listed in the PDF tables below are those which are used by ExifTool
to extract meta information, but they are only a small fraction of the total
number of available PDF tags.  See
L<http://www.adobe.com/devnet/pdf/pdf_reference.html> for the official PDF
specification.

ExifTool supports reading and writing PDF documents up to version 2.0,
including support for RC4, AES-128 and AES-256 encryption.  A
L<Password|../ExifTool.html#Password> option is provided to allow processing
of password-protected PDF files.

ExifTool may be used to write native PDF and XMP metadata to PDF files. It
uses an incremental update technique that has the advantages of being both
fast and reversible.  If ExifTool was used to modify a PDF file, the
original may be recovered by deleting the C<PDF-update> pseudo-group (with
C<-PDF-update:all=> on the command line).  However, there are two main
disadvantages to this technique:

1) A linearized PDF file is no longer linearized after the update, so it
must be subsequently re-linearized if this is required.

2) All metadata edits are reversible.  While this would normally be
considered an advantage, it is a potential security problem because old
information is never actually deleted from the file.  (However, after
running ExifTool the old information may be removed permanently using the
"qpdf" utility with this command: "qpdf --linearize in.pdf out.pdf".)
},
    DNG => q{
The main DNG tags are found in the EXIF table.  The tables below define only
information found within structures of these main DNG tag values.  See
L<http://www.adobe.com/products/dng/> for the official DNG specification.
},
    MPEG => q{
The MPEG format doesn't specify any file-level meta information.  In lieu of
this, information is extracted from the first audio and video frame headers
in the file.
},
    Real => q{
ExifTool recognizes three basic types of Real audio/video files: 1)
RealMedia (RM, RV and RMVB), 2) RealAudio (RA), and 3) Real Metafile (RAM
and RPM).
},
    Extra => q{
The extra tags provide extra features or extra information extracted or
generated by ExifTool that is not directly associated with another tag
group.  The B<Group> column lists the family 1 group name when reading. 
Tags with a "-" in this column are write-only.

Tags in the family 1 "System" group are referred to as "pseudo" tags because
they don't represent real metadata in the file.  Instead, this information
is stored in the directory structure of the filesystem.  The B<Writable>
System "pseudo" tags in this table may be written without modifying the file
itself.  The TestName tag is used for dry-run testing before writing
FileName.
},
    Composite => q{
The values of the composite tags are B<Derived From> the values of other
tags.  These are convenience tags which are calculated after all other
information is extracted.  Only a few of these tags are writable directly,
the others are changed by writing the corresponding B<Derived From> tags.
User-defined Composite tags, also useful for custom-formatting of tag
values, may created via the L<ExifTool configuration file|../config.html>.
},
    Shortcuts => q{
Shortcut tags are convenience tags that represent one or more other tag
names.  They are used like regular tags to read and write the information
for a specified set of tags.

The shortcut tags below have been pre-defined, but user-defined shortcuts
may be added via the %Image::ExifTool::UserDefined::Shortcuts lookup in the
~/.ExifTool_config file.  See the Image::ExifTool::Shortcuts documentation
for more details.
},
    MWG => q{
The Metadata Working Group (MWG) recommends techniques to allow certain
overlapping EXIF, IPTC and XMP tags to be reconciled when reading, and
synchronized when writing.  The MWG Composite tags below are designed to aid
in the implementation of these recommendations.  As well, the MWG defines
new XMP tags which are listed in the subsequent tables below.  See
L<https://web.archive.org/web/20180919181934/http://www.metadataworkinggroup.org/pdf/mwg_guidance.pdf>
for the official MWG specification.
},
    MacOS => q{
On MacOS systems, the there are additional MDItem and XAttr Finder tags that
may be extracted.  These tags are not extracted by default -- they must be
specifically requested or enabled via an API option.  (Except when reading
MacOS "._" files directly, see below.)

The tables below list some of the tags that may be extracted, but ExifTool
will extract all available information even for tags not listed.

Tags in these tables are referred to as "pseudo" tags because their
information is not stored in the file itself.  As such, B<Writable> tags in
these tables may be changed without having to rewrite the file.
},
    PodTrailer => q{
~head1 NOTES

This document generated automatically by
L<Image::ExifTool::BuildTagLookup|Image::ExifTool::BuildTagLookup>.

~head1 AUTHOR

Copyright 2003-2024, Phil Harvey (philharvey66 at gmail.com)

This library is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.

~head1 SEE ALSO

L<Image::ExifTool(3pm)|Image::ExifTool>

~cut
},
);

# notes for Shortcuts tags
my %shortcutNotes = (
    AllDates => q{
        contrary to the shortcut name, this represents only the common EXIF
        date/time tags.  To access all date/time tags, use Time:All instead
    },
    MakerNotes => q{
        useful when copying tags between files to either copy the maker notes as a
        block or prevent it from being copied
    },
    ColorSpaceTags => q{
        standard tags which carry color space information.  Useful for preserving
        color space when deleting all other metadata
    },
    CommonIFD0 => q{
        common metadata tags found in IFD0 of TIFF-format images.  Used to simpify
        deletion of all metadata from these images.  See
        L<FAQ number 7|../faq.html#Q7> for details
    },
    Unsafe => q{
        I<Unsafe> tags in JPEG images which are normally not copied.  Defined here
        as a shortcut to use when rebuilding JPEG EXIF from scratch. See
        L<FAQ number 20|../faq.html#Q20> for more information
    },
    LargeTags => q{
        large binary data tags which may be excluded to reduce memory usage if
        memory limitations are a problem
    },
   'ls-l' => q{
        mimics columns shown by Unix "ls -l" command.  Includes some tags which are
        extracted only if the API L<SystemTags|../ExifTool.html#SystemTags> option
        is enabled
    },
);

# lookup for RIFF INFO tags which are found in the EXIF spec
my %riffSpec = (
    IARL => 1,  ICRD => 1,  IGNR => 1,  IPLT => 1,  ISRC => 1,
    IART => 1,  ICRP => 1,  IKEY => 1,  IPRD => 1,  ISRF => 1,
    ICMS => 1,  IDIM => 1,  ILGT => 1,  ISBJ => 1,  ITCH => 1,
    ICMT => 1,  IDPI => 1,  IMED => 1,  ISFT => 1,
    ICOP => 1,  IENG => 1,  INAM => 1,  ISHP => 1,
);
# same thing for XMP namespaces
my %xmpSpec = (
    aux       => 1,   'x'        => 1,
    crs       => 1,    xmp       => 1,
    dc        => 1,    xmpBJ     => 1,
    exif      => 1,    xmpDM     => 1,
    pdf       => 1,    xmpMM     => 1,
    pdfx      => 1,    xmpNote   => 1,
    photoshop => 1,    xmpRights => 1,
    tiff      => 1,    xmpTPg    => 1,
);

#------------------------------------------------------------------------------
# New - create new BuildTagLookup object
# Inputs: 0) reference to BuildTagLookup object or BuildTagLookup class name
sub new
{
    local $_;
    my $that = shift;
    my $class = ref($that) || $that || 'Image::ExifTool::BuildTagLookup';
    my $self = bless {}, $class;
    my (%subdirs, %isShortcut);
    my %count = (
        'unique tag names' => 0,
        'total tags' => 0,
    );
#
# loop through all tables, accumulating TagLookup and TagName information
#
    my (%tagNameInfo, %id, %longID, %longName, %shortName, %tableNum,
        %tagLookup, %tagExists, %noLookup, %tableWritable, %sepTable, %case,
        %structs, %compositeModules, %isPlugin, %flattened, %structLookup,
        @writePseudo, %dupXmpTag);
    $self->{TAG_NAME_INFO} = \%tagNameInfo;
    $self->{ID_LOOKUP} = \%id;
    $self->{LONG_ID} = \%longID;
    $self->{LONG_NAME} = \%longName;
    $self->{SHORT_NAME} = \%shortName;
    $self->{TABLE_NUM} = \%tableNum;
    $self->{TAG_LOOKUP} = \%tagLookup;
    $self->{TAG_EXISTS} = \%tagExists;
    $self->{FLATTENED} = \%flattened;
    $self->{TABLE_WRITABLE} = \%tableWritable;
    $self->{SEPARATE_TABLE} = \%sepTable;
    $self->{STRUCTURES} = \%structs;
    $self->{STRUCT_LOOKUP} = \%structLookup;  # lookup for Struct hash ref based on Struct name
    $self->{COMPOSITE_MODULES} = \%compositeModules;
    $self->{COUNT} = \%count;
    $self->{WRITE_PSEUDO} = \@writePseudo;

    Image::ExifTool::LoadAllTables();
    my @tableNames = sort keys %allTables;
    # add Shortcuts after other tables
    push @tableNames, 'Image::ExifTool::Shortcuts::Main';
    # add plug-in modules last
    foreach (@pluginTables) {
        push @tableNames, $_;
        $isPlugin{$_} = 1;
    }

    my $tableNum = 0;
    my $et = new Image::ExifTool;
    my ($tableName, $tag);
    # create lookup for short table names
    foreach $tableName (@tableNames) {
        my $short = $tableName;
        $short =~ s/^Image::ExifTool:://;
        $short =~ s/::Main$//;
        $short =~ s/::/ /;
        $short =~ s/^(.+)Tags$/\u$1/ unless $short eq 'Nikon AVITags';
        $short =~ s/^Exif\b/EXIF/;
        # change underlines to dashes in XMP-mwg group names
        # (we used underlines just because Perl variables can't contain dashes)
        $short =~ s/^XMP mwg_/XMP mwg-/;
        $shortName{$tableName} = $short;    # remember short name
        $tableNum{$tableName} = $tableNum++;
    }
    # validate DICOM UID values
    foreach (values %Image::ExifTool::DICOM::uid) {
        next unless /[\0-\x1f\x7f-\xff]/;
        warn "Warning: Special characters in DICOM UID value ($_)\n";
    }
    # make lookup table to check for shortcut tags
    foreach $tag (keys %Image::ExifTool::Shortcuts::Main) {
        my $entry = $Image::ExifTool::Shortcuts::Main{$tag};
        # ignore if shortcut tag name includes itself
        next if ref $entry eq 'ARRAY' and grep /^$tag$/, @$entry;
        $isShortcut{lc($tag)} = 1;
    }
    foreach $tableName (@tableNames) {
        # create short table name
        my $short = $shortName{$tableName};
        my $info = $tagNameInfo{$tableName} = [ ];
        my $isPlugin = $isPlugin{$tableName};
        my ($table, $shortcut, %isOffset, %datamember, %hasSubdir);
        if ($short eq 'Shortcuts') {
            # can't use GetTagTable() for Shortcuts (not a normal table)
            $table = \%Image::ExifTool::Shortcuts::Main;
            $shortcut = 1;
        } elsif ($isPlugin) {
            $table = GetTagTable($tableName);
            # don't add to allTables list because this messes our table order
            delete $allTables{$tableName};
            pop @tableOrder;
        } else {
            $table = GetTagTable($tableName);
        }
        my $tableNum = $tableNum{$tableName};
        my $writeProc = $$table{WRITE_PROC};
        my $vars = $$table{VARS} || { };
        $longID{$tableName} = 0;
        $longName{$tableName} = 0;
        # save all tag names
        my ($tagID, $binaryTable, $noID, $hexID, $isIPTC, $isXMP);
        $isIPTC = 1 if $writeProc and $writeProc eq \&Image::ExifTool::IPTC::WriteIPTC;
        # generate flattened tag names for structure fields if this is an XMP table
        if ($$table{GROUPS} and $$table{GROUPS}{0} eq 'XMP') {
            Image::ExifTool::XMP::AddFlattenedTags($table);
            $isXMP = 1;
        }
        $noID = 1 if $isXMP or $short =~ /^(Shortcuts|ASF.*)$/ or $$vars{NO_ID};
        $hexID = $$vars{HEX_ID};
        my $processBinaryData = ($$table{PROCESS_PROC} and (
            $$table{PROCESS_PROC} eq \&Image::ExifTool::ProcessBinaryData or
            $$table{PROCESS_PROC} eq \&Image::ExifTool::Nikon::ProcessNikonEncrypted or
            $$table{PROCESS_PROC} eq \&Image::ExifTool::Sony::ProcessEnciphered));
        if ($$vars{ID_LABEL} or $processBinaryData) {
            my $s = $$table{FORMAT} ? Image::ExifTool::FormatSize($$table{FORMAT}) || 1 : 1;
            $binaryTable = 1;
            $id{$tableName} = $$vars{ID_LABEL} || "Index$s";
        } elsif ($isIPTC and $$table{PROCESS_PROC}) { #only the main IPTC table has a PROCESS_PROC
            $id{$tableName} = 'Record';
        } elsif (not $noID) {
            $id{$tableName} = 'Tag ID';
        }
        $caseInsensitive = $isXMP;
        $numbersFirst = 2;
        $numbersFirst = -1 if $$table{VARS} and $$table{VARS}{ALPHA_FIRST};
        my @keys = SortedTagTableKeys($table);
        $numbersFirst = 1;
        my $defFormat = $table->{FORMAT};
        # use default format for binary data tables
        $defFormat = 'int8u' if not $defFormat and $binaryTable;

TagID:  foreach $tagID (@keys) {
            my ($tagInfo, @tagNames, $subdir, @values);
            my (@infoArray, @require, @writeGroup, @writable);
            if ($shortcut) {
                # must build a dummy tagInfo list since Shortcuts is not a normal table
                $tagInfo = {
                    Name => $tagID,
                    Notes => $shortcutNotes{$tagID},
                    Writable => 1,
                    Require => { },
                };
                my $i;
                for ($i=0; $i<@{$$table{$tagID}}; ++$i) {
                    $tagInfo->{Require}->{$i} = $table->{$tagID}->[$i];
                }
                @infoArray = ( $tagInfo );
            } else {
                @infoArray = GetTagInfoList($table,$tagID);
            }
            foreach $tagInfo (@infoArray) {
                my $name = $$tagInfo{Name};
                if ($$tagInfo{WritePseudo}) {
                    push @writePseudo, $name;
                    warn "Writable pseudo tag $name is not protected!\n" unless $$tagInfo{Protected};
                }
                unless ($$tagInfo{SubDirectory} or $$tagInfo{Struct}) {
                    my $lc = lc $name;
                    warn "Different case for $tableName $name $case{$lc}\n" if $case{$lc} and $case{$lc} ne $name;
                    $case{$lc} = $name;
                }
                my $format = $$tagInfo{Format};
                # check TagID's to make sure they don't start with 'ID-'
                my @grps = $et->GetGroup($tagInfo);
                foreach (@grps) {
                    warn "Group name starts with 'ID-' for $short $name\n" if /^ID-/i;
                }
                if ($isXMP and not $$tagInfo{Avoid} and not $$tagInfo{Struct} and
                    ($$tagInfo{Writable} or $$table{WRITABLE}))
                {
                    $dupXmpTag{$name} and warn "Duplicate writable XMP tag $name\n";
                    $dupXmpTag{$name} = 1;
                }
                # validate Name (must not start with a digit or else XML output will not be valid;
                # must not start with a dash or exiftool command line may get confused)
                if ($name !~ /^[_A-Za-z][-\w]+$/ and
                    # single-character subdirectory names are allowed
                    (not $$tagInfo{SubDirectory} or $name !~ /^[_A-Za-z]$/))
                {
                    warn "Warning: Invalid tag name $short '${name}'\n";
                }
                # validate list type
                if ($$tagInfo{List} and $$tagInfo{List} !~ /^(1|Alt|Bag|Seq|array|string)$/) {
                    warn "Warning: Unknown List type ($$tagInfo{List}) for $name in $tableName\n";
                }
                # accumulate information for consistency check of BinaryData tables
                if ($processBinaryData and $$table{WRITABLE}) {
                    # can't currently write tag if Condition accesses $valPt
                    if ($$tagInfo{Condition} and not $$tagInfo{SubDirectory} and
                        $$tagInfo{Condition} =~ /\$valPt/ and
                        ($$tagInfo{Writable} or not defined $$tagInfo{Writable}))
                    {
                        warn "Warning: Tag not writable due to Condition - $short $name\n";
                    }
                    $isOffset{$tagID} = $name if $$tagInfo{IsOffset};
                    $hasSubdir{$tagID} = $name if $$tagInfo{SubDirectory};
                    # require DATAMEMBER for writable var-format tags, Hook and DataMember tags
                    if ($format and $format =~ /^var_/) {
                        $datamember{$tagID} = $name;
                        unless (defined $$tagInfo{Writable} and not $$tagInfo{Writable}) {
                            warn "Warning: Var-format tag is writable - $short $name\n"
                        }
                        # also need DATAMEMBER for tags used in length of var-sized value
                        while ($format =~ /\$val\{(.*?)\}/g) {
                            my $id = $1;
                            $id = hex($id) if $id =~ /^0x/; # convert from hex if necessary
                            $datamember{$id} = $$table{$id}{Name} if ref $$table{$id} eq 'HASH';
                            unless ($datamember{$id}) {
                                warn "Warning: Unknown ID ($id) used in Format - $short $name\n";
                            }
                        }
                    } elsif ($$tagInfo{Hook} or ($$tagInfo{RawConv} and
                             $$tagInfo{RawConv} =~ /\$self(->)?\{\w+\}\s*=(?!~)/))
                    {
                        $datamember{$tagID} = $name;
                    }
                    if ($format and $format =~ /\$val\{/ and
                        ($$tagInfo{Writable} or not defined $$tagInfo{Writable}))
                    {
                        warn "Warning: \$var{} used in Format of writable tag - $short $name\n"
                    }
                }
                if ($$tagInfo{Hidden}) {
                    if ($tagInfo == $infoArray[0]) {
                        next TagID; # hide all tags with this ID if first tag in list is hidden
                    } else {
                        next;       # only hide this tag
                    }
                }
                my $writable;
                if (defined $$tagInfo{Writable}) {
                    $writable = $$tagInfo{Writable};
                    # validate Writable
                    unless ($formatOK{$writable} or  ($writable =~ /(.*)\[/ and $formatOK{$1})) {
                        warn "Warning: Unknown Writable ($writable) - $short $name\n",
                    }
                } elsif (not $$tagInfo{SubDirectory}) {
                    $writable = $$table{WRITABLE};
                }
                # in general, we can't write unless we have a WRITE_PROC
                if ($writable and not ($$table{WRITE_PROC} or $tableName =~ /Shortcuts/ or $writable eq '2')) {
                    undef $writable;
                }
                # validate some characteristics of obvious date/time tags
                my @g = $et->GetGroup($tagInfo);
                if ($$tagInfo{List} and $g[2] eq 'Time' and $writable and not $$tagInfo{Protected} and
                    not $$tagInfo{PrintConvInv})
                {
                    # (this is a problem because shifting Time:All would create a new list entry)
                    warn "Writable List-type Time tag $g[1]:$name has no PrintConvInv and is not Protected!\n";
                }
                if ($$tagInfo{PrintConv} and $$tagInfo{PrintConv} eq '$self->ConvertDateTime($val)') {
                    warn "$short $name should be in 'Time' group!\n" unless $g[2] eq 'Time';
                    if ($writable and not defined $$tagInfo{Shift} and $short ne 'PostScript') {
                        warn "$short $name is not shiftable!\n";
                    }
                    if ($writable and (not $$tagInfo{PrintConvInv} or
                        $$tagInfo{PrintConvInv} !~ /InverseDateTime/))
                    {
                        warn "$short $name missing InverseDateTime PrintConvInv\n";
                    }
                } elsif ($name =~ /DateTime(?!Stamp)/ and (not $$tagInfo{Groups}{2} or
                    $$tagInfo{Groups}{2} ne 'Time') and $short ne 'DICOM') {
                    warn "$short $name should be in 'Time' group!\n";
                }
                if ($name eq 'DateTimeOriginal' and (not $$tagInfo{Description} or
                    $$tagInfo{Description} ne 'Date/Time Original'))
                {
                    warn "Need Description for $short DateTimeOriginal\n";
                }
                # validate Description (can't contain special characters)
                if ($$tagInfo{Description} and
                    $$tagInfo{Description} ne EscapeHTML($$tagInfo{Description}))
                {
                    # this is a problem because the Escape option currently only
                    # escapes descriptions if the default Lang option isn't default
                    warn "$name description contains special characters!\n";
                }
                # generate structure lookup
                my $struct = $$tagInfo{Struct};
                my $strTable;
                if (ref $struct) {
                    $strTable = $struct;
                    $struct = $$strTable{STRUCT_NAME};
                    if ($struct) {
                        my $oldTable = $structLookup{$struct};
                        if ($oldTable and $oldTable ne $strTable) {
                            warn "Duplicate XMP structure with name $struct\n";
                        } else {
                            $structLookup{$struct} = $strTable;
                        }
                    } else {
                        warn "Missing STRUCT_NAME for structure in $$tagInfo{Name}\n";
                        undef $strTable;
                    }
                } elsif ($struct) {
                    $strTable = $structLookup{$struct};
                    unless ($strTable) {
                        $struct = "XMP $struct" unless $struct =~ / /;
                        warn "Missing $struct structure!\n";
                        undef $struct;
                    }
                }
                # validate SubIFD flag
                my $subdir = $$tagInfo{SubDirectory};
                my $isSub = ($subdir and $$subdir{Start} and $$subdir{Start} =~ /\$val\b/);
                if ($$tagInfo{SubIFD}) {
                    warn "Warning: Wrong SubDirectory Start for SubIFD tag - $short $name\n" unless $isSub;
                } else {
                    warn "Warning: SubIFD flag not set for $short $name\n" if $isSub and not $processBinaryData;
                }
                if ($$tagInfo{Notes}) {
                    my $note = $$tagInfo{Notes};
                    # remove leading/trailing blank lines
                    $note =~ s/^\s+//; $note =~ s/\s+$//;
                    # remove leading/trailing spaces on each line
                    $note =~ s/(^[ \t]+|[ \t]+$)//mg;
                    push @values, "($note)";
                }
                if ($isXMP and (lc $tagID ne lc $name or $$tagInfo{NotFlat})) {
                    my $note;
                    if ($$tagInfo{NotFlat}) {
                        $note = 'NOT a flattened tag!';
                    } else {
                        # add note about different XMP Tag ID
                        $note = $$tagInfo{RootTagInfo} ? $tagID : "called $tagID by the spec";
                    }
                    if ($$tagInfo{Notes}) {
                        $values[-1] =~ s/^\(/($note; /;
                    } else {
                        push @values, "($note)";
                    }
                }
                my $writeGroup;
                if ($short eq 'Extra') {
                    $writeGroup = $$tagInfo{WriteOnly} ? '-' : $g[1];
                } else {
                    $writeGroup = $$tagInfo{WriteGroup};
                }
                unless ($writeGroup) {
                    $writeGroup = $$table{WRITE_GROUP} if $writable;
                    $writeGroup = '-' unless $writeGroup;
                }
                if (defined $format) {
                    # validate Format
                    unless ($formatOK{$format} or $short eq 'PICT' or
                        ($format =~ /^(var_)?(.*)\[/ and $formatOK{$2}))
                    {
                        warn "Warning: Unknown Format ($format) for $short $name\n";
                    }
                } else {
                    $format = $defFormat;
                }
                if ($subdir) {
                    my $subTable = $$subdir{TagTable} || $tableName;
                    push @values, $shortName{$subTable}
                } elsif ($struct) {
                    push @values, $struct;
                    $structs{$struct} = 1;
                }
                my $type;
                foreach $type ('Require','Desire') {
                    my $require = $$tagInfo{$type};
                    if (ref $require) {
                        foreach (sort { $a <=> $b } keys %$require) {
                            push @require, $$require{$_};
                        }
                    } elsif ($require) {
                        push @require, $require;
                    }
                }
                my $printConv = $$tagInfo{PrintConv};
                if ($$tagInfo{Mask}) {
                    my $val = $$tagInfo{Mask};
                    my $bsh = $$tagInfo{BitShift};
                    if ($bsh) {
                        push @values, sprintf('[val >> %d & 0x%x]',$bsh,$val>>$bsh);
                    } else {
                        push @values, sprintf('[val & 0x%x]',$val);
                    }
                    # verify that all values are within the mask
                    if (ref $printConv eq 'HASH') {
                        $val >>= $$tagInfo{BitShift};
                        foreach (keys %$printConv) {
                            next if $_ !~ /^\d+$/ or ($_ & $val) == $_;
                            my $hex = sprintf '0x%.2x', $_;
                            warn "$short $name PrintConv value $hex is not in Mask!\n";
                        }
                    }
                }
                if (ref($printConv) =~ /^(HASH|ARRAY)$/) {
                    my (@printConvList, @indexList, $index, $valueConvHash);
                    if (ref $printConv eq 'ARRAY') {
                        for ($index=0; $index<@$printConv; ++$index) {
                            if (ref $$printConv[$index] eq 'HASH') {
                                next unless %{$$printConv[$index]};
                                push @printConvList, $$printConv[$index];
                                push @indexList, $index;
                            } elsif ($$printConv[$index] and $$printConv[$index] eq 'REPEAT' and $index) {
                                push @printConvList, $$printConv[$index-1];
                                push @indexList, 'N';
                            } else {
                                next;
                            }
                            # collapse values with identical PrintConv's
                            if (@printConvList >= 2 and $printConvList[-1] eq $printConvList[-2]) {
                                if (ref $indexList[-2]) {
                                    push @{$indexList[-2]}, $indexList[-1];
                                } else {
                                    $indexList[-2] = [ $indexList[-2], $indexList[-1] ];
                                }
                                pop @printConvList;
                                pop @indexList;
                            }
                        }
                        $printConv = shift @printConvList;
                        $index = shift @indexList;
                    } else {
                        $valueConvHash = $$tagInfo{ValueConv} if ref $$tagInfo{ValueConv} eq 'HASH';
                    }
                    while (defined $printConv) {
                        if (defined $index) {
                            # (print indices of original values if reorganized)
                            my $s = '';
                            my $idx = $$tagInfo{Relist} ? $tagInfo->{Relist}->[$index] : $index;
                            if (ref $idx) {
                                $s = 's' if @$idx > 1;
                                # collapse consecutive number ranges
                                my ($i, @i, $rngStart);
                                for ($i=0; $i<@$idx; ++$i) {
                                    if ($i < @$idx - 1 and ($$idx[$i+1] eq 'N' or $$idx[$i+1] == $$idx[$i] + 1)) {
                                        $rngStart = $$idx[$i] unless defined $rngStart;
                                        next;
                                    }
                                    push @i, (defined($rngStart) ? "$rngStart-" : '') . $$idx[$i];
                                }
                                ($idx = join ', ', @i) =~ s/(.*),/$1 and/;
                            } elsif (not $$tagInfo{Relist}) {
                                while (@printConvList and $printConv eq $printConvList[0]) {
                                    shift @printConvList;
                                    $index = shift @indexList;
                                }
                                if ($idx != $index) {
                                    $idx = "$idx-$index";
                                    $s = 's';
                                }
                            }
                            push @values, "[Value$s $idx]";
                        }
                        if ($$tagInfo{SeparateTable}) {
                            $subdir = 1;
                            my $s = $$tagInfo{SeparateTable};
                            $s = $name if $s eq '1';
                            # add module name if not specified
                            $s =~ / / or ($short =~ /^(\w+)/ and $s = "$1 $s");
                            push @values, $s;
                            $sepTable{$s} = $printConv;
                            # add PrintHex flag to PrintConv so we can check it later
                            $$printConv{PrintHex} = 1 if $$tagInfo{PrintHex};
                            $$printConv{PrintInt} = 1 if $$tagInfo{PrintInt};
                            $$printConv{PrintString} = 1 if $$tagInfo{PrintString};
                        } else {
                            $caseInsensitive = 0;
                            my @pk;
                            if ($$tagInfo{PrintSort}) {
                                @pk = sort { NumbersFirst($$printConv{$a},$$printConv{$b}) } keys %$printConv;
                            } else {
                                @pk = sort { NumbersFirst($a,$b) } keys %$printConv;
                            }
                            my $n = scalar @values;
                            my ($bits, $i, $v);
                            foreach (@pk) {
                                next if $_ eq '' and $$printConv{$_} eq '';
                                $_ eq 'BITMASK' and $bits = $$printConv{$_}, next;
                                $_ eq 'OTHER' and next;
                                my $index = $_;
                                $index =~ s/\.\d+$// if $$tagInfo{PrintInt};
                                if (($$tagInfo{PrintHex} or $$printConv{BITMASK}) and $index =~ /^-?\d+$/) {
                                    my $dig = $$tagInfo{PrintHex} || 1;
                                    if ($index >= 0) {
                                        $index = sprintf('0x%.*x', $dig, $index);
                                    } elsif ($format and $format =~ /int(16|32)/) {
                                        # mask off unused bits of signed integer hex value
                                        my $mask = { 16 => 0xffff, 32 => 0xffffffff }->{$1};
                                        $index = sprintf('0x%.*x', $dig, $index & $mask);
                                    }
                                } elsif ($$tagInfo{PrintString} or not /^[+-]?(?=\d|\.\d)\d*(\.\d*)?$/) {
                                    # translate unprintable values
                                    if ($index =~ s/([\x00-\x1f\x80-\xff])/sprintf("\\x%.2x",ord $1)/eg) {
                                        $index = qq{"$index"};
                                    } else {
                                        $index = qq{'${index}'};
                                    }
                                }
                                push @values, "$index = " . $$printConv{$_};
                                if ($valueConvHash) {
                                    foreach $v (keys %$valueConvHash) {
                                        next unless $$valueConvHash{$v} eq $_;
                                        $values[-1] = "$v => " . $values[-1];
                                        last;
                                    }
                                }
                                # validate all PrintConv values
                                if ($$printConv{$_} =~ /[\0-\x1f\x7f-\xff]/) {
                                    warn "Warning: Special characters in $short $name PrintConv ($$printConv{$_})\n";
                                }
                            }
                            if ($bits) {
                                my @pk = sort { NumbersFirst($a,$b) } keys %$bits;
                                foreach (@pk) {
                                    push @values, "Bit $_ = " . $$bits{$_};
                                }
                            }
                            # organize values into columns if specified
                            my $cols = $$tagInfo{PrintConvColumns};
                            if (not $cols and scalar(@values) - $n >= 6) {
                                # do columns if more than 6 short entries
                                my $maxLen = 0;
                                for ($i=$n; $i<@values; ++$i) {
                                    next unless $maxLen < length $values[$i];
                                    $maxLen = length $values[$i];
                                }
                                my $num = scalar(@values) - $n;
                                $cols = int(50 / ($maxLen + 2)); # (50 chars max width)
                                # have 3 rows minimum
                                --$cols while $cols and $num / $cols < 3;
                            }
                            if ($cols) {
                                my @new = splice @values, $n;
                                my $v = '[!HTML]<table class=cols><tr>';
                                my $rows = int((scalar(@new) + $cols - 1) / $cols);
                                for ($n=0; ;) {
                                    $v .= "\n  <td>";
                                    for ($i=0; $i<$rows and $n+$i<@new; ++$i) {
                                        $v .= "\n  <br>" if $i;
                                        $v .= EscapeHTML($new[$n+$i]);
                                    }
                                    $v .= '</td>';
                                    last if ($n += $rows) >= @new;
                                    $v .= '<td>&nbsp;&nbsp;</td>'; # add spaces between columns
                                }
                                push @values, $v . "</tr></table>\n";
                            }
                        }
                        last unless @printConvList;
                        $printConv = shift @printConvList;
                        $index = shift @indexList;
                    }
                # look inside scalar PrintConv for a bit/byte conversion
                # (see Photoshop:PrintFlags for use of "$byte" decoding)
                } elsif ($printConv and $printConv =~ /DecodeBits\(\$(val|byte),\s*(\\\%[\w:]+|\{.*\})\s*\)/s) {
                    my $type = $1 eq 'byte' ? 'Byte' : 'Bit';
                    $$self{Model} = '';   # needed for Nikon ShootingMode
                    my $bits = eval $2;
                    delete $$self{Model};
                    if ($@) {
                        warn $@;
                    } else {
                        my @pk = sort { NumbersFirst($a,$b) } keys %$bits;
                        foreach (@pk) {
                            push @values, "$type $_ = " . $$bits{$_};
                        }
                    }
                }
                if ($subdir and not $$tagInfo{SeparateTable}) {
                    if ($writable) {
                        # subdirectories are only writable if specified explicitly
                        my $tw = $$tagInfo{Writable};
                        $writable = 'yes' if $tw and $writable eq '1' or $writable eq '2';
                        $writable = '-' . ($tw ? $writable : '');
                        $writable .= '!' if $tw and ($$tagInfo{Protected} || 0) & 0x01;
                        $writable .= '+' if $$tagInfo{List};
                        if (defined $$tagInfo{Permanent}) {
                            $writable .= '^' unless $$tagInfo{Permanent};
                        } elsif (defined $$table{PERMANENT}) {
                            $writable .= '^' unless $$table{PERMANENT};
                        }
                    } else {
                        $writable = '-';
                    }
                } else {
                    # not writable if we can't do the inverse conversions
                    my $noPrintConvInv;
                    if ($writable) {
                        foreach ('PrintConv','ValueConv') {
                            next unless $$tagInfo{$_};
                            next if defined $$tagInfo{$_ . 'Inv'};
                            # (undefined inverse conversion overrides hash lookup)
                            unless (exists $$tagInfo{$_ . 'Inv'}) {
                                next if ref($$tagInfo{$_}) =~ /^(HASH|ARRAY)$/;
                                next if $$tagInfo{WriteAlso};
                            }
                            if ($_ eq 'ValueConv') {
                                undef $writable;
                            } else {
                                $noPrintConvInv = 1;
                            }
                        }
                    }
                    if (not $writable) {
                        $writable = 'no';
                    } else {
                        $writable = $format ? $format : 'yes' if $writable eq '1' or $writable eq '2';
                        my $count = $$tagInfo{Count} || 1;
                        # adjust count to Writable size if different than Format
                        if ($writable and $format and $writable ne $format and
                            $writable ne 'string' and $format ne 'string' and
                            $Image::ExifTool::Exif::formatNumber{$writable} and
                            $Image::ExifTool::Exif::formatNumber{$format})
                        {
                            my $n1 = $Image::ExifTool::Exif::formatNumber{$format};
                            my $n2 = $Image::ExifTool::Exif::formatNumber{$writable};
                            $count *= $Image::ExifTool::Exif::formatSize[$n1] /
                                      $Image::ExifTool::Exif::formatSize[$n2];
                        }
                        if ($count != 1) {
                            $count = 'n' if $count < 0;
                            $writable .= "[$count]";
                        }
                        $writable .= '~' if $noPrintConvInv;
                        # add a '*' if this tag is protected or a '!' for unsafe tags
                        if ($$tagInfo{Protected}) {
                            $writable .= '*' if $$tagInfo{Protected} & 0x02;
                            $writable .= '!' if $$tagInfo{Protected} & 0x01;
                        }
                        $writable .= '/' if $$tagInfo{Avoid};
                    }
                    $writable = "=struct" if $struct;
                    $writable .= '_' if defined $$tagInfo{Flat};
                    $writable .= '+' if $$tagInfo{List};
                    $writable .= ':' if $$tagInfo{Mandatory};
                    if (defined $$tagInfo{Permanent}) {
                        $writable .= '^' unless $$tagInfo{Permanent};
                    } elsif (defined $$table{PERMANENT}) {
                        $writable .= '^' unless $$table{PERMANENT};
                    }
                    # separate tables link like subdirectories (flagged with leading '-')
                    $writable = "-$writable" if $subdir;
                }
                # don't duplicate a tag name unless an entry is different
                my $lcName = lc($name);
                # check for conflicts with shortcut names
                if ($isShortcut{$lcName} and $short ne 'Shortcuts' and
                    ($$tagInfo{Writable} or not $$tagInfo{SubDirectory}))
                {
                    warn "WARNING: $short $name is a shortcut tag!\n";
                }
                $name .= '?' if $$tagInfo{Unknown};
                unless (@tagNames and $tagNames[-1] eq $name and
                    $writeGroup[-1] eq $writeGroup and $writable[-1] eq $writable)
                {
                    push @tagNames, $name;
                    push @writeGroup, $writeGroup;
                    push @writable, $writable;
                }
#
# add this tag to the tag lookup unless NO_LOOKUP is set or shortcut or plug-in tag
#
                next if $shortcut or $isPlugin;
                # count tags
                if ($$tagInfo{SubDirectory}) {
                    next if $$vars{NO_LOOKUP};
                    $subdirs{$lcName} or $subdirs{$lcName} = 0;
                    ++$subdirs{$lcName};
                } else {
                    ++$count{'total tags'};
                    unless ($tagExists{$lcName} and
                        (not $subdirs{$lcName} or $subdirs{$lcName} == $tagExists{$lcName}))
                    {
                        ++$count{'unique tag names'} unless $noLookup{$lcName};
                    }
                    # don't add to tag lookup if specified
                    $$vars{NO_LOOKUP} and $noLookup{$lcName} = 1, next;
                }
                $tagExists{$lcName} or $tagExists{$lcName} = 0;
                ++$tagExists{$lcName};
                # only add writable tags to lookup table (for speed)
                my $wflag = $$tagInfo{Writable};
                next unless $writeProc and ($wflag or ($$table{WRITABLE} and
                    not defined $wflag and not $$tagInfo{SubDirectory}));
                $tagLookup{$lcName} or $tagLookup{$lcName} = { };
                # add to lookup for flattened tags if necessary
                if ($$tagInfo{RootTagInfo}) {
                    $flattened{$lcName} or $flattened{$lcName} = { };
                    $flattened{$lcName}{$tableNum} = $$tagInfo{RootTagInfo}{TagID};
                }
                # remember number for this table
                my $tagIDs = $tagLookup{$lcName}->{$tableNum};
                # must allow for duplicate tags with the same name in a single table!
                if ($tagIDs) {
                    if (ref $tagIDs eq 'HASH') {
                        $$tagIDs{$tagID} = 1;
                        next;
                    } elsif ($tagID eq $tagIDs) {
                        next;
                    } else {
                        $tagIDs = { $tagIDs => 1, $tagID => 1 };
                    }
                } else {
                    $tagIDs = $tagID;
                }
                $tableWritable{$tableName} = 1;
                $tagLookup{$lcName}->{$tableNum} = $tagIDs;
                # keep track of extra modules needed for Composite tags
                $compositeModules{$lcName} = $$tagInfo{Module} if $$tagInfo{Module};
            }
#
# save TagName information
#
            my $tagIDstr;
            if ($tagID =~ /^(-)?\d+(\.\d+)?$/) {
                if ($1) {
                    $tagIDstr = $tagID;
                } elsif (defined $hexID) {
                    $tagIDstr = $hexID ? sprintf('0x%.4x',$tagID) : $tagID;
                } elsif (not $2 and not $binaryTable and not $isIPTC and
                         not ($short =~ /^CanonCustom/ and $tagID < 256))
                {
                    $tagIDstr = sprintf('0x%.4x',$tagID);
                } elsif ($tagID < 0x10000) {
                    $tagIDstr = $tagID;
                } else {
                    $tagIDstr = sprintf('0x%.8x',$tagID);
                }
            } elsif ($short eq 'DICOM') {
                ($tagIDstr = $tagID) =~ s/_/,/;
            } elsif ($tagID =~ /^0x([0-9a-f]+)\.(\d+)$/) {  # DR4 tags like '0x20500.0'
                $tagIDstr = $tagID;
            } else {
                # convert non-printable characters to hex escape sequences
                if ($tagID =~ s/([\x00-\x1f\x7f-\xff])/'\x'.unpack('H*',$1)/eg) {
                    $tagID =~ s/\\x00/\\0/g;
                    next if $tagID eq 'jP\x1a\x1a'; # ignore abnormal JP2 signature tag
                    $tagIDstr = qq{"$tagID"};
                } else {
                    $tagIDstr = "'${tagID}'";
                }
            }
            my $len = length $tagIDstr;
            $longID{$tableName} = $len if $longID{$tableName} < $len;
            foreach (@tagNames) {
                $len = length $_;
                $longName{$tableName} = $len if $longName{$tableName} < $len;
            }
            push @$info, [ $tagIDstr, \@tagNames, \@writable, \@values, \@require, \@writeGroup ];
        }
        # do consistency check of writable BinaryData tables
        if ($processBinaryData and $$table{WRITABLE}) {
            my %lookup = (
                IS_OFFSET => \%isOffset,
                IS_SUBDIR => \%hasSubdir,
                DATAMEMBER => \%datamember,
            );
            my ($var, $tagID);
            foreach $var (sort keys %lookup) {
                my $hash = $lookup{$var};
                if ($$table{$var}) {
                    foreach $tagID (@{$$table{$var}}) {
                        $$hash{$tagID} and delete($$hash{$tagID}), next;
                        warn sprintf("Warning: Extra %s for %s tag %d (0x%.4x)\n",
                                     $var, $short, $tagID, $tagID);
                    }
                }
                foreach $tagID (sort keys %$hash) {
                    warn sprintf("Warning: Missing %s for %s %s, tag %s (0x%.4x)\n",
                                 $var, $short, $$hash{$tagID}, $tagID, $tagID);
                }
            }
        }
    }
    # save information about structures
    my $strName;
    foreach $strName (keys %structs) {
        my $struct = $structLookup{$strName};
        my $fullName = ($strName =~ / / ? '' : 'XMP ') . "$strName Struct";
        my $info = $tagNameInfo{$fullName} = [ ];
        my $tag;
        foreach $tag (sort keys %$struct) {
            my $tagInfo = $$struct{$tag};
            next unless ref $tagInfo eq 'HASH' and $tag ne 'NAMESPACE' and $tag ne 'GROUPS';
            warn "WARNING: $strName Struct containes $tag\n" if $Image::ExifTool::specialTags{$tag};
            my $writable = $$tagInfo{Writable};
            my @vals;
            unless ($writable) {
                $writable = $$tagInfo{Struct};
                ref $writable and $writable = $$writable{STRUCT_NAME};
                if ($writable) {
                    push @vals, $writable;
                    $structs{$writable} = 1;
                    $writable = "=$writable";
                } else {
                    $writable = 'string';
                }
            }
            $writable .= '+' if $$tagInfo{List};
            push @vals, "($$tagInfo{Notes})" if $$tagInfo{Notes};
            # handle PrintConv lookups in Structure elements
            my $printConv = $$tagInfo{PrintConv};
            if (ref $printConv eq 'HASH') {
                foreach (sort keys %$printConv) {
                    next if /^(OTHER|BITMASK)$/;
                    push @vals, "$_ = $$printConv{$_}";
                }
            }
            push @$info, [
                $tag,
                [ $$tagInfo{Name} || ucfirst($tag) ],
                [ $writable ],
                \@vals,
                [], []
            ];
        }
    }
    return $self;
}

#------------------------------------------------------------------------------
# Rewrite this file to build the lookup tables
# Inputs: 0) BuildTagLookup object reference
#         1) output tag lookup module name (eg. 'lib/Image/ExifTool/TagLookup.pm')
# Returns: true on success
sub WriteTagLookup($$)
{
    local ($_, *INFILE, *OUTFILE);
    my ($self, $file) = @_;
    my $tagLookup = $self->{TAG_LOOKUP};
    my $tagExists = $self->{TAG_EXISTS};
    my $flattened = $self->{FLATTENED};
    my $tableWritable = $self->{TABLE_WRITABLE};
#
# open/create necessary files and transfer file headers
#
    my $tmpFile = "${file}_tmp";
    open(INFILE, $file) or warn("Can't open $file\n"), return 0;
    unless (open(OUTFILE, ">$tmpFile")) {
        warn "Can't create temporary file $tmpFile\n";
        close(INFILE);
        return 0;
    }
    my $success;
    while (<INFILE>) {
        print OUTFILE $_ or last;
        if (/^#\+{4} Begin/) {
            $success = 1;
            last;
        }
    }
    print OUTFILE "\n# list of tables containing writable tags\n";
    print OUTFILE "my \@tableList = (\n";

#
# write table list
#
    my @tableNames = sort keys %allTables;
    my $tableName;
    my %wrNum;      # translate from allTables index to writable tables index
    my $count = 0;
    my $num = 0;
    foreach $tableName (@tableNames) {
        if ($$tableWritable{$tableName}) {
            print OUTFILE "\t'${tableName}',\n";
            $wrNum{$count} = $num++;
        }
        $count++;
    }
#
# write the tag lookup table
#
    my $tag;
    # verify that certain critical tag names aren't duplicated
    foreach $tag (qw{filename directory}) {
        next unless $$tagLookup{$tag};
        my $n = scalar keys %{$$tagLookup{$tag}};
        warn "Warning: $n writable '${tag}' tags!\n" if $n > 1;
    }
    print OUTFILE ");\n\n# lookup for all writable tags\nmy \%tagLookup = (\n";
    foreach $tag (sort keys %$tagLookup) {
        print OUTFILE "\t'${tag}' => { ";
        my @tableNums = sort { $a <=> $b } keys %{$$tagLookup{$tag}};
        my (@entries, $tableNum);
        foreach $tableNum (@tableNums) {
            my $tagID = $$tagLookup{$tag}{$tableNum};
            my $rootID = $$flattened{$tag}{$tableNum};
            my $entry;
            if (ref $tagID eq 'HASH' or $rootID) {
                $tagID = { $tagID => 1 } unless ref $tagID eq 'HASH';
                my @tagIDs = sort keys %$tagID;
                foreach (@tagIDs) {
                    if (/^\d+$/) {
                        $_ = sprintf('0x%x',$_);
                    } else {
                        my $quot = "'";
                        # escape non-printable characters in tag ID if necessary
                        $quot = '"' if s/[\x00-\x1f,\x7f-\xff]/sprintf('\\x%.2x',ord($&))/ge;
                        $_ = $quot . $_ . $quot;
                    }
                }
                # reference to root structure ID must come first in lookup
                # (so we can generate the flattened tags just before we need them)
                unshift @tagIDs, "\\'${rootID}'" if $rootID;
                $entry = '[' . join(',', @tagIDs) . ']';
            } elsif ($tagID =~ /^\d+$/) {
                $entry = sprintf('0x%x',$tagID);
            } else {
                my $quot = "'";
                # escape non-printable characters in tag ID if necessary
                $quot = '"' if $tagID =~ s/[\x00-\x1f,\x7f-\xff]/sprintf('\\x%.2x',ord($&))/ge;
                $entry = "$quot${tagID}$quot";
            }
            my $wrNum = $wrNum{$tableNum};
            push @entries, "$wrNum => $entry";
        }
        print OUTFILE join(', ', @entries);
        print OUTFILE " },\n";
    }
#
# write tag exists lookup
#
    print OUTFILE ");\n\n# lookup for non-writable tags to check if the name exists\n";
    print OUTFILE "my \%tagExists = (\n";
    foreach $tag (sort keys %$tagExists) {
        next if $$tagLookup{$tag};
        print OUTFILE "\t'${tag}' => 1,\n";
    }
#
# write module lookup for writable composite tags
#
    my $compositeModules = $self->{COMPOSITE_MODULES};
    print OUTFILE ");\n\n# module names for writable Composite tags\n";
    print OUTFILE "my \%compositeModules = (\n";
    foreach (sort keys %$compositeModules) {
        print OUTFILE "\t'${_}' => '$$compositeModules{$_}',\n";
    }
    print OUTFILE ");\n\n";
#
# finish writing TagLookup.pm and clean up
#
    if ($success) {
        $success = 0;
        while (<INFILE>) {
            $success or /^#\+{4} End/ or next;
            print OUTFILE $_;
            $success = 1;
        }
    }
    close(INFILE);
    close(OUTFILE) or $success = 0;
#
# return success code
#
    if ($success) {
        local (*ORG, *TMP);
        # only rename the file if something changed
        open ORG, $file or return 0;
        open TMP, $tmpFile or return 0;
        my ($buff, $buf2, $changed);
        for (;;) {
            my $n1 = read ORG, $buff, 65536;
            my $n2 = read TMP, $buf2, 65536;
            $n1 eq $n2 or $changed = 1, last;
            last unless $n1;
            $buff eq $buf2 or $changed = 1, last;
        }
        close ORG;
        close TMP;
        if ($changed) {
            rename($tmpFile, $file) or warn("Error renaming $tmpFile\n"), $success = 0;
        } else {
            unlink($tmpFile);
        }
    } else {
        unlink($tmpFile);
        warn "Error rewriting file\n";
    }
    return $success;
}

#------------------------------------------------------------------------------
# Sort numbers first numerically, then strings alphabetically (case insensitive)
# - two global variables are used to change the sort algorithm:
#   $numbersFirst: -1 = put numbers after other strings
#                   1 = put numbers before other strings
#                   2 = put numbers first, but negative numbers last
#   $caseInsensitive: flag set for case-insensitive sorting
sub NumbersFirst($$)
{
    my ($a, $b) = @_;
    my $rtnVal;
    my ($bNum, $bDec);
    ($bNum, $bDec) = ($1, $3) if $b =~ /^(-?[0-9]+)(\.(\d*))?$/;
    if ($a =~ /^(-?[0-9]+)(\.(\d*))?$/) {
        if (defined $bNum) {
            $bNum += 1e9 if $numbersFirst == 2 and $bNum < 0;
            my $aInt = $1;
            $aInt += 1e9 if $numbersFirst == 2 and $aInt < 0;
            # compare integer part as a number
            $rtnVal = $aInt <=> $bNum;
            unless ($rtnVal) {
                my $aDec = $3 || 0;
                $bDec or $bDec = 0;
                # compare decimal part as an integer too
                # (so that "1.10" comes after "1.9")
                $rtnVal = $aDec <=> $bDec;
            }
        } else {
            $rtnVal = -$numbersFirst;
        }
    } elsif (defined $bNum) {
        $rtnVal = $numbersFirst;
    } else {
        my ($a2, $b2) = ($a, $b);
        # expand numbers to 3 digits (with restrictions to avoid messing up ascii-hex tags)
        $a2 =~ s/(\d+)/sprintf("%.3d",$1)/eg if $a2 =~ /^(APP|DMC-\w+ )?[.0-9 ]*$/ and length($a2)<16;
        $b2 =~ s/(\d+)/sprintf("%.3d",$1)/eg if $b2 =~ /^(APP|DMC-\w+ )?[.0-9 ]*$/ and length($b2)<16;
        $caseInsensitive and $rtnVal = (lc($a2) cmp lc($b2));
        $rtnVal or $rtnVal = ($a2 cmp $b2);
    }
    return $rtnVal;
}

#------------------------------------------------------------------------------
# Convert our pod-like documentation to pod
# (funny, I know, but the pod headings must be hidden to prevent confusing
#  the pod parser)
# Inputs: 0-N) documentation strings
sub Doc2Pod($;@)
{
    my $doc = shift;
    local $_;
    $doc .= shift while @_;
    $doc =~ s/\n~/\n=/g;
    $doc =~ s/L<[^>]+?\|(http[^>]+)>/L<$1>/g; # POD doesn't support text for http links
    $doc =~ s/L<([^>]+?)\|[^>]+\.html(#\w+)?>/$1/g;  # remove relative HTML links
    return $doc;
}

#------------------------------------------------------------------------------
# Convert our pod-like documentation to html
# Inputs: 0) string
sub Doc2Html($)
{
    my $doc = EscapeHTML(shift);
    $doc =~ s/\n\n/<\/p>\n\n<p>/g;
    $doc =~ s/B&lt;(.*?)&gt;/<b>$1<\/b>/sg;
    $doc =~ s/C&lt;(.*?)&gt;/<code>$1<\/code>/sg;
    $doc =~ s/I&lt;(.*?)&gt;/<i>$1<\/i>/sg;
    # L<some text|https://exiftool.org/struct.html#Fields> --> <a href="../struct.html#Fields">some text</a>
    $doc =~ s{L&lt;([^&]+?)\|\Q$homePage\E/(.*?)&gt;}{<a href="../$2">$1<\/a>}sg;
    # L<https://exiftool.org/struct.html> --> <a href="https://exiftool.org/struct.html">https://exiftool.org/struct.html</a>
    $doc =~ s{L&lt;\Q$homePage\E/(.*?)&gt;}{<a href="../$1">$1<\/a>}sg;
    # L<XMP DICOM Tags|Image::ExifTool::TagNames/XMP DICOM Tags> --> <a href="XMP.html#DICOM">XMP DICOM Tags</a>
    # (specify "Image::ExifTool::TagNames" to link to another html file)
    $doc =~ s{L&lt;([^&]+?)\|Image::ExifTool::TagNames/(\w+) ([^/&|]+) Tags&gt;}{<a href="$2.html#$3">$1</a>}sg;
    # L<DICOM Tags|Image::ExifTool::TagNames/DICOM Tags> --> <a href="DICOM.html">DICOM Tags</a>
    $doc =~ s{L&lt;([^&]+?)\|Image::ExifTool::TagNames/(\w+) Tags&gt;}{<a href="$2.html">$1</a>}sg;
    # L<dc|/XMP dc Tags> --> <a href="#dc">dc</a>
    # (a relative POD link turns into a relative HTML link)
    $doc =~ s{L&lt;([^&]+?)\|/\w+ ([^/&|]+) Tags&gt;}{<a href="#$2">$1</a>}sg;
    # L<sample config file|../config.html> --> <a href="../config.html">sample config file</a>
    $doc =~ s/L&lt;([^&]+?)\|(.+?)&gt;/<a href="$2">$1<\/a>/sg;
    # L<http://some.web.site/> --> <a href="http://some.web.site">http://some.web.site</a>
    $doc =~ s/L&lt;(http.*?)&gt;/<a href="$1">$1<\/a>/sg;
    return $doc;
}

#------------------------------------------------------------------------------
# Tweak order of tables
# Inputs: 0) table list ref, 1) reference to tweak hash
sub TweakOrder($$)
{
    local $_;
    my ($sortedTables, $tweakOrder) = @_;
    my @tweak = sort keys %$tweakOrder;
    my (%addedMain, @sorted);
    # flag files which have a "Main" table
    foreach (@$sortedTables) {
        $addedMain{$1} = 0 if /^Image::ExifTool::(\w+)::(\w+)/ and $2 eq 'Main';
    }
    # make sure that the main table always comes first in each file
    foreach (@$sortedTables) {
        if (/^Image::ExifTool::(\w+)::(\w+)/) {
            if ($addedMain{$1}) {
                next if $2 eq 'Main';   # don't add again
            } elsif (defined $addedMain{$1}) {
                push @sorted, "Image::ExifTool::${1}::Main" if $2 ne 'Main';
                $addedMain{$1} = 1;
            }
        }
        push @sorted, $_;
    }
    @$sortedTables = @sorted;
    # apply manual tweaks
    while (@tweak) {
        my $table = shift @tweak;
        my $first = $$tweakOrder{$table};
        if ($$tweakOrder{$first}) {
            push @tweak, $table;    # must defer this till later
            next;
        }
        delete $$tweakOrder{$table}; # because the table won't move again
        my @moving = grep /^Image::ExifTool::$table\b/, @$sortedTables;
        my @notMoving = grep !/^Image::ExifTool::$table\b/, @$sortedTables;
        my @after;
        while (@notMoving) {
            last if $notMoving[-1] =~ /^Image::ExifTool::$first\b/;
            unshift @after, pop @notMoving;
        }
        @$sortedTables = (@notMoving, @moving, @after);
    }
}

#------------------------------------------------------------------------------
# Get a list of sorted tag ID's from a table
# Inputs: 0) tag table ref
# Returns: list of sorted keys
sub SortedTagTableKeys($)
{
    my $table = shift;
    my $vars = $$table{VARS} || { };
    my @keys = TagTableKeys($table);
    if ($$vars{NO_ID}) {
        # sort by tag name if ID not shown
        my ($key, %name);
        foreach $key (@keys) {
            my ($tagInfo) = GetTagInfoList($table, $key);
            $name{$key} = $$tagInfo{Name};
        }
        return sort { $name{$a} cmp $name{$b} or $a cmp $b } @keys;
    } else {
        my $sortProc = $$vars{SORT_PROC} || \&NumbersFirst;
        return sort { &$sortProc($a,$b) } @keys;
    }
}

#------------------------------------------------------------------------------
# Get the order that we want to print the tables in the documentation
# Inputs: 0-N) Extra tables to add at end
# Returns: tables in the order we want
sub GetTableOrder(@)
{
    my %gotTable;
    my @tableNames = @tableOrder;
    my (@orderedTables, %mainTables, @outOfOrder);
    my $lastTable = '';

    while (@tableNames) {
        my $tableName = shift @tableNames;
        next if $gotTable{$tableName};
        if ($tableName =~ /^Image::ExifTool::(\w+)::Main/) {
            $mainTables{$1} = 1;
        } elsif ($lastTable and not $tableName =~ /^${lastTable}::/) {
            push @outOfOrder, $tableName;
        }
        ($lastTable) = ($tableName =~ /^(Image::ExifTool::\w+)/);
        push @orderedTables, $tableName;
        $gotTable{$tableName} = 1;
        my $table = GetTagTable($tableName);
        # recursively scan through tables in subdirectories
        my @moreTables;
        $caseInsensitive = ($$table{GROUPS} and $$table{GROUPS}{0} eq 'XMP');
        $numbersFirst = -1 if $$table{VARS} and $$table{VARS}{ALPHA_FIRST};
        my @keys = SortedTagTableKeys($table);
        $numbersFirst = 1;
        foreach (@keys) {
            my @infoArray = GetTagInfoList($table,$_);
            my $tagInfo;
            foreach $tagInfo (@infoArray) {
                my $subdir = $$tagInfo{SubDirectory} or next;
                $tableName = $$subdir{TagTable} or next;
                next if $gotTable{$tableName};  # next if table already loaded
                push @moreTables, $tableName;   # must scan this one too
            }
        }
        unshift @tableNames, @moreTables;
    }
    # clean up the order for tables which are out of order
    # (groups all Canon and Kodak tables together)
    my %fixOrder;
    foreach (@outOfOrder) {
        next unless /^Image::ExifTool::(\w+)/;
        # only re-order tables which have a corresponding main table
        next unless $mainTables{$1};
        $fixOrder{$1} = [];     # fix the order of these tables
    }
    my (@sortedTables, %fixPos, $pos);
    foreach (@orderedTables) {
        if (/^Image::ExifTool::(\w+)/ and $fixOrder{$1}) {
            my $fix = $fixOrder{$1};
            unless (@$fix) {
                $pos = @sortedTables;
                $fixPos{$pos} or $fixPos{$pos} = [];
                push @{$fixPos{$pos}}, $1;
            }
            push @{$fix}, $_;
        } else {
            push @sortedTables, $_;
        }
    }
    # insert back in better order
    foreach $pos (sort { $b <=> $a } keys %fixPos) { # (reverse sort)
        my $fix = $fixPos{$pos};
        foreach (@$fix) {
            splice(@sortedTables, $pos, 0, @{$fixOrder{$_}});
        }
    }
    # add the extra tables
    push @sortedTables, @_;
    # tweak the table order
    TweakOrder(\@sortedTables, \%tweakOrder);

    return @sortedTables;
}

#------------------------------------------------------------------------------
# Open HTMLFILE and print header and description
# Inputs: 0) Filename, 1) optional category
# Returns: True on success
my %createdFiles;
sub OpenHtmlFile($;$$)
{
    my ($htmldir, $category, $sepTable) = @_;
    my ($htmlFile, $head, $title, $url, $class);
    my $top = '';

    if ($category) {
        my @names = split ' ', $category;
        $class = shift @names;
        $htmlFile = "$htmldir/TagNames/$class.html";
        $head = $category;
        if ($head =~ /^\S+ .+ Struct$/) {
            pop @names;
        } else {
            $head .= ($sepTable ? ' Values' : ' Tags');
        }
        ($title = $head) =~ s/ .* / /;
        @names and $url = join '_', @names;
    } else {
        $htmlFile = "$htmldir/TagNames/index.html";
        $category = $class = 'ExifTool';
        $head = $title = 'ExifTool Tag Names';
    }
    if ($createdFiles{$htmlFile}) {
        open(HTMLFILE, ">>${htmlFile}_tmp") or return 0;
    } else {
        open(HTMLFILE, ">${htmlFile}_tmp") or return 0;
        print HTMLFILE "$docType<html>\n";
        print HTMLFILE "<!-- (this file generated automatically by Image::ExifTool::BuildTagLookup) -->\n";
        print HTMLFILE "<head>\n<title>$title</title>\n";
        print HTMLFILE "<link rel=stylesheet type='text/css' href='style.css' title='Style'>\n";
        print HTMLFILE "</head>\n<body>\n";
        if ($category ne $class and $docs{$class}) {
            print HTMLFILE "<h2 class=top>$class Tags</h2>\n" or return 0;
            print HTMLFILE '<p>',Doc2Html($docs{$class}),"</p>\n" or return 0;
        } else {
            $top = " class=top";
        }
    }
    $head = "<a name='${url}'>$head</a>" if $url;
    print HTMLFILE "<h2$top>$head</h2>\n" or return 0;
    print HTMLFILE '<p>',Doc2Html($docs{$category}),"</p>\n" if $docs{$category};
    $createdFiles{$htmlFile} = 1;
    return 1;
}

#------------------------------------------------------------------------------
# Close all html files and write trailers
# Returns: true on success
# Inputs: 0) BuildTagLookup object reference
sub CloseHtmlFiles($)
{
    my $self = shift;
    my $preserveDate = $$self{PRESERVE_DATE};
    my $success = 1;
    # get the date
    my ($sec,$min,$hr,$day,$mon,$yr) = localtime;
    my @month = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
    $yr += 1900;
    my $date = "$month[$mon] $day, $yr";
    my $htmlFile;
    my $countNewFiles = 0;
    my $countSameFiles = 0;
    foreach $htmlFile (keys %createdFiles) {
        my $tmpFile = $htmlFile . '_tmp';
        my $fileDate = $date;
        if ($preserveDate) {
            my @lines = `grep '<i>Last revised' $htmlFile`;
            $fileDate = $1 if @lines and $lines[-1] =~ m{<i>Last revised (.*)</i>};
        }
        open(HTMLFILE, ">>$tmpFile") or $success = 0, next;
        # write the trailers
        print HTMLFILE "<hr>\n";
        print HTMLFILE "(This document generated automatically by Image::ExifTool::BuildTagLookup)\n";
        print HTMLFILE "<br><i>Created $createDate</i>\n" if $htmlFile eq 'html/TagNames/index.html';
        print HTMLFILE "<br><i>Last revised $fileDate</i>\n";
        print HTMLFILE "<p class=lf><a href=";
        if ($htmlFile =~ /index\.html$/) {
            print HTMLFILE "'../index.html'>&lt;-- Back to ExifTool home page</a></p>\n";
        } else {
            print HTMLFILE "'index.html'>&lt;-- ExifTool Tag Names</a></p>\n"
        }
        print HTMLFILE "</body>\n</html>\n" or $success = 0;
        close HTMLFILE or $success = 0;
        # check for differences and only use new file if it was changed
        # (so the date only gets updated if changes were really made)
        my $useNewFile;
        if ($success) {
            open (TEMPFILE, $tmpFile) or $success = 0, last;
            if (open (HTMLFILE, $htmlFile)) {
                while (<HTMLFILE>) {
                    my $newLine = <TEMPFILE>;
                    if (defined $newLine) {
                        next if /^<br><i>Last revised/;
                        next if $_ eq $newLine;
                    }
                    # files are different -- use the new file
                    $useNewFile = 1;
                    last;
                }
                $useNewFile = 1 if <TEMPFILE>;
                close HTMLFILE;
            } else {
                $useNewFile = 1;
            }
            close TEMPFILE;
            if ($useNewFile) {
                ++$countNewFiles;
                rename $tmpFile, $htmlFile or warn("Error renaming temporary file\n"), $success = 0;
            } else {
                ++$countSameFiles;
                unlink $tmpFile;   # erase new file and use existing file
            }
        }
        last unless $success;
    }
    # save number of files processed so we can check the results later
    $self->{COUNT}->{'HTML files changed'} = $countNewFiles;
    $self->{COUNT}->{'HTML files unchanged'} = $countSameFiles;
    return $success;
}

#------------------------------------------------------------------------------
# Get bitmask for POD documentation
# Inputs: mask string from HTML docs
# Returns: mask string for POD, or ''
sub PodMask($)
{
    my $mask = shift;
    return '' unless $mask =~ /^\[val( >> (\d+))? \& (0x[\da-f]+)\]/;
    return sprintf(' & 0x%.2x', hex($3) << ($2 || 0));
}

#------------------------------------------------------------------------------
# Write the TagName HTML and POD documentation
# Inputs: 0) BuildTagLookup object reference
#         1) output pod file (eg. 'lib/Image/ExifTool/TagNames.pod')
#         2) output html directory (eg. 'html')
# Returns: true on success
# Notes: My apologies for the patchwork code, but this is only used to generate the docs.
sub WriteTagNames($$)
{
    my ($self, $podFile, $htmldir) = @_;
    my ($tableName, $short, $url, @sepTables, @structs);
    my $tagNameInfo = $self->{TAG_NAME_INFO} or return 0;
    my $idLabel = $self->{ID_LOOKUP};
    my $shortName = $self->{SHORT_NAME};
    my $sepTable = $self->{SEPARATE_TABLE};
    my $structs = $self->{STRUCTURES};
    my $structLookup = $self->{STRUCT_LOOKUP};
    my $success = 1;
    my $columns = 6;    # number of columns in html index
    my $percent = int(100 / $columns);

    # open the file and write the header
    open(PODFILE, ">$podFile") or return 0;
    $docs{ExifTool} = eval qq{"$docs{ExifTool}"};
    print PODFILE Doc2Pod($docs{PodHeader}, $docs{ExifTool}, $docs{ExifTool2});
    mkdir "$htmldir/TagNames", 0777;
    OpenHtmlFile($htmldir) or return 0;
    print HTMLFILE "<blockquote>\n";
    print HTMLFILE "<table width='100%' class=frame><tr><td>\n";
    print HTMLFILE "<table width='100%' class=inner cellspacing=1><tr class=h>\n";
    print HTMLFILE "<th colspan=$columns><span class=l>Tag Table Index</span></th></tr>\n";
    print HTMLFILE "<tr class=b><td width='$percent%'>\n";

    # get the full table list, adding shortcuts and plug-in tables to the end
    my @tableNames = GetTableOrder('Image::ExifTool::Shortcuts::Main', @pluginTables);

    # get list of headings and add any missing ones
    my $heading = 'xxx';
    my (@tableIndexNames, @headings);
    foreach $tableName (@tableNames) {
        $short = $$shortName{$tableName};
        my @names = split ' ', $short;
        my $class = shift @names;
        if (@names and $class ne 'Other') {
            # add heading for tables without a Main
            unless ($heading eq $class) {
                $heading = $class;
                push @tableIndexNames, $heading;
                push @headings, $heading;
            }
        } else {
            $heading = $short;
            push @headings, $heading;
        }
        push @tableIndexNames, $tableName;
    }
    @tableNames = @tableIndexNames;
    # print html index of headings only
    my $count = 0;
    my $lines = int((scalar(@headings) + $columns - 1) / $columns);
    foreach $tableName (@headings) {
        if ($count) {
            if ($count % $lines) {
                print HTMLFILE "<br>\n";
            } else {
                print HTMLFILE "</td><td width='$percent%'>\n";
            }
        }
        $short = $$shortName{$tableName};
        $short = $tableName unless $short;
        $url = "$short.html";
        # handle various tables in "Other.pm"
        if ($short =~ /^Other (.*)/) {
            $short = $1;
            $url = 'Other.html#' . $1;
        } else {
            $url = "$short.html";
        }
        print HTMLFILE "<a href='${url}'>$short</a>";
        ++$count;
    }
    print HTMLFILE "\n</td></tr></table></td></tr></table></blockquote>\n";
    print HTMLFILE '<p>',Doc2Html($docs{ExifTool2}),"</p>\n";

    # write all the tag tables
    while (@tableNames or @sepTables or @structs) {
        while (@sepTables) {
            $tableName = shift @sepTables;
            my $printConv = $$sepTable{$tableName};
            next unless ref $printConv eq 'HASH';
            $$sepTable{$tableName} = 1;
            my $notes = $$printConv{Notes};
            if ($notes) {
                # remove unnecessary whitespace
                $notes =~ s/^\s+//; $notes =~ s/\s+$//;
                $notes =~ s/(^[ \t]+|[ \t]+$)//mg;
            }
            my $head = $tableName;
            $head =~ s/^.* //s;
            close HTMLFILE;
            if (OpenHtmlFile($htmldir, $tableName, 1)) {
                print HTMLFILE '<p>', Doc2Html($notes), "</p>\n" if $notes;
                print HTMLFILE "<blockquote>\n";
                print HTMLFILE "<table class=frame><tr><td>\n";
                print HTMLFILE "<table class='inner sep' cellspacing=1>\n";
                my $align = ' class=r';
                my $wid = 0;
                my @keys;
                foreach (sort { NumbersFirst($a,$b) } keys %$printConv) {
                    next if /^(Notes|PrintHex|PrintInt|PrintString|OTHER)$/;
                    $align = '' if $align and /[^\d]/;
                    my $w = length($_) + length($$printConv{$_});
                    $wid = $w if $wid < $w;
                    push @keys, $_;
                    if ($$printConv{$_} =~ /[\0-\x1f\x7f-\xff]/) {
                        warn "Warning: Special characters in $tableName PrintConv ($$printConv{$_})\n";
                    }
                }
                $wid = length($tableName)+7 if $wid < length($tableName)+7;
                # print in multiple columns if there is room
                my $cols = int(110 / ($wid + 4));
                $cols = 1 if $cols < 1 or $cols > @keys or @keys < 4;
                my $rows = int((scalar(@keys) + $cols - 1) / $cols);
                my ($r, $c);
                print HTMLFILE '<tr class=h>';
                for ($c=0; $c<$cols; ++$c) {
                    print HTMLFILE "<th>Value</th><th>$head</th>";
                }
                print HTMLFILE "</tr>\n";
                for ($r=0; $r<$rows; ++$r) {
                    print HTMLFILE '<tr>';
                    for ($c=0; $c<$cols; ++$c) {
                        my $key = $keys[$r + $c*$rows];
                        my ($index, $prt);
                        if (defined $key) {
                            $index = $key;
                            $prt = '= ' . EscapeHTML($$printConv{$key});
                            $index =~ s/\.\d+$// if $$printConv{PrintInt};
                            if ($$printConv{PrintHex}) {
                                $index =~ s/(\.\d+)$//; # remove decimal
                                $index = sprintf('0x%x',$index);
                                $index .= $1 if $1; # add back decimal
                            } elsif ($$printConv{PrintString} or
                                $index !~ /^[+-]?(?=\d|\.\d)\d*(\.\d*)?$/)
                            {
                                $index = "'" . EscapeHTML($index) . "'";
                            }
                        } else {
                            $index = $prt = '&nbsp;';
                        }
                        my ($ic, $pc);
                        if ($c & 0x01) {
                            $pc = ' class=b';
                            $ic = $align ? " class='r b'" : $pc;
                        } else {
                            $ic = $align;
                            $pc = '';
                        }
                        print HTMLFILE "<td$ic>$index</td><td$pc>$prt</td>\n";
                    }
                    print HTMLFILE '</tr>';
                }
                print HTMLFILE "</table></td></tr></table></blockquote>\n\n";
            }
        }
        last unless @tableNames or @structs;
        my $isStruct;
        if (@structs) {
            $tableName = shift @structs;
            next if $$structs{$tableName} == 2; # only list each structure once
            $$structs{$tableName} = 2;
            $isStruct = $$structLookup{$tableName};
            $isStruct or warn("Missing structure $tableName\n"), next;
            $tableName = "XMP $tableName" unless $tableName =~ / /;
            $short = $tableName = "$tableName Struct";
            my $maxLen = 0;
            $maxLen < length and $maxLen = length foreach keys %$isStruct;
            $$self{LONG_ID}{$tableName} = $maxLen;
        } else {
            $tableName = shift @tableNames;
            $short = $$shortName{$tableName};
            unless ($short) {
                # this is just an index heading
                print PODFILE "\n=head2 $tableName Tags\n";
                print PODFILE Doc2Pod($docs{$tableName}) if $docs{$tableName};
                next;
            }
        }
        my $isExif = ($tableName eq 'Image::ExifTool::Exif::Main');
        my $isRiff = ($tableName eq 'Image::ExifTool::RIFF::Info');
        my $isXmpMain = ($tableName eq 'Image::ExifTool::XMP::Main');
        my $info = $$tagNameInfo{$tableName};
        my $id = $$idLabel{$tableName};
        my ($hid, $showGrp);
        # widths of the different columns in the POD documentation
        my ($wID,$wTag,$wReq,$wGrp) = (8,36,24,10);
        my ($composite, $derived, $notes, $longTags, $wasLong, $prefix);
        if ($short eq 'Shortcuts') {
            $derived = '<th>Refers To</th>';
            $composite = 2;
        } elsif ($isStruct) {
            $derived = '';
            $notes = $$isStruct{NOTES};
        } else {
            my $table = GetTagTable($tableName);
            $notes = $$table{NOTES};
            if ($$table{NAMESPACE}) {
                my $ns = $Image::ExifTool::XMP::stdXlatNS{$$table{NAMESPACE}} || $$table{NAMESPACE};
                my $msg = "These tags belong to the ExifTool XMP-$ns family 1 group.";
                if ($notes) {
                    $notes =~ s/\s+$//;
                    $notes .= "\n\n" . $msg;
                } else {
                    $notes = $msg;
                }
            }
            $longTags = $$table{VARS}{LONG_TAGS} if $$table{VARS};
            if ($$table{GROUPS}{0} eq 'Composite') {
                $composite = 1;
                $derived = '<th>Derived From</th>';
            } else {
                $composite = 0;
                $derived = '';
            }
        }
        my $podIdLen = $self->{LONG_ID}->{$tableName};
        if ($notes) {
            # remove unnecessary whitespace
            $notes =~ s/^\s+//; $notes =~ s/\s+$//;
            $notes =~ s/(^[ \t]+|[ \t]+$)//mg;
            if ($notes =~ /leading '(.*?_)' which/) {
                $prefix = $1;
                $podIdLen -= length $prefix;
            }
        }
        if ($podIdLen <= $wID) {
            $podIdLen = $wID;
        } elsif ($short eq 'DICOM') {
            $podIdLen = 10;
        } else {
            # align tag names in secondary columns if possible
            my $col = ($podIdLen <= 10) ? 12 : 20;
            $podIdLen = $col if $podIdLen < $col;
        }
        if ($id) {
            ($hid = "<th>$id</th>") =~ s/ /&nbsp;/g;
            $wTag -= $podIdLen - $wID;
            $wID = $podIdLen;
            my $longTag = $self->{LONG_NAME}->{$tableName};
            if ($wTag < $longTag) {
                if ($wID - $longTag + $wTag >= 6) { # don't let ID column get too narrow
                    $wID -= $longTag - $wTag;
                    $wTag = $longTag;
                }
                $wasLong = 1 if $wID <= $self->{LONG_ID}->{$tableName};
            }
        } elsif ($composite) {
            $wTag += $wID - $wReq;
            $hid = '';
        } else {
            $wTag += 9;
            $hid = '';
        }
        if ($short eq 'EXIF' or $short eq 'Extra') {
            $derived = '<th>Group</th>';
            $showGrp = 1;
            $wGrp += 8 if $short eq 'Extra';    # tweak Group column width for "Extra" tags
            $wTag -= $wGrp + 1;
        }
        my $head = ($short =~ / /) ? 'head3' : 'head2';
        my $str = $isStruct ? '' : ' Tags';
        print PODFILE "\n=$head $short$str\n";
        print PODFILE Doc2Pod($docs{$short}) if $docs{$short};
        print PODFILE "\n", Doc2Pod($notes), "\n" if $notes;
        my $line = "\n";
        if ($id) {
            # shift over 'Index' heading by one character for a bit more balance
            $id = " $id" if $id eq 'Index';
            $line .= sprintf "  %-${wID}s", $id;
        } else {
            $line .= ' ';
        }
        my $tagNameHeading = ($short eq 'XMP') ? 'Namespace' : ($isStruct?'Field':'Tag').' Name';
        $line .= sprintf " %-${wTag}s", $tagNameHeading;
        $line .= sprintf " %-${wReq}s", $composite == 2 ? 'Refers To' : 'Derived From' if $composite;
        $line .= sprintf " %-${wGrp}s", 'Group' if $showGrp;
        $line .= ' Writable';
        print PODFILE $line;
        $line =~ s/^(\s*\w.{6}\w) /$1\t/;   # change space to tab after long ID label (eg. "Sequence")
        $line =~ s/\S/-/g;
        $line =~ s/- -/---/g;
        $line =~ tr/\t/ /;                  # change tab back to space
        print PODFILE $line,"\n";
        close HTMLFILE;
        OpenHtmlFile($htmldir, $short) or $success = 0;
        print HTMLFILE '<p>',Doc2Html($notes), "</p>\n" if $notes;
        print HTMLFILE "<blockquote>\n";
        print HTMLFILE "<table class=frame><tr><td>\n";
        print HTMLFILE "<table class=inner cellspacing=1>\n";
        print HTMLFILE "<tr class=h>$hid<th>$tagNameHeading</th>\n";
        print HTMLFILE "<th>Writable</th>$derived<th>Values / ${noteFont}Notes</span></th></tr>\n";
        my $rowClass = 1;
        my $infoCount = 0;
        my $infoList;
        foreach $infoList (@$info) {
            ++$infoCount;
            my ($tagIDstr, $tagNames, $writable, $values, $require, $writeGroup) = @$infoList;
            my ($align, $idStr, $w, $tip);
            my $wTag2 = $wTag;
            if (not $id) {
                $idStr = '  ';
            } elsif ($tagIDstr =~ /^-?\d+(\.\d+)?$/) {
                $w = $wID - 3;
                $idStr = sprintf "  %${w}g    ", $tagIDstr;
                my $tooLong = length($idStr) - 6 - $w;
                if ($tooLong) {
                    $tooLong = 3 if $tooLong > 3;
                    $idStr = substr($idStr, 0, -$tooLong);
                }
                $align = " class=r";
            } else {
                $tagIDstr =~ s/^'$prefix/'/ if $prefix;
                $w = $wID;
                my $over = length($tagIDstr) - $w;
                if ($over > 0) {
                    # shift over tag name if there is room
                    if ($over <= $wTag - length($$tagNames[0])) {
                        $wTag2 -= $over;
                        $w += $over;
                    } else {
                        # put tag name on next line if ID is too long
                        $idStr = "  $tagIDstr\n   " . (' ' x $w);
                        if ($longTags) {
                            --$longTags;
                        } else {
                            warn "Notice: Split $$tagNames[0] line\n";
                        }
                    }
                }
                $idStr = sprintf "  %-${w}s ", $tagIDstr unless defined $idStr;
                $align = '';
            }
            my @reqs;
            my @tags = @$tagNames;
            my @wGrp = @$writeGroup;
            my @vals = @$writable;
            my $wrStr = shift @vals;
            my $subdir;
            my @masks = grep /^\[val( >> \d+)? \& 0x[\da-f]+\]/, @$values;
            my $tag = shift @tags;
            # if this is a subdirectory or structure, print subdir name (from values) instead of writable
            if ($wrStr =~ /^[-=]/) {
                $subdir = 1;
                if (@masks) {
                    # combine any mask into the format string
                    $wrStr .= PodMask($masks[0]);
                    shift @masks;
                    @vals = grep !/^\[val( >> \d+)? \& 0x[\da-f]+\]/, @$values;
                } else {
                    @vals = @$values;
                }
                # remove Notes if subdir has Notes as well
                shift @vals if $vals[0] =~ /^\(/ and @vals >= @$writable;
                foreach (@vals) { /^\(/ and $_ = '-' }
                my $i;  # fill in any missing entries from non-directory tags
                for ($i=0; $i<@$writable; ++$i) {
                    $vals[$i] = $$writable[$i] unless defined $vals[$i];
                    if (@masks) {
                        $vals[$i] .= PodMask($masks[0]);
                        shift @masks;
                    }
                }
                if ($$sepTable{$vals[0]}) {
                    $wrStr =~ s/^[-=]//;
                    $wrStr = 'no' unless $wrStr;
                } elsif ($$structs{$vals[0]}) {
                    my $flags = $wrStr =~ /([+_]+)$/ ? $1 : '';
                    $wrStr = "$vals[0] Struct$flags";
                } else {
                    $wrStr = $vals[0];
                }
                shift @vals;
            } elsif ($wrStr and $wrStr ne 'no' and @masks) {
                # fill in missing entries if masks are different
                my $mask = shift @masks;
                while (@masks > @vals) {
                    last if $masks[@vals] eq $mask;
                    push @vals, $wrStr;
                    push @tags, $tag if @tags < @vals;
                }
                # add Mask to Writable column in POD doc
                $wrStr .= PodMask($mask);
            }
            my $pod = sprintf "%s%-${wTag2}s", $idStr, $tag;
            my $tGrp = $wGrp;
            if ($id and length($tag) > $wTag2) {
                my $madeRoom;
                if ($showGrp) {
                    my $wGrp0 = length($wGrp[0] || '-');
                    if (not $composite and $wGrp > $wGrp0) {
                        $tGrp = $wGrp - (length($tag) - $wTag2);
                        if ($tGrp < length $wGrp0) {
                            $tGrp = length $wGrp0;
                        } else {
                            $madeRoom = 1;
                        }
                    }
                }
                warn "Warning: Pushed $tag\n" unless $madeRoom;
            }
            $pod .= sprintf " %-${tGrp}s", shift(@wGrp) || '-' if $showGrp;
            if ($composite) {
                @reqs = @$require;
                $w = $wReq; # Keep writable column in line
                length($tag) > $wTag2 and $w -= length($tag) - $wTag2;
                $pod .= sprintf " %-${w}s", shift(@reqs) || '';
            }
            $pod .= " $wrStr";
            # limit line length to 82 characters (even if it means messing up column alignment)
            if (length $pod > 82 and $pod !~ /\n/) {
                my $remove = length($pod) - 82;
                $pod =~ s/(\w)\s{$remove}/$1/;
            }
            print PODFILE $pod, "\n";
            my $numTags = scalar @$tagNames;
            my $n = 0;
            while (@tags or @reqs or @vals) {
                my $more = (@tags or @reqs);
                $line = '  ';
                $line .= ' 'x($wID+1) if $id;
                $line .= sprintf("%-${wTag2}s", shift(@tags) || '');
                $line .= sprintf(" %-${wReq}s", shift(@reqs) || '') if $composite;
                $line .= sprintf(" %-${wGrp}s", shift(@wGrp) || '-') if $showGrp;
                ++$n;
                if (@vals) {
                    my $val = shift @vals;
                    # use writable if this is a note
                    my $wrStr = $$writable[$n];
                    if ($subdir and ($val =~ /^\(/ or $val =~ /=/ or ($wrStr and $wrStr !~ /^[-=]/))) {
                        $val = $wrStr;
                        if (defined $val) {
                            $val =~ s/^[-=]//;
                        } else {
                            # done with tag if nothing else to print
                            last unless $more;
                        }
                    }
                    if (defined $val) {
                        $line .= " $val";
                        if (@masks) {
                            $line .= PodMask($masks[0]);
                            shift @masks;
                        }
                    }
                }
                $line =~ s/\s+$//;  # trim trailing white space
                print PODFILE "$line\n";
            }
            my @htmlTags;
            foreach (@$tagNames) {
                push @htmlTags, EscapeHTML($_);
            }
            if (($isExif and $Image::ExifTool::Validate::exifSpec{hex $tagIDstr}) or
                ($isRiff and $tagIDstr=~/(\w+)/ and $riffSpec{$1}) or
                ($isXmpMain and $tagIDstr=~/([-\w]+)/ and $xmpSpec{$1}))
            {
                # underline "unknown" makernote tags only
                my $n = $tagIDstr eq '0x927c' ? -1 : 0;
                $htmlTags[$n] = "<u>$htmlTags[$n]</u>";
            }
            $rowClass = $rowClass ? '' : " class=b";
            my $isSubdir;
            if ($$writable[0] =~ /^[-=]/) {
                $isSubdir = 1;
                s/^[-=](.+)/$1/ foreach @$writable;
            }
            # add tooltip for hex conversion of Tag ID
            if ($tagIDstr =~ /^(0x[0-9a-f]+)(\.\d+)?$/i) {
                $tip = sprintf(" title='$tagIDstr = %u%s'", hex($1), $2||'');
            } elsif ($tagIDstr =~ /^(\d+)(\.\d*)?$/) {
                $tip = sprintf(" title='%u = 0x%x'", $1, $1);
            } else {
                $tip = '';
                # use copyright symbol in QuickTime UserData tags
                $tagIDstr =~ s/^"\\xa9/"&copy;/;
                # escape necessary characters in html
                $tagIDstr =~ s/>/&gt;/g;
                $tagIDstr =~ s/</&lt;/g;
            }
            # add tooltip for special writable attributes
            my $wtip = '';
            my %wattr = (
                '_' => 'Flattened',
                '+' => 'List',
                '/' => 'Avoid',
                '~' => 'Writable only with -n',
                '!' => 'Unsafe',
                '*' => 'Protected',
                ':' => 'Mandatory',
                '^' => 'Deletable',
            );
            my ($wstr, %hasAttr, @hasAttr);
            foreach $wstr (@$writable) {
                next unless $wstr =~ m{([+/~!*:_^]+)$};
                my @a = split //, $1;
                foreach (@a) {
                    next if $hasAttr{$_};
                    push @hasAttr, $_;
                    $hasAttr{$_} = 1;
                }
            }
            if (@hasAttr) {
                $wtip = " title='";
                my $n = 0;
                foreach (@hasAttr) {
                    $wtip .= "\n" if $n;
                    $wtip .= " $_ = $wattr{$_}";
                    ++$n;
                }
                $wtip .= "'";
            }
            # print this row in the tag table
            print HTMLFILE "<tr$rowClass>\n";
            print HTMLFILE "<td$align$tip>$tagIDstr</td>\n" if $id;
            print HTMLFILE "<td>", join("\n  <br>",@htmlTags), "</td>\n";
            print HTMLFILE "<td class=c$wtip>",join('<br>',@$writable),"</td>\n";
            print HTMLFILE '<td class=n>',join("\n  <br>",@$require),"</td>\n" if $composite;
            print HTMLFILE "<td class=c>",join('<br>',@$writeGroup),"</td>\n" if $showGrp;
            print HTMLFILE "<td>";
            if (@$values) {
                if ($isSubdir) {
                    my ($smallNote, @values);
                    foreach (@$values) {
                        if (/^\(/) {
                            $_ = Doc2Html($_);
                            # set the note font
                            $smallNote = 1 if $numTags < 2;
                            push @values, ($smallNote ? $noteFontSmall : $noteFont) . "$_</span>";
                            next;
                        }
                        # make text in square brackets small
                        if (/^\[/) {
                            if (s/^\[!HTML\]//) {
                                push @values, $_;
                            } else {
                                $_ = Doc2Html($_);
                                push @values, "<span class=s>$_</span>";
                            }
                            next;
                        }
                        /=/ and push(@values, $_), next;
                        my @names = split;
                        my $suffix = ' Tags';
                        if ($$sepTable{$_}) {
                            push @sepTables, $_;
                            $suffix = ' Values';
                        }
                        # handle structures specially
                        if ($$structs{$_}) {
                            # assume XMP module for this struct unless otherwise specified
                            unshift @names, 'XMP' unless / /;
                            push @structs, $_;  # list this later
                            # hack to put Area Struct in with XMP tags,
                            # even though it is only used by the MWG module
                            push @structs, 'Area' if $_ eq 'Dimensions';
                            $suffix = ' Struct';
                        }
                        $url = (shift @names) . '.html';
                        @names and $url .= '#' . join '_', @names;
                        push @values, "--&gt; <a href='${url}'>$_$suffix</a>";
                    }
                    # put small note last
                    $smallNote and push @values, shift @values;
                    print HTMLFILE join("\n  <br>",@values);
                } else {
                    my ($close, $br) = ('', '');
                    foreach (@$values) {
                        if (s/^\[!HTML\]//) {
                            print HTMLFILE $close if $close;
                            print HTMLFILE $_;
                            $close = $br = '';
                        } else {
                            if (/^\(/) {
                                # notes can use POD syntax
                                $_ = $noteFont . Doc2Html($_) . "</span>";
                            } else {
                                $_ = EscapeHTML($_);
                            }
                            $close or $_ = "<span class=s>$_", $close = '</span>';
                            print HTMLFILE $br, $_;
                            $br = "\n  <br>";
                        }
                    }
                    print HTMLFILE $close if $close;
                }
            } else {
                print HTMLFILE '&nbsp;';
            }
            print HTMLFILE "</td></tr>\n";
        }
        warn "$longTags unaccounted-for long tags in $tableName\n" if $longTags;
        if ($wasLong and not defined $longTags) {
            warn "Notice: Long tags in $tableName table\n";
        }
        unless ($infoCount) {
            print PODFILE "  [no tags known]\n";
            my $cols = 3;
            ++$cols if $hid;
            ++$cols if $derived;
            print HTMLFILE "<tr><td colspan=$cols class=c><i>[no tags known]</i></td></tr>\n";
        }
        print HTMLFILE "</table></td></tr></table></blockquote>\n\n";
    }
    close(HTMLFILE) or $success = 0;
    CloseHtmlFiles($self) or $success = 0;
    print PODFILE Doc2Pod($docs{PodTrailer}) or $success = 0;
    close(PODFILE) or $success = 0;
    return $success;
}

1;  # end


__END__

=head1 NAME

Image::ExifTool::BuildTagLookup - Build ExifTool tag lookup tables

=head1 DESCRIPTION

This module is used to generate the tag lookup tables in
Image::ExifTool::TagLookup.pm and tag name documentation in
Image::ExifTool::TagNames.pod, as well as HTML tag name documentation.  It
is used before each new ExifTool release to update the lookup tables and
documentation, but it is not used otherwise.  It also performs some
validation and consistency checks on the tag tables.

=head1 SYNOPSIS

  use Image::ExifTool::BuildTagLookup;

  $builder = new Image::ExifTool::BuildTagLookup;

  # update Image::ExifTool::TagLookup
  $ok = $builder->WriteTagLookup('lib/Image/ExifTool/TagLookup.pm');

  # update the tag name documentation
  $ok = $builder->WriteTagNames('lib/Image/ExifTool/TagNames.pod','html');

  # print some statistics
  my $count = $$builder{COUNT};
  foreach (sort keys %$count) {
      printf "%5d %s\n", $$count{$_}, $_;
  }

=head1 MEMBER VARIABLES

=over 4

=item PRESERVE_DATE

Flag to preserve "Last revised" date in HTML files.  Set before calling
WriteTagNames().

=item COUNT

Reference to hash containing counting statistics.  Keys are the
descriptions, and values are the numerical counts.  Valid after
BuildTagLookup object is created, but additional statistics are added by
WriteTagNames().

=item WRITE_PSEUDO

Returned list of writable pseudo tags.

=back

=head1 AUTHOR

Copyright 2003-2024, Phil Harvey (philharvey66 at gmail.com)

This library is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.

=head1 SEE ALSO

L<Image::ExifTool(3pm)|Image::ExifTool>,
L<Image::ExifTool::TagLookup(3pm)|Image::ExifTool::TagLookup>,
L<Image::ExifTool::TagNames(3pm)|Image::ExifTool::TagNames>

=cut