openseadragon.js与deep zoom java实现艺术品图片展示
openseadragon.js 是一款用来做图像缩放的插件,它可以用来做图片展示,做展示的插件很多,也很优秀,但大多数都解决不了图片尺寸过大的问题。
艺术品图像展示就是最简单的例子,展示此类图片一般要求比较精细,所以图片尺寸很大,如果按照普通的方式直接将整个图片加载,要耗费巨大的带宽。
openseadragon.js 即为此而生,它展示的图像,必须经过切割处理,当放大图像时,才去加载更大的尺寸,真正做到了按需加载。
值得一提的是,openseadragon.js是微软公司的一款开源产品,非常难得。
openseadragon.js用法很简单。
定义html容器
1 <div id="container" style="width: 100%; height: 600px;"></div>
初始化openseadragon.js
1 (function(win){ 2 var viewer = OpenSeadragon({ 3 // debugMode: true, 4 id: "container", //容器id 5 prefixUrl: "./lib/openseadragon/images/", //openseadragon插件资源路径 6 tileSources: "./image/earth.xml", //openseadragon 图片资源xml 7 showNavigator:true //是否显示控制按钮 8 }); 9 console.log(viewer); 10 })(window);
效果图
对,你没有看错,使用openseadragon.js,只需要copy一段初始化代码,初始化代码格式基本固定,只有tileSources是需要变化的,想显示哪个图,就写哪个图的xml链接。
那么这个xml是个什么东西呢?这是微软定义的一套Deep Zoom技术,或者叫规范,通过图像分割,然后配上一个xml文件,即可按照用户缩放的尺寸,按需加载图像,直到最清晰为止。这本来是微软Silverlight里的技术,它的JavaScript实现就是openseadragon.js。
笔者从google code上找到了一份java程序,专门用来生成Deep Zoom。笔者在原作基础上做了一些小小的改进,原作有些错误。。。
Deep Zoom For Java
1 import java.awt.Graphics2D; 2 import java.awt.RenderingHints; 3 import java.awt.image.BufferedImage; 4 import java.io.File; 5 import java.io.FileOutputStream; 6 import java.io.IOException; 7 import java.nio.ByteBuffer; 8 import java.nio.channels.FileChannel; 9 import javax.imageio.ImageIO; 10 11 /** 12 * Deep Zoom Converter 13 * 14 * @author 杨元 15 * 16 */ 17 public class DeepZoomUtil { 18 static final String xmlHeader = "<?xml version=\"1.0\" encoding=\"utf-8\"?>"; 19 static final String schemaName = "http://schemas.microsoft.com/deepzoom/2009"; 20 21 static Boolean deleteExisting = true; 22 static String tileFormat = "jpg"; 23 24 // settings 25 static int tileSize = 256; 26 static int tileOverlap = 1; 27 static Boolean verboseMode = false; 28 static Boolean debugMode = false; 29 30 /** 31 * @param args the command line arguments 32 */ 33 public static void main(String[] args) { 34 35 try { 36 processImageFile(new File("d:/earth.jpg"), new File("d:/earth")); 37 } catch (Exception e) { 38 e.printStackTrace(); 39 } 40 } 41 42 /** 43 * Process the given image file, producing its Deep Zoom output files 44 * in a subdirectory of the given output directory. 45 * @param inFile the file containing the image 46 * @param outputDir the output directory 47 */ 48 private static void processImageFile(File inFile, File outputDir) throws IOException { 49 if (verboseMode) 50 System.out.printf("Processing image file: %s\n", inFile); 51 52 String fileName = inFile.getName(); 53 String nameWithoutExtension = fileName.substring(0, fileName.lastIndexOf('.')); 54 String pathWithoutExtension = outputDir + File.separator + nameWithoutExtension; 55 56 BufferedImage image = loadImage(inFile); 57 58 int originalWidth = image.getWidth(); 59 int originalHeight = image.getHeight(); 60 61 double maxDim = Math.max(originalWidth, originalHeight); 62 63 int nLevels = (int)Math.ceil(Math.log(maxDim) / Math.log(2)); 64 65 if (debugMode) 66 System.out.printf("nLevels=%d\n", nLevels); 67 68 // Delete any existing output files and folders for this image 69 70 File descriptor = new File(pathWithoutExtension + ".xml"); 71 if (descriptor.exists()) { 72 if (deleteExisting) 73 deleteFile(descriptor); 74 else 75 throw new IOException("File already exists in output dir: " + descriptor); 76 } 77 78 File imgDir = new File(pathWithoutExtension); 79 if (imgDir.exists()) { 80 if (deleteExisting) { 81 if (debugMode) 82 System.out.printf("Deleting directory: %s\n", imgDir); 83 deleteDir(imgDir); 84 } else 85 throw new IOException("Image directory already exists in output dir: " + imgDir); 86 } 87 88 imgDir = createDir(outputDir, nameWithoutExtension.concat("_files")); 89 90 double width = originalWidth; 91 double height = originalHeight; 92 93 for (int level = nLevels; level >= 0; level--) { 94 int nCols = (int)Math.ceil(width / tileSize); 95 int nRows = (int)Math.ceil(height / tileSize); 96 if (debugMode) 97 System.out.printf("level=%d w/h=%f/%f cols/rows=%d/%d\n", 98 level, width, height, nCols, nRows); 99 100 File dir = createDir(imgDir, Integer.toString(level)); 101 for (int col = 0; col < nCols; col++) { 102 for (int row = 0; row < nRows; row++) { 103 BufferedImage tile = getTile(image, row, col); 104 saveImage(tile, dir + File.separator + col + '_' + row); 105 } 106 } 107 108 // Scale down image for next level 109 width = Math.ceil(width / 2); 110 height = Math.ceil(height / 2); 111 if (width > 10 && height > 10) { 112 // resize in stages to improve quality 113 image = resizeImage(image, width * 1.66, height * 1.66); 114 image = resizeImage(image, width * 1.33, height * 1.33); 115 } 116 image = resizeImage(image, width, height); 117 } 118 119 saveImageDescriptor(originalWidth, originalHeight, descriptor); 120 } 121 122 123 /** 124 * Delete a file 125 * @param path the path of the directory to be deleted 126 */ 127 private static void deleteFile(File file) throws IOException { 128 if (!file.delete()) 129 throw new IOException("Failed to delete file: " + file); 130 } 131 132 /** 133 * Recursively deletes a directory 134 * @param path the path of the directory to be deleted 135 */ 136 private static void deleteDir(File dir) throws IOException { 137 if (!dir.isDirectory()) 138 deleteFile(dir); 139 else { 140 for (File file : dir.listFiles()) { 141 if (file.isDirectory()) 142 deleteDir(file); 143 else 144 deleteFile(file); 145 } 146 if (!dir.delete()) 147 throw new IOException("Failed to delete directory: " + dir); 148 } 149 } 150 151 /** 152 * Creates a directory 153 * @param parent the parent directory for the new directory 154 * @param name the new directory name 155 */ 156 private static File createDir(File parent, String name) throws IOException { 157 assert(parent.isDirectory()); 158 File result = new File(parent + File.separator + name); 159 if (!result.mkdir()) 160 throw new IOException("Unable to create directory: " + result); 161 return result; 162 } 163 164 /** 165 * Loads image from file 166 * @param file the file containing the image 167 */ 168 private static BufferedImage loadImage(File file) throws IOException { 169 BufferedImage result = null; 170 try { 171 result = ImageIO.read(file); 172 } catch (Exception e) { 173 throw new IOException("Cannot read image file: " + file); 174 } 175 return result; 176 } 177 178 /** 179 * Gets an image containing the tile at the given row and column 180 * for the given image. 181 * @param img - the input image from whihc the tile is taken 182 * @param row - the tile's row (i.e. y) index 183 * @param col - the tile's column (i.e. x) index 184 */ 185 private static BufferedImage getTile(BufferedImage img, int row, int col) { 186 int x = col * tileSize - (col == 0 ? 0 : tileOverlap); 187 int y = row * tileSize - (row == 0 ? 0 : tileOverlap); 188 int w = tileSize + (col == 0 ? 1 : 2) * tileOverlap; 189 int h = tileSize + (row == 0 ? 1 : 2) * tileOverlap; 190 191 if (x + w > img.getWidth()) 192 w = img.getWidth() - x; 193 if (y + h > img.getHeight()) 194 h = img.getHeight() - y; 195 196 if (debugMode) 197 System.out.printf("getTile: row=%d, col=%d, x=%d, y=%d, w=%d, h=%d\n", 198 row, col, x, y, w, h); 199 200 assert(w > 0); 201 assert(h > 0); 202 203 BufferedImage result = new BufferedImage(w, h, img.getType()); 204 Graphics2D g = result.createGraphics(); 205 g.drawImage(img, 0, 0, w, h, x, y, x+w, y+h, null); 206 207 return result; 208 } 209 210 /** 211 * Returns resized image 212 * NB - useful reference on high quality image resizing can be found here: 213 * http://today.java.net/pub/a/today/2007/04/03/perils-of-image-getscaledinstance.html 214 * @param width the required width 215 * @param height the frequired height 216 * @param img the image to be resized 217 */ 218 private static BufferedImage resizeImage(BufferedImage img, double width, double height) { 219 int w = (int)width; 220 int h = (int)height; 221 BufferedImage result = new BufferedImage(w, h, img.getType()); 222 Graphics2D g = result.createGraphics(); 223 g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, 224 RenderingHints.VALUE_INTERPOLATION_BICUBIC); 225 g.drawImage(img, 0, 0, w, h, 0, 0, img.getWidth(), img.getHeight(), null); 226 return result; 227 } 228 229 /** 230 * Saves image to the given file 231 * @param img the image to be saved 232 * @param path the path of the file to which it is saved (less the extension) 233 */ 234 private static void saveImage(BufferedImage img, String path) throws IOException { 235 File outputFile = new File(path + "." + tileFormat); 236 try { 237 ImageIO.write(img, tileFormat, outputFile); 238 } catch (IOException e) { 239 throw new IOException("Unable to save image file: " + outputFile); 240 } 241 } 242 243 /** 244 * Write image descriptor XML file 245 * @param width image width 246 * @param height image height 247 * @param file the file to which it is saved 248 */ 249 private static void saveImageDescriptor(int width, int height, File file) throws IOException { 250 StringBuilder sb = new StringBuilder(256); 251 sb.append(xmlHeader); 252 sb.append("<Image TileSize=\""); 253 sb.append(tileSize); 254 sb.append("\" Overlap=\""); 255 sb.append(tileOverlap); 256 sb.append("\" Format=\""); 257 sb.append(tileFormat); 258 sb.append("\" ServerFormat=\"Default\" xmlns=\""); 259 sb.append(schemaName); 260 sb.append("\">"); 261 sb.append("<Size Width=\""); 262 sb.append(width); 263 sb.append("\" Height=\""); 264 sb.append(height); 265 sb.append("\" />"); 266 sb.append("</Image>"); 267 saveText(sb.toString().getBytes("UTF-8"), file); 268 } 269 270 /** 271 * Saves strings as text to the given file 272 * @param bytes the image to be saved 273 * @param file the file to which it is saved 274 */ 275 private static void saveText(byte[] bytes, File file) throws IOException { 276 try { 277 //输出流 278 FileOutputStream fos = new FileOutputStream(file); 279 //从输出流中创建写通道 280 FileChannel writeChannel = fos.getChannel(); 281 //将既有数组作为buffer内存空间 282 ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); 283 //将buffer写到磁盘 284 writeChannel.write(byteBuffer); 285 286 writeChannel.close(); 287 } catch (IOException e) { 288 throw new IOException("Unable to write to text file: " + file); 289 } 290 } 291 }
调用非常简单,只有一句话:processImageFile(new File("d:/earth.jpg"), new File("d:/earth"));,第一个参数是图片路径,第二个参数是生成的Deep Zoom保存路径。本例将会在d:/earth目录下生成一个earth.xml文件和一个earth_files文件夹,xml的文件名默认和图片的文件名一致,然后直接把earth.xml的url返回给前端的openseadragon.js,就可以实现图像缩放。
需要注意的是,*_files文件夹必须和xml文件在同一目录,并且*要和xml文件名保持一致。
想要预览openseadragon.js效果,必须在真实的http容器中,不可以直接在文件中打开。
打包的openseadragon.js笔者做了一些UI上的美化,个人觉得漂亮些,如果读者不喜欢,可以用包里的原版。