1 | // Jomic - a viewer for comic book archives. |
2 | // Copyright (C) 2004-2011 Thomas Aglassinger |
3 | // |
4 | // This program is free software: you can redistribute it and/or modify |
5 | // it under the terms of the GNU General Public License as published by |
6 | // the Free Software Foundation, either version 3 of the License, or |
7 | // (at your option) any later version. |
8 | // |
9 | // This program is distributed in the hope that it will be useful, |
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 | // GNU General Public License for more details. |
13 | // |
14 | // You should have received a copy of the GNU General Public License |
15 | // along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | package net.sf.jomic.comic; |
17 | |
18 | import java.awt.Graphics2D; |
19 | import java.awt.RenderingHints; |
20 | import java.awt.geom.AffineTransform; |
21 | import java.awt.image.RenderedImage; |
22 | |
23 | import net.sf.jomic.common.ComicSheetRenderSettings; |
24 | import net.sf.jomic.common.Settings; |
25 | import net.sf.jomic.tools.ImageTools; |
26 | |
27 | import org.apache.commons.logging.Log; |
28 | import org.apache.commons.logging.LogFactory; |
29 | |
30 | /** |
31 | * Layout specifying how to render the images of a ComicSheet. |
32 | * |
33 | * @author Thomas Aglassinger |
34 | * @see net.sf.jomic.comic.ComicSheet |
35 | */ |
36 | public class ComicSheetLayout |
37 | { |
38 | public static final int RENDER_LEFT = 1; |
39 | public static final int RENDER_RIGHT = 2; |
40 | public static final int RENDER_BOTH = 3; |
41 | static final double NO_SCALE = -1.0; |
42 | |
43 | /** |
44 | * Threshold for how much heights of two images might differ while still being considered to be |
45 | * "similar enough" to be leveled (1.0 means 100%). |
46 | */ |
47 | private static final double SAME_HEIGHT_THRESHOLD = 0.1; |
48 | |
49 | /** |
50 | * Threshold for how much ratios of two images might differ before they are not considered |
51 | * "similar", and one of them is filled up with a border using the background color (1.0 |
52 | * means 100%). |
53 | */ |
54 | private static final double SAME_RATIO_THRESHOLD = 0.1; |
55 | |
56 | private ImageTools imageTools; |
57 | private Log logger; |
58 | private /*@ spec_public nullable @*/ ComicSheetRenderSettings renderSettings; |
59 | private /*@ spec_public nullable @*/ RenderedImage leftImage; |
60 | private /*@ spec_public nullable @*/ RenderedImage rightImage; |
61 | private /*@ spec_public @*/ int leftImageHeight; |
62 | private /*@ spec_public @*/ int leftImageWidth; |
63 | private /*@ spec_public @*/ int rightImageHeight; |
64 | private /*@ spec_public @*/ int rightImageWidth; |
65 | private Settings settings; |
66 | |
67 | //@ invariant leftImageHeight >= 0; |
68 | //@ invariant leftImageWidth >= 0; |
69 | //@ invariant rightImageHeight >= 0; |
70 | //@ invariant rightImageWidth >= 0; |
71 | //@ invariant (rightImage != null) ==> (leftImage != null); |
72 | |
73 | public ComicSheetLayout() { |
74 | logger = LogFactory.getLog(ComicSheetLayout.class); |
75 | settings = Settings.instance(); |
76 | imageTools = ImageTools.instance(); |
77 | } |
78 | |
79 | /** |
80 | * Set images and settings to be used for layout. |
81 | */ |
82 | //@ ensures leftImage == newLeftImage; |
83 | //@ ensures rightImage == newRightImage; |
84 | //@ ensures renderSettings == newRenderSettings; |
85 | public void prepare(RenderedImage newLeftImage, /*@ nullable @*/ RenderedImage newRightImage, |
86 | ComicSheetRenderSettings newRenderSettings) { |
87 | leftImage = newLeftImage; |
88 | leftImageWidth = leftImage.getWidth(); |
89 | leftImageHeight = leftImage.getHeight(); |
90 | rightImage = newRightImage; |
91 | if (rightImage == null) { |
92 | rightImageWidth = 0; |
93 | rightImageHeight = 0; |
94 | } else { |
95 | rightImageWidth = rightImage.getWidth(); |
96 | rightImageHeight = rightImage.getHeight(); |
97 | } |
98 | renderSettings = newRenderSettings; |
99 | } |
100 | |
101 | /** |
102 | * Get transformation to rotate image(s) during <code>render()</code>. |
103 | */ |
104 | //@ requires targetWidth > 0; |
105 | //@ requires targetHeight > 0; |
106 | public AffineTransform getTargetRotationTransformation(int targetWidth, int targetHeight) { |
107 | AffineTransform result = new AffineTransform(); |
108 | int rotation = renderSettings.getRotation(); |
109 | |
110 | if (rotation == ImageTools.ROTATE_COUNTERCLOCKWISE) { |
111 | result.rotate(-Math.PI / 2); |
112 | result.translate(-targetHeight, 0); |
113 | } else if (rotation == ImageTools.ROTATE_CLOCKWISE) { |
114 | result.rotate(Math.PI / 2); |
115 | result.translate(0, -targetWidth); |
116 | } else if (rotation == ImageTools.ROTATE_UPSIDE_DOWN) { |
117 | result.rotate(Math.PI); |
118 | result.translate(-targetWidth, -targetHeight); |
119 | } else { |
120 | //@ assert rotation = ImageTools.ROTATE_NONE; |
121 | } |
122 | return result; |
123 | } |
124 | |
125 | /** |
126 | * Get scales needed to render <code>leftImage</code> and/or |
127 | * <code>rightImage</code> in a target with size <code>targetWidth</code> |
128 | * x <code>targetHeight</code>. |
129 | * |
130 | * @param imagesToRender |
131 | * which images to render: RENDER_LEFT, RENDER_RIGHT, RENDER_BOTH |
132 | * @return result[0] is the scale for the left image and result[1] for the |
133 | * right image or <code>NO_SCALE</code> if there is no such image |
134 | */ |
135 | //@ requires leftImage != null; |
136 | //@ requires targetWidth > 0; |
137 | //@ requires targetHeight > 0; |
138 | //@ requires (imagesToRender == RENDER_LEFT) |
139 | //@ || (imagesToRender == RENDER_RIGHT) |
140 | //@ || (imagesToRender == RENDER_BOTH); |
141 | //@ requires (imagesToRender == RENDER_RIGHT) ==> (rightImage != null); |
142 | //@ ensures \result.length == 2; |
143 | //@ ensures (imagesToRender != RENDER_RIGHT) ==> \result[0] > 0.0; |
144 | //@ ensures (imagesToRender == RENDER_RIGHT) ==> (\result[0] == NO_SCALE); |
145 | //@ ensures (imagesToRender == RENDER_LEFT) ==> (\result[1] == NO_SCALE); |
146 | //@ ensures (rightImage == null) ==> (\result[1] == NO_SCALE); |
147 | //@ ensures ((rightImage != null) && (imagesToRender != RENDER_LEFT))==> (\result[1] > 0.0); |
148 | public double[] getTargetScales(int targetWidth, int targetHeight, int imagesToRender) { |
149 | double[] result = new double[] {NO_SCALE, NO_SCALE}; |
150 | if ((imagesToRender == RENDER_LEFT) || renderBothButHasOnlyLeft(imagesToRender)) { |
151 | result[0] = imageTools.getScaleToSqueeze(targetWidth, targetHeight, |
152 | leftImageWidth, leftImageHeight, renderSettings.getScaleMode()); |
153 | } else if (imagesToRender == RENDER_RIGHT) { |
154 | result[1] = imageTools.getScaleToSqueeze(targetWidth, targetHeight, |
155 | rightImageWidth, rightImageHeight, renderSettings.getScaleMode()); |
156 | } else if (imagesToRender == RENDER_BOTH) { |
157 | if (renderSettings.getScaleMode().equals(ImageTools.SCALE_ACTUAL)) { |
158 | result[0] = 1.0; |
159 | result[1] = 1.0; |
160 | } else { |
161 | result = getTargetScalesForBoth(targetWidth, targetHeight, |
162 | leftImageWidth, leftImageHeight, rightImageWidth, rightImageHeight); |
163 | } |
164 | } else { |
165 | //@ assert false; |
166 | } |
167 | return result; |
168 | } |
169 | |
170 | /** |
171 | * Get the top left corner in for drawing the sheet in a target with size |
172 | * <code>targetWidth</code> x <code>targetHeight</code>. |
173 | */ |
174 | //@ requires leftImage != null; |
175 | //@ requires targetWidth > 0; |
176 | //@ requires targetHeight > 0; |
177 | //@ requires (imagesToRender == RENDER_LEFT) |
178 | //@ || (imagesToRender == RENDER_RIGHT) |
179 | //@ || (imagesToRender == RENDER_BOTH); |
180 | //@ requires (imagesToRender == RENDER_RIGHT) ==> (rightImage != null); |
181 | //@ ensures \result.length == 2; |
182 | //@ ensures \result[0] >= 0; |
183 | //@ ensures \result[1] >= 0; |
184 | //@ signals_only IllegalArgumentException; |
185 | public int[] getTargetTopLeft(int targetWidth, int targetHeight, int imagesToRender, double[] scales) { |
186 | int[] result; |
187 | double leftScale = scales[0]; |
188 | double rightScale = scales[1]; |
189 | int totalWidth; |
190 | int totalHeight; |
191 | |
192 | if ((imagesToRender == RENDER_LEFT) || renderBothButHasOnlyLeft(imagesToRender)) { |
193 | totalWidth = (int) Math.ceil(leftImageWidth * leftScale); |
194 | totalHeight = (int) Math.ceil(leftImageHeight * leftScale); |
195 | } else if (imagesToRender == RENDER_RIGHT) { |
196 | totalWidth = (int) Math.ceil(rightImageWidth * rightScale); |
197 | totalHeight = (int) Math.ceil(rightImageHeight * rightScale); |
198 | } else if (imagesToRender == RENDER_BOTH) { |
199 | totalWidth = (int) (Math.ceil(leftImageWidth * leftScale) + Math.ceil(rightImageWidth * rightScale)); |
200 | totalHeight = (int) Math.ceil(Math.max(leftImageHeight * leftScale, rightImageHeight * rightScale)); |
201 | } else { |
202 | throw new IllegalArgumentException("imagesToRender=" + imagesToRender); |
203 | } |
204 | result = new int[] {(targetWidth - totalWidth) / 2, (targetHeight - totalHeight) / 2}; |
205 | return result; |
206 | } |
207 | |
208 | //@ requires (imagesToRender == RENDER_LEFT) |
209 | //@ || (imagesToRender == RENDER_RIGHT) |
210 | //@ || (imagesToRender == RENDER_BOTH); |
211 | private /*@ pure @*/ boolean renderBothButHasOnlyLeft(int imagesToRender) { |
212 | return (imagesToRender == RENDER_BOTH) && (rightImage == null); |
213 | } |
214 | |
215 | private boolean isRotatedOnce() { |
216 | int rotation = renderSettings.getRotation(); |
217 | |
218 | return (rotation == ImageTools.ROTATE_CLOCKWISE) || (rotation == ImageTools.ROTATE_COUNTERCLOCKWISE); |
219 | } |
220 | |
221 | public void renderTo(Graphics2D myGraphics, int screenWidth, int screenHeight, int imageIndex, |
222 | ComicSheet comicSheet, RenderedImage leftImageToRender, RenderedImage rightImageToRender, |
223 | ComicSheetRenderSettings newRenderSettings) { |
224 | int imagesToRender = getImagesToRender(imageIndex, comicSheet, rightImageToRender, newRenderSettings); |
225 | |
226 | prepare(leftImageToRender, rightImageToRender, newRenderSettings); |
227 | assert renderSettings == newRenderSettings; |
228 | |
229 | AffineTransform rotationTransformation; |
230 | double[] scales; |
231 | int[] topLeftCorner; |
232 | |
233 | // Figure out if we really need a rotation |
234 | boolean isSuppressableImage = isNonRotatableImage(leftImageToRender, imagesToRender); |
235 | boolean actuallyRotate = !isRotatedOnce() || !isSuppressableImage; |
236 | logger.debug("rotation=" + renderSettings.getRotation()); |
237 | logger.debug("isRotatedOnce=" + isRotatedOnce()); |
238 | logger.debug("rotateOnlySingle=" + renderSettings.getRotateOnlySinglePortraitImages()); |
239 | logger.debug("twoPageMode=" + renderSettings.getTwoPageMode()); |
240 | logger.debug("isRenderLeft=" + (imagesToRender == RENDER_LEFT)); |
241 | logger.debug("isLandscape=" + imageTools.isLandscape(leftImageToRender)); |
242 | logger.debug("isSuppressableImage=" + isSuppressableImage); |
243 | logger.debug("actuallyRotate=" + actuallyRotate); |
244 | |
245 | if (actuallyRotate) { |
246 | rotationTransformation = getTargetRotationTransformation(screenWidth, screenHeight); |
247 | } else { |
248 | rotationTransformation = new AffineTransform(); |
249 | } |
250 | |
251 | if (actuallyRotate && isRotatedOnce()) { |
252 | scales = getTargetScales(screenHeight, screenWidth, imagesToRender); |
253 | topLeftCorner = getTargetTopLeft(screenHeight, screenWidth, imagesToRender, scales); |
254 | } else { |
255 | scales = getTargetScales(screenWidth, screenHeight, imagesToRender); |
256 | topLeftCorner = getTargetTopLeft(screenWidth, screenHeight, imagesToRender, scales); |
257 | } |
258 | |
259 | myGraphics.setBackground(settings .getFillColor()); |
260 | myGraphics.clearRect(0, 0, screenWidth, screenHeight); |
261 | render(myGraphics, imagesToRender, scales, topLeftCorner, rotationTransformation); |
262 | } |
263 | |
264 | // TODO: Rename to isRotatableImage and adjust logic. |
265 | private boolean isNonRotatableImage(RenderedImage leftImageToRender, |
266 | int imagesToRender) { |
267 | return (renderSettings.getRotateOnlySinglePortraitImages() |
268 | && !renderSettings.getTwoPageMode() |
269 | && (imagesToRender == RENDER_LEFT) |
270 | && imageTools.isLandscape(leftImageToRender)); |
271 | } |
272 | |
273 | private int getImagesToRender(int imageIndex, ComicSheet comicSheet, |
274 | RenderedImage rightImageToRender, |
275 | ComicSheetRenderSettings newRenderSettings) { |
276 | int imagesToRender; |
277 | if (rightImageToRender == null) { |
278 | imagesToRender = RENDER_LEFT; |
279 | } else if (newRenderSettings.getTwoPageMode()) { |
280 | imagesToRender = RENDER_BOTH; |
281 | } else { |
282 | if (imageIndex == comicSheet.getLeftImageIndex()) { |
283 | imagesToRender = RENDER_LEFT; |
284 | } else { |
285 | imagesToRender = RENDER_RIGHT; |
286 | } |
287 | } |
288 | return imagesToRender; |
289 | } |
290 | |
291 | /** |
292 | * Render images of comic sheet to <code>target</code>. |
293 | * |
294 | * @param target |
295 | * where to render images |
296 | * @param imagesToRender |
297 | * which images to render: RENDER_LEFT, RENDER_RIGHT, RENDER_BOTH |
298 | */ |
299 | //@ requires leftImage != null; |
300 | //@ requires (imagesToRender == RENDER_LEFT) |
301 | //@ || (imagesToRender == RENDER_RIGHT) |
302 | //@ || (imagesToRender == RENDER_BOTH); |
303 | //@ requires (imagesToRender == RENDER_RIGHT) ==> (rightImage != null); |
304 | //@ requires scales.length == 2; |
305 | //@ requires topLeft.length == 2; |
306 | void render(Graphics2D target, int imagesToRender, double[] scales, int[] topLeft, |
307 | AffineTransform rotationTransformation) { |
308 | double leftScale; |
309 | double rightScale; |
310 | int topX = topLeft[0]; |
311 | int topY = topLeft[1]; |
312 | RenderedImage leftImageToRender; |
313 | RenderedImage rightImageToRender; |
314 | |
315 | target.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); |
316 | target.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); |
317 | if (renderSettings.getSwapLeftAndRightImage() |
318 | && (imagesToRender == RENDER_BOTH) |
319 | && !renderBothButHasOnlyLeft(imagesToRender)) { |
320 | leftImageToRender = rightImage; |
321 | leftScale = scales[1]; |
322 | rightImageToRender = leftImage; |
323 | rightScale = scales[0]; |
324 | } else { |
325 | leftImageToRender = leftImage; |
326 | leftScale = scales[0]; |
327 | rightImageToRender = rightImage; |
328 | rightScale = scales[1]; |
329 | } |
330 | |
331 | if ((imagesToRender == RENDER_LEFT) || renderBothButHasOnlyLeft(imagesToRender)) { |
332 | AffineTransform leftTransformation = new AffineTransform(rotationTransformation); |
333 | |
334 | leftTransformation.translate(topX, topY); |
335 | leftTransformation.scale(leftScale, leftScale); |
336 | target.drawRenderedImage(leftImageToRender, leftTransformation); |
337 | } else if (imagesToRender == RENDER_RIGHT) { |
338 | AffineTransform rightTransformation = new AffineTransform(rotationTransformation); |
339 | |
340 | rightTransformation.translate(topX, topY); |
341 | rightTransformation.scale(rightScale, rightScale); |
342 | target.drawRenderedImage(rightImageToRender, rightTransformation); |
343 | } else if (imagesToRender == RENDER_BOTH) { |
344 | AffineTransform leftTransformation = new AffineTransform(rotationTransformation); |
345 | |
346 | leftTransformation.translate(topX, topY); |
347 | leftTransformation.scale(leftScale, leftScale); |
348 | target.drawRenderedImage(leftImageToRender, leftTransformation); |
349 | |
350 | AffineTransform rightTransformation = new AffineTransform(rotationTransformation); |
351 | double rightDeltaX = Math.ceil(leftScale * leftImageToRender.getWidth()); |
352 | |
353 | rightTransformation.translate(topX + rightDeltaX, topY); |
354 | rightTransformation.scale(rightScale, rightScale); |
355 | target.drawRenderedImage(rightImageToRender, rightTransformation); |
356 | } else { |
357 | //@ assert false; |
358 | } |
359 | } |
360 | |
361 | //@ requires !renderSettings.getScaleMode().equals(ImageTools.SCALE_ACTUAL); |
362 | //@ ensures \result.length == 2; |
363 | private double[] getTargetScalesForBoth(int viewWidth, int viewHeight, |
364 | int leftWidth, int leftHeight, int rightWidth, int rightHeight) { |
365 | double[] result = new double[]{NO_SCALE, NO_SCALE}; |
366 | String scaleMode = renderSettings.getScaleMode(); |
367 | double heigthRatio = ((double) leftHeight) / rightHeight; |
368 | double heightMeasure = Math.abs(heigthRatio - 1); |
369 | boolean heightsAreSimilar = heightMeasure < SAME_HEIGHT_THRESHOLD; |
370 | double leftRatio = ((double) leftWidth) / leftHeight; |
371 | double rightRatio = ((double) rightWidth) / rightHeight; |
372 | double ratioMeasure = Math.abs(Math.abs(leftRatio / rightRatio) - 1); |
373 | boolean ratiosAreSimilar = ratioMeasure < SAME_RATIO_THRESHOLD; |
374 | int actualViewWidth = viewWidth; |
375 | int actualViewHeight = viewHeight; |
376 | int leftViewWidth; |
377 | int leftViewHeight; |
378 | int rightViewWidth; |
379 | int rightViewHeight; |
380 | |
381 | if (logger.isDebugEnabled()) { |
382 | logger.debug("size: left=" + leftWidth + "x" + leftHeight + ", right=" |
383 | + rightWidth + "x" + rightHeight); |
384 | logger.debug("ratio: left=" + leftRatio + ", right=" + rightRatio |
385 | + ", heightMeasure=" + heightMeasure |
386 | + ", heightsAreSimilar=" + heightsAreSimilar |
387 | + ", ratioMeasure=" + ratioMeasure |
388 | + ", ratiosAreSimilar=" + ratiosAreSimilar); |
389 | } |
390 | if (heightsAreSimilar || ratiosAreSimilar) { |
391 | logger.debug("level heights"); |
392 | if (heigthRatio > 1) { |
393 | leftRatio = 1; |
394 | rightRatio = heigthRatio; |
395 | } else { |
396 | leftRatio = 1.0 / heigthRatio; |
397 | rightRatio = 1; |
398 | } |
399 | } else { |
400 | logger.debug("level widths"); |
401 | double widthRatio = ((double) leftWidth) / rightWidth; |
402 | |
403 | if (widthRatio > 1) { |
404 | leftRatio = 1; |
405 | rightRatio = widthRatio; |
406 | } else { |
407 | leftRatio = 1.0 / widthRatio; |
408 | rightRatio = 1; |
409 | } |
410 | } |
411 | if (logger.isDebugEnabled()) { |
412 | logger.debug("adjusted ratio: left=" + leftRatio + ", right=" + rightRatio); |
413 | } |
414 | |
415 | // Compute the rectangle both images have to share, |
416 | // independent of the scale mode |
417 | double adjustedLeftWidth = leftRatio * leftWidth; |
418 | double adjustedLeftHeight = leftRatio * leftHeight; |
419 | double adjustedRightWidth = rightRatio * rightWidth; |
420 | double adjustedRightHeight = rightRatio * rightHeight; |
421 | double adjustedTotalWidth = adjustedLeftWidth + adjustedRightWidth; |
422 | double adjustedTotalHeight = Math.max(adjustedLeftHeight, adjustedRightHeight); |
423 | |
424 | if (logger.isDebugEnabled()) { |
425 | logger.debug("adjusted: squeeze=" + adjustedTotalWidth + "x" + adjustedTotalHeight); |
426 | } |
427 | |
428 | |
429 | if (scaleMode.equals(ImageTools.SCALE_HEIGHT)) { |
430 | logger.debug("setting viewHeight to maximum"); |
431 | actualViewWidth = Integer.MAX_VALUE; |
432 | } else if (scaleMode.equals(ImageTools.SCALE_WIDTH)) { |
433 | logger.debug("setting viewWidth to maximum"); |
434 | actualViewHeight = Integer.MAX_VALUE; |
435 | } |
436 | |
437 | leftViewWidth = (int) Math.round(actualViewWidth * adjustedLeftWidth / adjustedTotalWidth); |
438 | leftViewHeight = (int) Math.round(actualViewHeight * adjustedLeftHeight / adjustedTotalHeight); |
439 | rightViewWidth = actualViewWidth - leftViewWidth; |
440 | rightViewHeight = (int) Math.round(actualViewHeight * adjustedRightHeight / adjustedTotalHeight); |
441 | |
442 | if (logger.isDebugEnabled()) { |
443 | logger.debug("full: view=" + actualViewWidth + "x" + actualViewHeight); |
444 | logger.debug( |
445 | "left: view=" |
446 | + leftViewWidth |
447 | + "x" |
448 | + leftViewHeight |
449 | + ", adjusted=" |
450 | + (int) adjustedLeftWidth |
451 | + "x" |
452 | + (int) adjustedLeftHeight); |
453 | logger.debug( |
454 | "right: view=" |
455 | + rightViewWidth |
456 | + "x" |
457 | + rightViewHeight |
458 | + ", adjusted=" |
459 | + (int) adjustedRightWidth |
460 | + "x" |
461 | + (int) adjustedRightHeight); |
462 | } |
463 | |
464 | result[0] = imageTools.getScaleToSqueeze( |
465 | leftViewWidth, leftViewHeight, leftWidth, leftHeight, scaleMode); |
466 | result[1] = imageTools.getScaleToSqueeze( |
467 | rightViewWidth, rightViewHeight, rightWidth, rightHeight, scaleMode); |
468 | return result; |
469 | } |
470 | } |