moebiusT7/book-ocr-studio
0
1"""Inspect rendered reader geometry and explicitly labelled page navigation."""2import math3 4SCAN=r'''() => {5 const visible = el => {const r=el.getBoundingClientRect(),s=getComputedStyle(el);return r.width>0&&r.height>0&&s.display!=='none'&&s.visibility!=='hidden';};6 const surfaces=[...document.querySelectorAll('canvas,img')].filter(visible).map(el=>{7 const r=el.getBoundingClientRect();return {x:r.x,y:r.y,width:r.width,height:r.height,tag:el.tagName};8 }).filter(r=>r.width>=200&&r.height>=200);9 const next=[...document.querySelectorAll('button,[role="button"],a')].filter(visible).map(el=>{10 const name=[el.getAttribute('aria-label'),el.getAttribute('title'),el.textContent].filter(Boolean).join(' ').trim();const r=el.getBoundingClientRect();11 return {name:name.slice(0,160),x:r.x+r.width/2,y:r.y+r.height/2,disabled:el.disabled||el.getAttribute('aria-disabled')==='true'};12 }).filter(b=>!b.disabled&&(/next\s*page|次のページ|次ページ|ページを進む|ページをめくる/i.test(b.name)));13 return {surfaces,next,width:innerWidth,height:innerHeight};14}'''15 16def detect(page):17 view=page.viewport_size;surfaces=[];buttons=[]18 for frame in page.frames:19 try:20 info=frame.evaluate(SCAN);ox=oy=0;sx=sy=121 if frame!=page.main_frame:22 box=frame.frame_element().bounding_box()23 if not box:continue24 ox,oy=box['x'],box['y'];sx=box['width']/info['width'];sy=box['height']/info['height']25 for item in info['surfaces']:26 item.update(x=ox+item['x']*sx,y=oy+item['y']*sy,width=item['width']*sx,height=item['height']*sy)27 if item['x']<view['width'] and item['y']<view['height'] and item['x']+item['width']>0 and item['y']+item['height']>0:surfaces.append(item)28 for b in info['next']:29 b.update(x=ox+b['x']*sx,y=oy+b['y']*sy)30 if 0<=b['x']<view['width'] and 0<=b['y']<view['height']:buttons.append(b)31 except Exception:continue32 if not surfaces:return dict(selection=None,reason='Could not identify the text area. Refresh the reader or select it manually.',clipped=False)33 largest=max(s['width']*s['height'] for s in surfaces)34 substantial=[s for s in surfaces if s['width']*s['height']>=largest*.45]35 x=min(s['x'] for s in substantial);y=min(s['y'] for s in substantial)36 right=max(s['x']+s['width'] for s in substantial);bottom=max(s['y']+s['height'] for s in substantial)37 clipped=x < -2 or y < -2 or right>view['width']+2 or bottom>view['height']+238 clip=dict(x=max(0,math.floor(x)),y=max(0,math.floor(y)),width=0,height=0)39 clip['width']=min(view['width'],math.ceil(right))-clip['x'];clip['height']=min(view['height'],math.ceil(bottom))-clip['y']40 # Ambiguous navigation stays manual; never silently guess RTL vs LTR.41 unique={(round(b['x']),round(b['y'])):b for b in buttons}42 nxt=list(next(iter(unique))) if len(unique)==1 else None43 return dict(selection=dict(clip=clip,next=nxt),reason='Detected the text area and page navigation.' if nxt else 'Detected the text area. Select the next-page arrow in the image.',clipped=clipped,surface_count=len(substantial))44 45def fit_and_detect(page):46 # Browser keyboard zoom is not reliable through automation. Apply reversible layout zoom47 # to the document root, then re-measure the rendered surfaces after every change.48 page.bring_to_front()49 page.evaluate("() => {document.documentElement.style.zoom='1';}")50 page.wait_for_timeout(400)51 result=detect(page);steps=0;zoom=1.052 while result['clipped'] and steps<4:53 zoom*=.854 page.evaluate("z => {document.documentElement.style.zoom=String(z);window.dispatchEvent(new Event('resize'));}",zoom)55 page.wait_for_timeout(500);steps+=1;result=detect(page)56 result['zoom_out_steps']=steps;result['layout_zoom']=zoom57 if result['clipped']:result['reason']='The whole page does not fit yet. Choose the full-page view in Kindle display settings.'58 return result59 60 61from contextlib import contextmanager62 63@contextmanager64def clean_reader_capture(page):65 """Temporarily remove observed Kindle overlay chrome; preserve position text."""66 token = page.evaluate("""() => {67 const elements = [...document.querySelectorAll('#reader-header, #kr-scrubber-bar, ion-footer.reader-footer, .bookmark[role="checkbox"]')];68 const saved = elements.map(e => ({selector: e.id ? '#'+e.id : e.tagName.toLowerCase()+'.'+(e.classList.contains('reader-footer')?'reader-footer':'bookmark'), value:e.style.getPropertyValue('opacity'), priority:e.style.getPropertyPriority('opacity')}));69 elements.forEach(e => e.style.setProperty('opacity','0','important'));70 return saved;71 }""")72 try:73 page.evaluate("() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))")74 yield75 finally:76 page.evaluate("""saved => {for(const item of saved) {77 const e=document.querySelector(item.selector);if(!e)continue;78 if(item.value)e.style.setProperty('opacity',item.value,item.priority);79 else e.style.removeProperty('opacity');80 }}""", token)81 