@@ -301,24 +301,24 @@ def fix_book_language(self, default_language='en', epub_path=None):
301301 'glv' , 'nor' , 'nno' , 'por' , 'oci' , 'roh' , 'gla' , 'spa' , 'swe' , 'tam' , 'cym' , 'wel' ,
302302 ]
303303
304- # Find OPF file
305- if 'META-INF/container.xml' not in self .files :
306- print ('Cannot find META-INF/container.xml' )
307- return
304+ try :
305+ # Find OPF file
306+ if 'META-INF/container.xml' not in self .files :
307+ print ('Cannot find META-INF/container.xml' )
308+ return
308309
309- container_xml = minidom .parseString (self .files ['META-INF/container.xml' ])
310- opf_filename = None
311- for rootfile in container_xml .getElementsByTagName ('rootfile' ):
312- if rootfile .getAttribute ('media-type' ) == 'application/oebps-package+xml' :
313- opf_filename = rootfile .getAttribute ('full-path' )
314- break
310+ container_xml = minidom .parseString (self .files ['META-INF/container.xml' ])
311+ opf_filename = None
312+ for rootfile in container_xml .getElementsByTagName ('rootfile' ):
313+ if rootfile .getAttribute ('media-type' ) == 'application/oebps-package+xml' :
314+ opf_filename = rootfile .getAttribute ('full-path' )
315+ break
315316
316- # Read OPF file
317- if not opf_filename or opf_filename not in self .files :
318- print ('Cannot find OPF file!' )
319- return
317+ # Read OPF file
318+ if not opf_filename or opf_filename not in self .files :
319+ print ('Cannot find OPF file!' )
320+ return
320321
321- try :
322322 opf = minidom .parseString (self .files [opf_filename ])
323323 language_tags = opf .getElementsByTagName ('dc:language' )
324324 language = None
@@ -386,10 +386,30 @@ def fix_book_language(self, default_language='en', epub_path=None):
386386
387387 # Only write if we actually changed something
388388 if original_language != language or not original_language :
389- self .files [opf_filename ] = opf .toxml ()
389+ # Use regex replacement to preserve XML formatting instead of minidom.toxml()
390+ # This prevents attribute reordering which can break Amazon's parser
391+ opf_content = self .files [opf_filename ]
392+
393+ if not language_tags :
394+ # Need to add language tag - insert before </metadata>
395+ opf_content = opf_content .replace (
396+ '</metadata>' ,
397+ f' <dc:language>{ language } </dc:language>\n </metadata>'
398+ )
399+ else :
400+ # Replace existing language tag content
401+ opf_content = re .sub (
402+ r'<dc:language>.*?</dc:language>' ,
403+ f'<dc:language>{ language } </dc:language>' ,
404+ opf_content ,
405+ count = 1 ,
406+ flags = re .DOTALL
407+ )
408+
409+ self .files [opf_filename ] = opf_content
390410
391411 except Exception as e :
392- print (f'Error trying to parse OPF file as XML : { e } ' )
412+ print_and_log (f'[cwa-kindle-epub-fixer] Skipping language validation - EPUB has non-standard structure : { e } ' , log = self . manually_triggered )
393413
394414 def _detect_language_from_metadata (self , epub_path = None ):
395415 """Attempt to detect language from Calibre's metadata.db"""
@@ -449,6 +469,238 @@ def fix_stray_img(self):
449469 self .fixed_problems .append (f"Remove stray image tag(s) in { filename } " )
450470 self .files [filename ] = dom .toxml ()
451471
472+ def strip_embedded_fonts (self ):
473+ """Remove embedded font files and @font-face CSS declarations for Kindle compatibility"""
474+ # Remove font files from binary files
475+ font_extensions = ('.ttf' , '.otf' , '.woff' , '.woff2' , '.eot' )
476+ fonts_removed = []
477+
478+ for filename in list (self .binary_files .keys ()):
479+ if filename .lower ().endswith (font_extensions ):
480+ del self .binary_files [filename ]
481+ fonts_removed .append (filename )
482+
483+ if fonts_removed :
484+ self .fixed_problems .append (f"Removed { len (fonts_removed )} embedded font file(s) for Kindle compatibility" )
485+
486+ # Also remove font references from OPF manifest
487+ opf_path = 'content.opf'
488+ if opf_path in self .files :
489+ opf_content = self .files [opf_path ]
490+ for font_file in fonts_removed :
491+ # Remove manifest item for this font
492+ # Match: <item href="fonts/00001.ttf" id="..." media-type="..."/>
493+ pattern = re .compile (
494+ r'<item[^>]*href=["\']' + re .escape (font_file ) + r'["\'][^>]*/?>' ,
495+ re .IGNORECASE
496+ )
497+ opf_content = pattern .sub ('' , opf_content )
498+
499+ self .files [opf_path ] = opf_content
500+
501+ # Remove @font-face declarations from CSS files
502+ font_face_pattern = re .compile (r'@font-face\s*\{[^}]*\}' , re .IGNORECASE | re .DOTALL )
503+
504+ for filename in list (self .files .keys ()):
505+ if filename .endswith ('.css' ):
506+ original_css = self .files [filename ]
507+ cleaned_css = font_face_pattern .sub ('' , original_css )
508+
509+ if cleaned_css != original_css :
510+ self .files [filename ] = cleaned_css
511+ self .fixed_problems .append (f"Removed @font-face declarations from { filename } " )
512+
513+ def remove_javascript (self ):
514+ """Remove JavaScript code for Kindle compatibility (not supported)"""
515+ script_pattern = re .compile (r'<script[^>]*>.*?</script>' , re .IGNORECASE | re .DOTALL )
516+
517+ for filename in list (self .files .keys ()):
518+ ext = filename .split ('.' )[- 1 ]
519+ if ext in ['html' , 'xhtml' , 'htm' ]:
520+ original_content = self .files [filename ]
521+ cleaned_content = script_pattern .sub ('' , original_content )
522+
523+ if cleaned_content != original_content :
524+ self .files [filename ] = cleaned_content
525+ self .fixed_problems .append (f"Removed JavaScript from { filename } " )
526+
527+ def validate_images (self ):
528+ """Validate images for Kindle compatibility and report issues"""
529+ issues = []
530+ total_size = 0
531+
532+ # Supported formats by Kindle
533+ supported_formats = {
534+ b'\xff \xd8 \xff ' : 'JPEG' ,
535+ b'\x89 PNG' : 'PNG' ,
536+ b'GIF87a' : 'GIF' ,
537+ b'GIF89a' : 'GIF'
538+ }
539+
540+ for filename in list (self .binary_files .keys ()):
541+ ext = filename .split ('.' )[- 1 ].lower ()
542+ if ext in ['jpg' , 'jpeg' , 'png' , 'gif' , 'svg' , 'webp' , 'bmp' ]:
543+ file_data = self .binary_files [filename ]
544+ file_size = len (file_data )
545+ total_size += file_size
546+
547+ # Check for unsupported formats
548+ if ext in ['svg' , 'webp' ]:
549+ issues .append (f"{ filename } : { ext .upper ()} format has limited Kindle support" )
550+
551+ # Check individual file size (warn if > 2MB)
552+ if file_size > 2 * 1024 * 1024 :
553+ size_mb = file_size / (1024 * 1024 )
554+ issues .append (f"{ filename } : Large image ({ size_mb :.1f} MB) may cause issues" )
555+
556+ # Verify actual format matches extension
557+ format_detected = None
558+ for magic_bytes , format_name in supported_formats .items ():
559+ if file_data .startswith (magic_bytes ):
560+ format_detected = format_name
561+ break
562+
563+ if format_detected and ext in ['jpg' , 'jpeg' ] and format_detected != 'JPEG' :
564+ issues .append (f"{ filename } : File type mismatch (ext: { ext } , actual: { format_detected } )" )
565+ elif format_detected and ext == 'png' and format_detected != 'PNG' :
566+ issues .append (f"{ filename } : File type mismatch (ext: { ext } , actual: { format_detected } )" )
567+
568+ if issues :
569+ for issue in issues :
570+ self .fixed_problems .append (f"Image validation warning: { issue } " )
571+
572+ # Check total EPUB size (warn if approaching 50MB uncompressed)
573+ total_size_mb = total_size / (1024 * 1024 )
574+ if total_size_mb > 40 :
575+ self .fixed_problems .append (f"Warning: Total image size is { total_size_mb :.1f} MB (Kindle works best with <50MB total)" )
576+
577+ def validate_css (self ):
578+ """Validate CSS and only fix actual syntax errors, warn about potential Kindle issues"""
579+ for filename in list (self .files .keys ()):
580+ if filename .endswith ('.css' ):
581+ original_css = self .files [filename ]
582+ issues_found = []
583+
584+ # Check for syntax errors that would break rendering
585+ # 1. Unclosed braces
586+ open_braces = original_css .count ('{' )
587+ close_braces = original_css .count ('}' )
588+ if open_braces != close_braces :
589+ issues_found .append (f"CSS syntax error: mismatched braces ({ open_braces } open, { close_braces } close)" )
590+
591+ # 2. Invalid @import statements (must be at top)
592+ lines = original_css .split ('\n ' )
593+ import_after_rules = False
594+ seen_rule = False
595+ for line in lines :
596+ stripped = line .strip ()
597+ if stripped and not stripped .startswith ('/*' ) and not stripped .startswith ('*/' ):
598+ if '@import' in stripped :
599+ if seen_rule :
600+ import_after_rules = True
601+ elif stripped .startswith ('@' ) or '{' in stripped :
602+ seen_rule = True
603+
604+ if import_after_rules :
605+ issues_found .append ("CSS syntax error: @import must appear before other rules" )
606+
607+ # 3. Check for common Kindle-problematic features (warning only, don't remove)
608+ kindle_warnings = []
609+ if re .search (r'position\s*:\s*(absolute|fixed)' , original_css , re .IGNORECASE ):
610+ kindle_warnings .append ("absolute/fixed positioning" )
611+ if re .search (r'@media' , original_css , re .IGNORECASE ):
612+ kindle_warnings .append ("media queries" )
613+ if re .search (r'javascript:' , original_css , re .IGNORECASE ):
614+ kindle_warnings .append ("javascript URLs" )
615+
616+ # Only report if there are actual syntax errors
617+ if issues_found :
618+ for issue in issues_found :
619+ self .fixed_problems .append (f"CSS validation: { issue } in { filename } " )
620+
621+ # Add informational note about Kindle compatibility (not counted as a fix needing action)
622+ if kindle_warnings :
623+ warning_str = ', ' .join (kindle_warnings )
624+ print_and_log (f"[cwa-kindle-epub-fixer] Note: { filename } uses { warning_str } (may render differently on Kindle)" , log = self .manually_triggered )
625+
626+ def strip_amazon_identifiers (self ):
627+ """Remove ASIN/Amazon identifiers and Calibre metadata from OPF file.
628+
629+ Amazon may reject books that already have an ASIN in their system.
630+ Also removes Calibre-specific metadata that's not needed for Kindle.
631+ """
632+ opf_path = 'content.opf'
633+ if opf_path not in self .files :
634+ return
635+
636+ opf_content = self .files [opf_path ]
637+
638+ try :
639+ dom = minidom .parseString (opf_content )
640+ package = dom .getElementsByTagName ('package' )[0 ]
641+ metadata = dom .getElementsByTagName ('metadata' )[0 ]
642+
643+ changes_made = False
644+
645+ # Remove Calibre namespace from package tag
646+ if package .hasAttribute ('xmlns:calibre' ):
647+ package .removeAttribute ('xmlns:calibre' )
648+ changes_made = True
649+ print_and_log ("[cwa-kindle-epub-fixer] Removing Calibre namespace from package tag" , log = self .manually_triggered )
650+
651+ # Remove Calibre namespace from metadata tag
652+ if metadata .hasAttribute ('xmlns:calibre' ):
653+ metadata .removeAttribute ('xmlns:calibre' )
654+ changes_made = True
655+
656+ # Remove Amazon/MOBI-ASIN identifiers using regex (preserve structure)
657+ identifiers_removed = []
658+
659+ # Match AMAZON and MOBI-ASIN identifiers
660+ asin_pattern = re .compile (
661+ r'<dc:identifier[^>]*opf:scheme=["\'](?:AMAZON|MOBI-ASIN|mobi-asin|calibre)["\'][^>]*>.*?</dc:identifier>' ,
662+ re .IGNORECASE | re .DOTALL
663+ )
664+
665+ matches = asin_pattern .findall (opf_content )
666+ if matches :
667+ for match in matches :
668+ # Extract scheme and value for logging
669+ scheme_match = re .search (r'opf:scheme=["\']([^"\']+)["\']' , match )
670+ if scheme_match :
671+ identifiers_removed .append (scheme_match .group (1 ))
672+
673+ opf_content = asin_pattern .sub ('' , opf_content )
674+ changes_made = True
675+
676+ if identifiers_removed :
677+ print_and_log (f"[cwa-kindle-epub-fixer] Removing Amazon/Calibre identifiers: { ', ' .join (identifiers_removed )} " , log = self .manually_triggered )
678+ self .fixed_problems .append (f"Removed { len (identifiers_removed )} Amazon/Calibre identifier(s)" )
679+
680+ # Remove Calibre-specific meta tags using regex (preserve structure)
681+ calibre_meta_pattern = re .compile (
682+ r'<meta[^>]*name=["\']calibre:[^"\']+["\'][^>]*/>' ,
683+ re .IGNORECASE
684+ )
685+
686+ calibre_matches = calibre_meta_pattern .findall (opf_content )
687+ if calibre_matches :
688+ opf_content = calibre_meta_pattern .sub ('' , opf_content )
689+ changes_made = True
690+ print_and_log (f"[cwa-kindle-epub-fixer] Removing { len (calibre_matches )} Calibre meta tag(s)" , log = self .manually_triggered )
691+ self .fixed_problems .append (f"Removed { len (calibre_matches )} Calibre meta tag(s)" )
692+
693+ # Remove xmlns:calibre from metadata tag if present
694+ if 'xmlns:calibre=' in opf_content :
695+ opf_content = re .sub (r'\s*xmlns:calibre="[^"]*"' , '' , opf_content )
696+ changes_made = True
697+
698+ if changes_made :
699+ self .files [opf_path ] = opf_content
700+
701+ except Exception as e :
702+ print_and_log (f"[cwa-kindle-epub-fixer] Warning: Could not strip Amazon identifiers: { e } " , log = self .manually_triggered )
703+
452704 def write_epub (self , output_path ):
453705 """Write EPUB file"""
454706 with zipfile .ZipFile (output_path , 'w' , zipfile .ZIP_DEFLATED ) as zip_ref :
@@ -514,12 +766,23 @@ def process(self, input_path, output_path=None, default_language='en'):
514766 # Run fixing procedures
515767 print_and_log ("[cwa-kindle-epub-fixer] Checking linking to body ID to prevent unresolved hyperlinks..." , log = self .manually_triggered )
516768 self .fix_body_id_link ()
769+ print_and_log ("[cwa-kindle-epub-fixer] Checking UTF-8 encoding declaration..." , log = self .manually_triggered )
770+ self .fix_encoding ()
517771 print_and_log ("[cwa-kindle-epub-fixer] Checking language field tag is valid..." , log = self .manually_triggered )
518772 self .fix_book_language (default_language , input_path )
519773 print_and_log ("[cwa-kindle-epub-fixer] Checking for stray images..." , log = self .manually_triggered )
520774 self .fix_stray_img ()
521- print_and_log ("[cwa-kindle-epub-fixer] Checking UTF-8 encoding declaration..." , log = self .manually_triggered )
522- self .fix_encoding ()
775+
776+ # New Kindle-specific fixes
777+ print_and_log ("[cwa-kindle-epub-fixer] Stripping embedded fonts for Kindle compatibility..." , log = self .manually_triggered )
778+ self .strip_embedded_fonts ()
779+ print_and_log ("[cwa-kindle-epub-fixer] Removing JavaScript (not supported on Kindle)..." , log = self .manually_triggered )
780+ self .remove_javascript ()
781+ print_and_log ("[cwa-kindle-epub-fixer] Validating images for Kindle compatibility..." , log = self .manually_triggered )
782+ self .validate_images ()
783+ print_and_log ("[cwa-kindle-epub-fixer] Validating CSS syntax..." , log = self .manually_triggered )
784+ self .validate_css ()
785+ # NOTE: Skipping strip_amazon_identifiers() - users want complete metadata preserved
523786
524787 # Notify user and/or write to log
525788 self .export_issue_summary (input_path )
0 commit comments