@@ -76,14 +76,15 @@ fn parse_layer_elements<R: Read + std::io::Seek>(
7676) -> ( Vec < FixedElement > , Vec < ConvertWarning > ) {
7777 let images: SlideImageMap = load_slide_images ( layer_path, archive) ;
7878 let empty_table_styles: table_styles:: TableStyleMap = table_styles:: TableStyleMap :: new ( ) ;
79- parse_slide_xml (
79+ parse_slide_xml_inner (
8080 layer_xml,
8181 & images,
8282 theme,
8383 color_map,
8484 label,
8585 text_style_defaults,
8686 & empty_table_styles,
87+ true , // skip placeholder shapes in master/layout layers
8788 )
8889 . unwrap_or_default ( )
8990}
@@ -480,6 +481,7 @@ fn finalize_shape(
480481 paragraphs : & mut Vec < PptxParagraphEntry > ,
481482 text_box_padding : Insets ,
482483 text_box_vertical_align : TextBoxVerticalAlign ,
484+ text_box_no_wrap : bool ,
483485) -> Vec < FixedElement > {
484486 // Resolve effective fill: explicit > noFill > style fallback.
485487 let effective_fill: Option < Color > = if shape. fill . is_some ( ) {
@@ -556,6 +558,7 @@ fn finalize_shape(
556558 opacity : None ,
557559 stroke : None ,
558560 shape_kind : None ,
561+ no_wrap : text_box_no_wrap,
559562 } ) ,
560563 } ) ;
561564 } else {
@@ -573,6 +576,7 @@ fn finalize_shape(
573576 opacity : shape. opacity ,
574577 stroke,
575578 shape_kind : None ,
579+ no_wrap : text_box_no_wrap,
576580 } ) ,
577581 } ) ;
578582 }
@@ -700,6 +704,12 @@ struct SlideXmlParser<'a> {
700704 inherited_text_body_defaults : & ' a PptxTextBodyStyleDefaults ,
701705 table_styles : & ' a table_styles:: TableStyleMap ,
702706
707+ // ── Options ─────────────────────────────────────────────────────
708+ /// When true, shapes with `<p:ph>` (placeholder) are skipped.
709+ /// Used when parsing master/layout layers whose placeholder content
710+ /// should not render unless the slide overrides it.
711+ skip_placeholders : bool ,
712+
703713 // ── Output accumulators ─────────────────────────────────────────
704714 elements : Vec < FixedElement > ,
705715 warnings : Vec < ConvertWarning > ,
@@ -713,6 +723,7 @@ struct SlideXmlParser<'a> {
713723 paragraphs : Vec < PptxParagraphEntry > ,
714724 text_box_padding : Insets ,
715725 text_box_vertical_align : TextBoxVerticalAlign ,
726+ text_box_no_wrap : bool ,
716727 text_body_style_defaults : PptxTextBodyStyleDefaults ,
717728
718729 // ── Paragraph state (`<a:p>`) ───────────────────────────────────
@@ -770,6 +781,8 @@ impl<'a> SlideXmlParser<'a> {
770781 inherited_text_body_defaults,
771782 table_styles,
772783
784+ skip_placeholders : false ,
785+
773786 elements : Vec :: new ( ) ,
774787 warnings : Vec :: new ( ) ,
775788
@@ -780,6 +793,7 @@ impl<'a> SlideXmlParser<'a> {
780793 paragraphs : Vec :: new ( ) ,
781794 text_box_padding : default_pptx_text_box_padding ( ) ,
782795 text_box_vertical_align : TextBoxVerticalAlign :: Top ,
796+ text_box_no_wrap : false ,
783797 text_body_style_defaults : PptxTextBodyStyleDefaults :: default ( ) ,
784798
785799 in_para : false ,
@@ -863,6 +877,7 @@ impl<'a> SlideXmlParser<'a> {
863877 self . paragraphs . clear ( ) ;
864878 self . text_box_padding = default_pptx_text_box_padding ( ) ;
865879 self . text_box_vertical_align = TextBoxVerticalAlign :: Top ;
880+ self . text_box_no_wrap = false ;
866881 }
867882 b"sp" | b"cxnSp" if self . in_shape => {
868883 self . shape . depth += 1 ;
@@ -885,6 +900,10 @@ impl<'a> SlideXmlParser<'a> {
885900 self . shape . prst_geom = Some ( prst) ;
886901 }
887902 }
903+ // Treat custom geometry as a rectangle fallback so the fill renders.
904+ b"custGeom" if self . shape . in_sp_pr && self . shape . prst_geom . is_none ( ) => {
905+ self . shape . prst_geom = Some ( "rect" . to_string ( ) ) ;
906+ }
888907 b"noFill" if self . shape . in_sp_pr && !self . shape . in_ln && !self . in_rpr => {
889908 self . shape . explicit_no_fill = true ;
890909 }
@@ -944,6 +963,7 @@ impl<'a> SlideXmlParser<'a> {
944963 e,
945964 & mut self . text_box_padding ,
946965 & mut self . text_box_vertical_align ,
966+ & mut self . text_box_no_wrap ,
947967 ) ;
948968 }
949969 b"lstStyle" if self . in_shape && self . in_txbody => {
@@ -1172,19 +1192,27 @@ impl<'a> SlideXmlParser<'a> {
11721192 . map ( pptx_dash_to_border_style)
11731193 . unwrap_or ( BorderLineStyle :: Solid ) ;
11741194 }
1195+ // Handle self-closing <p:ph type="..."/> (placeholder marker).
1196+ b"ph" if self . in_shape => {
1197+ self . shape . has_placeholder = true ;
1198+ }
11751199 // Handle self-closing <a:bodyPr anchor="ctr"/> (no child elements).
11761200 b"bodyPr" if self . in_shape && self . in_txbody => {
11771201 extract_pptx_text_box_body_props (
11781202 e,
11791203 & mut self . text_box_padding ,
11801204 & mut self . text_box_vertical_align ,
1205+ & mut self . text_box_no_wrap ,
11811206 ) ;
11821207 }
11831208 b"prstGeom" if self . shape . in_sp_pr => {
11841209 if let Some ( prst) = get_attr_str ( e, b"prst" ) {
11851210 self . shape . prst_geom = Some ( prst) ;
11861211 }
11871212 }
1213+ b"custGeom" if self . shape . in_sp_pr && self . shape . prst_geom . is_none ( ) => {
1214+ self . shape . prst_geom = Some ( "rect" . to_string ( ) ) ;
1215+ }
11881216 b"ln" if self . shape . in_sp_pr => {
11891217 self . shape . ln_width_emu = get_attr_i64 ( e, b"w" ) . unwrap_or ( 12700 ) ;
11901218 }
@@ -1336,12 +1364,19 @@ impl<'a> SlideXmlParser<'a> {
13361364 b"sp" | b"cxnSp" if self . in_shape => {
13371365 self . shape . depth -= 1 ;
13381366 if self . shape . depth == 0 {
1339- self . elements . extend ( finalize_shape (
1340- & mut self . shape ,
1341- & mut self . paragraphs ,
1342- self . text_box_padding ,
1343- self . text_box_vertical_align ,
1344- ) ) ;
1367+ // Skip placeholder shapes when parsing master/layout layers.
1368+ // Placeholder content is only visible when the slide itself
1369+ // overrides it; master/layout placeholder text (e.g.
1370+ // "마스터 제목 스타일 편집") should never be rendered.
1371+ if !( self . skip_placeholders && self . shape . has_placeholder ) {
1372+ self . elements . extend ( finalize_shape (
1373+ & mut self . shape ,
1374+ & mut self . paragraphs ,
1375+ self . text_box_padding ,
1376+ self . text_box_vertical_align ,
1377+ self . text_box_no_wrap ,
1378+ ) ) ;
1379+ }
13451380 self . in_shape = false ;
13461381 }
13471382 }
@@ -1458,6 +1493,29 @@ pub(super) fn parse_slide_xml(
14581493 warning_context : & str ,
14591494 inherited_text_body_defaults : & PptxTextBodyStyleDefaults ,
14601495 table_styles : & table_styles:: TableStyleMap ,
1496+ ) -> Result < ( Vec < FixedElement > , Vec < ConvertWarning > ) , ConvertError > {
1497+ parse_slide_xml_inner (
1498+ xml,
1499+ images,
1500+ theme,
1501+ color_map,
1502+ warning_context,
1503+ inherited_text_body_defaults,
1504+ table_styles,
1505+ false ,
1506+ )
1507+ }
1508+
1509+ #[ allow( clippy:: too_many_arguments) ]
1510+ fn parse_slide_xml_inner (
1511+ xml : & str ,
1512+ images : & SlideImageMap ,
1513+ theme : & ThemeData ,
1514+ color_map : & ColorMapData ,
1515+ warning_context : & str ,
1516+ inherited_text_body_defaults : & PptxTextBodyStyleDefaults ,
1517+ table_styles : & table_styles:: TableStyleMap ,
1518+ skip_placeholders : bool ,
14611519) -> Result < ( Vec < FixedElement > , Vec < ConvertWarning > ) , ConvertError > {
14621520 let mut reader = Reader :: from_str ( xml) ;
14631521 let mut parser = SlideXmlParser :: new (
@@ -1469,6 +1527,7 @@ pub(super) fn parse_slide_xml(
14691527 inherited_text_body_defaults,
14701528 table_styles,
14711529 ) ;
1530+ parser. skip_placeholders = skip_placeholders;
14721531
14731532 loop {
14741533 match reader. read_event ( ) {
0 commit comments