001package Torello.HTML.Tools.Images;
002
003import Torello.Java.StrPrint;
004import Torello.Java.StrPrint;
005
006import java.awt.image.BufferedImage;
007import java.io.Serializable;
008import java.net.URL;
009import java.util.Objects;
010
011/**
012 * Simple Image Data-Class that is instantiated by the {@link ImageScraper}, and passed to any of
013 * the {@code FunctionalInterface}'s or Lambda-Targets that are non-null / available in the user's
014 * {@link Request} object instance.
015 * 
016 * <EMBED CLASS='external-html' DATA-FILE-ID=IMAGE_INFO_EXAMPLE>
017 */
018@Torello.JavaDoc.JDHeaderBackgroundImg(EmbedTagFileID="IMAGE_SCRAPER_CLASS")
019public class ImageInfo implements Cloneable, Serializable
020{
021    /** <EMBED CLASS='external-html' DATA-FILE-ID=SVUID> */
022    public static final long serialVersionUID = 1;
023
024
025    // ********************************************************************************************
026    // ********************************************************************************************
027    // Public Fields
028    // ********************************************************************************************
029    // ********************************************************************************************
030
031
032    /**
033     * The {@code URL} that was used to download the image.  Note that anytime this instance of
034     * {@code ImageInfo} represents an image that was located on a web-page encoded using a 
035     * Base-64 {@code String}, then this field will be null, and the B-64 Fields will contain the
036     * relevant image data (not this {@code URL}).
037     */
038    public final URL url;
039
040    /**
041     * If the image whose details are contained by this class-instance are from an image that
042     * was encoded using the {@code String}-literal Base-64 Encoding Algorithm, then this boolean
043     * flag will contain {@code TRUE}.
044     * 
045     * <BR /><BR />An HTML {@code <IMG SRC=...>} Tag is the only way to enter a Base-64 Image into
046     * the Image-Scraper class.
047     */
048    public final boolean isB64EncodedImage;
049
050    /**
051     * A web-page has the ability to inline (smaller) images directly into an HTML
052     * {@code <IMG SRC=...>} tag by encoding the picture into a Base-64 {@code String}.  The
053     * {@code String} is saved inside the {@code SRC}-Attribute of the {@code <IMG>} tag, and
054     * contains two separate sub-strings.
055     * 
056     * <BR /><BR />This is the first sub-string, and it just identifies / lists the format 
057     * ({@code .jpg, .gif, .png} etc...) in which the picture was saved before translating it into
058     * Base-64 Encoded Text.
059     * 
060     * <BR /><BR />If the picture represented by this instance of {@code ImageInfo} was downloaded
061     * from a {@code URL}, then this field will be null, and the {@link #url} field will contain
062     * the Image {@code URL}.
063     * 
064     * @see #isB64EncodedImage
065     * @see #b64EncodedImage
066     */
067    public final String imageFormatStr;
068
069    /**
070     * A web-page has the ability to inline (smaller) images directly into an HTML
071     * {@code <IMG SRC=...>} tag by encoding the picture into a Base-64 {@code String}.  The
072     * {@code String} is saved inside the {@code SRC}-Attribute of the {@code <IMG>} tag, and
073     * contains two separate sub-strings.
074     * 
075     * <BR /><BR />This is the second sub-string, and it is the text after translating the picture
076     * into Base-64 Encoded Text.
077     * 
078     * <BR /><BR />If the picture represented by this instance of {@code ImageInfo} was downloaded
079     * from a {@code URL}, then this field will be null, and the {@link #url} field will contain
080     * the Image {@code URL}.
081     * 
082     * @see #isB64EncodedImage
083     * @see #imageFormatStr
084     */
085    public final String b64EncodedImage;
086
087    /** The image, as prepared for saving to disk. */
088    public final byte[] imgByteArr;
089
090    /** The image, as an instance of {@code java.awt.image.BufferedImage} */
091    public final BufferedImage bufferedImage;
092
093    /**
094     * The value returned by {@code bufferedImage.getWidth()} - <I>the downloaded image's
095     * width</I>.
096     */
097    public final int width;
098
099    /**
100     * The value returned by {@code bufferedImage.getHeight()} - <I>the downloaded image's
101     * height</I>.
102     */
103    public final int height;
104
105    /**
106     * This shall help identify whether the image-in-question is a {@code GIF, JPG, PNG, etc...}.
107     * The field {@code 'guessedImageFormat'} shall simply contain the image-type based on the
108     * extension found in the {@code URL's} file-name.
109     * 
110     * <BR /><BR />There are web-pages &amp; web-sites that do not provide a file-name extension
111     * for the images they use on their page(s).  In such cases, the downloader will eventually
112     * attempt to 'guess' the format of an image that has been downloaded.  In these cases, this
113     * parameter will be passed null, and the parameter {@code actualImageFormat} will contain the
114     * format that was actually used to successfully save the data to disk.
115     */
116    public final IF guessedExtension;
117
118    /**
119     * If the image has been properly converted, and is ready to be written to disk, this parameter
120     * will contain the {@link IF} / image-format that was used to successfully save the image.
121     * 
122     * <BR /><BR />Note that often (but not always), this extension / {@link IF} instance will be
123     * identical to the parameter {@code 'guessedImageFormat'}.  There will be cases, as mentioned
124     * above, when {@code 'guessImageFormat'} is null.  Furthermore, there may be (very rare)
125     * situations when an image-format for a particular {@code URL} was incorrect, and was saved
126     * properly using a different format &amp; extension.
127     */
128    public final IF actualExtension;
129
130    /**
131     * Identifies the <B STYLE='color: red;'>count</B> in the {@code Iterator's} retrieval.  Since
132     * this {@code int} is used as an array-index pointer, it is initialized to {@code '0'} (zero).
133     * Specifically, if this method were called upon completion of three iterations of
134     * Image-{@code URL} retrieval, this counter would contain the integer {@code '2'} (two).
135     */
136    public final int iteratorCounter;
137
138    /**
139     * This identifies how many images have successfully downloaded, not the number of images for
140     * which a "download attempt" occurred.  Since this {@code int} is used as an array-index
141     * pointer, it is initialized to {@code '0'} (zero).  If on the third iteration of the
142     * source-{@code Iterator}, an {@code IOException} occurred between the Java-Virtual-Machine
143     * and the internet, the following invocation of this method would have {@code successCounter}
144     * as {@code '2'}, but the {@code iteratorCounter} would be {@code '3'}.
145     */
146    public final int successCounter;
147
148    // This data that is saved in this field isn't saved at the time of construction, and therefore
149    // this field cannot be 'final'.  If this field cannot be 'final', it cannot be public, so I 
150    // guess there just has to be a getter-method for this one.
151
152    private String fileName = null;
153
154
155    // ********************************************************************************************
156    // ********************************************************************************************
157    // Package-Private Constructor
158    // ********************************************************************************************
159    // ********************************************************************************************
160
161
162    // Used in the "Main Loop Body", only Once
163    ImageInfo(
164            // Image-URL (very common)
165            URL url,
166
167            // Base-64 Image Stuff (rare, but not impossible)
168            boolean isB64EncodedImage,
169            String[] b64ImageData,
170
171            // The actual downloaded and converted images, themselves
172            BufferedImage bufferedImage,
173            byte[] imgByteArr,
174
175            // URL-Aquired Extension & Ultimately-Decided-Upon Extension
176            IF guessedExtension,
177            IF actualExtension,
178
179            // class 'Results' Array-Counters (index-pointers)
180            int iteratorCounter,
181            int successCounter
182        )
183    {
184        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
185        // This is just "Paranoia" - but fortunately, it isn't very "expensive" paranoia
186        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
187
188        if ((url == null) && (! isB64EncodedImage)) throw new IllegalArgumentException
189            ("(url == null) && (! isB64EncodedImage)");
190
191        if ((url != null) && isB64EncodedImage) throw new IllegalArgumentException
192            ("(url != null) && isB64EncodedImage");
193
194        if (isB64EncodedImage && (b64ImageData == null)) throw new IllegalArgumentException
195            ("isB64EncodedImage && (b64ImageData == null)");
196
197        if ((! isB64EncodedImage) && (b64ImageData != null)) throw new IllegalArgumentException
198            ("(! isB64EncodedImage) && (b64ImageData != null)");
199
200
201        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
202        // Initialize all 'final' fields!  The only one left out is 'fileName' - it is done later
203        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
204
205        // Image-URL (very common)
206        this.url = url;
207
208        // Base-64 Image Stuff (rare, but not impossible)
209        this.isB64EncodedImage  = isB64EncodedImage;
210        this.imageFormatStr     = isB64EncodedImage ? b64ImageData[0] : null;
211        this.b64EncodedImage    = isB64EncodedImage ? b64ImageData[1] : null;
212
213        // The actual downloaded and converted images, themselves
214        this.imgByteArr = imgByteArr;
215        this.bufferedImage = bufferedImage;
216
217        // Note sure how necessary / important this is, but maybe...
218        this.width = bufferedImage.getWidth();
219        this.height = bufferedImage.getHeight();
220
221        // URL-Aquired Extension & Ultimately-Decided-Upon Extension
222        this.guessedExtension = guessedExtension;
223        this.actualExtension = actualExtension;
224
225        // class 'Results' Array-Counters (index-pointers)
226        this.iteratorCounter = iteratorCounter;
227        this.successCounter = successCounter;
228    }
229
230
231    // ********************************************************************************************
232    // ********************************************************************************************
233    // Lone Accessor Method
234    // ********************************************************************************************
235    // ********************************************************************************************
236
237
238    /**
239     * A "getter" (Accessor-Method) for the {@code private}-Field named {@code fileName}.  This
240     * field is kept {@code private} because it cannot be declared {@code final} - since it is
241     * initialized several steps after construction.
242     * 
243     * <BR /><BR />If a user Lambda-Expression / Functional-Interface has recevied {@code 'this'}
244     * instance of {@code ImageInfo}, and needs access to the ultimately-decided-upon image
245     * file-name, then this field may be retrieved using this 'Getter' Method.
246     * 
247     * <BR /><BR /><DIV CLASS=JDHint>
248     * <B STYLE='color:red;'>Unitialized at Construction:</B> This method will return null if a
249     * user attempts to retrieve the file-name before it has been decided upon and set in this
250     * class.  This is actually the whole reason that it cannot be declared final, and therefore
251     * cannot be declared public (and requires this Getter-Method).
252     * </DIV>
253     */
254    public String fileName() { return fileName; }
255
256    // Package-Private, this is set inside class "ImageScraper"
257    void setFileName(String fileName) { this.fileName = fileName; }
258    
259
260    // ********************************************************************************************
261    // ********************************************************************************************
262    // interface java.lang.Cloneable
263    // ********************************************************************************************
264    // ********************************************************************************************
265
266
267    /**
268     * Generates a <B STYLE='color: red;'>Shallow Copy</B> of {@code 'this'} instance.  This means
269     * that the images themselves are not copied - rather only the references to the images are
270     * copied into the clone.
271     * 
272     * <BR /><BR />The non-reference, non-instance (primitive-type) fields are all "just copied
273     * like normal" :)
274     * 
275     * @return A duplicate instance of this class.
276     */
277    public ImageInfo clone()
278    { return new ImageInfo(this); }
279
280    // Private Constructor, used only for the 'clone()' method
281    private ImageInfo(ImageInfo r)
282    {
283        // Image-URL (very common)
284        this.url = r.url;
285
286        // Base-64 Image Stuff (rare, but not impossible)
287        this.isB64EncodedImage  = r.isB64EncodedImage;
288        this.imageFormatStr     = r.imageFormatStr;
289        this.b64EncodedImage    = r.b64EncodedImage;
290
291        // The actual downloaded and converted images, themselves
292        this.imgByteArr = r.imgByteArr;
293        this.bufferedImage = r.bufferedImage;
294
295        // Image-Width, Image-Height
296        this.width = r.width;
297        this.height = r.height;
298
299        // URL-Aquired Extension & Ultimately-Decided-Upon Extension
300        this.guessedExtension = r.guessedExtension;
301        this.actualExtension = r.actualExtension;
302
303        // class 'Results' Array-Counters (index-pointers)
304        this.iteratorCounter = r.iteratorCounter;
305        this.successCounter = r.successCounter;
306
307        // This is the lone / only 'non-final' field
308        this.fileName = r.fileName;
309    }
310
311
312    // ********************************************************************************************
313    // ********************************************************************************************
314    // java.lang.Object
315    // ********************************************************************************************
316    // ********************************************************************************************
317
318
319    /**
320     * Converts this class into a simple, readable {@code String}
321     * @return A {@code java.lang.String} representation of {@code 'this'} instance.
322     */
323    public String toString()
324    {
325        return
326            ((this.url != null)
327                ? ("URL: " + StrPrint.abbrev(this.url.toString(), 40, true, " ... ", 80))
328                : "") +
329
330            (this.isB64EncodedImage
331                ? ("B64-Encoded Format: " + this.imageFormatStr + ", IMG: " +
332                    StrPrint.abbrev(this.b64EncodedImage, 30, true, " ... ", 60))
333                : "") +
334
335            '\n' +
336            
337            "Byte-Array.length: "   + StrPrint.commas(this.imgByteArr.length) + '\n' +
338
339            "W: "                   + StrPrint.commas(this.width) + ", " +
340            "H: "                   + StrPrint.commas(this.height) + ", " +
341            "File-Extension: "      + Objects.toString(this.actualExtension) + ", " +
342            "URL-Extension: "       + Objects.toString(this.guessedExtension) + '\n' +
343
344            "Iterator-Count: "      + this.iteratorCounter + ", " +
345            "Downloaded-Count: "    + this.successCounter + '\n' +
346
347            "Saving File-Name: "    + this.fileName + '\n';
348    }
349
350    /**
351     * Checks whether {@code 'this'} instance is equal to {@code 'other'}.
352     *
353     * @param other Any Java Object, but only an instance {@code ImageInfo} (or a class that is
354     * assignable to it) could possible generate a {@code TRUE}-return value.
355     *
356     * @return {@code TRUE} If and only if {@code 'other'} is an instance of {@code ImageInfo}, and
357     * if the contents of that are instance are identical to the contents of {@code 'this'}
358     * instance.
359     */
360    public boolean equals(Object other)
361    {
362        if (other == null) return false;
363
364        if (! ImageInfo.class.isAssignableFrom(other.getClass())) return false;
365
366        ImageInfo ii = (ImageInfo) other;
367
368        // Note that using 'Objects.equals(...)' and 'Objects.deepEquals(...)' primarily prevents
369        // a NullPointerException from being thrown if the left side of an '.equals(...)' were to
370        // be null.  It's really nothing more than that.  (A small 'Convenience' that makes this
371        // method look less ridiculous than it already does.)
372        //
373        // 'deepEquals(...)' actually checks the entire contents of two array's for equality.
374
375        return
376        
377            // Image-URL (very common)
378            Objects.equals(this.url, ii.url)
379
380            // Base-64 Image Stuff (rare, but not impossible)
381            &&  (this.isB64EncodedImage == ii.isB64EncodedImage)
382            &&  (Objects.equals(this.imageFormatStr, ii.imageFormatStr))
383            &&  (Objects.equals(this.b64EncodedImage, ii.b64EncodedImage))
384
385            // The actual downloaded and converted images, themselves
386            &&  Objects.deepEquals(this.imgByteArr, ii.imgByteArr)
387            &&  Objects.equals(this.bufferedImage, ii.bufferedImage)
388
389            // Image-Width, Image-Height
390            &&  (this.width == ii.width)
391            &&  (this.height == ii.height)
392
393            // URL-Aquired Extension & Ultimately-Decided-Upon Extension
394            &&  Objects.equals(this.guessedExtension, ii.guessedExtension)
395            &&  Objects.equals(this.actualExtension, ii.actualExtension)
396
397            // class 'Results' Array-Counters (index-pointers)
398            &&  (this.iteratorCounter == ii.iteratorCounter)
399            &&  (this.successCounter == ii.successCounter)
400
401            // This is the lone / only 'non-final' field
402            &&  Objects.equals(this.fileName, ii.fileName);
403    }
404
405    /**
406     * Java's hash-code requirement.  The code is computed by summing the first 15
407     * {@link #imgByteArr} array elements.
408     * 
409     * @return A hash-code that may be used when storing this node in a java sorted-collection.
410     */
411    public int hashCode()
412    {
413       if (url != null) return url.toString().hashCode();
414
415       int sum = 0;
416
417       for (int i=0; (i < 15) && (i < imgByteArr.length); i++) sum += imgByteArr[i];
418
419       return sum;
420    }
421}