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
package Torello.HTML.Tools.Images;

import Torello.HTML.*;
import Torello.Java.*;

import Torello.HTML.NodeSearch.TagNodeFind;
import Torello.Java.Additional.Ret2;
import Torello.Java.Additional.Counter;
import Torello.Java.Shell.C;

import java.net.*;
import java.io.*;
import java.util.*;
import java.util.regex.*;
import java.util.function.*;
import javax.imageio.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.*;

import java.awt.image.BufferedImage;

/**
 * <CODE>ImageScraper - Documentation.</CODE><BR /><BR />
 * <EMBED CLASS="external-html" DATA-FILE-ID="ISR">
 */
public class ImageScraper
{
    /**
     * This is the default maximum wait time for an image to download ({@value}).  This value may
     * be reset or modified by instantiating a {@code ImageScraper.AdditionalParameters} class, and
     * passing the desired values to the constructor.  This value is measured in units of
     * {@code public static final java.util.concurrent.TimeUnit MAX_WAIT_TIME_UNIT}
     *
     * @see #MAX_WAIT_TIME_UNIT
     */
    public static final long        MAX_WAIT_TIME       = 10;

    /**
     * This is the default measuring unit for the {@code static final long MAX_WAIT_TIME} member.
     * This value may be reset or modified by instantiating a 
     * {@code ImageScraper.AdditionalParameters} class, and passing the desired values to the
     * constructor.
     *
     * @see #MAX_WAIT_TIME
     */
    public static final TimeUnit MAX_WAIT_TIME_UNIT  = TimeUnit.SECONDS;

    /** <EMBED CLASS="external-html" DATA-FILE-ID="ISUA"> */
    public static String USER_AGENT = "Chrome/61.0.3163.100";

    private final   Iterable<String>            source;
    private final   URL                         originalPageURL;

    private final   String                      targetDirectory;
    private final   TargetDirectoryRetriever    targetDirectoryRetriever;
    private final   ImageReceiver               imageReceiver;

    /**
     * <CODE>TargetDirectoryRetriever - Documentation.</CODE><BR /><BR />
     * <EMBED CLASS="external-html" DATA-FILE-ID="TDR">
     */
    @FunctionalInterface
    public static interface TargetDirectoryRetriever extends java.io.Serializable
    {
        /** <EMBED CLASS="external-html" DATA-FILE-ID="SVUIDFI">  */
        public static final long serialVersionUID = 1;

        /**
         * The {@code dir(...)} method within this interface will be called each time that an image
         * successfully downloads from the internet.  It's purpose is to allow the programmer to
         * supply a target directory for where to store this downloaded image.  Implement the
         * lone-method from this interface, and images will be saved to individual save-directories
         * on an image-by-image basis.  If a {@code interface TargetDirectorieRetriever} is not
         * provided, then all images will be saved to a single target-directory (or the
         * {@code interface 'ImageReceiver"} must be implemented).
         *
         * @param url This is the {@code URL} that was used to connect to the internet, and
         * download the image in question.
         *
         * @param fileName This parameter will receive the computed filename of the image.
         *
         * @param imageFormat This identifies whether the image-in-question is a {@code GIF, JPG, 
         * PNG} etc...  Remember the image might not be saved by the same name which was used in 
         * the HTML on the website from which this was downloaded.
         *
         * @param iteratorCount <EMBED CLASS="external-html" DATA-FILE-ID="ISIC">
         *
         * @param successCount <EMBED CLASS="external-html" DATA-FILE-ID="ISSC">
         *
         * @return It is up to the user implement this method such that it returns a {@code String}
         * that identifies an appropriate directory in the local filesystem where the image may be
         * saved.
         */
        public String dir
            (URL url, String fileName, IF imageFormat, int iteratorCount, int successCount);
    }

    /**
     * <CODE>ImageReceiver - Documentation.</CODE><BR /><BR />
     * <EMBED CLASS="external-html" DATA-FILE-ID="IMGR">
     */
    @FunctionalInterface
    public static interface ImageReceiver extends java.io.Serializable
    {
        /** <EMBED CLASS="external-html" DATA-FILE-ID="SVUIDFI">  */
        public static final long serialVersionUID = 1;

        /**
         * Implement this class if saving image files to a target-directory on the file-system is
         * not acceptable, and the programmer wishes to do something else with the downloaded
         * images.  The lone-method in this interface (the "save" method) will be invoked each
         * time and image is downloaded.
         *
         * @param url This is the {@code URL} that was used to connect to the internet, and
         * download the image in question.
         *
         * @param fileName This parameter will receive the computed filename of the image.
         *
         * @param imageFormat This identifies whether the image-in-question is a {@code GIF, JPG,
         * PNG, etc...}  Remember the image might not be saved by the same name which was used in
         * the HTML on the website from which this was downloaded.
         *
         * @param iteratorCount <EMBED CLASS="external-html" DATA-FILE-ID="ISIC">
         *
         * @param successCount <EMBED CLASS="external-html" DATA-FILE-ID="ISSC">
         *
         * @param image This is the newly downloaded image.
         */
        public void save(
            URL url, String fileName, IF imageFormat, int iteratorCount,
            int successCount, BufferedImage image
        );
    }

    /**
     * <CODE>FileNameRetriever - Documentation.</CODE><BR /><BR />
     * <EMBED CLASS="external-html" DATA-FILE-ID="FNR">
     */
    @FunctionalInterface
    public static interface FileNameRetriever extends java.io.Serializable
    {
        /** <EMBED CLASS="external-html" DATA-FILE-ID="SVUIDFI">  */
        public static final long serialVersionUID = 1;

        /**
         * The intention of implementing this method is to provide the code with an 'adjusted'
         * file name for saving images downloaded from the internet.
         *
         * @param url This is the {@code URL} that was used to connect to the internet, and
         * download the image in question.
         *
         * @param imageFormat This identifies whether the image-in-question is a {@code GIF, JPG, 
         * PNG,} etc...  Remember the image might not be saved by the same name which was used in
         * the HTML on the website from which this was downloaded.
         *
         * @param iteratorCount <EMBED CLASS="external-html" DATA-FILE-ID="ISIC">
         * @param successCount <EMBED CLASS="external-html" DATA-FILE-ID="ISSC">
         *
         * @return utilizing the information provided in the method-signature, the programmer is
         * expected to provide a file-name for saving the image that was provided.
         */
        public String fileName(URL url, IF imageFormat, int iteratorCount, int successCount);
    }

    // *************************************************************************************
    // source is Iterable<URL>
    // *************************************************************************************

    private static final Iterable<String> URLVecToStringVec(Iterable<URL> source)
    {
        Vector<String> ret = new Vector<>();
        source.forEach((URL url) -> ret.add(url.toString()));
        return ret;
    }

    /**
     * Convenience Constructor.  Invokes {@link #ImageScraper(URL, Iterable, String)}
     * <BR /><BR />Converts {@code Iterable<URL>} to {@code Iterable<String>}.
     */
    public ImageScraper(Iterable<URL> source, String targetDirectory)
    { this(null, URLVecToStringVec(source), targetDirectory); }

    /**
     * Convenience Constructor.  Invokes {@link #ImageScraper(URL, Iterable, TargetDirectoryRetriever)}
     * <BR /><BR />Converts {@code Iterable<URL>} to {@code Iterable<String>}.
     */
    public ImageScraper(Iterable<URL> source, TargetDirectoryRetriever targetDirectoryRetriever)
    { this(null, URLVecToStringVec(source), targetDirectoryRetriever); }

    /**
     * Convenience Constructor.  Invokes {@link #ImageScraper(URL, Iterable, ImageReceiver)}
     * <BR /><BR />Converts {@code Iterable<URL>} to {@code Iterable<String>}.
     */
    public ImageScraper(Iterable<URL> source, ImageReceiver imageReceiver)
    { this(null, URLVecToStringVec(source), imageReceiver); }

    // *************************************************************************************
    // source is Iterable<TagNode>, URL
    // *************************************************************************************

    private static final Iterable<String> TagNodeVecToStringVec(Iterable<TagNode> source)
    {
        Vector<String> ret = new Vector<>();
        source.forEach((TagNode tn) -> ret.add(tn.AV("src")));
        return ret;
    }

    /**
     * Convenience Constructor.  Invokes {@link #ImageScraper(URL, Iterable, String)}
     * <BR /><BR />Converts {@code Iterable<TagNode>} to {@String[]} using {@link TagNode#AV(String)}
     * 
     * @param source This may be any java {@code Iterable<TagNode>}.  The {@code TagNode's} are
     * expected to contain HTML {@code <IMG SRC="...">} tags.
     */
    public ImageScraper(Iterable<TagNode> source, URL originalPageURL, String targetDirectory)
    { this(originalPageURL, TagNodeVecToStringVec(source), targetDirectory); }

    /**
     * Convenience Constructor.  Invokes {@link #ImageScraper(URL, Iterable, TargetDirectoryRetriever)}
     * <BR /><BR />Converts {@code Iterable<TagNode>} to {@String[]} using {@link TagNode#AV(String)}
     * 
     * @param source This may be any java {@code Iterable<TagNode>}.  The {@code TagNode's} are
     * expected to contain HTML {@code <IMG SRC="...">} tags.
     */
    public ImageScraper(Iterable<TagNode> source, URL originalPageURL, TargetDirectoryRetriever targetDirectoryRetriever)
    { this(originalPageURL, TagNodeVecToStringVec(source), targetDirectoryRetriever); }

    /**
     * Convenience Constructor.  Invokes {@link #ImageScraper(URL, Iterable, ImageReceiver)}
     * <BR /><BR />Converts {@code Iterable<TagNode>} to {@String[]} using {@link TagNode#AV(String)}
     * 
     * @param source This may be any java {@code Iterable<TagNode>}.  The {@code TagNode's} are
     * expected to contain HTML {@code <IMG SRC="...">} tags.
     */
    public ImageScraper(Iterable<TagNode> source, URL originalPageURL, ImageReceiver imageReceiver)
    { this(originalPageURL, TagNodeVecToStringVec(source), imageReceiver); }

    // *************************************************************************************
    // source is Iterable<String>, URL
    // *************************************************************************************

    /**
     * Constructor that allows a user to provide a set of {@code URL's} as {@code String's} to the
     * download mechanism.
     *
     * @param source This is a {@code Vector<String>} of Image {@code URL's} saved as a
     * {@code String}.
     *
     * @param originalPageURL <EMBED CLASS="external-html" DATA-FILE-ID="ISOPURL">
     *
     * @param targetDirectory When this constructor is used, this {@code String} parameter
     * identifies the directory to where files must be saved.
     *
     * @throws NullPointerException If any of the elements of the input {@code Iterable<String>}
     * are null elements, then this Exception shall be thrown.
     *
     * @throws WritableDirectoryException This constructor shall check that parameter
     * {@code 'targetDirectory'} exists on the file-system, and is writable.  A small, temporary,
     * file shall be written to check this.
     */
    public ImageScraper(URL originalPageURL, Iterable<String> source, String targetDirectory)
    {
        this.source                     = source;
        this.originalPageURL            = originalPageURL;

        // Ensures that the target directory exists, and is writable
        WritableDirectoryException.check(targetDirectory);

        // Makes sure that the directory ends with a slash / file-separator.
        if (! targetDirectory.endsWith(File.separator)) if (targetDirectory.length() > 0)
            targetDirectory = targetDirectory + File.separator;

        this.targetDirectory            = targetDirectory;
        this.targetDirectoryRetriever   = null;
        this.imageReceiver              = null;

        if (source == null)
            throw new NullPointerException("parameter source is null");

        if (targetDirectory == null)
            throw new NullPointerException("parameter targetDirectory is null");
    }

    /**
     * Constructor that allows a user to provide a set of {@code URL's} as {@code String's} to
     * the download mechanism.
     *
     * @param source This is a {@code Vector<String>} of Image {@code URL's} saved as a
     * {@code String}.
     *
     * @param originalPageURL <EMBED CLASS="external-html" DATA-FILE-ID="ISOPURL">
     *
     * @param targetDirectoryRetriever This parameter must implement the static-inner
     * {@code class TargetDirectoryRetriever}.  This parameter allows the programmer to make a 
     * decision where image-files are stored after they are downloaded one a file-by-file basis.
     *
     * @throws NullPointerException If any of the elements of the input {@code Iterable<String>}
     * are null elements, then this Exception shall be thrown.
     */
    public ImageScraper(
        URL originalPageURL, Iterable<String> source,
        TargetDirectoryRetriever targetDirectoryRetriever
    )
    {
        this.source                     = source;
        this.originalPageURL            = originalPageURL;
        this.targetDirectory            = null;
        this.targetDirectoryRetriever   = targetDirectoryRetriever;
        this.imageReceiver              = null;

        if (source == null)
            throw new NullPointerException("parameter source is null");

        if (targetDirectoryRetriever == null)
            throw new NullPointerException("targetDirectoryRetriever is null");
    }

    /**
     * Constructor that allows a user to provide a set of {@code URL's} as {@code String's} to the
     * download mechanism.
     *
     * @param source This is a {@code Vector<String>} of Image {@code URL's} saved as a
     * {@code String}.
     *
     * @param originalPageURL <EMBED CLASS="external-html" DATA-FILE-ID="ISOPURL">
     *
     * @param imageReceiver This parameter allows the programmer to circumvent the "save-to-file"
     * portion of the code, and instead send the downloaded image to this interface.
     *
     * @throws NullPointerException If any of the elements of the input {@code Iterable<String>} 
     * are null elements, then this exception shall be thrown.
     */
    public ImageScraper(URL originalPageURL, Iterable<String> source, ImageReceiver imageReceiver)
    {
        this.source                     = source;
        this.originalPageURL            = originalPageURL;

        this.targetDirectory            = null;
        this.targetDirectoryRetriever   = null;
        this.imageReceiver              = imageReceiver;

        if (source == null)
            throw new NullPointerException("parameter source is null");

        if (imageReceiver == null)
            throw new NullPointerException("imageReceiver is null");
    }

    // *************************************************************************************
    // *************************************************************************************
    // More available download configuration parameters
    // *************************************************************************************
    // *************************************************************************************

    /**
     * <CODE>AdditionalParameters - Documentation.</CODE><BR /><BR />
     * <EMBED CLASS="external-html" DATA-FILE-ID="ADPA">
     */
    public static class AdditionalParameters
    {
        /** <EMBED CLASS="external-html" DATA-FILE-ID="SVUID"> */
        public static final long serialVersionUID = 1;
    
        /**
         * When this field is <B>TRUE</B>, if an attempt to download an image generates an
         * exception, the exception-throw <I>will not halt the download</I>, but rather the image
         * will be skipped, and download attempt will be performed on the next image in the list.
         * The exception will be stored in the {@code 'Results'} return object.
         */
        public final boolean                skipOnIOException;

        /**
         * When this field is null, it is ignored.  If this field is not null, then before any
         * {@code URL} is connected for download, the downloaded mechanism will ask this
         * {@code URL-Predicate} for permission first.  If this {@code Predicate} returns
         * <B>FALSE</B> for a particular <B>URL,</B> that image will not be downloaded, and
         * instead, skipped.
         */
        public final Predicate<URL>         skipURL;

        /**
         * When this field is null, it is ignored.  If not null, this {@code String} will be
         * <I>prepended</I> to each file-name that is saved or stored to the file-system.
         */
        public final String                 fileNamePrefix;

        /**
         * When true, images will be saved according to a counter.  When this is <B>FALSE</B>, the
         * software will attempt to save these images using their original filenames - picked from
         * the <B>URL.</B> Saving using a counter is the default behaviour for this class.
         */
        public final boolean                useDefaultCounterForImageFileNames;

        /**
         * When this field is null, it is ignored.  If not null, each time an image is written to
         * the file-system, this {@code java.util.function.Function<URL, String>} will be queried
         * for a file-name before writing the the image-file to the file-system.  If this field is
         * non-null, but images are being sent to {@code Consumer<BufferedImage, IF> 
         * downloadedImageAltTarget}, rather than being saved to the file-system, then this field
         * is <I>also ignored</I>.
         */
        public final FileNameRetriever      getImageFileSaveName;

        /**
         * This scraper has the ability to decode and save {@code Base-64} Images.  If an
         * {@code Iterable<TagNode>} is passed to the constructor, and one of those
         * {@code TagNode's} contain an Image Element
         * ({@code <IMG SRC="data:image/jpeg;base64,...data">}) this class has the ability to
         * interpret and save the image to a regular image file.  By default, {@code Base-64}
         * images are skipped, but they can also be downloaded as well.
         */
        public final boolean                skipBase64EncodedImages;

        /**
         * If you do not want the downloader to hang on an image, which is sometimes an issue
         * depending upon the site from which you are making a request, set this parameter, and the
         * downloader will not wait past that amount of time to download an image.  The default
         * value for this parameter is {@code 10 seconds}.  If you do not wish to set the
         * max-wait-time "the download time-out" counter, then leave the parameter
         * {@code "waitTimeUnits"} set to {@code null}, and this parameter will be ignored.
         */
        public final long                   maxDownloadWaitTime;

        /**
         * This is the "unit of measurement" for the field {@code long maxDownloadWaitTime}.
         * <BR /><BR /><B>NOTE:</B> <I>This parameter may be {@code null}, and if it is
         * <SPAN STYLE="color: red;"> both <B>this</B> parameter and the parameter <B>{@code long
         * maxDownloadWaitTime}</B> will be ignored</SPAN></I>, and the default maximum-wait-time
         * (download time-out settings) will be used instead.
         *
         * <BR /><BR /><B>READ:</B> java.util.concurrent.*; package, and about the {@code class
         * java.util.concurrent.TimeUnit} for more information.
         */
        public final TimeUnit               waitTimeUnits;

        /**
         * Use this constructor to instantiate this class.  Read what each of these parameters
         * means to the downloader, by reading the comment information for each of these fields
         * in this class (above).
         *
         * @param skipOnIOException This will "skip" an image, and prevent the downloading process from
         * halting if an image fails to download
         *
         * @param skipURL A java {@code Predicate} for deciding which images should be skipped.
         * This parameter may be 'null.'  If it is, it will be ignored, and the downloader will
         * attempt to download all images.
         *
         * @param fileNamePrefix A standard Java-{@code String} may be inserted before the
         * file-name of each image downloaded, as a 'file-name prefix'.  This parameter may be
         * null, and if it is file-name prefixes will not be used.
         *
         * @param useDefaultCounterForImageFileNames It is usually a good idea to replace the
         * file-name for an image retrieved from a web-site with a simple, three-digit,
         * counter-name.  Image file names on a web-site can often be long {@code PKID Strings}
         * obtained from {@code SQL} database queries. To use a standard "counter" set this
         * parameter to <B>TRUE</B>.
         *
         * @param getImageFileSaveName This parameter may be used to convert image file-names used
         * on a web-page to user-generated image-file-names.  This parameter may be null, and if it
         * is - it will be ignored.  If this parameter is non-null, it takes precedence over the
         * {@code boolean} passed to parameter {@code 'useDefaultCounterForImageFileNames'}
         *
         * @param skipBase64EncodedImages This will order the downloader to convert and save HTML
         * Image Elements whose image-data was encoded into HTML Element, itself, using
         * {@code Base-64} Image-Encoding.  Thumbnails and other small images are sometimes stored
         * on web-pages using such encoding.
         *
         * @param maxDownloadWaitTime This parameter will be ignored unless a non-null value has
         * been passed to parameter {@code 'waitTimeUnits'}.  This may be used to prevent the
         * downloader from hanging when collecting images for a web-page.
         *
         * @param waitTimeUnits This is java {@code class TimeUnit} parameter for describing what
         * units are being used for the previous parameter, {@code 'maxDownloadWaitTime'}.
         */
        public AdditionalParameters(
            boolean                 skipOnIOException,
            Predicate<URL>          skipURL,
            String                  fileNamePrefix,
            boolean                 useDefaultCounterForImageFileNames,
            FileNameRetriever       getImageFileSaveName,
            boolean                 skipBase64EncodedImages,
            long                    maxDownloadWaitTime,
            TimeUnit                waitTimeUnits
        )
        {
            this.skipOnIOException                      = skipOnIOException;
            this.skipURL                                = skipURL;
            this.fileNamePrefix                         = fileNamePrefix;
            this.useDefaultCounterForImageFileNames     = useDefaultCounterForImageFileNames;
            this.getImageFileSaveName                   = getImageFileSaveName;
            this.skipBase64EncodedImages                = skipBase64EncodedImages;
            this.maxDownloadWaitTime                    = maxDownloadWaitTime;
            this.waitTimeUnits                          = waitTimeUnits;

            if (maxDownloadWaitTime < 0) throw new IllegalArgumentException(
                "You have passed a negative number for parameter maxDownloadWaitTime, and this is " +
                "not allowed here."
            );
        }

        /**
         * This constructor will return an instance of {@code AdditionalParameters} whose values
         * provide the following <B>MOST COMMON</B> behaviour choices:
         *
         * <BR /><TABLE CLASS="BRIEFTABLE">
         * <TR><TH>Parameter</TH><TH>Value</TH></TR>
         * <TR><TD>{@code skipOnIOException}</TD><TD>{@code TRUE}</TD></TR>
         * <TR><TD>{@code useDefaultCounterForImageFileNames}</TD><TD>{@code TRUE}</TD></TR>
         * <TR><TD>{@code skipBase64EncodedImages}</TD><TD>{@code FALSE}</TD></TR>
         * <TR><TD COLSPAN="2"><I>All other parameters set to 'null', and will be ignored.</I>
         * </TD></TR>
         * </TABLE>
         */
        public AdditionalParameters()
        { this(true, null, null, true, null, false, 0, null); }
    }

    // *************************************************************************************
    // *************************************************************************************
    // Results inner class
    // *************************************************************************************
    // *************************************************************************************

    /**
     * <CODE>ImageScraper Results - Documentation.</CODE><BR /><BR />
     * <EMBED CLASS="external-html" DATA-FILE-ID="ISRES">
     */
    public static class Results
    {
        /**
         * The java serializable tools can be very beneficial for saving the state of a program you
         * are testing.  Though it is unlikely a programmer would want to transmit this
         * 'results-report' class around (or at least I cannot think of much use), saving the state
         * of web-page scrape and all the testing routines that have been used is something that
         * can be really helpful.  <I><SPAN STYLE="color: red;">This is why most of the classes
         * that can be created / instantiated - a.k.a. non-static classes - implement the
         * Serializable interface</SPAN></I>.  It's a great debugging tool.
         */
        public static final long serialVersionUID = 1;

        /**
         * This will contain a complete list of the {@code URL's} that were retrieved (or generated-
         * <I>if partially-resolved 'relative' {@code URL's} occurred</I>).  Every image downloaded
         * (or attempted for download) will have its {@code URL} saved here.
         *
         * <BR /><BR />The index of this array will be parallel to the input-source {@code URL}
         * retrieval order.
         */
        public final URL[] urls;

        /**
         * If the "skip" {@code Predicate} declares that a particular image-download should not be
         * attempted, <I>FALSE</I> will be stored in this array.
         *
         * <BR /><BR />The index of this array will be parallel to the input-source {@code URL}
         * retrieval order.
         */
        public final boolean[] skipped;

        /**
         * The names of the files that were retrieved and/or stored will be in this array.
         * If this image were skipped or an exception occurred, the array position for that
         * {@code URL} would contain 'null'.
         *
         * <BR /><BR />The index of this array will be parallel to the input-source {@code URL}
         * retrieval order.
         */
        public final String[] fileNames;

        /**
         * The location of the file-name saved directory, if an image did not successfully save to
         * the file system, or if an {@code ImageReceiver} were used, then the array-location would
         * contain {@code 'null.'}
         *
         * <BR /><BR />The index of this array will be parallel to the input-source {@code URL}
         * retrieval order.
         */
        public final String[] saveDirectories;

        /**
         * The image type of the files that were retrieved will be stored in this array.
         *
         * <BR /><BR />The index of this array will be parallel to the input-source {@code URL}
         * retrieval order.
         */
        public final IF[] imageFormats;

        /**
         * If an image download fails, this will contain a record of the exception.
         *
         * <BR /><BR />The index of this array will be parallel to the input-source {@code URL}
         * retrieval order.
         *
         * <BR /><BR />If the download succeeded, then the associated array location would contain
         * 'null.'
         */
        public final Exception[] exceptions;

        /**
         * This will contain a list of long-integers, each of which will have the file-size of the
         * downloaded image.  If the programmer has elected for the {@code 'ImageReceiver'} option
         * - <I>rather than direct download of the images to the underlying file-system</I> (save to
         * lambda, instead of save-as-file) - then the "fileSize" will be a calculated file-size,
         * and not reflect the actual size of any file on the file-system.  Obviously, this is
         * because no file was created!
         *
         * <BR /><BR />The index of this array will be parallel to the input-source {@code URL}
         * retrieval order.
         */
        public final long[] sizes;

        /**
         * This will contain a list of integers, each of which shall have the image-widths of the 
         * downloaded images.
         *
         * <BR /><BR />The index of this array will be parallel to the input-source {@code URL}
         * retrieval order.
         */
        public final int[] widths;

        /**
         * This shall contain a list of integers, each of which shall have the image-heights of 
         * the downloaded images.
         *
         * <BR /><BR />The index of this array will be parallel to the input-source {@code URL}
         * retrieval order.
         */
        public final int[] heights;

        /** next result received array position. */
        int pos = 0;

        /** number of successfully saved images. */
        int successCounter = 0;

        /** When images are downloaded, log information may be sent here */
        Appendable log = null;

        Results(int size, Appendable log)
        {
            this.log = log;

            urls                = new URL[size];
            skipped             = new boolean[size];
            fileNames           = new String[size];
            saveDirectories     = new String[size];
            imageFormats        = new IF[size];
            exceptions          = new Exception[size];
            sizes               = new long[size];
            widths              = new int[size];
            heights             = new int[size];

            for (int i=0; i < size; i++)
            {
                urls[i]             = null;
                skipped[i]          = false;
                fileNames[i]        = null;
                saveDirectories[i]  = null;
                imageFormats[i]     = null;
                exceptions[i]       = null;
                sizes[i]            = -1;
                widths[i]           = -1;
                heights[i]          = -1;
            }
        }

        void nullURL() throws IOException // The Appendable throws this
        {
            if (log != null) log.append
                ("\t\t" + C.RED + "No URL was passed, or URL not found." + C.RESET + '\n');

            skipped[pos]            = true;
            pos++;
        }

        void urlException(String src, Exception e) throws IOException // The Appendable throws this
        {
            if (log != null) log.append(
                "\t\t" + C.RED + "Failed Instantiate URL, src = " + src + ", " +
                e.getClass().getName() + ": " + e.getMessage() + C.RESET + '\n'
            );

            skipped[pos]            = true;
            exceptions[pos]         = e;
            pos++;
        }
    
        void skippedURL(URL url) throws IOException // The Appendable throws this
        {
            if (log != null) log.append("\t\t" + C.YELLOW + "*** SKIPPING" + C.RESET + '\n');

            urls[pos]               = url;
            skipped[pos]            = true;
            pos++;
        }

        void downloadException(URL url, Exception e) throws IOException // The Appendable throws this
        {
            String msg = (e.getMessage() != null) 
                ? e.getMessage()
                : "no exception-message provided, [e.getMessage()==null]";

            if (log != null) log.append(
                "\t\t" + C.RED + "DOWNLOAD-EXCEPTION:\t" + e.getClass().getName() + ": " + msg + C.RESET + '\n' +
                "\t\t" + "While Downloading URL:\t" + url.toString() + '\n'
            );

            urls[pos]               = url;
            skipped[pos]            = true;
            exceptions[pos]         = e;
            pos++;
        }

        void imageReceiverSuccess
            (URL url, String fileName, IF ext, long size, int width, int height)
            throws IOException // The Appendable throws this
        {
            if (log != null) log.append(
                "\t\t" + C.YELLOW + "Successfully sent [" + fileName + '.' + ext.extension + "] to class ImageReceiver" + C.RESET + '\n'
            );

            urls[pos]               = url;
            fileNames[pos]          = fileName + '.' + ext.extension;
            imageFormats[pos]       = ext;
            sizes[pos]              = size;
            widths[pos]             = width;
            heights[pos]            = height;
            pos++;
            successCounter++;
        }

        void saveSuccess(
                URL url, String targetDirectory, String fileName, IF ext, long size,
                int width, int height
            )
            throws IOException // The Appendable throws this
        {
            if (log != null) log.append(
                "\t\t" + C.YELLOW + "File Saved:\t" + targetDirectory + fileName + "." +
                ext.extension + C.RESET + '\n'
            );

            urls[pos]               = url;
            saveDirectories[pos]    = targetDirectory;
            fileNames[pos]          = fileName + '.' + ext.extension;
            imageFormats[pos]       = ext;
            sizes[pos]              = size;
            widths[pos]             = width;
            heights[pos]            = height;
            pos++;
            successCounter++;
        }

        void saveFail(URL url, String targetDirectory, String fileName, IF ext, Exception e)
             throws IOException // The Appendable throws this
        {
            if (log != null) log.append(
                "\t\t" + C.RED + "***FILE-SAVE-EXCEPTION:\t" + targetDirectory + fileName + "." +
                ext.extension +
                "\t\t" + e.getClass().getName() + ": " + e.getMessage() + C.RESET + '\n'
            );

            urls[pos]               = url;
            skipped[pos]            = true;
            saveDirectories[pos]    = targetDirectory;
            fileNames[pos]          = fileName + '.' + ext.extension;
            imageFormats[pos]       = ext;
            exceptions[pos]         = e;
            pos++;
        }

        void skipB64(String imgFormatStr, String encodedPartialStr)
            throws IOException // The Appendable throws this
        {
            if (log != null) log.append(
                "\t\t" + C.YELLOW + "Skipping B64 Encoded String: " + imgFormatStr + ", " + encodedPartialStr + C.RESET + '\n'
            );

            skipped[pos]            = true;
            pos++;
        }

        void b64ConvertException(Exception e)
            throws IOException // The Appendable throws this
        {
            if (log != null) log.append
                ("\t\t" + C.RED + "Error Converting and Decoding Base-64 Image" + C.RESET + '\n');

            skipped[pos]            = true;
            exceptions[pos]         = e;
            pos++;
        }
    }

    /**
        ******************************************************
        class ImageScraper
        ******************************************************
        Iterable<String>                    source;
        URL                                 originalPageURL;

        String                              targetDirectory;
        TargetDirectoryRetriever            targetDirectoryRetriever;
        ImageReceiver                       imageReceiver;

        ******************************************************
        class ImageScraper.AdditionalParameters
        ******************************************************
        boolean                             skipOnIOException;
        Predicate<URL>                      skipURL;
        String                              fileNamePrefix;
        boolean                             useDefaultCounterForImageFileNames;
        FileNameRetriever                   getImageFileSaveName;
        long                                maxDownloadWaitTime
        TimeUnit                            waitTimeUnits
    */

    // *************************************************************************************
    // *************************************************************************************
    // download methods
    // *************************************************************************************
    // *************************************************************************************

    /**
     * Convenience Method.  Invokes {@link #download(AdditionalParameters, Appendable)}.
     * <!-- NOTE: JavaDoc Upgrader REMOVES EXCEPTION THROWS... DO NOT MOVE THIS METHOD -->
     */
    public Results download()
        throws IOException, MalformedURLException, URISyntaxException
    { return download(null, null); }

    /**
     * This will iterate through the {@code URL's} and download them.  Note: Both the
     * {@code AdditionalParameters} and {@code 'log'} parameters may be null, and if they are, they
     * will be ignored.
     *
     * @param a This parameter takes customization requests for batch image downloads.  This 
     * parameter can be passed 'null' and when it is, customizations shall be ignored.
     * 
     * <BR /><BR /><B>SKIP ON EXCEPTION:</B> The most useful feature of the {@code class 
     * AdditionalParameters} is to facilitate a download where invalid or out-dated {@code URL's}
     * do not cause the download mechanism to break - which normally would require running an
     * image-download from the beginning.  There is a simple {@code AdditionalParameters}
     * constructor that quickly builds an instance of that class to have {@code boolean
     * skipOnIOException} initialized to <B>TRUE</B>.
     * 
     * @param log This shall receive text / log information.  If this parameter receives 'null',
     * it will be ignored.
     *
     * <EMBED CLASS="external-html" DATA-FILE-ID="APPENDABLE">
     *
     * @return an instance of {@code class Results} for the download.  The {@code class
     * ImageScraper.Results} contains several parallel arrays with information about images that
     * have downloaded.  If an image-download happens to fail due to an improperly formed {@code
     * URL} (or an 'incorrect' {@code URL}), then the information in the {@code Results} arrays 
     * will contain a 'null' value for the index at those array-positions corresponding to the
     * failed image.
     *
     * @throws IOException This might throw if there is an {@code IOException} when downloading an
     * image, or attempting to save an image to the file-system.  If the
     * {@code AdditionalParameters 'a'} parameter is set to suppress-exceptions (and continue to the
     * next Image {@code URL}, via the {@code boolean skipIOExceptions}), then this exception will
     * never throw.
     *
     * @throws MalformedURLException This will throw if there are problems de-referencing the
     * {@code URL's}.  If the {@code AdditionalParameters 'a'} parameter is set to 
     * suppress-exceptions (and continue to the next Image {@code URL}, via the {@code boolean
     * skipIOExceptions}), then this exception will never throw.
     *
     * @throws URISyntaxException Same as {@code MalformedURLException.}  Will not throw if 
     * exceptions are ignored.
     */
    public Results download(AdditionalParameters a, Appendable log)
        throws IOException, MalformedURLException, URISyntaxException
    {
        // Compute the size of the input, will make array-building much faster
        Counter counter = new Counter();
        source.forEach(url -> counter.addOne());

        Results     results = new Results(counter.size(), log);
 
        for (String src : source) 
            if (src == null)
            {
                results.nullURL();
                if ((a != null) && a.skipOnIOException) continue;
                else throw new NullPointerException("One of the SRC URL's was null.");
            }
            else
            {
                Matcher m = IF.B64_INIT_STRING.matcher(src);
                if (m.find())   CONVERT_B64(m.group(1), m.group(2), results, a);
                else            DOWNLOAD(COMPUTE_URL(src, results, a), results, a);
            }

        return results;
    }

    // *************************************************************************************
    // *************************************************************************************
    // Internal COMPUTE-URL / FILENAME Methods
    // *************************************************************************************
    // *************************************************************************************

    private void CONVERT_B64(
            String imageFormatStr, String b64EncodedImage, Results results,
            AdditionalParameters a
        )
        throws IIOException, IOException
    {
        if (results.log != null) 
            results.log.append(
                "\tBASE-64 IMAGE:\t" + imageFormatStr + ',' + b64EncodedImage.substring(0, 40) + '\n'
            );

        if ((a == null) || ((a != null) && a.skipBase64EncodedImages))
        {
            results.skipB64(imageFormatStr, b64EncodedImage.substring(0, 15));
            return;
        }

        IF              ext;
        BufferedImage   image;
        try {
            ext     = IF.get(imageFormatStr);
            image   = IF.decodeBase64ToImage(b64EncodedImage, ext);
            //image   = IF.decodeBase64ToImage_V2(b64EncodedImage, ext);
        } catch (Exception e)
        {
            results.b64ConvertException(e);
            if ((a != null) && a.skipOnIOException) return;
            throw e;
        }
        String fileName = FILENAME(null, ext, results, a);
        HANDLE_DOWNLOADED_IMAGE(null, fileName, ext, results, a, image);
    }

    private URL COMPUTE_URL(String src, Results results, AdditionalParameters a)
        throws IOException, URISyntaxException
    {
        if (results.log != null)
            results.log.append("\tChecking / Converting SRC-URL string:\t" + src + '\n');

        if (StrCmpr.startsWithXOR_CI(src.trim(), "http://", "https://"))

            try
                { return new URL(URLs.toProperURLV7(src)); }

            catch(MalformedURLException e)
            {
                results.urlException(src, e);
                if ((a != null) && a.skipOnIOException) return null;
                else throw e;
            }
            catch(URISyntaxException e)
            {
                results.urlException(src, e);
                if ((a != null) && a.skipOnIOException) return null;
                else throw e;
            }

        else if (originalPageURL == null)
        {
            MalformedURLException ex = new MalformedURLException(
                "You have passed a null 'originalPageURL' parameter, but at least one of the URL's " +
                "you have passed for downloading is either a partial URL, or else an invalid URL: " +
                "[" + src + "]"
            );

            results.urlException(src, ex);

            if ((a != null) && a.skipOnIOException) return null;
            else                                    throw ex;
        }

        Ret2<URL, MalformedURLException> ret = Links.resolve_KE(src, originalPageURL);

        if (ret == null) // I do not think this case is possible.  I'll leave it here anyway.
        {
            results.nullURL();
            return null;
        }
        if (ret.b != null)
        {
            results.urlException(src, ret.b);
            if ((a != null) && a.skipOnIOException) return null;
            else throw ret.b;
        }

        // ADDED in NOVEMBER, 2019
        // This micro-detail is the case where the "Resolved URL" also has ASCII-Escape characters
        // that need to be escaped.  This is rare, but it needs to be heeded.  If there are
        // ASCII-Escape character (which must be escaped).   Then "toProperURLV8" will handle that
        // well enough.
        //
        // CONSIDER IT: Post-Processing of the "Resolve URLs" class

        try
            { return new URL(URLs.toProperURLV8(ret.a)); }

        catch(MalformedURLException e)
        {
            results.urlException(src, e);
            if ((a != null) && a.skipOnIOException) return null;
            else throw e;
        }
        catch(URISyntaxException e)
        {
            results.urlException(src, e);
            if ((a != null) && a.skipOnIOException) return null;
            else throw e;
        }
    }

    private String FILENAME(URL url, IF ext, Results results, AdditionalParameters a)
    {
        String fileName = ((a != null) && (a.fileNamePrefix != null)) ? a.fileNamePrefix : "";

        if ((a != null) && (a.getImageFileSaveName != null))
            fileName = fileName + a.getImageFileSaveName.fileName
                (url, ext, results.pos, results.successCounter);

        else if ((a == null) || ((a != null) && a.useDefaultCounterForImageFileNames))
            fileName = fileName + StringParse.zeroPad(results.successCounter);

        else
        {
            fileName = url.getFile().substring(1);
            if (fileName.toLowerCase().endsWith('.' + ext.extension))
                fileName = fileName.substring(0, fileName.length() - 1 - ext.extension.length());
        }
        return fileName;
    }

    // *************************************************************************************
    // *************************************************************************************
    // Internal Download Methods
    // *************************************************************************************
    // *************************************************************************************

    /**
     * If this class has been used to make "multi-threaded" calls that use a Time-Out wait-period,
     * you might see your Java-Program hang for a few seconds when you would expect it to exit back
     * to your O.S. normally.
     *
     * <BR /><BR /><B><SPAN STYLE="color: red;">NOTE:</B></SPAN>
     * {@code AdditionalParameters.maxDownloadWaitTime, AdditionalParameters.waitTimeUnits} operate
     * by building a "Timeout &amp; Monitor" thread.  Thusly, when a program you have written
     * yourself reaches the end of its code, <I><B>if you have performed any time-dependent
     * Image-Downloads using {@code class ImageScraper}</B></I>, then your program <I>might not
     * exit immediately,</I> but rather sit at the command-prompt for anywhere between 10 and 30
     * seconds before this Timeout-Thread  dies.
     *
     * <BR /><BR /><B><SPAN STYLE="color: red">MULTI-THREADED:</B></SPAN> You may immediately
     * terminate any additional threads that were started using this method.
     */
    public static void shutdownTOThreads() { executor.shutdownNow(); }

    // ******************************
    private static final    ExecutorService executor    = Executors.newCachedThreadPool();
    private static final    Lock            lock        = new ReentrantLock();
    // ******************************

    private void DOWNLOAD(URL url, Results results, AdditionalParameters a) throws IOException
    {
        BufferedImage	image;
        if (url == null) return;

        Appendable log = results.log;
        if (log != null) log.append("\tIMAGE-URL:\t\t" + url.toString() + '\n');

        if ((a != null) && (a.skipURL != null) && (a.skipURL.test(url) == true))
        { results.skippedURL(url); return; }

        // ******************************
        // *** ADDED on May 1st, 2019 ***
        // ******************************
        Callable<BufferedImage> threadDownloader = new Callable<BufferedImage>()
        {
            public BufferedImage call() throws Exception
            {
                try { return ImageIO.read(url); }
                catch (IIOException e)
                {   
                    // This will **sometimes** help when connecting to a URL "expects" this "User-Agent"
                    // This won't *always* work - or will it?  It is a very large-internet, with many MANY types of web-servers.
                    // THIS IS SORT-OF "ATTEMPT TO DOWNLOAD #2"
                    // try {
                    if (log != null) log.append("\tUSING USER-AGENT:\t" + url.toString() + '\n');
                    HttpURLConnection con = (HttpURLConnection) url.openConnection();
                    con.setRequestMethod("GET");
                    con.setRequestProperty("User-Agent", "Chrome/61.0.3163.100");
                    InputStream is = con.getInputStream();
                    return ImageIO.read(is);
                }
            }
        };

        lock.lock();
        Future<BufferedImage> future = executor.submit(threadDownloader);
        lock.unlock();

        long wt = ((a != null) && (a.waitTimeUnits != null)) 
            ? a.maxDownloadWaitTime 
            : MAX_WAIT_TIME;

        TimeUnit tu = ((a != null) && (a.waitTimeUnits != null)) 
            ? a.waitTimeUnits
            : MAX_WAIT_TIME_UNIT;

        try
            { image = future.get(wt, tu); }

        catch (TimeoutException e)
        {
            if (e.getMessage() == null) e = new TimeoutException
                ("Waited: " + wt + " " + tu.toString());

            results.downloadException(url, e);

            if ((a != null) && a.skipOnIOException) return;

            throw new IOException
                ("The download timed-out, see getCause() for more information.", e);
        }

        catch (ExecutionException e)
        {
            Exception cause = ((e.getCause() != null) && (e.getCause() instanceof Exception)) 
                ? (Exception) e.getCause() 
                : e;

            results.downloadException(url, cause);

            if ((a != null) && a.skipOnIOException) return;

            throw new IOException
                ("The download had an exception, see getCause() for more information.", cause);
        }

        catch (InterruptedException e)
        {
            results.downloadException(url, e);

            if ((a != null) && a.skipOnIOException) return;

            throw new IOException(
                "The download was interrupted by another thread, see this Throwable.getCause() " +
                "for more information.", e
            );
        }

        IF      ext         = IF.getGuess(url.toString());
        String  fileName    = FILENAME(url, ext, results, a);

        HANDLE_DOWNLOADED_IMAGE(url, fileName, ext, results, a, image);
    }

    private void HANDLE_DOWNLOADED_IMAGE(
            URL url, String fileName, IF ext, Results results,
            AdditionalParameters a, BufferedImage image
        )
        throws IIOException, IOException
    {
        Appendable log = results.log;

        String dirName = null;

        if (targetDirectory != null)
            dirName = targetDirectory;

        else if (targetDirectoryRetriever != null)
            dirName = targetDirectoryRetriever.dir
                (url, fileName, ext, results.pos, results.successCounter);

        else if (imageReceiver != null)
        {
            imageReceiver.save(url, fileName + '.' + ext.extension, ext, results.pos, results.successCounter, image);
            
            long l;
            {   // CODE COPIED FROM STACK-OVERFLOW.  This should probably become a separate-method. 
                //  I am not 100% it works yet.  (The "Green Check Mark" was not checked on this answer!)
                ByteArrayOutputStream tmp = new ByteArrayOutputStream();
                ImageIO.write(image, ext.extension, tmp);
                tmp.close();
                l = tmp.size();
            }
            results.imageReceiverSuccess(url, fileName, ext, l, image.getWidth(), image.getHeight());
            return;
        }

        else throw new IllegalStateException
            ("Not image-target specified.  Illegal State - is a constructor overloaded?");

        if (! dirName.endsWith(File.separator)) dirName += File.separator;

        File f = null;
        if (ext != null)
            try {
                String fName = dirName + fileName + '.' + ext.extension;
                if (log != null) log.append("\tAttempting to save file: " + fName + '\n');

                f = new File(fName);
                ImageIO.write(image, ext.extension, f);
                results.saveSuccess(
                    url, dirName, fileName, ext, f.length(),
                    image.getWidth(), image.getHeight()
                );
                return;
            } catch (Exception e)
            {
                results.saveFail(url, dirName, fileName, ext, e);
                if ((a != null) && a.skipOnIOException) return;
                throw e;
            }
        else
        {
            String fName = dirName + fileName + '.';
            for (IF imageFormat : IF.values())
                try {
                    f = new File(fName + imageFormat.extension);
                    ImageIO.write(image, imageFormat.extension, f);
                    results.saveSuccess(
                        url, dirName, fileName, imageFormat, f.length(),
                        image.getWidth(), image.getHeight()
                    );
                    return;
                }
                catch (javax.imageio.IIOException e)	{ f.delete(); 	continue; }
                catch (Exception e)
                {
                    e.printStackTrace();
                    results.saveFail(url, dirName, fileName, imageFormat, e);
                    if ((a != null) && a.skipOnIOException) return;
                    throw e;
                }
        }
    }

    // *************************************************************************************
    // *************************************************************************************
    // Localize Images methods
    // *************************************************************************************
    // *************************************************************************************

    /** 
      * Convenience Method.  Invokes {@link #localizeImages(Vector, URL, Appendable, AdditionalParameters, String)}.
      * <BR /><BR />Passes null to {@link AdditionalParameters} and to root-{@code URL}.
      * <BR /><BR /><B>WARNING:</B> Presumes there are no partial-{@code URL's}
      */
    public static Ret2<int[], ImageScraper.Results> localizeImages
        (Vector<HTMLNode> page, Appendable log, String downloadDirectory)
        throws IOException
    { return localizeImages(page, null, log, null, downloadDirectory); }

    /** 
      * Convenience Method.  Invokes {@link #localizeImages(Vector, URL, Appendable, AdditionalParameters, String)}.
      * <BR /><BR />Passes null to {@link AdditionalParameters}.
      */
    public static Ret2<int[], ImageScraper.Results> localizeImages
        (Vector<HTMLNode> page, URL pageURL, Appendable log, String downloadDirectory)
        throws IOException
    { return localizeImages(page, pageURL, log, null, downloadDirectory); }

    /**
     * Downloads images located inside an HTML Page and updates the {@code SRC=...} {@code URL's}
     * so that the links point to a <I>local copy</I> of <I>local images</I>.
     *
     * <BR /><BR />After completion of this method, an HTML page which contained any HTML image
     * elements will have had those images downloaded to the local file-system, and also have had 
     * the HTML attribute {@code 'src=...'} changed to reflect the local image name instead of the
     * Internet URL name.
     *
     * @param page Any vectorized-html page or subpage.  This page should have HTML {@code <IMG ...>}
     * elements in it, or else this method will exit without doing anything.
     *
     * @param pageURL If any of the HTML image elements have {@code src='...'} attributes that are
     * partially resolved or <I>relative {@code URL's}</I> then this can be passed to the
     * {@code ImageScraper} constructors in order to convert partial or relative {@code URL's}
     * into complete {@code URL's.}  The Image Downloader simply cannot work with partially
     * resolved {@code URL's}, and will skip them if they are partially resolved.  This parameter
     * may be null, but if it is and there are incomplete-{@code URL's} those images will
     * simply not be downloaded.
     *
     * @param log This is the 'logger' for this method.  It may be null, and if it is - no output
     * will be sent to the terminal.
     *
     * <EMBED CLASS="external-html" DATA-FILE-ID="APPENDABLE">
     *
     * @param ap This is the {@link AdditionalParameters} parameter that allows to further
     * specify the request to the Image Downloader.  See the documentation for this class for more
     * information.  This parameter may be null, and if it is, it will be ignored and default
     * behavior will occur.
     * 
     * <BR /><BR /><B>SKIP ON EXCEPTION:</B> The most useful feature of the {@code class
     * AdditionalParameters} is to facilitate a download where invalid or out-dated {@code URL's}
     * do not cause the download mechanism to break - which normally would require running an
     * image-download from the beginning.  There is a simple {@code AdditionalParameters} 
     * constructor that quickly builds an instance of that class to have
     * {@code boolean skipOnIOException} initialized to <B>TRUE</B>.
     *
     * @param downloadDirectory This File-System directory where these files shall be stored.
     *
     * @return An instance of {@code Ret2<int[], ImageScraper.Results>}.  The two returned elements
     * of this class include:
     *
     * <BR /><BR /><UL CLASS="JDUL">
     * <LI> {@code Ret2.a (int[])}
     *      <BR /><BR />This shall contain an index-array for the indices of each HTML
     *      {@code '<IMG SRC=...>'} element found on the page.  It is not guaranteed that each of
     *      images will have been resolved or downloaded successfully, but rather just that an HTML
     *      {@code 'IMG'} element that had a {@code 'SRC'} attribute.  The second element of this
     *      return-type will contain information regarding which images downloaded successfully.
     *      <BR /><BR />
     * </LI>
     * <LI> {@code Ret2.b (ImageScraper.Results)}
     *      <BR /><BR />The second element of the return-type shall be the instance of
     *      {@link ImageScraper.Results} returned from the invocation of
     *      {@code ImageScraper.download(...)}.  This method will provide details about each of the
     *      images that were downloaded; or, if the download failed, the reasons for the failure.
     *      <I>This return element shall be null if no images were found on the page.</I>
     *      <BR />
     * </LI>
     * </UL>
     * 
     * <BR />These return {@code Object} references are not necessarily important - <I>and they
     * may be discarded if needed.</I>  They are provided as a matter of utility if further
     * verification or research into successful downloads is needed.
     *
     * @see AdditionalParameters
     */
    public static Ret2<int[], ImageScraper.Results> localizeImages(
        Vector<HTMLNode> page, URL pageURL, Appendable log, AdditionalParameters ap,
        String downloadDirectory
    )
        throws IOException
    {
        int[]               imgPosArr   = TagNodeFind.all(page, TC.Both, "img");
        Vector<TagNode>     vec         = new Vector<>();

        // No Images Found.
        if (imgPosArr.length == 0) return new Ret2<int[], Results>(imgPosArr, null);

        for (int pos : imgPosArr) vec.addElement((TagNode) page.elementAt(pos));

        ImageScraper is = new ImageScraper(vec, pageURL, downloadDirectory);
        ImageScraper.Results r;

        try
            { r = is.download(ap, log); }
        catch (URISyntaxException e)
        {
            throw new IOException(
                "There was a problem de-referencing one of the partial-URL's from the page URL.  " +
                "See this methods's Throwable.getCause() for details.",
                e
            ); 
        }

        // ImageScraper.shutdownTOThreads(); 
        // NOTE-TO-READER: Need to call this method, or function will not shutdown.
        // NOTE: Commented out for now.

        ReplaceNodes.r(page, imgPosArr, (HTMLNode n, int arrPos, int count) ->
        {
            if (    (r.fileNames[count] != null)
                &&  ((r.exceptions[count] == null)
                &&  (r.skipped[count] == false)))

                return ((TagNode) page.elementAt(arrPos))
                        .setAV("src", r.fileNames[count], SD.SingleQuotes);

            else
                return (TagNode) n;
        });

        return new Ret2<int[], Results>(imgPosArr, r);
    }
}