001package Torello.HTML.Tools.Images;
002
003import Torello.Java.StringParse;
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 /><B CLASS=JDDescLabel>Unitialized at Construction:</B>
248     * <BR />Note that this method will return null if a user attempts to retrieve the file-name
249     * before it has been decided upon and set in this class.  This is actually the whole reason
250     * that it cannot be declared final, and therefore cannot be declared public (and requires this
251     * Getter-Method).
252     */
253    public String fileName() { return fileName; }
254
255    // Package-Private, this is set inside class "ImageScraper"
256    void setFileName(String fileName) { this.fileName = fileName; }
257    
258
259    // ********************************************************************************************
260    // ********************************************************************************************
261    // interface java.lang.Cloneable
262    // ********************************************************************************************
263    // ********************************************************************************************
264
265
266    /**
267     * Generates a <B STYLE='color: red;'>Shallow Copy</B> of {@code 'this'} instance.  This means
268     * that the images themselves are not copied - rather only the references to the images are
269     * copied into the clone.
270     * 
271     * <BR /><BR />The non-reference, non-instance (primitive-type) fields are all "just copied
272     * like normal" :)
273     * 
274     * @return A duplicate instance of this class.
275     */
276    public ImageInfo clone()
277    { return new ImageInfo(this); }
278
279    // Private Constructor, used only for the 'clone()' method
280    private ImageInfo(ImageInfo r)
281    {
282        // Image-URL (very common)
283        this.url = r.url;
284
285        // Base-64 Image Stuff (rare, but not impossible)
286        this.isB64EncodedImage  = r.isB64EncodedImage;
287        this.imageFormatStr     = r.imageFormatStr;
288        this.b64EncodedImage    = r.b64EncodedImage;
289
290        // The actual downloaded and converted images, themselves
291        this.imgByteArr = r.imgByteArr;
292        this.bufferedImage = r.bufferedImage;
293
294        // Image-Width, Image-Height
295        this.width = r.width;
296        this.height = r.height;
297
298        // URL-Aquired Extension & Ultimately-Decided-Upon Extension
299        this.guessedExtension = r.guessedExtension;
300        this.actualExtension = r.actualExtension;
301
302        // class 'Results' Array-Counters (index-pointers)
303        this.iteratorCounter = r.iteratorCounter;
304        this.successCounter = r.successCounter;
305
306        // This is the lone / only 'non-final' field
307        this.fileName = r.fileName;
308    }
309
310
311    // ********************************************************************************************
312    // ********************************************************************************************
313    // java.lang.Object
314    // ********************************************************************************************
315    // ********************************************************************************************
316
317
318    /**
319     * Converts this class into a simple, readable {@code String}
320     * @return A {@code java.lang.String} representation of {@code 'this'} instance.
321     */
322    public String toString()
323    {
324        return
325            ((this.url != null)
326                ? ("URL: " + StrPrint.abbrev(this.url.toString(), 40, true, " ... ", 80))
327                : "") +
328
329            (this.isB64EncodedImage
330                ? ("B64-Encoded Format: " + this.imageFormatStr + ", IMG: " +
331                    StrPrint.abbrev(this.b64EncodedImage, 30, true, " ... ", 60))
332                : "") +
333
334            '\n' +
335            
336            "Byte-Array.length: "   + StringParse.commas(this.imgByteArr.length) + '\n' +
337
338            "W: "                   + StringParse.commas(this.width) + ", " +
339            "H: "                   + StringParse.commas(this.height) + ", " +
340            "File-Extension: "      + Objects.toString(this.actualExtension) + ", " +
341            "URL-Extension: "       + Objects.toString(this.guessedExtension) + '\n' +
342
343            "Iterator-Count: "      + this.iteratorCounter + ", " +
344            "Downloaded-Count: "    + this.successCounter + '\n' +
345
346            "Saving File-Name: "    + this.fileName + '\n';
347    }
348
349    /**
350     * Checks whether {@code 'this'} instance is equal to {@code 'other'}.
351     *
352     * @param other Any Java Object, but only an instance {@code ImageInfo} (or a class that is
353     * assignable to it) could possible generate a {@code TRUE}-return value.
354     *
355     * @return {@code TRUE} If and only if {@code 'other'} is an instance of {@code ImageInfo}, and
356     * if the contents of that are instance are identical to the contents of {@code 'this'}
357     * instance.
358     */
359    public boolean equals(Object other)
360    {
361        if (other == null) return false;
362
363        if (! ImageInfo.class.isAssignableFrom(other.getClass())) return false;
364
365        ImageInfo ii = (ImageInfo) other;
366
367        // Note that using 'Objects.equals(...)' and 'Objects.deepEquals(...)' primarily prevents
368        // a NullPointerException from being thrown if the left side of an '.equals(...)' were to
369        // be null.  It's really nothing more than that.  (A small 'Convenience' that makes this
370        // method look less ridiculous than it already does.)
371        //
372        // 'deepEquals(...)' actually checks the entire contents of two array's for equality.
373
374        return
375        
376            // Image-URL (very common)
377            Objects.equals(this.url, ii.url)
378
379            // Base-64 Image Stuff (rare, but not impossible)
380            &&  (this.isB64EncodedImage == ii.isB64EncodedImage)
381            &&  (Objects.equals(this.imageFormatStr, ii.imageFormatStr))
382            &&  (Objects.equals(this.b64EncodedImage, ii.b64EncodedImage))
383
384            // The actual downloaded and converted images, themselves
385            &&  Objects.deepEquals(this.imgByteArr, ii.imgByteArr)
386            &&  Objects.equals(this.bufferedImage, ii.bufferedImage)
387
388            // Image-Width, Image-Height
389            &&  (this.width == ii.width)
390            &&  (this.height == ii.height)
391
392            // URL-Aquired Extension & Ultimately-Decided-Upon Extension
393            &&  Objects.equals(this.guessedExtension, ii.guessedExtension)
394            &&  Objects.equals(this.actualExtension, ii.actualExtension)
395
396            // class 'Results' Array-Counters (index-pointers)
397            &&  (this.iteratorCounter == ii.iteratorCounter)
398            &&  (this.successCounter == ii.successCounter)
399
400            // This is the lone / only 'non-final' field
401            &&  Objects.equals(this.fileName, ii.fileName);
402    }
403
404    /**
405     * Java's hash-code requirement.  The code is computed by summing the first 15
406     * {@link #imgByteArr} array elements.
407     * 
408     * @return A hash-code that may be used when storing this node in a java sorted-collection.
409     */
410    public int hashCode()
411    {
412       if (url != null) return url.toString().hashCode();
413
414       int sum = 0;
415
416       for (int i=0; (i < 15) && (i < imgByteArr.length); i++) sum += imgByteArr[i];
417
418       return sum;
419    }
420}