#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright 2007 Catalin Francu # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import pygtk pygtk.require('2.0') import gtk import os import pango import sys BASE_FONT = 'monospace' MENU = ''' ''' ATTR_BOLD = 0 ATTR_ITALIC = 1 ATTR_UNDERLINE = 2 DIR_LEFT = 0 DIR_RIGHT = 1 DIR_TOP = 2 DIR_BOTTOM = 3 class Symbol: text = '' left = 0 right = 0 top = 0 bottom = 0 bold = False italic = False underline = False spaceBefore = False entry = None handlers = [] def setEntryFont(self): font = BASE_FONT if self.bold: font += ' bold' #endif if self.italic: font += ' italic' #endif self.entry.modify_font(pango.FontDescription(font)) if self.underline: self.entry.set_width_chars(len(unicode(self.text)) + 1) self.entry.set_text("'" + self.text) else: self.entry.set_width_chars(len(unicode(self.text))) self.entry.set_text(self.text) #endif #enddef def clone(self): s = Symbol() s.text = self.text s.left = self.left s.right = self.right s.top = self.top s.bottom = self.bottom s.bold = self.bold s.italic = self.italic s.underline = self.underline s.spaceBefore = self.spaceBefore s.entry = self.entry s.handlers = self.handlers return s #enddef def deleteLabelBefore(self): e = self.entry box = e.get_parent() pos = box.child_get_property(e, "position") label = box.get_children()[pos - 1] label.destroy() #enddef def __str__(self): return 'Text [%s] L%d R%d T%d B%d' % ( self.text, self.left, self.right, self.top, self.bottom) #enddef #endclass # Returns a list of lines. Each line contains a list of symbols # FIELD_* constants. def loadBoxData(boxName, height): f = open(boxName, 'r') result = [] symbolLine = [] prevRight = -1 for line in f: (text, left, bottom, right, top) = line.split() s = Symbol() if (text.startswith('@')): s.bold = True text = text[1:] #endif if (text.startswith('$')): s.italic = True text = text[1:] #endif if (text.startswith("'")): s.underline = True text = text[1:] #endif s.text = text s.left = int(left) s.right = int(right) s.top = height - int(top) s.bottom = height - int(bottom) s.spaceBefore = s.left >= prevRight + 6 and prevRight != -1 if s.left < prevRight - 10: result.append(symbolLine) symbolLine = [] #endif symbolLine.append(s) prevRight = s.right #endfor result.append(symbolLine) f.close() return result #enddef # Ensures that the adjustment is set to include the range of size "size" # starting at "start" def ensureVisible(adjustment, start, size): # Compute the visible range visLo = adjustment.value visHi = adjustment.value + adjustment.page_size if start <= visLo or start + size >= visHi: desired = start - (adjustment.page_size - size) / 2 if desired < 0: desired = 0 elif desired + adjustment.page_size >= adjustment.upper: desired = adjustment.upper - adjustment.page_size #endif adjustment.set_value(desired) #endif #enddef def findImageHeight(fileName): f = os.popen('identify -format "%h" ' + fileName) height = int(f.readline()) f.close() return height #enddef # Counts all the black pixels in column x def countBlackPixels(pixels, x): numPixels = 0; for row in pixels: if isBlack(row[x]): numPixels += 1 #endif #endfor return numPixels #enddef def isBlack(pixel): return pixel[0] + pixel[1] + pixel[2] < 128 * 3 #enddef class MainWindow: pixbuf = None selectedRow = None selectedColumn = None buttonUpdateInProgress = None boxes = None def reconnectEntries(self, rowIndex): row = self.boxes[rowIndex] for col in range(0, len(row)): e = row[col].entry for handler in row[col].handlers: e.disconnect(handler) #endfor row[col].handlers = [ e.connect('focus-in-event', self.onEntryFocus, rowIndex, col), e.connect('changed', self.onEntryChanged, rowIndex, col), e.connect("key-press-event", self.onEntryKeyPress, rowIndex, col)] #endfor #enddef def errorDialog(self, labelText, parent): dialog = gtk.Dialog("Error", parent, gtk.DIALOG_NO_SEPARATOR | gtk.DIALOG_MODAL, (gtk.STOCK_OK, gtk.RESPONSE_OK)) label = gtk.Label(labelText) dialog.vbox.pack_start(label, True, True, 0) label.show() dialog.run() dialog.destroy() #enddef def makeGtkEntry(self, symbol, row, col): symbol.entry = gtk.Entry(10) symbol.entry.set_text(symbol.text) symbol.entry.set_width_chars(len(unicode(symbol.text))) symbol.setEntryFont() symbol.handlers = [ symbol.entry.connect('focus-in-event', self.onEntryFocus, row, col), symbol.entry.connect('changed', self.onEntryChanged, row, col), symbol.entry.connect("key-press-event", self.onEntryKeyPress, row, col)] #enddef def invalidateImage(self): width, height = self.drawingArea.window.get_size() self.drawingArea.window.invalidate_rect((0, 0, width, height), False) #enddef def onCheckButtonToggled(self, widget, attr): if self.buttonUpdateInProgress or self.selectedRow == None: return #endif value = widget.get_active() symbol = self.boxes[self.selectedRow][self.selectedColumn] if attr == ATTR_BOLD: symbol.bold = value elif attr == ATTR_ITALIC: symbol.italic = value elif attr == ATTR_UNDERLINE: symbol.underline = value #endif symbol.setEntryFont() # The underline attribute does not apply to the entire word if attr == ATTR_UNDERLINE: return #endif row = self.boxes[self.selectedRow] i = self.selectedColumn - 1 while i >= 0 and not row[i + 1].spaceBefore: if attr == ATTR_BOLD: row[i].bold = value elif attr == ATTR_ITALIC: row[i].italic = value #endif row[i].setEntryFont() i -= 1 #endwhile i = self.selectedColumn + 1 while i < len(row) and not row[i].spaceBefore: if attr == ATTR_BOLD: row[i].bold = value elif attr == ATTR_ITALIC: row[i].italic = value #endif row[i].setEntryFont() i += 1 #endwhile #enddef def onEntryFocus(self, entry, ignored, row, column): self.selectedRow = row self.selectedColumn = column # Force the image to refresh self.invalidateImage() # Bring the rectangle into view if necessary s = self.boxes[row][column] width = s.right - s.left height = s.bottom - s.top ensureVisible(self.scrolledWindow.get_hadjustment(), s.left, width) ensureVisible(self.scrolledWindow.get_vadjustment(), s.top, height) # Activate the formatting checkboxes and set their values self.setSymbolControlSensitivity(True) self.buttonUpdateInProgress = True self.boldButton.set_active(s.bold) self.italicButton.set_active(s.italic) self.underlineButton.set_active(s.underline) # Update the spin buttons self.spinLeft.set_value(s.left) self.spinRight.set_value(s.right) self.spinTop.set_value(s.top) self.spinBottom.set_value(s.bottom) self.buttonUpdateInProgress = None #enddef def onEntryChanged(self, entry, row, col): symbol = self.boxes[row][col] symbol.text = entry.get_text() symbol.underline = symbol.text.startswith("'") entry.set_width_chars(len(unicode(symbol.text))) while symbol.text.startswith("'"): symbol.text = symbol.text[1:] #endwhile self.buttonUpdateInProgress = True self.underlineButton.set_active(symbol.underline) self.buttonUpdateInProgress = None #enddef # Intercept ctrl-arrow and ctrl-shift-arrow def onEntryKeyPress(self, entry, event, row, col): if not event.state & gtk.gdk.CONTROL_MASK: return False #endif shift = event.state & gtk.gdk.SHIFT_MASK s = self.boxes[row][col] if event.keyval == 65361: # Left arrow self.buttonUpdateInProgress = True if shift: s.left += 1 else: s.left -= 1 self.spinLeft.set_value(s.left) self.buttonUpdateInProgress = None self.invalidateImage() return True elif event.keyval == 65362: # Up arrow self.buttonUpdateInProgress = True if shift: s.top += 1 else: s.top -= 1 self.spinTop.set_value(s.top) self.buttonUpdateInProgress = None self.invalidateImage() return True elif event.keyval == 65363: # Right arrow self.buttonUpdateInProgress = True if shift: s.right -= 1 else: s.right += 1 self.spinRight.set_value(s.right) self.buttonUpdateInProgress = None self.invalidateImage() return True elif event.keyval == 65364: # Down arrow self.buttonUpdateInProgress = True if shift: s.bottom -= 1 else: s.bottom += 1 self.spinBottom.set_value(s.bottom) self.buttonUpdateInProgress = None self.invalidateImage() return True #endif return False #enddef def onSpinButtonChanged(self, button, dir): if self.buttonUpdateInProgress or self.selectedRow == None: return #endif value = int(button.get_value()) s = self.boxes[self.selectedRow][self.selectedColumn] prevValue = (s.left, s.right, s.top, s.bottom)[dir] if dir == DIR_LEFT: s.left = value elif dir == DIR_RIGHT: s.right = value elif dir == DIR_TOP: s.top = value elif dir == DIR_BOTTOM: s.bottom = value #endif self.invalidateImage() #enddef # Creates text entries from the boxes def populateTextVBox(self): row = 0 for line in self.boxes: col = 0 hbox = gtk.HBox() self.textVBox.pack_start(hbox) hbox.show() for s in line: if s.spaceBefore: label = gtk.Label(' ') hbox.pack_start(label, False, False, 0) label.show() #endif self.makeGtkEntry(s, row, col) hbox.pack_start(s.entry, False, False, 0) s.entry.show() col += 1 #endfor row += 1 #endfor #enddef def redrawArea(self, drawingArea, event): gc = drawingArea.get_style().fg_gc[gtk.STATE_NORMAL] if self.pixbuf: drawingArea.window.draw_pixbuf(gc, self.pixbuf, 0, 0, 0, 0) #endif if self.selectedRow != None: s = self.boxes[self.selectedRow][self.selectedColumn] width = s.right - s.left height = s.bottom - s.top drawingArea.window.draw_rectangle(gc, False, s.left, s.top, width, height) #endif #enddef def loadImageAndBoxes(self, imageName, fileChooser): (name, extension) = imageName.rsplit('.', 1) boxName = name + '.box' # Make sure that the image exists try: f = open(imageName, 'r') f.close() except IOError: self.errorDialog('Cannot find the specified file', fileChooser) return False #endtry height = findImageHeight(imageName) try: self.boxes = loadBoxData(boxName, height) self.loadedBoxFile = boxName except IOError: self.errorDialog('Cannot load image, because there is no ' 'corresponding box file', fileChooser) return False #endtry self.pixbuf = gtk.gdk.pixbuf_new_from_file(imageName) self.drawingArea.set_size_request(self.pixbuf.get_width(), height) self.populateTextVBox() # Set adjustments on all spin buttons self.spinLeft.set_adjustment( gtk.Adjustment(0, 0, self.pixbuf.get_width(), 1, 1)) self.spinRight.set_adjustment( gtk.Adjustment(0, 0, self.pixbuf.get_width(), 1, 1)) self.spinTop.set_adjustment(gtk.Adjustment(0, 0, height, 1, 1)) self.spinBottom.set_adjustment(gtk.Adjustment(0, 0, height, 1, 1)) self.setImageControlSensitivity(True) self.selectedRow = 0 self.selectedColumn = 0 self.boxes[0][0].entry.grab_focus() return True #enddef def mergeTextFile(self, fileName, fileChooser): row = 0 col = 0 try: f = open(fileName, 'r') for line in f: line = unicode(line) for char in line: if not char.isspace(): if row < len(self.boxes): self.boxes[row][col].text = char self.boxes[row][col].setEntryFont() col += 1 if col == len(self.boxes[row]): col = 0 row += 1 #endif #endif #endif #endfor #endfor f.close() except IOError: self.errorDialog('File ' + fileName + ' does not exist', fileChooser) #endtry #enddef def doFileOpen(self, action): chooser = gtk.FileChooserDialog( "Open Image", self.window, gtk.FILE_CHOOSER_ACTION_OPEN, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK)) filter = gtk.FileFilter() filter.set_name("TIFF files") filter.add_pattern("*.tif") filter.add_pattern("*.tiff") chooser.add_filter(filter) filter = gtk.FileFilter() filter.set_name("All files") filter.add_pattern("*") chooser.add_filter(filter) response = chooser.run() if response == gtk.RESPONSE_OK: fileName = chooser.get_filename() self.loadImageAndBoxes(fileName, chooser) #endif chooser.destroy() #enddef def doFileMergeText(self, action): chooser = gtk.FileChooserDialog( "Merge Text File", self.window, gtk.FILE_CHOOSER_ACTION_OPEN, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK)) filter = gtk.FileFilter() filter.set_name("All files") filter.add_pattern("*") chooser.add_filter(filter) response = chooser.run() if response == gtk.RESPONSE_OK: fileName = chooser.get_filename() self.mergeTextFile(fileName, chooser) #endif chooser.destroy() #enddef def doFileSave(self, action): if self.boxes == None: self.errorDialog('Nothing to save', self.window) return #endif height = self.pixbuf.get_height() f = open(self.loadedBoxFile, 'w') for row in self.boxes: for s in row: text = s.text if s.underline: text = "'" + text #endif if s.italic: text = '$' + text #endif if s.bold: text = '@' + text #endif f.write('%s %d %d %d %d\n' % (text, s.left, height - s.bottom, s.right, height - s.top)) #endfor #endfor f.close() #enddef def doHelpAbout(self, action): dialog = gtk.Dialog('About TesseractTrainer', self.window, gtk.DIALOG_NO_SEPARATOR | gtk.DIALOG_MODAL, (gtk.STOCK_OK, gtk.RESPONSE_OK)) dialog.set_size_request(450, 200) label = gtk.Label( 'TesseractTrainer\n' 'Copyright 2007 Cătălin Frâncu \n' '\n' 'This program is free software: you can redistribute it and/or ' 'modify it under the terms of the GNU General Public License') label.set_line_wrap(True) dialog.vbox.pack_start(label, True, True, 0) label.show() dialog.run() dialog.destroy() #enddef def doHelpShortcuts(self, action): dialog = gtk.Dialog('Keyboard shortcuts', self.window, gtk.DIALOG_NO_SEPARATOR | gtk.DIALOG_MODAL, (gtk.STOCK_OK, gtk.RESPONSE_OK)) dialog.set_size_request(450, 200) label = gtk.Label( 'Keyboard shortcuts\n' '\n' 'Ctrl-B: mark entire word as bold\n' 'Ctrl-I: mark entire word as italic\n' 'Ctrl-U: mark current symbol as italic\n' 'Ctrl-arrow: grow box up, down, left or right\n' 'Ctrl-Shift-arrow: shrink box up, down, left or right\n' 'Ctrl-1: merge current symbol with next symbol\n' 'Ctrl-2: split current symbol vertically\n' 'Ctrl-D: delete current symbol\n' ) label.set_line_wrap(True) dialog.vbox.pack_start(label, True, True, 0) label.show() dialog.run() dialog.destroy() #enddef # Looks 5 pixels to the left and right of the median divider def findSplitPoint(self, symbol): subpixbuf = self.pixbuf.subpixbuf(symbol.left, symbol.top, symbol.right - symbol.left, symbol.bottom - symbol.top) pixels = subpixbuf.get_pixels_array() height = len(pixels) width = len(pixels[0]) bestX = -1 bestNumPixels = 1000000 for x in range(width // 2 - 5, width // 2 + 6): numPixels = countBlackPixels(pixels, x) # print x, numPixels if numPixels < bestNumPixels: bestX = x bestNumPixels = numPixels #endif #endfor return bestX + symbol.left #enddef def doCommandsSplit(self, action): if self.selectedRow == None: self.errorDialog('Click into a cell first.', self.window) return #endif row = self.boxes[self.selectedRow] this = row[self.selectedColumn] clone = this.clone() this.right = self.findSplitPoint(this) clone.left = this.right clone.spaceBefore = False clone.text = '*' self.makeGtkEntry(clone, self.selectedRow, self.selectedColumn + 1) clone.setEntryFont() hbox = this.entry.get_parent() hbox.pack_start(clone.entry, False, False, 0) # To reorder the child, use col + 1 and add all the word breaks pos = self.selectedColumn + 1 for s in row[0:self.selectedColumn + 1]: if s.spaceBefore: pos += 1 #endif #endfor hbox.reorder_child(clone.entry, pos) clone.entry.show() row.insert(self.selectedColumn + 1, clone) self.reconnectEntries(self.selectedRow) self.invalidateImage() self.buttonUpdateInProgress = True self.spinLeft.set_value(this.left) self.spinRight.set_value(this.right) self.spinTop.set_value(this.top) self.spinBottom.set_value(this.bottom) self.buttonUpdateInProgress = None #enddef def doCommandsJoin(self, action): if self.selectedRow == None: self.errorDialog('Click into a cell first.', self.window) return #endif if self.selectedColumn + 1 == len(self.boxes[self.selectedRow]): self.errorDialog('There is no next symbol on this line!', self.window) return #endif this = self.boxes[self.selectedRow][self.selectedColumn] next = self.boxes[self.selectedRow][self.selectedColumn + 1] this.text += next.text this.left = min(this.left, next.left) this.right = max(this.right, next.right) this.top = min(this.top, next.top) this.bottom = max(this.bottom, next.bottom) this.setEntryFont() next.entry.destroy() del self.boxes[self.selectedRow][self.selectedColumn + 1] self.reconnectEntries(self.selectedRow) self.invalidateImage() self.buttonUpdateInProgress = True self.spinLeft.set_value(this.left) self.spinRight.set_value(this.right) self.spinTop.set_value(this.top) self.spinBottom.set_value(this.bottom) self.buttonUpdateInProgress = None #enddef def doCommandsDelete(self, action): if self.selectedRow == None: self.errorDialog('Click into a cell first.', self.window) return #endif row = self.boxes[self.selectedRow] this = row[self.selectedColumn] if self.selectedColumn + 1< len(row): next = row[self.selectedColumn + 1] else: next = None #endif if this.spaceBefore: if next != None and not next.spaceBefore: next.spaceBefore = True else: # delete the label before this symbol this.deleteLabelBefore() #endif #endif this.entry.destroy() del row[self.selectedColumn] self.reconnectEntries(self.selectedRow) # Find the next cell to focus if self.selectedColumn >= len(row): self.selectedRow += 1 self.selectedColumn = 0 #endif if self.selectedRow >= len(self.boxes): self.selectedRow = len(self.boxes) - 1 self.selectedColumn = len(self.boxes[self.selectedRow]) - 1 #endif self.boxes[self.selectedRow][self.selectedColumn].entry.grab_focus() #enddef def setImageControlSensitivity(self, bool): self.actionGroup.get_action('MergeText').set_sensitive(bool) self.actionGroup.get_action('Save').set_sensitive(bool) #enddef def setSymbolControlSensitivity(self, bool): self.buttonBox.set_sensitive(bool) self.actionGroup.get_action('Commands').set_sensitive(bool) #enddef def makeMenu(self): uiManager = gtk.UIManager() self.accelGroup = uiManager.get_accel_group() self.window.add_accel_group(self.accelGroup) self.actionGroup = gtk.ActionGroup('UIManagerExample') self.actionGroup.add_actions( [('Open', gtk.STOCK_OPEN, '_Open Image...', None, None, self.doFileOpen), ('MergeText', None, 'Merge _Text...', 'T', None, self.doFileMergeText), ('Save', gtk.STOCK_SAVE, '_Save Box Info', None, None, self.doFileSave), ('Quit', gtk.STOCK_QUIT, None, None, None, lambda w: gtk.main_quit()), ('File', None, '_File'), ('Commands', None, '_Commands'), ('Split', None, '_Split Symbol', '2', None, self.doCommandsSplit), ('JoinWithNext', None, '_Join with Next Symbol', '1', None, self.doCommandsJoin), ('Delete', None, '_Delete Symbol', 'D', None, self.doCommandsDelete), ('Help', None, '_Help'), ('About', None, '_About', None, None, self.doHelpAbout), ('Shortcuts', None, '_Keyboard shotcuts', None, None, self.doHelpShortcuts), ]) uiManager.insert_action_group(self.actionGroup, 0) uiManager.add_ui_from_string(MENU) return uiManager.get_widget('/MenuBar') #enddef def __init__(self): self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) self.window.set_title("Tesseract Box Editor") self.window.connect('destroy', lambda w: gtk.main_quit()) self.window.set_size_request(800, 600) vbox = gtk.VBox(False, 2) self.window.add(vbox) vbox.show() menuBar = self.makeMenu() vbox.pack_start(menuBar, False) self.scrolledWindow = gtk.ScrolledWindow() self.scrolledWindow.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) vbox.pack_start(self.scrolledWindow, True, True, 2) self.scrolledWindow.show() self.drawingArea = gtk.DrawingArea() self.drawingArea.connect("expose-event", self.redrawArea) self.scrolledWindow.add_with_viewport(self.drawingArea) self.drawingArea.show() self.textScroll = gtk.ScrolledWindow() self.textScroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) vbox.pack_start(self.textScroll, True, True, 2) self.textScroll.show() self.textVBox = gtk.VBox() self.textScroll.add_with_viewport(self.textVBox) self.textVBox.show() self.buttonBox = gtk.HBox(False, 0) vbox.pack_start(self.buttonBox, False, False, 2) self.buttonBox.show() b = gtk.CheckButton("_Bold", True) self.buttonBox.pack_start(b, False, False, 10) b.connect("toggled", self.onCheckButtonToggled, ATTR_BOLD) b.add_accelerator("activate", self.accelGroup, ord('B'), gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE) b.show() self.boldButton = b b = gtk.CheckButton("_Italic", True) self.buttonBox.pack_start(b, False, False, 10) b.connect("toggled", self.onCheckButtonToggled, ATTR_ITALIC) b.add_accelerator("activate", self.accelGroup, ord('I'), gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE) b.show() self.italicButton = b b = gtk.CheckButton("_Underline", True) self.buttonBox.pack_start(b, False, False, 10) b.connect("toggled", self.onCheckButtonToggled, ATTR_UNDERLINE) b.add_accelerator("activate", self.accelGroup, ord('U'), gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE) b.show() self.underlineButton = b self.spinBottom = gtk.SpinButton() self.spinBottom.connect("changed", self.onSpinButtonChanged, DIR_BOTTOM) self.buttonBox.pack_end(self.spinBottom, False, False, 0) self.spinBottom.show() l = gtk.Label(" Bottom:"); self.buttonBox.pack_end(l, False, False, 0) l.show() self.spinTop = gtk.SpinButton() self.spinTop.connect("changed", self.onSpinButtonChanged, DIR_TOP) self.buttonBox.pack_end(self.spinTop, False, False, 0) self.spinTop.show() l = gtk.Label(" Top:"); self.buttonBox.pack_end(l, False, False, 0) l.show() self.spinRight = gtk.SpinButton() self.spinRight.connect("changed", self.onSpinButtonChanged, DIR_RIGHT) self.buttonBox.pack_end(self.spinRight, False, False, 0) self.spinRight.show() l = gtk.Label(" Right:"); self.buttonBox.pack_end(l, False, False, 0) l.show() self.spinLeft = gtk.SpinButton() self.spinLeft.connect("changed", self.onSpinButtonChanged, DIR_LEFT) self.buttonBox.pack_end(self.spinLeft, False, False, 0) self.spinLeft.show() l = gtk.Label("Left:"); self.buttonBox.pack_end(l, False, False, 0) l.show() self.setImageControlSensitivity(False) self.setSymbolControlSensitivity(False) self.window.show() #enddef #endClass def main(): gtk.main() return 0 #enddef if __name__ == "__main__": MainWindow() main() Choose download locationDownload